Compare commits

..

5 Commits

Author SHA1 Message Date
Daniel Karzel a11c9d9c03
fixup! Move files from `protocol` to appropriate module
3 years ago
Daniel Karzel 52e2e7e489
Quote protocol asb/cli instead of alice/bob
3 years ago
Daniel Karzel c87af7bf84
Move files from `protocol` to appropriate module
3 years ago
Daniel Karzel 27c255646f
fixup! `swap_setup` instead of `spot_price` and `execution_setup`
3 years ago
Daniel Karzel 85f5635663
`swap_setup` instead of `spot_price` and `execution_setup`
3 years ago

@ -54,7 +54,7 @@ jobs:
run: git push origin release/${{ github.event.inputs.version }} --force
- name: Create pull request
uses: thomaseizinger/create-pull-request@1.2.1
uses: thomaseizinger/create-pull-request@1.1.0
with:
GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }}
head: release/${{ github.event.inputs.version }}

@ -7,64 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- An issue where the connection between ASB and CLI would get closed prematurely.
The CLI expects to be connected to the ASB throughout the entire swap and hence reconnects as soon as the connection is closed.
This resulted in a loop of connections being established but instantly closed again because the ASB deemed the connection to not be necessary.
See issue https://github.com/comit-network/xmr-btc-swap/issues/648.
## [0.8.1] - 2021-08-16
### Fixed
- An occasional error where users couldn't start a swap because of `InsufficientFunds` that were off by exactly 1 satoshi.
## [0.8.0] - 2021-07-09
### Added
- Printing the deposit address to the terminal as a QR code.
To not break automated scripts or integrations with other software, this behaviour is disabled if `--json` is passed to the application.
- Configuration setting for the websocket URL that the ASB connects to in order to receive price ticker updates.
Can be configured manually by editing the config.toml file directly.
It is expected that the server behind the url follows the same protocol as the [Kraken websocket api](https://docs.kraken.com/websockets/).
- Registration and discovery of ASBs using the [libp2p rendezvous protocol](https://github.com/libp2p/specs/blob/master/rendezvous/README.md).
ASBs can register with a rendezvous node upon startup and, once registered, can be automatically discovered by the CLI using the `list-sellers` command.
The rendezvous node address (`rendezvous_point`), as well as the ASB's external addresses (`external_addresses`) to be registered, is configured in the `network` section of the ASB config file.
A rendezvous node is provided at `/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o` for testing purposes.
Upon discovery using `list-sellers` CLI users are provided with quote and connection information for each ASB discovered through the rendezvous node.
- A mandatory `--change-address` parameter to the CLI's `buy-xmr` command.
The provided address is used to transfer Bitcoin in case of a refund and in case the user transfers more than the specified amount into the swap.
For more information see [#513](https://github.com/comit-network/xmr-btc-swap/issues/513).
### Fixed
- An issue where the ASB gives long price guarantees when setting up a swap.
Now, after sending a spot price the ASB will wait for one minute for the CLI's to trigger the execution setup, and three minutes to see the BTC lock transaction of the CLI in mempool after the swap started.
If the first timeout is triggered the execution setup will be aborted, if the second timeout is triggered the swap will be safely aborted.
- An issue where the default Monero node connection string would not work, because the public nodes were moved to a different domain.
The default monerod nodes were updated to use the [melo tool nodes](https://melo.tools/nodes.html).
### Changed
- 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
- The websocket transport from the CLI.
Websockets were only ever intended to be used for the ASB side to allow websites to retrieve quotes.
The CLI can use regular TCP connections and having both - TCP and websockets - causes problems and unnecessary overhead.
- The `--seller-addr` parameter from the CLI's `resume` command.
This information is now loaded from the database.
- The `--receive-address` parameter from the CLI's `resume` command.
This information is now loaded from the database.
## [0.7.0] - 2021-05-28
@ -188,9 +146,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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.
[Unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.8.1...HEAD
[0.8.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.8.0...0.8.1
[0.8.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.7.0...0.8.0
[Unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.7.0...HEAD
[0.7.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.6.0...0.7.0
[0.6.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.5.0...0.6.0
[0.5.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.4.0...0.5.0

@ -1,24 +0,0 @@
# Contribution guidelines
Thank you for wanting to contribute to this project!
## Contributing code
There are a couple of things we are going to look out for in PRs and knowing them upfront is going to reduce the number of times we will be going back and forth, making things more efficient.
1. We have CI checks in place that validate formatting and code style.
Make sure `dprint check` and `cargo clippy` both finish without any warnings or errors.
If you don't already have it installed, you can obtain in [various ways](https://dprint.dev/install/).
2. All text document (`CHANGELOG.md`, `README.md`, etc) should follow the [semantic linebreaks](https://sembr.org/) specification.
3. We strive for atomic commits with good commit messages.
As an inspiration, read [this](https://chris.beams.io/posts/git-commit/) blogpost.
An atomic commit is a cohesive diff with formatting checks, linter and build passing.
Ideally, all tests are passing as well but we acknowledge that this is not always possible depending on the change you are making.
4. If you are making any user visible changes, include a changelog entry.
## Contributing issues
When contributing a feature request, please focus on your _problem_ as much as possible.
It is okay to include ideas on how the feature should be implemented but they should be 2nd nature of your request.
For more loosely-defined problems and ideas, consider starting a [discussion](https://github.com/comit-network/xmr-btc-swap/discussions/new) instead of opening an issue.

811
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -5,39 +5,50 @@ 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).
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.
## Quick start - CLI
## 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.
This quickstart guide assumes that you are running the software on testnet (i.e. Bitcoin testnet3 and Monero stagenet):
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:
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
```shell
./swap --testnet list-sellers
```
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.
3. Swap with a seller:
## How it works
```shell
./swap --testnet buy-xmr --receive-address <YOUR MONERO ADDRESS> --change-address <YOUR BITCOIN CHANGE ADDRESS> --seller <SELLER MULTIADDRESS>
```
This repository primarily hosts two components:
For more detailed documentation on the CLI, see [this README](./docs/cli/README.md).
- the `swap` CLI
- the [`asb` service](/docs/asb/README.md)
## Becoming a Market Maker
### swap CLI
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.
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`.
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).
### asb service
## Safety
`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.
This software is using cryptography that has not been formally audited.
While we do our best to make it safe, it is up to the user to evaluate whether or not it is safe to use for their purposes.
Please also see section 15 and 16 of the [license](./LICENSE).
For details on how to run the ASB please refer to the [ASB docs](/docs/asb/README.md).
## Contact

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

@ -1,6 +1,6 @@
# Automated Swap Backend (ASB)
# XMR to BTC Atomic Swap - Automated Swap Backend (ASB)
## Quick Start
## Quick Start ASB
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.
@ -12,7 +12,10 @@ This quickstart guide assumes that you are running the software on testnet (i.e.
3. Run the ASB in terminal: `./asb --testnet start`
4. Follow the setup wizard in the terminal
Public Monero nodes for running the Monero Wallet RPC can be found [here](https://melo.tools/nodes.html).
Public Monero stagenet nodes for running the Monero Wallet RPC:
- `monero-stagenet.exan.tech:38081`
- `stagenet.community.xmr.to:38081`
Run `./asb --help` for more information.
@ -30,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-based](https://libp2p.io/) network protocols.
The ASB communicates with the CLI on various [libp2p](https://libp2p.io/)-based 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.
@ -39,22 +42,23 @@ Since the ASB is a long running task we specify the person running an ASB as ser
### ASB discovery
The ASB daemon supports the libp2p [rendezvous-protocol](https://github.com/libp2p/specs/tree/master/rendezvous).
Usage of the rendezvous functionality is entirely optional.
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.
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:
[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.
```toml
[network]
rendezvous_point = "/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o"
external_addresses = ["/dns4/example.com/tcp/9939"]
```
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`).
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.
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.
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.
### Setup Details
@ -92,37 +96,28 @@ 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:
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.
```toml
[maker]
min_buy_btc = 0.0001
max_buy_btc = 0.0001
ask_spread = 0.02
price_ticker_ws_url = "wss://ws.kraken.com"
```
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 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.
The maximum amount tradeable can be configured with the `--max-buy-btc` parameter.
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.
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, 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.
Currently the spot price is equal to the market price on Kraken.
#### 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.
If the ASB is restarted unfinished swaps will be resumed automatically.
When 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 back to the `asb-wallet`.
The ASB can then refund the Monero which will be automatically transferred 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.
@ -133,6 +128,7 @@ 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.

@ -1,139 +0,0 @@
# 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
--tor-socks5-port <tor-socks5-port> Your local Tor socks5 proxy port [default: 9050]
```
Running `swap --testnet list-sellers --rendezvous-point /dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o` 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.

@ -1,39 +0,0 @@
#!/bin/bash
# This is a utility script to showcase how the swap CLI can discover sellers and then trigger a swap using the discovered sellers
#
# 1st param: Path to the "swap" binary (aka the swap CLI)
# 2nd param: Multiaddress of the rendezvous node to be used for discovery
# 3rd param: Your Monero stagenet address where the XMR will be received
# 4th param: Your bech32 Bitcoin testnet address that will be used for any change output (e.g. refund scenario or when swapping an amount smaller than the transferred BTC)
#
# Example usage:
# discover_and_take.sh "PATH/TO/swap" "/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o" "YOUR_XMR_STAGENET_ADDRESS" "YOUR_BECH32_BITCOIN_TESTNET_ADDRESS"
CLI_PATH=$1
RENDEZVOUS_POINT=$2
YOUR_MONERO_ADDR=$3
YOUR_BITCOIN_ADDR=$4
CLI_LIST_SELLERS="$CLI_PATH --testnet --json --debug list-sellers --rendezvous-point $RENDEZVOUS_POINT"
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

@ -8,7 +8,7 @@ edition = "2018"
anyhow = "1"
curve25519-dalek = "3.1"
hex = "0.4"
jsonrpc_client = { version = "0.7", features = [ "reqwest" ] }
jsonrpc_client = { version = "0.6", features = [ "reqwest" ] }
monero = "0.12"
monero-epee-bin-serde = "1"
rand = "0.7"

@ -1,6 +1,6 @@
[package]
name = "swap"
version = "0.8.1"
version = "0.7.0"
authors = [ "The COMIT guys <hello@comit.network>" ]
edition = "2018"
description = "XMR/BTC trustless atomic swaps."
@ -15,11 +15,10 @@ async-trait = "0.1"
atty = "0.2"
backoff = { version = "0.3", features = [ "tokio" ] }
base64 = "0.13"
bdk = "0.10"
bdk = "0.8"
big-bytes = "1"
bitcoin = { version = "0.26", features = [ "rand", "use-serde" ] }
bmrng = "0.5"
comfy-table = "4.1.1"
config = { version = "0.11", default-features = false, features = [ "toml" ] }
conquer-once = "0.3"
curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" }
@ -30,11 +29,12 @@ ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features =
ed25519-dalek = "1"
futures = { version = "0.3", default-features = false }
itertools = "0.10"
libp2p = { git = "https://github.com/comit-network/rust-libp2p", branch = "rendezvous", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous" ] }
libp2p = { version = "0.38", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping" ] }
miniscript = { version = "5", features = [ "serde" ] }
monero = { version = "0.12", features = [ "serde_support" ] }
monero-rpc = { path = "../monero-rpc" }
pem = "0.8"
prettytable-rs = "0.8"
proptest = "1"
qrcode = "0.12"
rand = "0.8"
@ -45,7 +45,6 @@ rust_decimal_macros = "1"
serde = { version = "1", features = [ "derive" ] }
serde_cbor = "0.11"
serde_json = "1"
serde_with = { version = "1", features = [ "macros" ] }
sha2 = "0.9"
sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde" ] }
sled = "0.34"
@ -55,7 +54,7 @@ thiserror = "1"
time = "0.2"
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs", "net" ] }
tokio-socks = "0.5"
tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] }
tokio-tungstenite = { version = "0.14", features = [ "rustls-tls" ] }
tokio-util = { version = "0.6", features = [ "io" ] }
toml = "0.5"
torut = { version = "0.1", default-features = false, features = [ "v3", "control" ] }
@ -80,12 +79,7 @@ get-port = "3"
hyper = "0.14"
monero-harness = { path = "../monero-harness" }
port_check = "0.1"
proptest = "1"
serde_cbor = "0.11"
spectral = "0.6"
tempfile = "3"
testcontainers = "0.12"
[build-dependencies]
vergen = { version = "5", default-features = false, features = [ "git", "build" ] }
anyhow = "1"

@ -1,9 +0,0 @@
use anyhow::Result;
use vergen::{vergen, Config, SemverKind};
fn main() -> Result<()> {
let mut config = Config::default();
*config.git_mut().semver_kind_mut() = SemverKind::Lightweight;
vergen(config)
}

@ -1,7 +0,0 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 849f8b01f49fc9a913100203698a9151d8de8a37564e1d3b1e3b4169e192f58a # shrinks to funding_amount = 290250686, num_utxos = 3, sats_per_vb = 75.35638, key = ExtendedPrivKey { network: Regtest, depth: 0, parent_fingerprint: 00000000, child_number: Normal { index: 0 }, private_key: [private key data], chain_code: 0b7a29ca6990bbc9b9187c1d1a07e2cf68e32f5ce55d2df01edf8a4ac2ee2a4b }, alice = Point<Normal,Public,NonZero>(0299a8c6a662e2e9e8ee7c6889b75a51c432812b4bf70c1d76eace63abc1bdfb1b), bob = Point<Normal,Public,NonZero>(027165b1f9924030c90d38c511da0f4397766078687997ed34d6ef2743d2a7bbed)

@ -1,14 +1,14 @@
mod behaviour;
pub mod command;
pub mod config;
mod event_loop;
mod network;
mod rate;
mod recovery;
pub mod tracing;
pub mod transport;
pub use behaviour::{Behaviour, OutEvent};
pub use event_loop::{EventLoop, EventLoopHandle, FixedRate, KrakenRate, LatestRate};
pub use network::behaviour::{Behaviour, OutEvent};
pub use network::transport;
pub use rate::Rate;
pub use recovery::cancel::cancel;
pub use recovery::punish::punish;
@ -16,6 +16,3 @@ pub use recovery::redeem::{redeem, Finality};
pub use recovery::refund::refund;
pub use recovery::safely_abort::safely_abort;
pub use recovery::{cancel, refund};
#[cfg(test)]
pub use network::rendezous;

@ -0,0 +1,116 @@
use crate::asb::event_loop::LatestRate;
use crate::env;
use crate::network::quote::BidQuote;
use crate::network::swap_setup::alice;
use crate::network::swap_setup::alice::WalletSnapshot;
use crate::network::{encrypted_signature, quote, transfer_proof};
use crate::protocol::alice::State3;
use anyhow::{anyhow, Error};
use libp2p::ping::{Ping, PingEvent};
use libp2p::request_response::{RequestId, ResponseChannel};
use libp2p::{NetworkBehaviour, PeerId};
use uuid::Uuid;
#[derive(Debug)]
pub enum OutEvent {
SwapSetupInitiated {
send_wallet_snapshot: bmrng::RequestReceiver<bitcoin::Amount, WalletSnapshot>,
},
SwapSetupCompleted {
peer_id: PeerId,
swap_id: Uuid,
state3: Box<State3>,
},
SwapDeclined {
peer: PeerId,
error: alice::Error,
},
QuoteRequested {
channel: ResponseChannel<BidQuote>,
peer: PeerId,
},
TransferProofAcknowledged {
peer: PeerId,
id: RequestId,
},
EncryptedSignatureReceived {
msg: Box<encrypted_signature::Request>,
channel: ResponseChannel<()>,
peer: PeerId,
},
Failure {
peer: PeerId,
error: Error,
},
/// "Fallback" variant that allows the event mapping code to swallow certain
/// events that we don't want the caller to deal with.
Other,
}
impl OutEvent {
pub fn unexpected_request(peer: PeerId) -> OutEvent {
OutEvent::Failure {
peer,
error: anyhow!("Unexpected request received"),
}
}
pub fn unexpected_response(peer: PeerId) -> OutEvent {
OutEvent::Failure {
peer,
error: anyhow!("Unexpected response received"),
}
}
}
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)]
pub struct Behaviour<LR>
where
LR: LatestRate + Send + 'static,
{
pub quote: quote::Behaviour,
pub swap_setup: alice::Behaviour<LR>,
pub transfer_proof: transfer_proof::Behaviour,
pub encrypted_signature: encrypted_signature::Behaviour,
/// Ping behaviour that ensures that the underlying network connection is
/// still alive. If the ping fails a connection close event will be
/// emitted that is picked up as swarm event.
ping: Ping,
}
impl<LR> Behaviour<LR>
where
LR: LatestRate + Send + 'static,
{
pub fn new(
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
latest_rate: LR,
resume_only: bool,
env_config: env::Config,
) -> Self {
Self {
quote: quote::asb(),
swap_setup: alice::Behaviour::new(
min_buy,
max_buy,
env_config,
latest_rate,
resume_only,
),
transfer_proof: transfer_proof::alice(),
encrypted_signature: encrypted_signature::alice(),
ping: Ping::default(),
}
}
}
impl From<PingEvent> for OutEvent {
fn from(_: PingEvent) -> Self {
OutEvent::Other
}
}

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

@ -13,6 +13,7 @@ use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tracing::info;
use url::Url;
pub trait GetDefaults {
@ -26,7 +27,6 @@ pub struct Defaults {
listen_address_ws: Multiaddr,
electrum_rpc_url: Url,
monero_wallet_rpc_url: Url,
price_ticker_ws_url: Url,
bitcoin_confirmation_target: usize,
}
@ -41,7 +41,6 @@ impl GetDefaults for Testnet {
listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?,
electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:60002")?,
monero_wallet_rpc_url: Url::parse("http://127.0.0.1:38083/json_rpc")?,
price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?,
bitcoin_confirmation_target: 1,
};
@ -60,7 +59,6 @@ impl GetDefaults for Mainnet {
listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?,
electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:50002")?,
monero_wallet_rpc_url: Url::parse("http://127.0.0.1:18083/json_rpc")?,
price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?,
bitcoin_confirmation_target: 3,
};
@ -85,7 +83,6 @@ const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
const DEFAULT_SPREAD: f64 = 0.02f64;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub data: Data,
pub network: Network,
@ -118,10 +115,6 @@ pub struct Data {
#[serde(deny_unknown_fields)]
pub struct Network {
pub listen: Vec<Multiaddr>,
#[serde(default)]
pub rendezvous_point: Option<Multiaddr>,
#[serde(default)]
pub external_addresses: Vec<Multiaddr>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
@ -158,7 +151,6 @@ pub struct Maker {
#[serde(with = "::bitcoin::util::amount::serde::as_btc")]
pub max_buy_btc: bitcoin::Amount,
pub ask_spread: Decimal,
pub price_ticker_ws_url: Url,
}
impl Default for TorConf {
@ -176,9 +168,9 @@ pub struct ConfigNotInitialized {}
pub fn read_config(config_path: PathBuf) -> Result<Result<Config, ConfigNotInitialized>> {
if config_path.exists() {
tracing::info!(
info!(
path = %config_path.display(),
"Reading config file",
"Using config file at",
);
} else {
return Ok(Err(ConfigNotInitialized {}));
@ -196,7 +188,7 @@ pub fn initial_setup(config_path: PathBuf, config: Config) -> Result<()> {
ensure_directory_exists(config_path.as_path())?;
fs::write(&config_path, toml)?;
tracing::info!(
info!(
path = %config_path.as_path().display(),
"Initial setup complete, config file created",
);
@ -248,7 +240,7 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
.map(|str| str.parse())
.collect::<Result<Vec<Multiaddr>, _>>()?;
let electrum_rpc_url = Input::with_theme(&ColorfulTheme::default())
let electrum_rpc_url: Url = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Electrum RPC URL or hit return to use default")
.default(defaults.electrum_rpc_url)
.interact_text()?;
@ -289,23 +281,12 @@ 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 rendezvous_point = Input::<Multiaddr>::with_theme(&ColorfulTheme::default())
.with_prompt("Do you want to advertise your ASB instance with a rendezvous node? Enter an empty string if not.")
.allow_empty(true)
.interact_text()?;
println!();
Ok(Config {
data: Data { dir: data_dir },
network: Network {
listen: listen_addresses,
rendezvous_point: if rendezvous_point.is_empty() {
None
} else {
Some(rendezvous_point)
},
external_addresses: vec![],
},
bitcoin: Bitcoin {
electrum_rpc_url,
@ -326,7 +307,6 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
min_buy_btc: min_buy,
max_buy_btc: max_buy,
ask_spread,
price_ticker_ws_url: defaults.price_ticker_ws_url,
},
})
}
@ -355,8 +335,6 @@ mod tests {
},
network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
rendezvous_point: None,
external_addresses: vec![],
},
monero: Monero {
@ -369,7 +347,6 @@ mod tests {
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
price_ticker_ws_url: defaults.price_ticker_ws_url,
},
};
@ -398,8 +375,6 @@ mod tests {
},
network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
rendezvous_point: None,
external_addresses: vec![],
},
monero: Monero {
@ -412,7 +387,6 @@ mod tests {
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
price_ticker_ws_url: defaults.price_ticker_ws_url,
},
};

@ -1,10 +1,12 @@
use crate::asb::{Behaviour, OutEvent, Rate};
use crate::asb::behaviour::{Behaviour, OutEvent};
use crate::asb::Rate;
use crate::database::Database;
use crate::env::Config;
use crate::network::quote::BidQuote;
use crate::network::swap_setup::alice::WalletSnapshot;
use crate::network::transfer_proof;
use crate::protocol::alice::{AliceState, State3, Swap};
use crate::{bitcoin, env, kraken, monero};
use crate::{bitcoin, kraken, monero};
use anyhow::{Context, Result};
use futures::future;
use futures::future::{BoxFuture, FutureExt};
@ -36,7 +38,7 @@ where
LR: LatestRate + Send + 'static + Debug + Clone,
{
swarm: libp2p::Swarm<Behaviour<LR>>,
env_config: env::Config,
env_config: Config,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Arc<Database>,
@ -68,7 +70,7 @@ where
#[allow(clippy::too_many_arguments)]
pub fn new(
swarm: Swarm<Behaviour<LR>>,
env_config: env::Config,
env_config: Config,
bitcoin_wallet: Arc<bitcoin::Wallet>,
monero_wallet: Arc<monero::Wallet>,
db: Arc<Database>,
@ -147,7 +149,7 @@ where
loop {
tokio::select! {
swarm_event = self.swarm.select_next_some() => {
swarm_event = self.swarm.next_event() => {
match swarm_event {
SwarmEvent::Behaviour(OutEvent::SwapSetupInitiated { mut send_wallet_snapshot }) => {
@ -171,16 +173,16 @@ where
let _ = responder.respond(wallet_snapshot);
}
SwarmEvent::Behaviour(OutEvent::SwapSetupCompleted{peer_id, swap_id, state3}) => {
let _ = self.handle_execution_setup_done(peer_id, swap_id, state3).await;
let _ = self.handle_execution_setup_done(peer_id, swap_id, *state3).await;
}
SwarmEvent::Behaviour(OutEvent::SwapDeclined { peer, error }) => {
tracing::warn!(%peer, "Ignoring spot price request: {}", error);
tracing::warn!(%peer, "Ignoring spot price request because: {}", error);
}
SwarmEvent::Behaviour(OutEvent::QuoteRequested { channel, peer }) => {
let quote = match self.make_quote(self.min_buy, self.max_buy).await {
Ok(quote) => quote,
Err(error) => {
tracing::warn!(%peer, "Failed to make quote: {:#}", error);
tracing::warn!(%peer, "Failed to make quote. Error {:#}", error);
continue;
}
};
@ -244,16 +246,10 @@ where
channel
}.boxed());
}
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::Event::Registered { .. })) => {
tracing::info!("Successfully registered with rendezvous node");
}
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::Event::RegisterFailed(error))) => {
tracing::error!("Registration with rendezvous node failed: {:#}", error);
}
SwarmEvent::Behaviour(OutEvent::Failure {peer, error}) => {
tracing::error!(
%peer,
"Communication error: {:#}", error);
"Communication error. Error {:#}", error);
}
SwarmEvent::ConnectionEstablished { peer_id: peer, endpoint, .. } => {
tracing::debug!(%peer, address = %endpoint.get_remote_address(), "New connection established");
@ -268,16 +264,16 @@ where
}
}
SwarmEvent::IncomingConnectionError { send_back_addr: address, error, .. } => {
tracing::warn!(%address, "Failed to set up connection with peer: {:#}", error);
tracing::warn!(%address, "Failed to set up connection with peer. Error {:#}", error);
}
SwarmEvent::ConnectionClosed { peer_id: peer, num_established, endpoint, cause: Some(error) } if num_established == 0 => {
tracing::warn!(%peer, address = %endpoint.get_remote_address(), "Lost connection to peer: {:#}", error);
tracing::warn!(%peer, address = %endpoint.get_remote_address(), "Lost connection. Error {:#}", error);
}
SwarmEvent::ConnectionClosed { peer_id: peer, num_established, endpoint, cause: None } if num_established == 0 => {
tracing::info!(%peer, address = %endpoint.get_remote_address(), "Successfully closed connection");
}
SwarmEvent::NewListenAddr(address) => {
tracing::info!(%address, "New listen address reported");
tracing::info!(%address, "New listen address detected");
}
_ => {}
}
@ -295,7 +291,7 @@ where
self.inflight_transfer_proofs.insert(id, responder);
},
Some(Err(error)) => {
tracing::debug!("A swap stopped without sending a transfer proof: {:#}", error);
tracing::debug!("A swap stopped without sending a transfer proof. Error {:#}", error);
}
None => {
unreachable!("stream of transfer proof receivers must never terminate")
@ -354,11 +350,11 @@ where
match self.db.insert_peer_id(swap_id, bob_peer_id).await {
Ok(_) => {
if let Err(error) = self.swap_sender.send(swap).await {
tracing::warn!(%swap_id, "Failed to start swap: {}", error);
tracing::warn!(%swap_id, "Swap cannot be spawned: {}", error);
}
}
Err(error) => {
tracing::warn!(%swap_id, "Unable to save peer-id in database: {}", error);
tracing::warn!(%swap_id, "Unable to save peer-id, swap cannot be spawned: {}", error);
}
}
}

@ -1,441 +0,0 @@
use crate::asb::event_loop::LatestRate;
use crate::env;
use crate::network::quote::BidQuote;
use crate::network::rendezvous::XmrBtcNamespace;
use crate::network::swap_setup::alice;
use crate::network::swap_setup::alice::WalletSnapshot;
use crate::network::transport::authenticate_and_multiplex;
use crate::network::{encrypted_signature, quote, transfer_proof};
use crate::protocol::alice::State3;
use anyhow::{anyhow, Error, Result};
use futures::FutureExt;
use libp2p::core::connection::ConnectionId;
use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::Boxed;
use libp2p::dns::TokioDnsConfig;
use libp2p::ping::{Ping, PingConfig, PingEvent};
use libp2p::request_response::{RequestId, ResponseChannel};
use libp2p::swarm::{
DialPeerCondition, IntoProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction,
PollParameters, ProtocolsHandler,
};
use libp2p::tcp::TokioTcpConfig;
use libp2p::websocket::WsConfig;
use libp2p::{identity, Multiaddr, NetworkBehaviour, PeerId, Transport};
use std::task::Poll;
use std::time::Duration;
use uuid::Uuid;
pub mod transport {
use super::*;
/// Creates the libp2p transport for the ASB.
pub fn new(identity: &identity::Keypair) -> Result<Boxed<(PeerId, StreamMuxerBox)>> {
let tcp = TokioTcpConfig::new().nodelay(true);
let tcp_with_dns = TokioDnsConfig::system(tcp)?;
let websocket_with_dns = WsConfig::new(tcp_with_dns.clone());
let transport = tcp_with_dns.or_transport(websocket_with_dns).boxed();
authenticate_and_multiplex(transport, identity)
}
}
pub mod behaviour {
use super::*;
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum OutEvent {
SwapSetupInitiated {
send_wallet_snapshot: bmrng::RequestReceiver<bitcoin::Amount, WalletSnapshot>,
},
SwapSetupCompleted {
peer_id: PeerId,
swap_id: Uuid,
state3: State3,
},
SwapDeclined {
peer: PeerId,
error: alice::Error,
},
QuoteRequested {
channel: ResponseChannel<BidQuote>,
peer: PeerId,
},
TransferProofAcknowledged {
peer: PeerId,
id: RequestId,
},
EncryptedSignatureReceived {
msg: encrypted_signature::Request,
channel: ResponseChannel<()>,
peer: PeerId,
},
Rendezvous(libp2p::rendezvous::Event),
Failure {
peer: PeerId,
error: Error,
},
/// "Fallback" variant that allows the event mapping code to swallow
/// certain events that we don't want the caller to deal with.
Other,
}
impl OutEvent {
pub fn unexpected_request(peer: PeerId) -> OutEvent {
OutEvent::Failure {
peer,
error: anyhow!("Unexpected request received"),
}
}
pub fn unexpected_response(peer: PeerId) -> OutEvent {
OutEvent::Failure {
peer,
error: anyhow!("Unexpected response received"),
}
}
}
/// A `NetworkBehaviour` that represents an XMR/BTC swap node as Alice.
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "OutEvent", event_process = false)]
#[allow(missing_debug_implementations)]
pub struct Behaviour<LR>
where
LR: LatestRate + Send + 'static,
{
pub rendezvous: libp2p::swarm::toggle::Toggle<rendezous::Behaviour>,
pub quote: quote::Behaviour,
pub swap_setup: alice::Behaviour<LR>,
pub transfer_proof: transfer_proof::Behaviour,
pub encrypted_signature: encrypted_signature::Behaviour,
/// Ping behaviour that ensures that the underlying network connection
/// is still alive. If the ping fails a connection close event
/// will be emitted that is picked up as swarm event.
ping: Ping,
}
impl<LR> Behaviour<LR>
where
LR: LatestRate + Send + 'static,
{
pub fn new(
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
latest_rate: LR,
resume_only: bool,
env_config: env::Config,
rendezvous_params: Option<(identity::Keypair, PeerId, Multiaddr, XmrBtcNamespace)>,
) -> Self {
Self {
rendezvous: libp2p::swarm::toggle::Toggle::from(rendezvous_params.map(
|(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(),
swap_setup: alice::Behaviour::new(
min_buy,
max_buy,
env_config,
latest_rate,
resume_only,
),
transfer_proof: transfer_proof::alice(),
encrypted_signature: encrypted_signature::alice(),
ping: Ping::new(PingConfig::new().with_keep_alive(true)),
}
}
}
impl From<PingEvent> for OutEvent {
fn from(_: PingEvent) -> Self {
OutEvent::Other
}
}
impl From<libp2p::rendezvous::Event> for OutEvent {
fn from(event: libp2p::rendezvous::Event) -> Self {
OutEvent::Rendezvous(event)
}
}
}
pub mod rendezous {
use super::*;
use std::pin::Pin;
#[derive(PartialEq)]
enum ConnectionStatus {
Disconnected,
Dialling,
Connected,
}
enum RegistrationStatus {
RegisterOnNextConnection,
Pending,
Registered {
re_register_in: Pin<Box<tokio::time::Sleep>>,
},
}
pub struct Behaviour {
inner: libp2p::rendezvous::Rendezvous,
rendezvous_point: Multiaddr,
rendezvous_peer_id: PeerId,
namespace: XmrBtcNamespace,
registration_status: RegistrationStatus,
connection_status: ConnectionStatus,
registration_ttl: Option<u64>,
}
impl Behaviour {
pub fn new(
identity: identity::Keypair,
rendezvous_peer_id: PeerId,
rendezvous_address: Multiaddr,
namespace: XmrBtcNamespace,
registration_ttl: Option<u64>,
) -> Self {
Self {
inner: libp2p::rendezvous::Rendezvous::new(
identity,
libp2p::rendezvous::Config::default(),
),
rendezvous_point: rendezvous_address,
rendezvous_peer_id,
namespace,
registration_status: RegistrationStatus::RegisterOnNextConnection,
connection_status: ConnectionStatus::Disconnected,
registration_ttl,
}
}
fn register(&mut self) {
self.inner.register(
self.namespace.into(),
self.rendezvous_peer_id,
self.registration_ttl,
);
}
}
impl NetworkBehaviour for Behaviour {
type ProtocolsHandler =
<libp2p::rendezvous::Rendezvous as NetworkBehaviour>::ProtocolsHandler;
type OutEvent = libp2p::rendezvous::Event;
fn new_handler(&mut self) -> Self::ProtocolsHandler {
self.inner.new_handler()
}
fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec<Multiaddr> {
if peer_id == &self.rendezvous_peer_id {
return vec![self.rendezvous_point.clone()];
}
vec![]
}
fn inject_connected(&mut self, peer_id: &PeerId) {
if peer_id == &self.rendezvous_peer_id {
self.connection_status = ConnectionStatus::Connected;
match &self.registration_status {
RegistrationStatus::RegisterOnNextConnection => {
self.register();
self.registration_status = RegistrationStatus::Pending;
}
RegistrationStatus::Registered { .. } => {}
RegistrationStatus::Pending => {}
}
}
}
fn inject_disconnected(&mut self, peer_id: &PeerId) {
if peer_id == &self.rendezvous_peer_id {
self.connection_status = ConnectionStatus::Disconnected;
}
}
fn inject_event(
&mut self,
peer_id: PeerId,
connection: ConnectionId,
event: <<Self::ProtocolsHandler as IntoProtocolsHandler>::Handler as ProtocolsHandler>::OutEvent,
) {
self.inner.inject_event(peer_id, connection, event)
}
fn inject_dial_failure(&mut self, peer_id: &PeerId) {
if peer_id == &self.rendezvous_peer_id {
self.connection_status = ConnectionStatus::Disconnected;
}
}
#[allow(clippy::type_complexity)]
fn poll(&mut self, cx: &mut std::task::Context<'_>, params: &mut impl PollParameters) -> Poll<NetworkBehaviourAction<<<Self::ProtocolsHandler as IntoProtocolsHandler>::Handler as ProtocolsHandler>::InEvent, Self::OutEvent>>{
match &mut self.registration_status {
RegistrationStatus::RegisterOnNextConnection => match self.connection_status {
ConnectionStatus::Disconnected => {
self.connection_status = ConnectionStatus::Dialling;
return Poll::Ready(NetworkBehaviourAction::DialPeer {
peer_id: self.rendezvous_peer_id,
condition: DialPeerCondition::Disconnected,
});
}
ConnectionStatus::Dialling => {}
ConnectionStatus::Connected => {
self.registration_status = RegistrationStatus::Pending;
self.register();
}
},
RegistrationStatus::Registered { re_register_in } => {
if let Poll::Ready(()) = re_register_in.poll_unpin(cx) {
match self.connection_status {
ConnectionStatus::Connected => {
self.registration_status = RegistrationStatus::Pending;
self.register();
}
ConnectionStatus::Disconnected => {
self.registration_status =
RegistrationStatus::RegisterOnNextConnection;
return Poll::Ready(NetworkBehaviourAction::DialPeer {
peer_id: self.rendezvous_peer_id,
condition: DialPeerCondition::Disconnected,
});
}
ConnectionStatus::Dialling => {}
}
}
}
RegistrationStatus::Pending => {}
}
let inner_poll = self.inner.poll(cx, params);
// reset the timer if we successfully registered
if let Poll::Ready(NetworkBehaviourAction::GenerateEvent(
libp2p::rendezvous::Event::Registered { ttl, .. },
)) = &inner_poll
{
let half_of_ttl = Duration::from_secs(*ttl) / 2;
self.registration_status = RegistrationStatus::Registered {
re_register_in: Box::pin(tokio::time::sleep(half_of_ttl)),
};
}
inner_poll
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::test::{new_swarm, SwarmExt};
use futures::StreamExt;
use libp2p::swarm::SwarmEvent;
#[tokio::test]
async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node(
) {
let mut rendezvous_node = new_swarm(|_, identity| {
libp2p::rendezvous::Rendezvous::new(identity, libp2p::rendezvous::Config::default())
});
let rendezvous_address = rendezvous_node.listen_on_random_memory_address().await;
let mut asb = new_swarm(|_, identity| {
rendezous::Behaviour::new(
identity,
*rendezvous_node.local_peer_id(),
rendezvous_address,
XmrBtcNamespace::Testnet,
None,
)
});
asb.listen_on_random_memory_address().await; // this adds an external address
tokio::spawn(async move {
loop {
rendezvous_node.next().await;
}
});
let asb_registered = tokio::spawn(async move {
loop {
if let SwarmEvent::Behaviour(libp2p::rendezvous::Event::Registered { .. }) =
asb.select_next_some().await
{
break;
}
}
});
tokio::time::timeout(Duration::from_secs(10), asb_registered)
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn asb_automatically_re_registers() {
let min_ttl = 5;
let mut rendezvous_node = new_swarm(|_, identity| {
libp2p::rendezvous::Rendezvous::new(
identity,
libp2p::rendezvous::Config::default().with_min_ttl(min_ttl),
)
});
let rendezvous_address = rendezvous_node.listen_on_random_memory_address().await;
let mut asb = new_swarm(|_, identity| {
rendezous::Behaviour::new(
identity,
*rendezvous_node.local_peer_id(),
rendezvous_address,
XmrBtcNamespace::Testnet,
Some(5),
)
});
asb.listen_on_random_memory_address().await; // this adds an external address
tokio::spawn(async move {
loop {
rendezvous_node.next().await;
}
});
let asb_registered_three_times = tokio::spawn(async move {
let mut number_of_registrations = 0;
loop {
if let SwarmEvent::Behaviour(libp2p::rendezvous::Event::Registered { .. }) =
asb.select_next_some().await
{
number_of_registrations += 1
}
if number_of_registrations == 3 {
break;
}
}
});
tokio::time::timeout(Duration::from_secs(30), asb_registered_three_times)
.await
.unwrap()
.unwrap();
}
}
}

@ -0,0 +1,19 @@
use crate::network::transport::authenticate_and_multiplex;
use anyhow::Result;
use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::Boxed;
use libp2p::dns::TokioDnsConfig;
use libp2p::tcp::TokioTcpConfig;
use libp2p::websocket::WsConfig;
use libp2p::{identity, PeerId, Transport};
/// Creates the libp2p transport for the ASB.
pub fn new(identity: &identity::Keypair) -> Result<Boxed<(PeerId, StreamMuxerBox)>> {
let tcp = TokioTcpConfig::new().nodelay(true);
let tcp_with_dns = TokioDnsConfig::system(tcp)?;
let websocket_with_dns = WsConfig::new(tcp_with_dns.clone());
let transport = tcp_with_dns.or_transport(websocket_with_dns).boxed();
authenticate_and_multiplex(transport, identity)
}

@ -13,11 +13,10 @@
#![allow(non_snake_case)]
use anyhow::{bail, Context, Result};
use comfy_table::Table;
use libp2p::core::multiaddr::Protocol;
use libp2p::core::Multiaddr;
use libp2p::swarm::AddressScore;
use libp2p::Swarm;
use prettytable::{row, Table};
use std::env;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
@ -30,14 +29,17 @@ use swap::asb::config::{
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
use swap::database::Database;
use swap::monero::Amount;
use swap::network::rendezvous::XmrBtcNamespace;
use swap::network::swarm;
use swap::protocol::alice::run;
use swap::seed::Seed;
use swap::tor::AuthenticatedClient;
use swap::{asb, bitcoin, kraken, monero, tor};
use tracing::{debug, info, warn};
use tracing_subscriber::filter::LevelFilter;
#[macro_use]
extern crate prettytable;
const DEFAULT_WALLET_NAME: &str = "asb-wallet";
#[tokio::main]
@ -89,6 +91,11 @@ async fn main() -> Result<()> {
));
}
info!(
db_folder = %config.data.dir.display(),
"Database and Seed will be stored in",
);
let db_path = config.data.dir.join("database");
let db = Database::open(config.data.dir.join(db_path).as_path())
@ -104,27 +111,27 @@ async fn main() -> Result<()> {
let monero_wallet = init_monero_wallet(&config, env_config).await?;
let bitcoin_balance = bitcoin_wallet.balance().await?;
tracing::info!(%bitcoin_balance, "Initialized Bitcoin wallet");
info!(%bitcoin_balance, "Initialized Bitcoin wallet");
let monero_balance = monero_wallet.get_balance().await?;
if monero_balance == Amount::ZERO {
let monero_address = monero_wallet.get_main_address();
tracing::warn!(
warn!(
%monero_address,
"The Monero balance is 0, make sure to deposit funds at",
)
} else {
tracing::info!(%monero_balance, "Initialized Monero wallet");
info!(%monero_balance, "Initialized Monero wallet");
}
let kraken_price_updates = kraken::connect(config.maker.price_ticker_ws_url.clone())?;
let kraken_price_updates = kraken::connect()?;
// setup Tor hidden services
let tor_client =
tor::Client::new(config.tor.socks5_port).with_control_port(config.tor.control_port);
let _ac = match tor_client.assert_tor_running().await {
Ok(_) => {
tracing::info!("Setting up Tor hidden service");
tracing::info!("Tor found. Setting up hidden service");
let ac =
register_tor_services(config.network.clone().listen, tor_client, &seed)
.await?;
@ -144,33 +151,15 @@ async fn main() -> Result<()> {
kraken_rate.clone(),
resume_only,
env_config,
config.network.rendezvous_point.map(|rendezvous_point| {
(
rendezvous_point,
if testnet {
XmrBtcNamespace::Testnet
} else {
XmrBtcNamespace::Mainnet
},
)
}),
)?;
for listen in config.network.listen.clone() {
for listen in config.network.listen {
Swarm::listen_on(&mut swarm, listen.clone())
.with_context(|| format!("Failed to listen on network interface {}", listen))?;
}
tracing::info!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
for external_address in config.network.external_addresses {
let _ = Swarm::add_external_address(
&mut swarm,
external_address,
AddressScore::Infinite,
);
}
let (event_loop, mut swap_receiver) = EventLoop::new(
swarm,
env_config,
@ -190,10 +179,10 @@ async fn main() -> Result<()> {
let swap_id = swap.swap_id;
match run(swap, rate).await {
Ok(state) => {
tracing::debug!(%swap_id, final_state=%state, "Swap completed")
tracing::debug!(%swap_id, %state, "Swap finished with state")
}
Err(error) => {
tracing::error!(%swap_id, "Swap failed: {:#}", error)
tracing::error!(%swap_id, "Swap failed. Error {:#}", error)
}
}
});
@ -205,13 +194,14 @@ async fn main() -> Result<()> {
Command::History => {
let mut table = Table::new();
table.set_header(vec!["SWAP ID", "STATE"]);
table.add_row(row!["SWAP ID", "STATE"]);
for (swap_id, state) in db.all_alice()? {
table.add_row(vec![swap_id.to_string(), state.to_string()]);
table.add_row(row![swap_id, state]);
}
println!("{}", table);
// Print the table to stdout
table.printstd();
}
Command::WithdrawBtc { amount, address } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
@ -225,9 +215,7 @@ async fn main() -> Result<()> {
}
};
let psbt = bitcoin_wallet
.send_to_address(address, amount, None)
.await?;
let psbt = bitcoin_wallet.send_to_address(address, amount).await?;
let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?;
bitcoin_wallet.broadcast(signed_tx, "withdraw").await?;
@ -308,7 +296,7 @@ async fn init_bitcoin_wallet(
seed: &Seed,
env_config: swap::env::Config,
) -> Result<bitcoin::Wallet> {
tracing::debug!("Opening Bitcoin wallet");
debug!("Opening Bitcoin wallet");
let wallet_dir = config.data.dir.join("wallet");
let wallet = bitcoin::Wallet::new(
@ -330,7 +318,7 @@ async fn init_monero_wallet(
config: &Config,
env_config: swap::env::Config,
) -> Result<monero::Wallet> {
tracing::debug!("Opening Monero wallet");
debug!("Opening Monero wallet");
let wallet = monero::Wallet::open_or_create(
config.monero.wallet_rpc_url.clone(),
DEFAULT_WALLET_NAME.to_string(),
@ -378,7 +366,7 @@ async fn register_tor_services(
hidden_services_details.iter().for_each(|(port, _)| {
let onion_address = format!("/onion3/{}:{}", onion_address, port);
tracing::info!(%onion_address, "Successfully created hidden service");
tracing::info!(%onion_address);
});
Ok(ac)

@ -1,5 +1,4 @@
use anyhow::{Context, Result};
use url::Url;
#[tokio::main]
async fn main() -> Result<()> {
@ -7,9 +6,7 @@ async fn main() -> Result<()> {
tracing_subscriber::fmt().with_env_filter("debug").finish(),
)?;
let price_ticker_ws_url = Url::parse("wss://ws.kraken.com")?;
let mut ticker =
swap::kraken::connect(price_ticker_ws_url).context("Failed to connect to kraken")?;
let mut ticker = swap::kraken::connect().context("Failed to connect to kraken")?;
loop {
match ticker.wait_for_next_update().await? {

@ -12,8 +12,8 @@
#![forbid(unsafe_code)]
#![allow(non_snake_case)]
use anyhow::{Context, Result};
use comfy_table::Table;
use anyhow::{bail, Context, Result};
use prettytable::{row, Table};
use qrcode::render::unicode;
use qrcode::QrCode;
use std::cmp::min;
@ -24,19 +24,22 @@ 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, SellerStatus};
use swap::cli::EventLoop;
use swap::database::Database;
use swap::env::Config;
use swap::libp2p_ext::MultiAddrExt;
use swap::network::quote::BidQuote;
use swap::network::swarm;
use swap::protocol::bob;
use swap::protocol::bob::Swap;
use swap::seed::Seed;
use swap::{bitcoin, cli, monero};
use tracing::{debug, error, info, warn};
use url::Url;
use uuid::Uuid;
#[macro_use]
extern crate prettytable;
#[tokio::main]
async fn main() -> Result<()> {
let Arguments {
@ -55,17 +58,17 @@ async fn main() -> Result<()> {
match cmd {
Command::BuyXmr {
seller,
seller_peer_id,
seller_addr,
bitcoin_electrum_rpc_url,
bitcoin_target_block,
bitcoin_change_address,
monero_receive_address,
monero_daemon_address,
tor_socks5_port,
} => {
let swap_id = Uuid::new_v4();
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
cli::tracing::init(debug, json, data_dir.join("logs"), swap_id)?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
@ -83,15 +86,17 @@ async fn main() -> Result<()> {
init_monero_wallet(data_dir, monero_daemon_address, env_config).await?;
let bitcoin_wallet = Arc::new(bitcoin_wallet);
let seller_peer_id = seller
.extract_peer_id()
.context("Seller address must contain peer ID")?;
db.insert_address(seller_peer_id, seller.clone()).await?;
let behaviour = cli::Behaviour::new(seller_peer_id, env_config, bitcoin_wallet.clone());
let mut swarm =
swarm::cli(seed.derive_libp2p_identity(), tor_socks5_port, behaviour).await?;
swarm.behaviour_mut().add_address(seller_peer_id, seller);
let mut swarm = swarm::cli(
&seed,
seller_peer_id,
tor_socks5_port,
env_config,
bitcoin_wallet.clone(),
)
.await?;
swarm
.behaviour_mut()
.add_address(seller_peer_id, seller_addr);
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
@ -110,11 +115,9 @@ async fn main() -> Result<()> {
)
.await?;
tracing::info!(%amount, %fees, %swap_id, "Starting new swap");
info!(%amount, %fees, %swap_id, "Swapping");
db.insert_peer_id(swap_id, seller_peer_id).await?;
db.insert_monero_address(swap_id, monero_receive_address)
.await?;
let swap = Swap::new(
db,
@ -124,7 +127,6 @@ async fn main() -> Result<()> {
env_config,
event_loop_handle,
monero_receive_address,
bitcoin_change_address,
amount,
);
@ -144,27 +146,34 @@ async fn main() -> Result<()> {
let mut table = Table::new();
table.set_header(vec!["SWAP ID", "STATE"]);
table.add_row(row!["SWAP ID", "STATE"]);
for (swap_id, state) in db.all_bob()? {
table.add_row(vec![swap_id.to_string(), state.to_string()]);
table.add_row(row![swap_id, state]);
}
println!("{}", table);
// Print the table to stdout
table.printstd();
}
Command::Resume {
swap_id,
seller_addr,
bitcoin_electrum_rpc_url,
bitcoin_target_block,
monero_receive_address,
monero_daemon_address,
tor_socks5_port,
} => {
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
cli::tracing::init(debug, json, data_dir.join("logs"), swap_id)?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
if monero_receive_address.network != env_config.monero_network {
bail!("The given monero address is on network {:?}, expected address of network {:?}.", monero_receive_address.network, env_config.monero_network)
}
let bitcoin_wallet = init_bitcoin_wallet(
bitcoin_electrum_rpc_url,
&seed,
@ -178,25 +187,25 @@ async fn main() -> Result<()> {
let bitcoin_wallet = Arc::new(bitcoin_wallet);
let seller_peer_id = db.get_peer_id(swap_id)?;
let seller_addresses = db.get_addresses(seller_peer_id)?;
let behaviour = cli::Behaviour::new(seller_peer_id, env_config, bitcoin_wallet.clone());
let mut swarm =
swarm::cli(seed.derive_libp2p_identity(), tor_socks5_port, behaviour).await?;
let mut swarm = swarm::cli(
&seed,
seller_peer_id,
tor_socks5_port,
env_config,
bitcoin_wallet.clone(),
)
.await?;
let our_peer_id = swarm.local_peer_id();
tracing::debug!(peer_id = %our_peer_id, "Network layer initialized");
for seller_address in seller_addresses {
swarm
.behaviour_mut()
.add_address(seller_peer_id, seller_address);
}
tracing::debug!(peer_id = %our_peer_id, "Initializing network module");
swarm
.behaviour_mut()
.add_address(seller_peer_id, seller_addr);
let (event_loop, event_loop_handle) =
EventLoop::new(swap_id, swarm, seller_peer_id, env_config)?;
let handle = tokio::spawn(event_loop.run());
let monero_receive_address = db.get_monero_address(swap_id)?;
let swap = Swap::from_db(
db,
swap_id,
@ -222,7 +231,7 @@ async fn main() -> Result<()> {
bitcoin_electrum_rpc_url,
bitcoin_target_block,
} => {
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
cli::tracing::init(debug, json, data_dir.join("logs"), swap_id)?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
@ -241,10 +250,10 @@ async fn main() -> Result<()> {
match cancel {
Ok((txid, _)) => {
tracing::debug!("Cancel transaction successfully published with id {}", txid)
debug!("Cancel transaction successfully published with id {}", txid)
}
Err(cli::cancel::Error::CancelTimelockNotExpiredYet) => tracing::error!(
"The cancel transaction cannot be published yet, because the timelock has not expired. Please try again later"
Err(cli::cancel::Error::CancelTimelockNotExpiredYet) => error!(
"The Cancel Transaction cannot be published yet, because the timelock has not expired. Please try again later"
),
}
}
@ -254,7 +263,7 @@ async fn main() -> Result<()> {
bitcoin_electrum_rpc_url,
bitcoin_target_block,
} => {
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
cli::tracing::init(debug, json, data_dir.join("logs"), swap_id)?;
let db = Database::open(data_dir.join("database").as_path())
.context("Failed to open database")?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
@ -271,72 +280,6 @@ async fn main() -> Result<()> {
cli::refund(swap_id, Arc::new(bitcoin_wallet), db, force).await??;
}
Command::ListSellers {
rendezvous_point,
namespace,
tor_socks5_port,
} => {
let rendezvous_node_peer_id = rendezvous_point
.extract_peer_id()
.context("Rendezvous node address must contain peer ID")?;
cli::tracing::init(debug, json, data_dir.join("logs"), None)?;
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read in seed file")?;
let identity = seed.derive_libp2p_identity();
let sellers = list_sellers(
rendezvous_node_peer_id,
rendezvous_point,
namespace,
tor_socks5_port,
identity,
)
.await?;
if json {
for seller in sellers {
println!("{}", serde_json::to_string(&seller)?);
}
} else {
let mut table = Table::new();
table.set_header(vec![
"PRICE",
"MIN_QUANTITY",
"MAX_QUANTITY",
"STATUS",
"ADDRESS",
]);
for seller in sellers {
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);
}
}
};
Ok(())
}
@ -416,13 +359,13 @@ where
TS: Future<Output = Result<()>>,
FS: Fn() -> TS,
{
tracing::debug!("Requesting quote");
debug!("Requesting quote");
let bid_quote = bid_quote.await?;
tracing::info!(
info!(
price = %bid_quote.price,
minimum_amount = %bid_quote.min_quantity,
maximum_amount = %bid_quote.max_quantity,
"Received quote",
"Received quote: 1 XMR ~ ",
);
let mut max_giveable = max_giveable_fn().await?;
@ -436,35 +379,41 @@ where
eprintln!("{}", qr_code(&deposit_address)?);
}
loop {
tracing::info!(
%deposit_address,
%max_giveable,
%minimum_amount,
%maximum_amount,
"Waiting for Bitcoin deposit",
);
max_giveable = loop {
sync().await?;
let new_max_givable = max_giveable_fn().await?;
info!(
%deposit_address,
%max_giveable,
%minimum_amount,
%maximum_amount,
"Please deposit BTC you want to swap to",
);
if new_max_givable > max_giveable {
break new_max_givable;
loop {
sync().await?;
let new_max_givable = max_giveable_fn().await?;
if new_max_givable != max_giveable {
max_giveable = new_max_givable;
let new_balance = balance().await?;
tracing::info!(
%new_balance,
%max_giveable,
"Received BTC",
);
if max_giveable >= bid_quote.min_quantity {
break;
} else {
tracing::info!(
%minimum_amount,
%deposit_address,
"Please deposit more, not enough BTC to trigger swap with",
);
}
tokio::time::sleep(Duration::from_secs(1)).await;
};
let new_balance = balance().await?;
tracing::info!(%new_balance, %max_giveable, "Received Bitcoin");
if max_giveable < bid_quote.min_quantity {
tracing::info!("Deposited amount is less than `min_quantity`");
continue;
}
break;
tokio::time::sleep(Duration::from_secs(1)).await;
}
};
@ -480,16 +429,40 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::determine_btc_to_swap;
use ::bitcoin::Amount;
use std::sync::Mutex;
use swap::tracing_ext::capture_logs;
use tracing::level_filters::LevelFilter;
use ::bitcoin::Amount;
use tracing::subscriber;
use crate::determine_btc_to_swap;
use super::*;
struct MaxGiveable {
amounts: Vec<Amount>,
call_counter: usize,
}
impl MaxGiveable {
fn new(amounts: Vec<Amount>) -> Self {
Self {
amounts,
call_counter: 0,
}
}
fn give(&mut self) -> Result<Amount> {
let amount = self
.amounts
.get(self.call_counter)
.ok_or_else(|| anyhow::anyhow!("No more balances available"))?;
self.call_counter += 1;
Ok(*amount)
}
}
#[tokio::test]
async fn given_no_balance_and_transfers_less_than_max_swaps_max_giveable() {
let writer = capture_logs(LevelFilter::INFO);
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.0009).unwrap(),
@ -512,19 +485,11 @@ mod tests {
let expected_amount = Amount::from_btc(0.0009).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 max_giveable=0.00000000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
INFO swap: Received Bitcoin new_balance=0.00100000 BTC max_giveable=0.00090000 BTC
"
);
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_no_balance_and_transfers_more_then_swaps_max_quantity_from_quote() {
let writer = capture_logs(LevelFilter::INFO);
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.1).unwrap(),
@ -547,19 +512,12 @@ mod tests {
let expected_amount = Amount::from_btc(0.01).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 max_giveable=0.00000000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
INFO swap: Received Bitcoin new_balance=0.10010000 BTC max_giveable=0.10000000 BTC
"
);
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_initial_balance_below_max_quantity_swaps_max_givable() {
let writer = capture_logs(LevelFilter::INFO);
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.0049).unwrap(),
Amount::from_btc(99.9).unwrap(),
@ -582,17 +540,12 @@ mod tests {
let expected_amount = Amount::from_btc(0.0049).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
"
);
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_initial_balance_above_max_quantity_swaps_max_quantity() {
let writer = capture_logs(LevelFilter::INFO);
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.1).unwrap(),
Amount::from_btc(99.9).unwrap(),
@ -615,17 +568,12 @@ mod tests {
let expected_amount = Amount::from_btc(0.01).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
"
);
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_no_initial_balance_then_min_wait_for_sufficient_deposit() {
let writer = capture_logs(LevelFilter::INFO);
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.01).unwrap(),
@ -648,19 +596,12 @@ mod tests {
let expected_amount = Amount::from_btc(0.01).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.01000000 BTC maximum_amount=184467440737.09551615 BTC
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 max_giveable=0.00000000 BTC minimum_amount=0.01000000 BTC maximum_amount=184467440737.09551615 BTC
INFO swap: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
"
);
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_balance_less_then_min_wait_for_sufficient_deposit() {
let writer = capture_logs(LevelFilter::INFO);
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.0001).unwrap(),
Amount::from_btc(0.01).unwrap(),
@ -683,19 +624,12 @@ mod tests {
let expected_amount = Amount::from_btc(0.01).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees));
assert_eq!(
writer.captured(),
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.01000000 BTC maximum_amount=184467440737.09551615 BTC
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 max_giveable=0.00010000 BTC minimum_amount=0.01000000 BTC maximum_amount=184467440737.09551615 BTC
INFO swap: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
"
);
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_no_initial_balance_and_transfers_less_than_min_keep_waiting() {
let writer = capture_logs(LevelFilter::INFO);
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.01).unwrap(),
@ -721,82 +655,7 @@ mod tests {
.await
.unwrap_err();
assert!(matches!(error, tokio::time::error::Elapsed { .. }));
assert_eq!(
writer.captured(),
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 max_giveable=0.00000000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
INFO swap: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
INFO swap: Deposited amount is less than `min_quantity`
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 max_giveable=0.01000000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
"
);
}
#[tokio::test]
async fn given_longer_delay_until_deposit_should_not_spam_user() {
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::ZERO,
Amount::from_btc(0.2).unwrap(),
])));
tokio::time::timeout(
Duration::from_secs(10),
determine_btc_to_swap(
true,
async { Ok(quote_with_min(0.1)) },
get_dummy_address(),
|| async { Ok(Amount::from_btc(0.21)?) },
|| async {
let mut result = givable.lock().unwrap();
result.give()
},
|| async { Ok(()) },
),
)
.await
.unwrap()
.unwrap();
assert_eq!(
writer.captured(),
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 max_giveable=0.00000000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
INFO swap: Received Bitcoin new_balance=0.21000000 BTC max_giveable=0.20000000 BTC
"
);
}
struct MaxGiveable {
amounts: Vec<Amount>,
call_counter: usize,
}
impl MaxGiveable {
fn new(amounts: Vec<Amount>) -> Self {
Self {
amounts,
call_counter: 0,
}
}
fn give(&mut self) -> Result<Amount> {
let amount = self
.amounts
.get(self.call_counter)
.ok_or_else(|| anyhow::anyhow!("No more balances available"))?;
self.call_counter += 1;
Ok(*amount)
}
assert!(matches!(error, tokio::time::error::Elapsed { .. }))
}
fn quote_with_max(btc: f64) -> BidQuote {

@ -21,9 +21,6 @@ pub use ecdsa_fun::fun::Scalar;
pub use ecdsa_fun::Signature;
pub use wallet::Wallet;
#[cfg(test)]
pub use wallet::WalletBuilder;
use crate::bitcoin::wallet::ScriptStatus;
use ::bitcoin::hashes::hex::ToHex;
use ::bitcoin::hashes::Hash;
@ -320,8 +317,8 @@ mod tests {
#[tokio::test]
async fn calculate_transaction_weights() {
let alice_wallet = WalletBuilder::new(Amount::ONE_BTC.as_sat()).build();
let bob_wallet = WalletBuilder::new(Amount::ONE_BTC.as_sat()).build();
let alice_wallet = Wallet::new_funded_default_fees(Amount::ONE_BTC.as_sat());
let bob_wallet = Wallet::new_funded_default_fees(Amount::ONE_BTC.as_sat());
let spending_fee = Amount::from_sat(1_000);
let btc_amount = Amount::from_sat(500_000);
let xmr_amount = crate::monero::Amount::from_piconero(10000);

@ -11,7 +11,6 @@ use miniscript::{Descriptor, DescriptorTrait};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use std::ops::Add;
/// Represent a timelock, expressed in relative block height as defined in
@ -48,12 +47,6 @@ impl PartialEq<CancelTimelock> for u32 {
}
}
impl fmt::Display for CancelTimelock {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} blocks", self.0)
}
}
/// Represent a timelock, expressed in relative block height as defined in
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
/// E.g. The timelock expires 10 blocks after the reference transaction is

@ -7,11 +7,11 @@ use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
use anyhow::{bail, Result};
use bdk::database::BatchDatabase;
use bitcoin::Script;
use ecdsa_fun::fun::Point;
use miniscript::{Descriptor, DescriptorTrait};
use rand::thread_rng;
use serde::{Deserialize, Serialize};
const SCRIPT_SIZE: usize = 34;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TxLock {
inner: PartiallySignedTransaction,
@ -24,7 +24,6 @@ impl TxLock {
amount: Amount,
A: PublicKey,
B: PublicKey,
change: bitcoin::Address,
) -> Result<Self>
where
C: EstimateFeeRate,
@ -35,9 +34,7 @@ impl TxLock {
.address(wallet.get_network())
.expect("can derive address from descriptor");
let psbt = wallet
.send_to_address(address, amount, Some(change))
.await?;
let psbt = wallet.send_to_address(address, amount).await?;
Ok(Self {
inner: psbt,
@ -112,7 +109,12 @@ impl TxLock {
/// Calculate the size of the script used by this transaction.
pub fn script_size() -> usize {
SCRIPT_SIZE
build_shared_output_descriptor(
Point::random(&mut thread_rng()),
Point::random(&mut thread_rng()),
)
.script_pubkey()
.len()
}
pub fn script_pubkey(&self) -> Script {
@ -146,14 +148,14 @@ impl TxLock {
witness: Vec::new(),
};
let fee = spending_fee.as_sat();
let spending_fee = spending_fee.as_sat();
tracing::debug!(%spending_fee, "Redeem tx fee");
let tx_out = TxOut {
value: self.inner.clone().extract_tx().output[self.lock_output_vout()].value - fee,
value: self.inner.clone().extract_tx().output[self.lock_output_vout()].value
- spending_fee,
script_pubkey: spend_address.script_pubkey(),
};
tracing::debug!(%fee, "Constructed Bitcoin spending transaction");
Transaction {
version: 2,
lock_time: 0,
@ -183,12 +185,11 @@ impl Watchable for TxLock {
mod tests {
use super::*;
use crate::bitcoin::wallet::StaticFeeRate;
use crate::bitcoin::WalletBuilder;
#[tokio::test]
async fn given_bob_sends_good_psbt_when_reconstructing_then_succeeeds() {
let (A, B) = alice_and_bob();
let wallet = WalletBuilder::new(50_000).build();
let wallet = Wallet::new_funded_default_fees(50000);
let agreed_amount = Amount::from_sat(10000);
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
@ -202,7 +203,7 @@ mod tests {
let (A, B) = alice_and_bob();
let fees = 610;
let agreed_amount = Amount::from_sat(10000);
let wallet = WalletBuilder::new(agreed_amount.as_sat() + fees).build();
let wallet = Wallet::new_funded_default_fees(agreed_amount.as_sat() + fees);
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
assert_eq!(
@ -218,7 +219,7 @@ mod tests {
#[tokio::test]
async fn given_bob_is_sending_less_than_agreed_when_reconstructing_txlock_then_fails() {
let (A, B) = alice_and_bob();
let wallet = WalletBuilder::new(50_000).build();
let wallet = Wallet::new_funded_default_fees(50000);
let agreed_amount = Amount::from_sat(10000);
let bad_amount = Amount::from_sat(5000);
@ -231,7 +232,7 @@ mod tests {
#[tokio::test]
async fn given_bob_is_sending_to_a_bad_output_reconstructing_txlock_then_fails() {
let (A, B) = alice_and_bob();
let wallet = WalletBuilder::new(50_000).build();
let wallet = Wallet::new_funded_default_fees(50000);
let agreed_amount = Amount::from_sat(10000);
let E = eve();
@ -241,17 +242,6 @@ mod tests {
result.expect_err("PSBT to be invalid");
}
proptest::proptest! {
#[test]
fn estimated_tx_lock_script_size_never_changes(a in crate::proptest::ecdsa_fun::point(), b in crate::proptest::ecdsa_fun::point()) {
proptest::prop_assume!(a != b);
let computed_size = build_shared_output_descriptor(a, b).script_pubkey().len();
assert_eq!(computed_size, SCRIPT_SIZE);
}
}
/// Helper function that represents Bob's action of constructing the PSBT.
///
/// Extracting this allows us to keep the tests concise.
@ -261,11 +251,7 @@ mod tests {
wallet: &Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate>,
amount: Amount,
) -> PartiallySignedTransaction {
let change = wallet.new_address().await.unwrap();
TxLock::new(&wallet, amount, A, B, change)
.await
.unwrap()
.into()
TxLock::new(&wallet, amount, A, B).await.unwrap().into()
}
fn alice_and_bob() -> (PublicKey, PublicKey) {

@ -16,6 +16,7 @@ use reqwest::Url;
use rust_decimal::prelude::*;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
use std::fmt;
@ -144,12 +145,16 @@ impl Wallet {
let new_status = match client.lock().await.status_of_script(&tx) {
Ok(new_status) => new_status,
Err(error) => {
tracing::warn!(%txid, "Failed to get status of script: {:#}", error);
tracing::warn!(%txid, "Failed to get status of script. Error {:#}", error);
return;
}
};
last_status = Some(print_status_change(txid, last_status, new_status));
if Some(new_status) != last_status {
tracing::debug!(%txid, status = %new_status, "Transaction");
}
last_status = Some(new_status);
let all_receivers_gone = sender.send(new_status).is_err();
@ -173,20 +178,6 @@ impl Wallet {
}
}
fn print_status_change(txid: Txid, old: Option<ScriptStatus>, new: ScriptStatus) -> ScriptStatus {
match (old, new) {
(None, new_status) => {
tracing::debug!(%txid, status = %new_status, "Found relevant Bitcoin transaction");
}
(Some(old_status), new_status) if old_status != new_status => {
tracing::debug!(%txid, %new_status, %old_status, "Bitcoin transaction status changed");
}
_ => {}
}
new
}
/// Represents a subscription to the status of a given transaction.
#[derive(Debug, Clone)]
pub struct Subscription {
@ -306,8 +297,7 @@ where
.iter()
.find(|tx| tx.txid == txid)
.context("Could not find tx in bdk wallet when trying to determine fees")?
.fee
.expect("fees are always present with Electrum backend");
.fees;
Ok(Amount::from_sat(fees))
}
@ -320,18 +310,11 @@ where
&self,
address: Address,
amount: Amount,
change_override: Option<Address>,
) -> Result<PartiallySignedTransaction> {
if self.network != address.network {
bail!("Cannot build PSBT because network of given address is {} but wallet is on network {}", address.network, self.network);
}
if let Some(change) = change_override.as_ref() {
if self.network != change.network {
bail!("Cannot build PSBT because network of given address is {} but wallet is on network {}", change.network, self.network);
}
}
let wallet = self.wallet.lock().await;
let client = self.client.lock().await;
let fee_rate = client.estimate_feerate(self.target_block)?;
@ -343,31 +326,22 @@ where
let (psbt, _details) = tx_builder.finish()?;
let mut psbt: PartiallySignedTransaction = psbt;
match psbt.global.unsigned_tx.output.as_mut_slice() {
// our primary output is the 2nd one? reverse the vectors
[_, second_txout] if second_txout.script_pubkey == script => {
psbt.outputs.reverse();
psbt.global.unsigned_tx.output.reverse();
}
[first_txout, _] if first_txout.script_pubkey == script => {
// no need to do anything
// When subscribing to transactions we depend on the relevant script being at
// output index 0, thus we ensure the relevant output to be at index `0`.
psbt.outputs.sort_by(|a, _| {
if a.witness_script.as_ref() == Some(&script) {
Ordering::Less
} else {
Ordering::Greater
}
[_] => {
// single output, no need do anything
});
psbt.global.unsigned_tx.output.sort_by(|a, _| {
if a.script_pubkey == script {
Ordering::Less
} else {
Ordering::Greater
}
_ => bail!("Unexpected transaction layout"),
}
if let ([_, change], [_, psbt_output], Some(change_override)) = (
&mut psbt.global.unsigned_tx.output.as_mut_slice(),
&mut psbt.outputs.as_mut_slice(),
change_override,
) {
change.script_pubkey = change_override.script_pubkey();
// Might be populated based on the previously set change address, but for the
// overwrite we don't know unless we ask the user for more information.
psbt_output.bip32_derivation.clear();
}
});
Ok(psbt)
}
@ -395,16 +369,14 @@ where
let mut tx_builder = wallet.build_tx();
let dummy_script = Script::from(vec![0u8; locking_script_size]);
tx_builder.drain_to(dummy_script);
tx_builder.set_single_recipient(dummy_script);
tx_builder.drain_wallet();
tx_builder.fee_rate(fee_rate);
let response = tx_builder.finish();
match response {
Ok((_, details)) => {
let max_giveable = details.sent
- details
.fee
.expect("fees are always present with Electrum backend");
let max_giveable = details.sent - details.fees;
Ok(Amount::from_sat(max_giveable))
}
Err(bdk::Error::InsufficientFunds { .. }) => Ok(Amount::ZERO),
@ -422,7 +394,9 @@ where
) -> Result<bitcoin::Amount> {
let client = self.client.lock().await;
let fee_rate = client.estimate_feerate(self.target_block)?;
let min_relay_fee = client.min_relay_fee()?;
tracing::debug!("Min relay fee: {}", min_relay_fee);
estimate_fee(weight, transfer_amount, fee_rate, min_relay_fee)
}
@ -454,21 +428,20 @@ fn estimate_fee(
let weight = Decimal::from(weight);
let weight_factor = dec!(4.0);
let fee_rate = Decimal::from_f32(fee_rate_svb).context("Failed to parse fee rate")?;
let fee_rate = Decimal::from_f32(fee_rate_svb).context("Could not parse fee_rate.")?;
let sats_per_vbyte = weight / weight_factor * fee_rate;
tracing::debug!(
%weight,
%fee_rate,
%sats_per_vbyte,
"Estimated fee for transaction",
"Estimated fee for weight: {} for fee_rate: {:?} is in total: {}",
weight,
fee_rate,
sats_per_vbyte
);
let transfer_amount = Decimal::from(transfer_amount.as_sat());
let max_allowed_fee = transfer_amount * MAX_RELATIVE_TX_FEE;
let min_relay_fee = Decimal::from(min_relay_fee.as_sat());
let min_relay_fee = Decimal::from(min_relay_fee.as_sat());
let recommended_fee = if sats_per_vbyte < min_relay_fee {
tracing::warn!(
"Estimated fee of {} is smaller than the min relay fee, defaulting to min relay fee {}",
@ -494,7 +467,6 @@ fn estimate_fee(
let amount = recommended_fee
.map(bitcoin::Amount::from_sat)
.context("Could not estimate tranasction fee.")?;
Ok(amount)
}
@ -550,84 +522,48 @@ impl EstimateFeeRate for StaticFeeRate {
}
#[cfg(test)]
pub struct WalletBuilder {
utxo_amount: u64,
sats_per_vb: f32,
min_relay_fee_sats: u64,
key: bitcoin::util::bip32::ExtendedPrivKey,
num_utxos: u8,
}
#[cfg(test)]
impl WalletBuilder {
impl Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate> {
/// Creates a new, funded wallet with sane default fees.
///
/// Unless you are testing things related to fees, this is likely what you
/// want.
pub fn new(amount: u64) -> Self {
WalletBuilder {
utxo_amount: amount,
sats_per_vb: 1.0,
min_relay_fee_sats: 1000,
key: "tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m".parse().unwrap(),
num_utxos: 1,
}
}
pub fn with_zero_fees(self) -> Self {
Self {
sats_per_vb: 0.0,
min_relay_fee_sats: 0,
..self
}
}
pub fn with_fees(self, sats_per_vb: f32, min_relay_fee_sats: u64) -> Self {
Self {
sats_per_vb,
min_relay_fee_sats,
..self
}
}
pub fn with_key(self, key: bitcoin::util::bip32::ExtendedPrivKey) -> Self {
Self { key, ..self }
pub fn new_funded_default_fees(amount: u64) -> Self {
Self::new_funded(amount, 1.0, 1000)
}
pub fn with_num_utxos(self, number: u8) -> Self {
Self {
num_utxos: number,
..self
}
/// Creates a new, funded wallet that doesn't pay any fees.
///
/// This will create invalid transactions but can be useful if you want full
/// control over the output amounts.
pub fn new_funded_zero_fees(amount: u64) -> Self {
Self::new_funded(amount, 0.0, 0)
}
pub fn build(self) -> Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate> {
/// Creates a new, funded wallet to be used within tests.
pub fn new_funded(amount: u64, sats_per_vb: f32, min_relay_fee_sats: u64) -> Self {
use bdk::database::MemoryDatabase;
use bdk::{ConfirmationTime, LocalUtxo, TransactionDetails};
use bdk::{LocalUtxo, TransactionDetails};
use bitcoin::OutPoint;
use testutils::testutils;
let descriptors = testutils!(@descriptors (&format!("wpkh({}/*)", self.key)));
let descriptors = testutils!(@descriptors ("wpkh(tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m/*)"));
let mut database = MemoryDatabase::new();
for index in 0..self.num_utxos {
bdk::populate_test_db!(
&mut database,
testutils! {
@tx ( (@external descriptors, index as u32) => self.utxo_amount ) (@confirmations 1)
},
Some(100)
);
}
bdk::populate_test_db!(
&mut database,
testutils! {
@tx ( (@external descriptors, 0) => amount ) (@confirmations 1)
},
Some(100)
);
let wallet =
bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database).unwrap();
Wallet {
Self {
client: Arc::new(Mutex::new(StaticFeeRate {
fee_rate: FeeRate::from_sat_per_vb(self.sats_per_vb),
min_relay_fee: bitcoin::Amount::from_sat(self.min_relay_fee_sats),
fee_rate: FeeRate::from_sat_per_vb(sats_per_vb),
min_relay_fee: bitcoin::Amount::from_sat(min_relay_fee_sats),
})),
wallet: Arc::new(Mutex::new(wallet)),
finality_confirmations: 1,
@ -894,9 +830,7 @@ impl fmt::Display for ScriptStatus {
mod tests {
use super::*;
use crate::bitcoin::{PublicKey, TxLock};
use crate::tracing_ext::capture_logs;
use proptest::prelude::*;
use tracing::level_filters::LevelFilter;
#[test]
fn given_depth_0_should_meet_confirmation_target_one() {
@ -1086,7 +1020,7 @@ mod tests {
#[tokio::test]
async fn given_no_balance_returns_amount_0() {
let wallet = WalletBuilder::new(0).with_fees(1.0, 1).build();
let wallet = Wallet::new_funded(0, 1.0, 1);
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
assert_eq!(amount, Amount::ZERO);
@ -1094,7 +1028,7 @@ mod tests {
#[tokio::test]
async fn given_balance_below_min_relay_fee_returns_amount_0() {
let wallet = WalletBuilder::new(1000).with_fees(1.0, 1001).build();
let wallet = Wallet::new_funded(1000, 1.0, 1001);
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
assert_eq!(amount, Amount::ZERO);
@ -1102,7 +1036,7 @@ mod tests {
#[tokio::test]
async fn given_balance_above_relay_fee_returns_amount_greater_0() {
let wallet = WalletBuilder::new(10_000).build();
let wallet = Wallet::new_funded_default_fees(10_000);
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
assert!(amount.as_sat() > 0);
@ -1122,14 +1056,13 @@ mod tests {
let balance = 2000;
// We don't care about fees in this test, thus use a zero fee rate
let wallet = WalletBuilder::new(balance).with_zero_fees().build();
let wallet = Wallet::new_funded_zero_fees(balance);
// sorting is only relevant for amounts that have a change output
// if the change output is below dust it will be dropped by the BDK
for amount in above_dust..(balance - (above_dust - 1)) {
let (A, B) = (PublicKey::random(), PublicKey::random());
let change = wallet.new_address().await.unwrap();
let txlock = TxLock::new(&wallet, bitcoin::Amount::from_sat(amount), A, B, change)
let txlock = TxLock::new(&wallet, bitcoin::Amount::from_sat(amount), A, B)
.await
.unwrap();
let txlock_output = txlock.script_pubkey();
@ -1144,81 +1077,4 @@ mod tests {
);
}
}
#[tokio::test]
async fn can_override_change_address() {
let wallet = WalletBuilder::new(50_000).build();
let custom_change = "bcrt1q08pfqpsyrt7acllzyjm8q5qsz5capvyahm49rw"
.parse::<Address>()
.unwrap();
let psbt = wallet
.send_to_address(
wallet.new_address().await.unwrap(),
Amount::from_sat(10_000),
Some(custom_change.clone()),
)
.await
.unwrap();
let transaction = wallet.sign_and_finalize(psbt).await.unwrap();
match transaction.output.as_slice() {
[first, change] => {
assert_eq!(first.value, 10_000);
assert_eq!(change.script_pubkey, custom_change.script_pubkey());
}
_ => panic!("expected exactly two outputs"),
}
}
#[test]
fn printing_status_change_doesnt_spam_on_same_status() {
let writer = capture_logs(LevelFilter::DEBUG);
let tx = Txid::default();
let mut old = None;
old = Some(print_status_change(tx, old, ScriptStatus::Unseen));
old = Some(print_status_change(tx, old, ScriptStatus::InMempool));
old = Some(print_status_change(tx, old, ScriptStatus::InMempool));
old = Some(print_status_change(tx, old, ScriptStatus::InMempool));
old = Some(print_status_change(tx, old, ScriptStatus::InMempool));
old = Some(print_status_change(tx, old, ScriptStatus::InMempool));
old = Some(print_status_change(tx, old, ScriptStatus::InMempool));
old = Some(print_status_change(tx, old, confs(1)));
old = Some(print_status_change(tx, old, confs(2)));
old = Some(print_status_change(tx, old, confs(3)));
old = Some(print_status_change(tx, old, confs(3)));
print_status_change(tx, old, confs(3));
assert_eq!(
writer.captured(),
r"DEBUG swap::bitcoin::wallet: Found relevant Bitcoin transaction txid=0000000000000000000000000000000000000000000000000000000000000000 status=unseen
DEBUG swap::bitcoin::wallet: Bitcoin transaction status changed txid=0000000000000000000000000000000000000000000000000000000000000000 new_status=in mempool old_status=unseen
DEBUG swap::bitcoin::wallet: Bitcoin transaction status changed txid=0000000000000000000000000000000000000000000000000000000000000000 new_status=confirmed with 1 blocks old_status=in mempool
DEBUG swap::bitcoin::wallet: Bitcoin transaction status changed txid=0000000000000000000000000000000000000000000000000000000000000000 new_status=confirmed with 2 blocks old_status=confirmed with 1 blocks
DEBUG swap::bitcoin::wallet: Bitcoin transaction status changed txid=0000000000000000000000000000000000000000000000000000000000000000 new_status=confirmed with 3 blocks old_status=confirmed with 2 blocks
"
)
}
fn confs(confirmations: u32) -> ScriptStatus {
ScriptStatus::from_confirmations(confirmations)
}
proptest::proptest! {
#[test]
fn funding_never_fails_with_insufficient_funds(funding_amount in 3000u32.., num_utxos in 1..5u8, sats_per_vb in 1.0..500.0f32, key in crate::proptest::bitcoin::extended_priv_key(), alice in crate::proptest::ecdsa_fun::point(), bob in crate::proptest::ecdsa_fun::point()) {
proptest::prop_assume!(alice != bob);
tokio::runtime::Runtime::new().unwrap().block_on(async move {
let wallet = WalletBuilder::new(funding_amount as u64).with_key(key).with_num_utxos(num_utxos).with_fees(sats_per_vb, 1000).build();
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
let psbt: PartiallySignedTransaction = TxLock::new(&wallet, amount, PublicKey::from(alice), PublicKey::from(bob), wallet.new_address().await.unwrap()).await.unwrap().into();
let result = wallet.sign_and_finalize(psbt).await;
result.expect("transaction to be signed");
});
}
}
}

@ -2,7 +2,6 @@ mod behaviour;
pub mod cancel;
pub mod command;
mod event_loop;
mod list_sellers;
pub mod refund;
pub mod tracing;
pub mod transport;
@ -10,170 +9,4 @@ pub mod transport;
pub use behaviour::{Behaviour, OutEvent};
pub use cancel::cancel;
pub use event_loop::{EventLoop, EventLoopHandle};
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, Status};
use crate::network::quote;
use crate::network::quote::BidQuote;
use crate::network::rendezvous::XmrBtcNamespace;
use crate::network::test::{new_swarm, SwarmExt};
use futures::StreamExt;
use libp2p::multiaddr::Protocol;
use libp2p::request_response::RequestResponseEvent;
use libp2p::swarm::{AddressScore, NetworkBehaviourEventProcess};
use libp2p::{identity, Multiaddr, PeerId};
use std::collections::HashSet;
use std::iter::FromIterator;
use std::time::Duration;
#[tokio::test]
async fn list_sellers_should_report_all_registered_asbs_with_a_quote() {
let namespace = XmrBtcNamespace::Mainnet;
let (rendezvous_address, rendezvous_peer_id) = setup_rendezvous_point().await;
let expected_seller_1 =
setup_asb(rendezvous_peer_id, rendezvous_address.clone(), namespace).await;
let expected_seller_2 =
setup_asb(rendezvous_peer_id, rendezvous_address.clone(), namespace).await;
let list_sellers = list_sellers(
rendezvous_peer_id,
rendezvous_address,
namespace,
0,
identity::Keypair::generate_ed25519(),
);
let sellers = tokio::time::timeout(Duration::from_secs(15), list_sellers)
.await
.unwrap()
.unwrap();
assert_eq!(
HashSet::<Seller>::from_iter(sellers),
HashSet::<Seller>::from_iter([expected_seller_1, expected_seller_2])
)
}
async fn setup_rendezvous_point() -> (Multiaddr, PeerId) {
let mut rendezvous_node = new_swarm(|_, identity| RendezvousPointBehaviour {
rendezvous: libp2p::rendezvous::Rendezvous::new(
identity,
libp2p::rendezvous::Config::default(),
),
ping: Default::default(),
});
let rendezvous_address = rendezvous_node.listen_on_tcp_localhost().await;
let rendezvous_peer_id = *rendezvous_node.local_peer_id();
tokio::spawn(async move {
loop {
rendezvous_node.next().await;
}
});
(rendezvous_address, rendezvous_peer_id)
}
async fn setup_asb(
rendezvous_peer_id: PeerId,
rendezvous_address: Multiaddr,
namespace: XmrBtcNamespace,
) -> Seller {
let static_quote = BidQuote {
price: bitcoin::Amount::from_sat(1337),
min_quantity: bitcoin::Amount::from_sat(42),
max_quantity: bitcoin::Amount::from_sat(9001),
};
let mut asb = new_swarm(|_, identity| StaticQuoteAsbBehaviour {
rendezvous: asb::rendezous::Behaviour::new(
identity,
rendezvous_peer_id,
rendezvous_address,
namespace,
None,
),
ping: Default::default(),
quote: quote::asb(),
static_quote,
registered: false,
});
let asb_address = asb.listen_on_tcp_localhost().await;
asb.add_external_address(asb_address.clone(), AddressScore::Infinite);
let asb_peer_id = *asb.local_peer_id();
// avoid race condition where `list_sellers` tries to discover before we are
// registered block this function until we are registered
while !asb.behaviour().registered {
asb.next().await;
}
tokio::spawn(async move {
loop {
asb.next().await;
}
});
Seller {
multiaddr: asb_address.with(Protocol::P2p(asb_peer_id.into())),
status: Status::Online(static_quote),
}
}
#[derive(libp2p::NetworkBehaviour)]
struct StaticQuoteAsbBehaviour {
rendezvous: asb::rendezous::Behaviour,
// Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed.
ping: libp2p::ping::Ping,
quote: quote::Behaviour,
#[behaviour(ignore)]
static_quote: BidQuote,
#[behaviour(ignore)]
registered: bool,
}
impl NetworkBehaviourEventProcess<libp2p::rendezvous::Event> for StaticQuoteAsbBehaviour {
fn inject_event(&mut self, event: libp2p::rendezvous::Event) {
if let libp2p::rendezvous::Event::Registered { .. } = event {
self.registered = true;
}
}
}
impl NetworkBehaviourEventProcess<libp2p::ping::PingEvent> for StaticQuoteAsbBehaviour {
fn inject_event(&mut self, _: libp2p::ping::PingEvent) {}
}
impl NetworkBehaviourEventProcess<quote::OutEvent> for StaticQuoteAsbBehaviour {
fn inject_event(&mut self, event: quote::OutEvent) {
if let RequestResponseEvent::Message {
message: quote::Message::Request { channel, .. },
..
} = event
{
self.quote
.send_response(channel, self.static_quote)
.unwrap();
}
}
}
#[derive(libp2p::NetworkBehaviour)]
struct RendezvousPointBehaviour {
rendezvous: libp2p::rendezvous::Rendezvous,
// Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed.
ping: libp2p::ping::Ping,
}
impl NetworkBehaviourEventProcess<libp2p::rendezvous::Event> for RendezvousPointBehaviour {
fn inject_event(&mut self, _: libp2p::rendezvous::Event) {}
}
impl NetworkBehaviourEventProcess<libp2p::ping::PingEvent> for RendezvousPointBehaviour {
fn inject_event(&mut self, _: libp2p::ping::PingEvent) {}
}
}

@ -5,7 +5,7 @@ use crate::protocol::bob::State2;
use crate::{bitcoin, env};
use anyhow::{anyhow, Error, Result};
use libp2p::core::Multiaddr;
use libp2p::ping::{Ping, PingConfig, PingEvent};
use libp2p::ping::{Ping, PingEvent};
use libp2p::request_response::{RequestId, ResponseChannel};
use libp2p::{NetworkBehaviour, PeerId};
use std::sync::Arc;
@ -83,7 +83,7 @@ impl Behaviour {
transfer_proof: transfer_proof::bob(),
encrypted_signature: encrypted_signature::bob(),
redial: redial::Behaviour::new(alice, Duration::from_secs(2)),
ping: Ping::new(PingConfig::new().with_keep_alive(true)),
ping: Ping::default(),
}
}

File diff suppressed because it is too large Load Diff

@ -94,7 +94,7 @@ impl EventLoop {
loop {
// Note: We are making very elaborate use of `select!` macro's feature here. Make sure to read the documentation thoroughly: https://docs.rs/tokio/1.4.0/tokio/macro.select.html
tokio::select! {
swarm_event = self.swarm.select_next_some() => {
swarm_event = self.swarm.next_event().fuse() => {
match swarm_event {
SwarmEvent::Behaviour(OutEvent::QuoteReceived { id, response }) => {
if let Some(responder) = self.inflight_quote_requests.remove(&id) {

@ -1,365 +0,0 @@
use crate::network::quote::BidQuote;
use crate::network::rendezvous::XmrBtcNamespace;
use crate::network::{quote, swarm};
use anyhow::{Context, Result};
use futures::StreamExt;
use libp2p::multiaddr::Protocol;
use libp2p::ping::{Ping, PingConfig, PingEvent};
use libp2p::rendezvous::{Namespace, Rendezvous};
use libp2p::request_response::{RequestResponseEvent, RequestResponseMessage};
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;
/// Returns sorted list of sellers, with [Online](Status::Online) listed first.
///
/// First uses the rendezvous node to discover peers in the given namespace,
/// then fetches a quote from each peer that was discovered. If fetching a quote
/// from a discovered peer fails the seller's status will be
/// [Unreachable](Status::Unreachable).
pub async fn list_sellers(
rendezvous_node_peer_id: PeerId,
rendezvous_node_addr: Multiaddr,
namespace: XmrBtcNamespace,
tor_socks5_port: u16,
identity: identity::Keypair,
) -> Result<Vec<Seller>> {
let behaviour = Behaviour {
rendezvous: Rendezvous::new(identity.clone(), rendezvous::Config::default()),
quote: quote::cli(),
ping: Ping::new(
PingConfig::new()
.with_keep_alive(false)
.with_interval(Duration::from_secs(86_400)),
),
};
let mut swarm = swarm::cli(identity, tor_socks5_port, behaviour).await?;
swarm
.behaviour_mut()
.quote
.add_address(&rendezvous_node_peer_id, rendezvous_node_addr.clone());
swarm
.dial(&rendezvous_node_peer_id)
.context("Failed to dial rendezvous node")?;
let event_loop = EventLoop::new(
swarm,
rendezvous_node_peer_id,
rendezvous_node_addr,
namespace,
);
let sellers = event_loop.run().await;
Ok(sellers)
}
#[serde_as]
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Seller {
pub status: Status,
#[serde_as(as = "DisplayFromStr")]
pub multiaddr: Multiaddr,
}
#[derive(Debug, Serialize, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)]
pub enum Status {
Online(BidQuote),
Unreachable,
}
#[derive(Debug)]
enum OutEvent {
Rendezvous(rendezvous::Event),
Quote(quote::OutEvent),
Ping(PingEvent),
}
impl From<rendezvous::Event> for OutEvent {
fn from(event: rendezvous::Event) -> Self {
OutEvent::Rendezvous(event)
}
}
impl From<quote::OutEvent> for OutEvent {
fn from(event: quote::OutEvent) -> Self {
OutEvent::Quote(event)
}
}
#[derive(libp2p::NetworkBehaviour)]
#[behaviour(event_process = false)]
#[behaviour(out_event = "OutEvent")]
struct Behaviour {
rendezvous: Rendezvous,
quote: quote::Behaviour,
ping: Ping,
}
#[derive(Debug)]
enum QuoteStatus {
Pending,
Received(Status),
}
#[derive(Debug)]
enum State {
WaitForDiscovery,
WaitForQuoteCompletion,
}
struct EventLoop {
swarm: Swarm<Behaviour>,
rendezvous_peer_id: PeerId,
rendezvous_addr: Multiaddr,
namespace: XmrBtcNamespace,
reachable_asb_address: HashMap<PeerId, Multiaddr>,
unreachable_asb_address: HashMap<PeerId, Multiaddr>,
asb_quote_status: HashMap<PeerId, QuoteStatus>,
state: State,
}
impl EventLoop {
fn new(
swarm: Swarm<Behaviour>,
rendezvous_peer_id: PeerId,
rendezvous_addr: Multiaddr,
namespace: XmrBtcNamespace,
) -> Self {
Self {
swarm,
rendezvous_peer_id,
rendezvous_addr,
namespace,
reachable_asb_address: Default::default(),
unreachable_asb_address: Default::default(),
asb_quote_status: Default::default(),
state: State::WaitForDiscovery,
}
}
async fn run(mut self) -> Vec<Seller> {
loop {
tokio::select! {
swarm_event = self.swarm.select_next_some() => {
match swarm_event {
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } => {
if peer_id == self.rendezvous_peer_id{
tracing::info!(
"Connected to rendezvous point, discovering nodes in '{}' namespace ...",
self.namespace
);
self.swarm.behaviour_mut().rendezvous.discover(
Some(Namespace::new(self.namespace.to_string()).expect("our namespace to be a correct string")),
None,
None,
self.rendezvous_peer_id,
);
} else {
let address = endpoint.get_remote_address();
self.reachable_asb_address.insert(peer_id, address.clone());
}
}
SwarmEvent::UnreachableAddr { peer_id, error, address, .. } => {
if address == self.rendezvous_addr {
tracing::error!(
"Failed to connect to rendezvous point at {}: {}",
address,
error
);
// if the rendezvous node is unreachable we just stop
return Vec::new();
} else {
tracing::debug!(
"Failed to connect to peer at {}: {}",
address,
error
);
self.unreachable_asb_address.insert(peer_id, address.clone());
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(
rendezvous::Event::Discovered { registrations, .. },
)) => {
self.state = State::WaitForQuoteCompletion;
for registration in registrations {
let peer = registration.record.peer_id();
for address in registration.record.addresses() {
tracing::info!("Discovered peer {} at {}", peer, address);
let p2p_suffix = Protocol::P2p(*peer.as_ref());
let _address_with_p2p = if !address
.ends_with(&Multiaddr::empty().with(p2p_suffix.clone()))
{
address.clone().with(p2p_suffix)
} else {
address.clone()
};
self.asb_quote_status.insert(peer, QuoteStatus::Pending);
// add all external addresses of that peer to the quote behaviour
self.swarm.behaviour_mut().quote.add_address(&peer, address.clone());
}
// request the quote, if we are not connected to the peer it will be dialed automatically
let _request_id = self.swarm.behaviour_mut().quote.send_request(&peer, ());
}
}
SwarmEvent::Behaviour(OutEvent::Quote(quote_response)) => {
match quote_response {
RequestResponseEvent::Message { peer, message } => {
match message {
RequestResponseMessage::Response { response, .. } => {
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);
}
}
RequestResponseMessage::Request { .. } => unreachable!()
}
}
RequestResponseEvent::OutboundFailure { peer, error, .. } => {
if peer == self.rendezvous_peer_id {
tracing::debug!(%peer, "Outbound failure when communicating with rendezvous node: {:#}", error);
} else {
tracing::debug!(%peer, "Ignoring seller, because unable to request quote: {:#}", error);
self.asb_quote_status.remove(&peer);
}
}
RequestResponseEvent::InboundFailure { peer, error, .. } => {
if peer == self.rendezvous_peer_id {
tracing::debug!(%peer, "Inbound failure when communicating with rendezvous node: {:#}", error);
} else {
tracing::debug!(%peer, "Ignoring seller, because unable to request quote: {:#}", error);
self.asb_quote_status.remove(&peer);
}
},
RequestResponseEvent::ResponseSent { .. } => unreachable!()
}
}
_ => {}
}
}
}
match self.state {
State::WaitForDiscovery => {
continue;
}
State::WaitForQuoteCompletion => {
let all_quotes_fetched = self
.asb_quote_status
.iter()
.map(|(peer_id, quote_status)| match quote_status {
QuoteStatus::Pending => Err(StillPending {}),
QuoteStatus::Received(Status::Online(quote)) => {
let address = self
.reachable_asb_address
.get(&peer_id)
.expect("if we got a quote we must have stored an address");
Ok(Seller {
multiaddr: address.clone(),
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,
})
}
})
.collect::<Result<Vec<_>, _>>();
match all_quotes_fetched {
Ok(mut sellers) => {
sellers.sort();
break sellers;
}
Err(StillPending {}) => continue,
}
}
}
}
}
}
#[derive(Debug)]
struct StillPending {}
impl From<PingEvent> for OutEvent {
fn from(event: PingEvent) -> Self {
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
},
])
}
}

@ -1,5 +1,4 @@
use anyhow::Result;
use std::option::Option::Some;
use std::path::Path;
use tracing::subscriber::set_global_default;
use tracing::{Event, Level, Subscriber};
@ -9,7 +8,7 @@ use tracing_subscriber::layer::{Context, SubscriberExt};
use tracing_subscriber::{fmt, EnvFilter, FmtSubscriber, Layer, Registry};
use uuid::Uuid;
pub fn init(debug: bool, json: bool, dir: impl AsRef<Path>, swap_id: Option<Uuid>) -> Result<()> {
pub fn init(debug: bool, json: bool, dir: impl AsRef<Path>, swap_id: Uuid) -> Result<()> {
if json {
let level = if debug { Level::DEBUG } else { Level::INFO };
@ -25,7 +24,7 @@ pub fn init(debug: bool, json: bool, dir: impl AsRef<Path>, swap_id: Option<Uuid
.init();
Ok(())
} else if let Some(swap_id) = swap_id {
} else {
let level_filter = EnvFilter::try_new("swap=debug")?;
let registry = Registry::default().with(level_filter);
@ -46,20 +45,6 @@ pub fn init(debug: bool, json: bool, dir: impl AsRef<Path>, swap_id: Option<Uuid
set_global_default(registry.with(file_logger).with(info_terminal_printer()))?;
}
Ok(())
} else {
let level = if debug { Level::DEBUG } else { Level::INFO };
let is_terminal = atty::is(atty::Stream::Stderr);
FmtSubscriber::builder()
.with_env_filter(format!("swap={}", level))
.with_writer(std::io::stderr)
.with_ansi(is_terminal)
.with_level(false)
.without_time()
.with_target(false)
.init();
Ok(())
}
}

@ -3,7 +3,7 @@ pub use bob::Bob;
use anyhow::{anyhow, bail, Context, Result};
use itertools::Itertools;
use libp2p::{Multiaddr, PeerId};
use libp2p::PeerId;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
@ -68,8 +68,6 @@ impl Swap {
pub struct Database {
swaps: sled::Tree,
peers: sled::Tree,
addresses: sled::Tree,
monero_addresses: sled::Tree,
}
impl Database {
@ -81,15 +79,8 @@ impl Database {
let swaps = db.open_tree("swaps")?;
let peers = db.open_tree("peers")?;
let addresses = db.open_tree("addresses")?;
let monero_addresses = db.open_tree("monero_addresses")?;
Ok(Database {
swaps,
peers,
addresses,
monero_addresses,
})
Ok(Database { swaps, peers })
}
pub async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()> {
@ -119,79 +110,6 @@ impl Database {
Ok(PeerId::from_str(peer_id.as_str())?)
}
pub async fn insert_monero_address(
&self,
swap_id: Uuid,
address: monero::Address,
) -> Result<()> {
let key = swap_id.as_bytes();
let value = serialize(&address)?;
self.monero_addresses.insert(key, value)?;
self.monero_addresses
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
pub fn get_monero_address(&self, swap_id: Uuid) -> Result<monero::Address> {
let encoded = self
.monero_addresses
.get(swap_id.as_bytes())?
.ok_or_else(|| {
anyhow!(
"No Monero address found for swap id {} in database",
swap_id
)
})?;
let monero_address = deserialize(&encoded)?;
Ok(monero_address)
}
pub async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()> {
let key = peer_id.to_bytes();
let existing_addresses = self.addresses.get(&key)?;
let new_addresses = {
let existing_addresses = existing_addresses.clone();
Some(match existing_addresses {
Some(encoded) => {
let mut addresses = deserialize::<Vec<Multiaddr>>(&encoded)?;
addresses.push(address);
serialize(&addresses)?
}
None => serialize(&[address])?,
})
};
self.addresses
.compare_and_swap(key, existing_addresses, new_addresses)??;
self.addresses
.flush_async()
.await
.map(|_| ())
.context("Could not flush db")
}
pub fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>> {
let key = peer_id.to_bytes();
let addresses = match self.addresses.get(&key)? {
Some(encoded) => deserialize(&encoded).context("Failed to deserialize addresses")?,
None => vec![],
};
Ok(addresses)
}
pub async fn insert_latest_state(&self, swap_id: Uuid, state: Swap) -> Result<()> {
let key = serialize(&swap_id)?;
let new_value = serialize(&state).context("Could not serialize new state value")?;
@ -441,37 +359,4 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn save_and_load_addresses() -> Result<()> {
let db_dir = tempfile::tempdir()?;
let peer_id = PeerId::random();
let home1 = "/ip4/127.0.0.1/tcp/1".parse::<Multiaddr>()?;
let home2 = "/ip4/127.0.0.1/tcp/2".parse::<Multiaddr>()?;
{
let db = Database::open(db_dir.path())?;
db.insert_address(peer_id, home1.clone()).await?;
db.insert_address(peer_id, home2.clone()).await?;
}
let addresses = Database::open(db_dir.path())?.get_addresses(peer_id)?;
assert_eq!(addresses, vec![home1, home2]);
Ok(())
}
#[tokio::test]
async fn save_and_load_monero_address() -> Result<()> {
let db_dir = tempfile::tempdir()?;
let swap_id = Uuid::new_v4();
Database::open(db_dir.path())?.insert_monero_address(swap_id, "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse()?).await?;
let loaded_monero_address = Database::open(db_dir.path())?.get_monero_address(swap_id)?;
assert_eq!(loaded_monero_address.to_string(), "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a");
Ok(())
}
}

@ -3,17 +3,13 @@ use crate::protocol::bob;
use crate::protocol::bob::BobState;
use monero_rpc::wallet::BlockHeight;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use std::fmt;
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum Bob {
Started {
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
btc_amount: bitcoin::Amount,
#[serde_as(as = "DisplayFromStr")]
change_address: bitcoin::Address,
},
ExecutionSetupDone {
state2: bob::State2,
@ -49,13 +45,7 @@ pub enum BobEndState {
impl From<BobState> for Bob {
fn from(bob_state: BobState) -> Self {
match bob_state {
BobState::Started {
btc_amount,
change_address,
} => Bob::Started {
btc_amount,
change_address,
},
BobState::Started { btc_amount } => Bob::Started { btc_amount },
BobState::SwapSetupCompleted(state2) => Bob::ExecutionSetupDone { state2 },
BobState::BtcLocked(state3) => Bob::BtcLocked { state3 },
BobState::XmrLockProofReceived {
@ -87,13 +77,7 @@ impl From<BobState> for Bob {
impl From<Bob> for BobState {
fn from(db_state: Bob) -> Self {
match db_state {
Bob::Started {
btc_amount,
change_address,
} => BobState::Started {
btc_amount,
change_address,
},
Bob::Started { btc_amount } => BobState::Started { btc_amount },
Bob::ExecutionSetupDone { state2 } => BobState::SwapSetupCompleted(state2),
Bob::BtcLocked { state3 } => BobState::BtcLocked(state3),
Bob::XmrLockProofReceived {

@ -5,16 +5,11 @@ use std::convert::{Infallible, TryFrom};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch;
use url::Url;
/// Connect to Kraken websocket API for a constant stream of rate updates.
///
/// If the connection fails, it will automatically be re-established.
///
/// price_ticker_ws_url must point to a websocket server that follows the kraken
/// price ticker protocol
/// See: https://docs.kraken.com/websockets/
pub fn connect(price_ticker_ws_url: Url) -> Result<PriceUpdates> {
pub fn connect() -> Result<PriceUpdates> {
let (price_update, price_update_receiver) = watch::channel(Err(Error::NotYetAvailable));
let price_update = Arc::new(price_update);
@ -31,9 +26,8 @@ pub fn connect(price_ticker_ws_url: Url) -> Result<PriceUpdates> {
backoff,
|| {
let price_update = price_update.clone();
let price_ticker_ws_url = price_ticker_ws_url.clone();
async move {
let mut stream = connection::new(price_ticker_ws_url).await?;
let mut stream = connection::new().await?;
while let Some(update) = stream.try_next().await.map_err(to_backoff)? {
let send_result = price_update.send(Ok(update));
@ -129,8 +123,8 @@ mod connection {
use futures::stream::BoxStream;
use tokio_tungstenite::tungstenite;
pub async fn new(ws_url: Url) -> Result<BoxStream<'static, Result<wire::PriceUpdate, Error>>> {
let (mut rate_stream, _) = tokio_tungstenite::connect_async(ws_url)
pub async fn new() -> Result<BoxStream<'static, Result<wire::PriceUpdate, Error>>> {
let (mut rate_stream, _) = tokio_tungstenite::connect_async("wss://ws.kraken.com")
.await
.context("Failed to connect to Kraken websocket API")?;

@ -23,15 +23,10 @@ pub mod database;
pub mod env;
pub mod fs;
pub mod kraken;
pub mod libp2p_ext;
pub mod monero;
pub mod network;
pub mod protocol;
pub mod seed;
pub mod tor;
pub mod tracing_ext;
mod monero_ext;
#[cfg(test)]
mod proptest;

@ -1,15 +0,0 @@
use libp2p::multiaddr::Protocol;
use libp2p::{Multiaddr, PeerId};
pub trait MultiAddrExt {
fn extract_peer_id(&self) -> Option<PeerId>;
}
impl MultiAddrExt for Multiaddr {
fn extract_peer_id(&self) -> Option<PeerId> {
match self.iter().last()? {
Protocol::P2p(multihash) => PeerId::from_multihash(multihash).ok(),
_ => None,
}
}
}

@ -157,12 +157,12 @@ impl Wallet {
Err(error) => {
tracing::warn!(
address = %self.main_address,
"Failed to transfer Monero to default wallet: {:#}", error
"Transferring Monero back to default wallet failed. Error {:#}", error
);
}
},
Err(error) => {
tracing::warn!("Failed to refresh generated wallet: {:#}", error);
tracing::warn!("Refreshing the generated wallet failed. Error {:#}", error);
}
}
@ -192,7 +192,7 @@ impl Wallet {
%amount,
to = %public_spend_key,
tx_id = %res.tx_hash,
"Successfully initiated Monero transfer"
"Sent transfer"
);
Ok(TransferProof::new(
@ -202,7 +202,7 @@ impl Wallet {
))
}
pub async fn watch_for_transfer(&self, request: WatchRequest) -> Result<(), InsufficientFunds> {
pub async fn watch_for_transfer(&self, request: WatchRequest) -> Result<()> {
let WatchRequest {
conf_target,
public_view_key,
@ -314,7 +314,7 @@ where
Err(error) => {
tracing::debug!(
%txid,
"Failed to retrieve tx from blockchain: {:#}", error
"Failed to retrieve tx from blockchain. Error {:#}", error
);
continue; // treating every error as transient and retrying
// is obviously wrong but the jsonrpc client is

@ -128,7 +128,7 @@ impl WalletRpc {
tracing::debug!(
%port,
"Starting monero-wallet-rpc"
"Starting monero-wallet-rpc on"
);
let network_flag = match network {

@ -1,11 +1,12 @@
mod impl_from_rr_event;
pub mod alice;
pub mod bob;
pub mod cbor_request_response;
pub mod encrypted_signature;
pub mod json_pull_codec;
pub mod quote;
pub mod redial;
pub mod rendezvous;
pub mod swap_setup;
pub mod swarm;
pub mod tor_transport;

@ -52,7 +52,7 @@ impl From<(PeerId, Message)> for asb::OutEvent {
Message::Request {
request, channel, ..
} => Self::EncryptedSignatureReceived {
msg: request,
msg: Box::new(request),
channel,
peer,
},

@ -9,8 +9,8 @@ use libp2p::PeerId;
use serde::{Deserialize, Serialize};
const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0";
pub type OutEvent = RequestResponseEvent<(), BidQuote>;
pub type Message = RequestResponseMessage<(), BidQuote>;
type OutEvent = RequestResponseEvent<(), BidQuote>;
type Message = RequestResponseMessage<(), BidQuote>;
pub type Behaviour = RequestResponse<JsonPullCodec<BidQuoteProtocol, BidQuote>>;
@ -24,7 +24,7 @@ impl ProtocolName for BidQuoteProtocol {
}
/// Represents a quote for buying XMR.
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BidQuote {
/// The price at which the maker is willing to buy at.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]

@ -1,29 +0,0 @@
use libp2p::rendezvous::Namespace;
use std::fmt;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum XmrBtcNamespace {
Mainnet,
Testnet,
}
const MAINNET: &str = "xmr-btc-swap-mainnet";
const TESTNET: &str = "xmr-btc-swap-testnet";
impl fmt::Display for XmrBtcNamespace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
XmrBtcNamespace::Mainnet => write!(f, "{}", MAINNET),
XmrBtcNamespace::Testnet => write!(f, "{}", TESTNET),
}
}
}
impl From<XmrBtcNamespace> for Namespace {
fn from(namespace: XmrBtcNamespace) -> Self {
match namespace {
XmrBtcNamespace::Mainnet => Namespace::from_static(MAINNET),
XmrBtcNamespace::Testnet => Namespace::from_static(TESTNET),
}
}
}

@ -8,7 +8,7 @@ use crate::protocol::{Message0, Message2, Message4};
use crate::{asb, bitcoin, env, monero};
use anyhow::{anyhow, Context, Result};
use futures::future::{BoxFuture, OptionFuture};
use futures::{AsyncWriteExt, FutureExt};
use futures::FutureExt;
use libp2p::core::connection::ConnectionId;
use libp2p::core::upgrade;
use libp2p::swarm::{
@ -96,7 +96,7 @@ impl From<OutEvent> for asb::OutEvent {
} => asb::OutEvent::SwapSetupCompleted {
peer_id: bob_peer_id,
swap_id,
state3,
state3: Box::new(state3),
},
OutEvent::Error { peer_id, error } => asb::OutEvent::Failure {
peer: peer_id,
@ -227,8 +227,8 @@ impl<LR> Handler<LR> {
env_config,
latest_rate,
resume_only,
timeout: Duration::from_secs(120),
keep_alive: KeepAlive::Until(Instant::now() + Duration::from_secs(10)),
timeout: Duration::from_secs(60),
keep_alive: KeepAlive::Until(Instant::now() + Duration::from_secs(5)),
}
}
}
@ -383,15 +383,6 @@ where
.receive(message4)
.context("Failed to transition state2 -> state3 using message4")?;
substream
.flush()
.await
.context("Failed to flush substream after all messages were sent")?;
substream
.close()
.await
.context("Failed to close substream after all messages were sent")?;
Ok((swap_id, state3))
});
@ -445,6 +436,7 @@ where
}
if let Some(result) = futures::ready!(self.inbound_stream.poll_unpin(cx)) {
self.keep_alive = KeepAlive::No;
self.inbound_stream = OptionFuture::from(None);
return Poll::Ready(ProtocolsHandlerEvent::Custom(HandlerOutEvent::Completed(
result,

@ -7,7 +7,7 @@ use crate::protocol::{Message1, Message3};
use crate::{bitcoin, cli, env, monero};
use anyhow::Result;
use futures::future::{BoxFuture, OptionFuture};
use futures::{AsyncWriteExt, FutureExt};
use futures::FutureExt;
use libp2p::core::connection::ConnectionId;
use libp2p::core::upgrade;
use libp2p::swarm::{
@ -19,7 +19,7 @@ use libp2p::{Multiaddr, PeerId};
use std::collections::VecDeque;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use std::time::{Duration, Instant};
use uuid::Uuid;
use void::Void;
@ -109,10 +109,10 @@ impl Handler {
Self {
env_config,
outbound_stream: OptionFuture::from(None),
timeout: Duration::from_secs(120),
timeout: Duration::from_secs(60),
new_swaps: VecDeque::default(),
bitcoin_wallet,
keep_alive: KeepAlive::Yes,
keep_alive: KeepAlive::Until(Instant::now() + Duration::from_secs(5)),
}
}
}
@ -188,9 +188,6 @@ impl ProtocolsHandler for Handler {
write_cbor_message(&mut substream, state2.next_message()).await?;
substream.flush().await?;
substream.close().await?;
Ok(state2)
});
@ -240,6 +237,7 @@ impl ProtocolsHandler for Handler {
}
if let Some(result) = futures::ready!(self.outbound_stream.poll_unpin(cx)) {
self.keep_alive = KeepAlive::No;
self.outbound_stream = OptionFuture::from(None);
return Poll::Ready(ProtocolsHandlerEvent::Custom(Completed(result)));
}

@ -1,12 +1,11 @@
use crate::asb::LatestRate;
use crate::libp2p_ext::MultiAddrExt;
use crate::network::rendezvous::XmrBtcNamespace;
use crate::seed::Seed;
use crate::{asb, bitcoin, cli, env, tor};
use anyhow::{Context, Result};
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder};
use libp2p::{identity, Multiaddr, Swarm};
use anyhow::Result;
use libp2p::swarm::SwarmBuilder;
use libp2p::{PeerId, Swarm};
use std::fmt::Debug;
use std::sync::Arc;
#[allow(clippy::too_many_arguments)]
pub fn asb<LR>(
@ -16,32 +15,13 @@ pub fn asb<LR>(
latest_rate: LR,
resume_only: bool,
env_config: env::Config,
rendezvous_params: Option<(Multiaddr, XmrBtcNamespace)>,
) -> Result<Swarm<asb::Behaviour<LR>>>
where
LR: LatestRate + Send + 'static + Debug + Clone,
{
let identity = seed.derive_libp2p_identity();
let rendezvous_params = if let Some((address, namespace)) = rendezvous_params {
let peer_id = address
.extract_peer_id()
.context("Rendezvous node address must contain peer ID")?;
Some((identity.clone(), peer_id, address, namespace))
} else {
None
};
let behaviour = asb::Behaviour::new(
min_buy,
max_buy,
latest_rate,
resume_only,
env_config,
rendezvous_params,
);
let behaviour = asb::Behaviour::new(min_buy, max_buy, latest_rate, resume_only, env_config);
let identity = seed.derive_libp2p_identity();
let transport = asb::transport::new(&identity)?;
let peer_id = identity.public().into_peer_id();
@ -54,19 +34,21 @@ where
Ok(swarm)
}
pub async fn cli<T>(
identity: identity::Keypair,
pub async fn cli(
seed: &Seed,
alice: PeerId,
tor_socks5_port: u16,
behaviour: T,
) -> Result<Swarm<T>>
where
T: NetworkBehaviour,
{
env_config: env::Config,
bitcoin_wallet: Arc<bitcoin::Wallet>,
) -> Result<Swarm<cli::Behaviour>> {
let maybe_tor_socks5_port = match tor::Client::new(tor_socks5_port).assert_tor_running().await {
Ok(()) => Some(tor_socks5_port),
Err(_) => None,
};
let behaviour = cli::Behaviour::new(alice, env_config, bitcoin_wallet);
let identity = seed.derive_libp2p_identity();
let transport = cli::transport::new(&identity, maybe_tor_socks5_port)?;
let peer_id = identity.public().into_peer_id();

@ -1,19 +1,19 @@
use async_trait::async_trait;
use futures::stream::FusedStream;
use futures::{future, Future, Stream, StreamExt};
use futures::future;
use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::upgrade::Version;
use libp2p::core::transport::MemoryTransport;
use libp2p::core::upgrade::SelectUpgrade;
use libp2p::core::{identity, Executor, Multiaddr, PeerId, Transport};
use libp2p::core::transport::memory::MemoryTransport;
use libp2p::core::upgrade::{SelectUpgrade, Version};
use libp2p::core::{Executor, Multiaddr};
use libp2p::mplex::MplexConfig;
use libp2p::noise::{Keypair, NoiseConfig, X25519Spec};
use libp2p::swarm::{AddressScore, NetworkBehaviour, Swarm, SwarmBuilder, SwarmEvent};
use libp2p::tcp::TokioTcpConfig;
use libp2p::yamux::YamuxConfig;
use libp2p::noise::{self, NoiseConfig, X25519Spec};
use libp2p::swarm::{
IntoProtocolsHandler, NetworkBehaviour, ProtocolsHandler, SwarmBuilder, SwarmEvent,
};
use libp2p::{identity, yamux, PeerId, Swarm, Transport};
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use tokio::time;
/// An adaptor struct for libp2p that spawns futures into the current
/// thread-local runtime.
@ -25,193 +25,138 @@ impl Executor for GlobalSpawnTokioExecutor {
}
}
pub fn new_swarm<B, F>(behaviour_fn: F) -> Swarm<B>
#[allow(missing_debug_implementations)]
pub struct Actor<B: NetworkBehaviour> {
pub swarm: Swarm<B>,
pub addr: Multiaddr,
pub peer_id: PeerId,
}
pub async fn new_connected_swarm_pair<B, F>(behaviour_fn: F) -> (Actor<B>, Actor<B>)
where
B: NetworkBehaviour,
<B as NetworkBehaviour>::OutEvent: Debug,
F: Fn(PeerId, identity::Keypair) -> B + Clone,
<<<B as NetworkBehaviour>::ProtocolsHandler as IntoProtocolsHandler>::Handler as ProtocolsHandler>::InEvent: Clone,
<B as NetworkBehaviour>::OutEvent: Debug{
let (swarm, addr, peer_id) = new_swarm(behaviour_fn.clone());
let mut alice = Actor {
swarm,
addr,
peer_id,
};
let (swarm, addr, peer_id) = new_swarm(behaviour_fn);
let mut bob = Actor {
swarm,
addr,
peer_id,
};
connect(&mut alice.swarm, &mut bob.swarm).await;
(alice, bob)
}
pub fn new_swarm<B: NetworkBehaviour, F: Fn(PeerId, identity::Keypair) -> B>(
behaviour_fn: F,
) -> (Swarm<B>, Multiaddr, PeerId)
where
B: NetworkBehaviour,
F: FnOnce(PeerId, identity::Keypair) -> B,
{
let identity = identity::Keypair::generate_ed25519();
let peer_id = PeerId::from(identity.public());
let id_keys = identity::Keypair::generate_ed25519();
let peer_id = PeerId::from(id_keys.public());
let dh_keys = Keypair::<X25519Spec>::new()
.into_authentic(&identity)
let dh_keys = noise::Keypair::<X25519Spec>::new()
.into_authentic(&id_keys)
.expect("failed to create dh_keys");
let noise = NoiseConfig::xx(dh_keys).into_authenticated();
let transport = MemoryTransport::default()
.or_transport(TokioTcpConfig::new())
.upgrade(Version::V1)
.authenticate(noise)
.multiplex(SelectUpgrade::new(
YamuxConfig::default(),
yamux::YamuxConfig::default(),
MplexConfig::new(),
))
.timeout(Duration::from_secs(5))
.map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer)))
.boxed();
SwarmBuilder::new(transport, behaviour_fn(peer_id, identity), peer_id)
let mut swarm: Swarm<B> = SwarmBuilder::new(transport, behaviour_fn(peer_id, id_keys), peer_id)
.executor(Box::new(GlobalSpawnTokioExecutor))
.build()
}
.build();
fn get_rand_memory_address() -> Multiaddr {
let address_port = rand::random::<u64>();
let addr = format!("/memory/{}", address_port)
.parse::<Multiaddr>()
.unwrap();
format!("/memory/{}", address_port).parse().unwrap()
}
Swarm::listen_on(&mut swarm, addr.clone()).unwrap();
async fn get_local_tcp_address() -> Multiaddr {
let random_port = {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
listener.local_addr().unwrap().port()
};
format!("/ip4/127.0.0.1/tcp/{}", random_port)
.parse()
.unwrap()
(swarm, addr, peer_id)
}
pub async fn await_events_or_timeout<A, B, E1, E2>(
swarm_1: &mut (impl Stream<Item = SwarmEvent<A, E1>> + FusedStream + Unpin),
swarm_2: &mut (impl Stream<Item = SwarmEvent<B, E2>> + FusedStream + Unpin),
) -> (SwarmEvent<A, E1>, SwarmEvent<B, E2>)
where
SwarmEvent<A, E1>: Debug,
SwarmEvent<B, E2>: Debug,
{
tokio::time::timeout(
Duration::from_secs(30),
future::join(
swarm_1
.inspect(|event| tracing::debug!("Swarm1 emitted {:?}", event))
.select_next_some(),
swarm_2
.inspect(|event| tracing::debug!("Swarm2 emitted {:?}", event))
.select_next_some(),
),
pub async fn await_events_or_timeout<A, B>(
alice_event: impl Future<Output = A>,
bob_event: impl Future<Output = B>,
) -> (A, B) {
time::timeout(
Duration::from_secs(10),
future::join(alice_event, bob_event),
)
.await
.expect("network behaviours to emit an event within 10 seconds")
}
/// An extension trait for [`Swarm`] that makes it easier to set up a network of
/// [`Swarm`]s for tests.
#[async_trait]
pub trait SwarmExt {
/// Establishes a connection to the given [`Swarm`], polling both of them
/// until the connection is established.
async fn block_on_connection<T>(&mut self, other: &mut Swarm<T>)
where
T: NetworkBehaviour,
<T as NetworkBehaviour>::OutEvent: Debug;
/// Listens on a random memory address, polling the [`Swarm`] until the
/// transport is ready to accept connections.
async fn listen_on_random_memory_address(&mut self) -> Multiaddr;
/// Listens on a TCP port for localhost only, polling the [`Swarm`] until
/// the transport is ready to accept connections.
async fn listen_on_tcp_localhost(&mut self) -> Multiaddr;
}
#[async_trait]
impl<B> SwarmExt for Swarm<B>
/// Connects two swarms with each other.
///
/// This assumes the transport that is in use can be used by Bob to connect to
/// the listen address that is emitted by Alice. In other words, they have to be
/// on the same network. The memory transport used by the above `new_swarm`
/// function fulfills this.
///
/// We also assume that the swarms don't emit any behaviour events during the
/// connection phase. Any event emitted is considered a bug from this functions
/// PoV because they would be lost.
pub async fn connect<BA, BB>(alice: &mut Swarm<BA>, bob: &mut Swarm<BB>)
where
B: NetworkBehaviour,
<B as NetworkBehaviour>::OutEvent: Debug,
BA: NetworkBehaviour,
BB: NetworkBehaviour,
<BA as NetworkBehaviour>::OutEvent: Debug,
<BB as NetworkBehaviour>::OutEvent: Debug,
{
async fn block_on_connection<T>(&mut self, other: &mut Swarm<T>)
where
T: NetworkBehaviour,
<T as NetworkBehaviour>::OutEvent: Debug,
{
let addr_to_dial = other.external_addresses().next().unwrap().addr.clone();
self.dial_addr(addr_to_dial.clone()).unwrap();
let mut dialer_done = false;
let mut listener_done = false;
loop {
let dialer_event_fut = self.select_next_some();
tokio::select! {
dialer_event = dialer_event_fut => {
match dialer_event {
SwarmEvent::ConnectionEstablished { .. } => {
dialer_done = true;
}
SwarmEvent::UnknownPeerUnreachableAddr { address, error } if address == addr_to_dial => {
panic!("Failed to dial address {}: {}", addr_to_dial, error)
}
other => {
tracing::debug!("Ignoring {:?}", other);
}
}
},
listener_event = other.select_next_some() => {
match listener_event {
SwarmEvent::ConnectionEstablished { .. } => {
listener_done = true;
}
SwarmEvent::IncomingConnectionError { error, .. } => {
panic!("Failure in incoming connection {}", error);
}
other => {
tracing::debug!("Ignoring {:?}", other);
}
}
}
}
let mut alice_connected = false;
let mut bob_connected = false;
if dialer_done && listener_done {
return;
while !alice_connected && !bob_connected {
let (alice_event, bob_event) = future::join(alice.next_event(), bob.next_event()).await;
match alice_event {
SwarmEvent::ConnectionEstablished { .. } => {
alice_connected = true;
}
SwarmEvent::NewListenAddr(addr) => {
bob.dial_addr(addr).unwrap();
}
SwarmEvent::Behaviour(event) => {
panic!(
"alice unexpectedly emitted a behaviour event during connection: {:?}",
event
);
}
_ => {}
}
}
async fn listen_on_random_memory_address(&mut self) -> Multiaddr {
let multiaddr = get_rand_memory_address();
self.listen_on(multiaddr.clone()).unwrap();
block_until_listening_on(self, &multiaddr).await;
// Memory addresses are externally reachable because they all share the same
// memory-space.
self.add_external_address(multiaddr.clone(), AddressScore::Infinite);
multiaddr
}
async fn listen_on_tcp_localhost(&mut self) -> Multiaddr {
let multiaddr = get_local_tcp_address().await;
self.listen_on(multiaddr.clone()).unwrap();
block_until_listening_on(self, &multiaddr).await;
multiaddr
}
}
async fn block_until_listening_on<B>(swarm: &mut Swarm<B>, multiaddr: &Multiaddr)
where
B: NetworkBehaviour,
<B as NetworkBehaviour>::OutEvent: Debug,
{
loop {
match swarm.select_next_some().await {
SwarmEvent::NewListenAddr(addr) if &addr == multiaddr => {
break;
match bob_event {
SwarmEvent::ConnectionEstablished { .. } => {
bob_connected = true;
}
other => {
tracing::debug!(
"Ignoring {:?} while waiting for listening to succeed",
other
SwarmEvent::Behaviour(event) => {
panic!(
"bob unexpectedly emitted a behaviour event during connection: {:?}",
event
);
}
_ => {}
}
}
}

@ -6,9 +6,8 @@ use libp2p::core::transport::TransportError;
use libp2p::core::Transport;
use libp2p::tcp::tokio::{Tcp, TcpStream};
use libp2p::tcp::TcpListenStream;
use std::borrow::Cow;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::{fmt, io};
use std::io;
use std::net::Ipv4Addr;
use tokio_socks::tcp::Socks5Stream;
/// A [`Transport`] that can dial onion addresses through a running Tor daemon.
@ -35,17 +34,13 @@ impl Transport for TorDialOnlyTransport {
}
fn dial(self, addr: Multiaddr) -> Result<Self::Dial, TransportError<Self::Error>> {
let address = TorCompatibleAddress::from_multiaddr(Cow::Borrowed(&addr))?;
if address.is_certainly_not_reachable_via_tor_daemon() {
return Err(TransportError::MultiaddrNotSupported(addr));
}
let tor_address_string = fmt_as_address_string(addr.clone())?;
let dial_future = async move {
tracing::trace!(address = %addr, "Establishing connection through Tor proxy");
tracing::trace!("Connecting through Tor proxy to address: {}", addr);
let stream =
Socks5Stream::connect((Ipv4Addr::LOCALHOST, self.socks_port), address.to_string())
Socks5Stream::connect((Ipv4Addr::LOCALHOST, self.socks_port), tor_address_string)
.await
.map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, e))?;
@ -62,72 +57,36 @@ impl Transport for TorDialOnlyTransport {
}
}
/// Represents an address that is _compatible_ with Tor, i.e. can be resolved by
/// the Tor daemon.
#[derive(Debug)]
enum TorCompatibleAddress {
Onion3 { host: String, port: u16 },
Dns { address: String, port: u16 },
Ip4 { address: Ipv4Addr, port: u16 },
Ip6 { address: Ipv6Addr, port: u16 },
}
impl TorCompatibleAddress {
/// Constructs a new [`TorCompatibleAddress`] from a [`Multiaddr`].
fn from_multiaddr(multi: Cow<'_, Multiaddr>) -> Result<Self, TransportError<io::Error>> {
match multi.iter().collect::<Vec<_>>().as_slice() {
[Protocol::Onion3(onion), ..] => Ok(TorCompatibleAddress::Onion3 {
host: BASE32.encode(onion.hash()).to_lowercase(),
port: onion.port(),
}),
[Protocol::Ip4(address), Protocol::Tcp(port) | Protocol::Udp(port), ..] => {
Ok(TorCompatibleAddress::Ip4 {
address: *address,
port: *port,
})
}
[Protocol::Dns(address) | Protocol::Dns4(address), Protocol::Tcp(port) | Protocol::Udp(port), ..] => {
Ok(TorCompatibleAddress::Dns {
address: format!("{}", address),
port: *port,
})
}
[Protocol::Ip6(address), Protocol::Tcp(port) | Protocol::Udp(port), ..] => {
Ok(TorCompatibleAddress::Ip6 {
address: *address,
port: *port,
})
}
_ => Err(TransportError::MultiaddrNotSupported(multi.into_owned())),
}
}
/// Checks if the address is reachable via the Tor daemon.
///
/// The Tor daemon can dial onion addresses, resolve DNS names and dial
/// IP4/IP6 addresses reachable via the public Internet.
/// We can't guarantee that an address is reachable via the Internet but we
/// can say that some addresses are almost certainly not reachable, for
/// example, loopback addresses.
fn is_certainly_not_reachable_via_tor_daemon(&self) -> bool {
match self {
TorCompatibleAddress::Onion3 { .. } => false,
TorCompatibleAddress::Dns { address, .. } => address == "localhost",
TorCompatibleAddress::Ip4 { address, .. } => address.is_loopback(),
TorCompatibleAddress::Ip6 { address, .. } => address.is_loopback(),
/// Formats the given [`Multiaddr`] as an "address" string.
///
/// For our purposes, we define an address as {HOST}(.{TLD}):{PORT}. This format
/// is what is compatible with the Tor daemon and allows us to route traffic
/// through Tor.
fn fmt_as_address_string(multi: Multiaddr) -> Result<String, TransportError<io::Error>> {
let mut protocols = multi.iter();
let address_string = match protocols.next() {
// if it is an Onion address, we have all we need and can return
Some(Protocol::Onion3(addr)) => {
return Ok(format!(
"{}.onion:{}",
BASE32.encode(addr.hash()).to_lowercase(),
addr.port()
))
}
}
}
impl fmt::Display for TorCompatibleAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TorCompatibleAddress::Onion3 { host, port } => write!(f, "{}.onion:{}", host, port),
TorCompatibleAddress::Dns { address, port } => write!(f, "{}:{}", address, port),
TorCompatibleAddress::Ip4 { address, port } => write!(f, "{}:{}", address, port),
TorCompatibleAddress::Ip6 { address, port } => write!(f, "{}:{}", address, port),
}
}
// Deal with non-onion addresses
Some(Protocol::Ip4(addr)) => format!("{}", addr),
Some(Protocol::Ip6(addr)) => format!("{}", addr),
Some(Protocol::Dns(addr) | Protocol::Dns4(addr)) => format!("{}", addr),
_ => return Err(TransportError::MultiaddrNotSupported(multi)),
};
let port = match protocols.next() {
Some(Protocol::Tcp(port) | Protocol::Udp(port)) => port,
_ => return Err(TransportError::MultiaddrNotSupported(multi)),
};
Ok(format!("{}:{}", address_string, port))
}
#[cfg(test)]
@ -136,76 +95,69 @@ pub mod test {
#[test]
fn test_tor_address_string() {
let address = tor_compatible_address_from_str("/onion3/oarchy4tamydxcitaki6bc2v4leza6v35iezmu2chg2bap63sv6f2did:1024/p2p/12D3KooWPD4uHN74SHotLN7VCH7Fm8zZgaNVymYcpeF1fpD2guc9");
assert!(!address.is_certainly_not_reachable_via_tor_daemon());
let address =
"/onion3/oarchy4tamydxcitaki6bc2v4leza6v35iezmu2chg2bap63sv6f2did:1024/p2p/12D3KooWPD4uHN74SHotLN7VCH7Fm8zZgaNVymYcpeF1fpD2guc9"
;
let address_string = fmt_as_address_string(address.parse().unwrap())
.expect("To be a multi formatted address.");
assert_eq!(
address.to_string(),
address_string,
"oarchy4tamydxcitaki6bc2v4leza6v35iezmu2chg2bap63sv6f2did.onion:1024"
);
}
#[test]
fn tcp_to_address_string_should_be_some() {
let address = tor_compatible_address_from_str("/ip4/127.0.0.1/tcp/7777");
assert!(address.is_certainly_not_reachable_via_tor_daemon());
assert_eq!(address.to_string(), "127.0.0.1:7777");
let address = "/ip4/127.0.0.1/tcp/7777";
let address_string = fmt_as_address_string(address.parse().unwrap())
.expect("To be a formatted multi address. ");
assert_eq!(address_string, "127.0.0.1:7777");
}
#[test]
fn ip6_to_address_string_should_be_some() {
let address =
tor_compatible_address_from_str("/ip6/2001:db8:85a3:8d3:1319:8a2e:370:7348/tcp/7777");
assert!(!address.is_certainly_not_reachable_via_tor_daemon());
assert_eq!(
address.to_string(),
"2001:db8:85a3:8d3:1319:8a2e:370:7348:7777"
);
let address = "/ip6/2001:db8:85a3:8d3:1319:8a2e:370:7348/tcp/7777";
let address_string = fmt_as_address_string(address.parse().unwrap())
.expect("To be a formatted multi address. ");
assert_eq!(address_string, "2001:db8:85a3:8d3:1319:8a2e:370:7348:7777");
}
#[test]
fn udp_to_address_string_should_be_some() {
let address = tor_compatible_address_from_str("/ip4/127.0.0.1/udp/7777");
assert!(address.is_certainly_not_reachable_via_tor_daemon());
assert_eq!(address.to_string(), "127.0.0.1:7777");
let address = "/ip4/127.0.0.1/udp/7777";
let address_string = fmt_as_address_string(address.parse().unwrap())
.expect("To be a formatted multi address. ");
assert_eq!(address_string, "127.0.0.1:7777");
}
#[test]
fn ws_to_address_string_should_be_some() {
let address = tor_compatible_address_from_str("/ip4/127.0.0.1/tcp/7777/ws");
assert!(address.is_certainly_not_reachable_via_tor_daemon());
assert_eq!(address.to_string(), "127.0.0.1:7777");
let address = "/ip4/127.0.0.1/tcp/7777/ws";
let address_string = fmt_as_address_string(address.parse().unwrap())
.expect("To be a formatted multi address. ");
assert_eq!(address_string, "127.0.0.1:7777");
}
#[test]
fn dns4_to_address_string_should_be_some() {
let address = tor_compatible_address_from_str("/dns4/randomdomain.com/tcp/7777");
assert!(!address.is_certainly_not_reachable_via_tor_daemon());
assert_eq!(address.to_string(), "randomdomain.com:7777");
let address = "/dns4/randomdomain.com/tcp/7777";
let address_string = fmt_as_address_string(address.parse().unwrap())
.expect("To be a formatted multi address. ");
assert_eq!(address_string, "randomdomain.com:7777");
}
#[test]
fn dns_to_address_string_should_be_some() {
let address = tor_compatible_address_from_str("/dns/randomdomain.com/tcp/7777");
assert!(!address.is_certainly_not_reachable_via_tor_daemon());
assert_eq!(address.to_string(), "randomdomain.com:7777");
let address = "/dns/randomdomain.com/tcp/7777";
let address_string = fmt_as_address_string(address.parse().unwrap())
.expect("To be a formatted multi address. ");
assert_eq!(address_string, "randomdomain.com:7777");
}
#[test]
fn dnsaddr_to_address_string_should_be_error() {
fn dnsaddr_to_address_string_should_be_none() {
let address = "/dnsaddr/randomdomain.com";
let _ =
TorCompatibleAddress::from_multiaddr(Cow::Owned(address.parse().unwrap())).unwrap_err();
}
fn tor_compatible_address_from_str(str: &str) -> TorCompatibleAddress {
TorCompatibleAddress::from_multiaddr(Cow::Owned(str.parse().unwrap())).unwrap()
let address_string = fmt_as_address_string(address.parse().unwrap()).ok();
assert_eq!(address_string, None);
}
}

@ -1,29 +0,0 @@
use proptest::prelude::*;
pub mod ecdsa_fun {
use super::*;
use ::ecdsa_fun::fun::marker::{Mark, NonZero, Normal};
use ::ecdsa_fun::fun::{Point, Scalar, G};
pub fn point() -> impl Strategy<Value = Point> {
scalar().prop_map(|mut scalar| Point::from_scalar_mul(&G, &mut scalar).mark::<Normal>())
}
pub fn scalar() -> impl Strategy<Value = Scalar> {
prop::array::uniform32(0..255u8).prop_filter_map("generated the 0 element", |bytes| {
Scalar::from_bytes_mod_order(bytes).mark::<NonZero>()
})
}
}
pub mod bitcoin {
use super::*;
use ::bitcoin::util::bip32::ExtendedPrivKey;
use ::bitcoin::Network;
pub fn extended_priv_key() -> impl Strategy<Value = ExtendedPrivKey> {
prop::array::uniform8(0..255u8).prop_filter_map("invalid secret key generated", |bytes| {
ExtendedPrivKey::new_master(Network::Regtest, &bytes).ok()
})
}
}

@ -8,6 +8,7 @@ use crate::{bitcoin, database, monero};
use anyhow::{bail, Context, Result};
use tokio::select;
use tokio::time::timeout;
use tracing::{error, info, warn};
use uuid::Uuid;
pub async fn run<LR>(swap: Swap, rate_service: LR) -> Result<AliceState>
@ -65,7 +66,7 @@ where
.latest_rate()
.map_or("NaN".to_string(), |rate| format!("{}", rate));
tracing::info!(%state, %rate, "Advancing state");
info!(%state, %rate, "Advancing state");
Ok(match state {
AliceState::Started { state3 } => {
@ -77,7 +78,7 @@ where
.await
{
Err(_) => {
tracing::info!(
info!(
minutes = %env_config.bitcoin_lock_mempool_timeout.as_secs_f64() / 60.0,
"TxLock lock was not seen in mempool in time",
);
@ -98,7 +99,7 @@ where
.await
{
Err(_) => {
tracing::info!(
info!(
confirmations_needed = %env_config.bitcoin_finality_confirmations,
minutes = %env_config.bitcoin_lock_confirmed_timeout.as_secs_f64() / 60.0,
"TxLock lock did not get enough confirmations in time",
@ -203,7 +204,7 @@ where
}
}
enc_sig = event_loop_handle.recv_encrypted_signature() => {
tracing::info!("Received encrypted signature");
info!("Received encrypted signature");
AliceState::EncSigLearned {
monero_wallet_restore_blockheight,
@ -231,7 +232,10 @@ where
}
},
Err(error) => {
tracing::error!("Failed to publish redeem transaction: {:#}", error);
error!(
"Publishing the redeem transaction failed. Error {:#}",
error
);
tx_lock_status
.wait_until_confirmed_with(state3.cancel_timelock)
.await?;
@ -244,12 +248,8 @@ where
}
},
Err(error) => {
tracing::error!("Failed to construct redeem transaction: {:#}", error);
tracing::info!(
timelock = %state3.cancel_timelock,
"Waiting for cancellation timelock to expire",
);
error!(
"Constructing the redeem transaction failed. Attempting to wait for cancellation now. Error {:#}", error);
tx_lock_status
.wait_until_confirmed_with(state3.cancel_timelock)
.await?;
@ -361,7 +361,10 @@ where
match punish {
Ok(_) => AliceState::BtcPunished,
Err(error) => {
tracing::warn!("Failed to publish punish transaction: {:#}", error);
warn!(
"Falling back to refund because punish transaction failed. Error {:#}",
error
);
// Upon punish failure we assume that the refund tx was included but we
// missed seeing it. In case we fail to fetch the refund tx we fail
@ -370,8 +373,6 @@ where
// because a punish tx failure is not recoverable (besides re-trying) if the
// refund tx was not included.
tracing::info!("Falling back to refund");
let published_refund_tx = bitcoin_wallet
.get_raw_transaction(state3.tx_refund().txid())
.await?;

@ -20,7 +20,7 @@ pub struct Swap {
pub monero_wallet: Arc<monero::Wallet>,
pub env_config: env::Config,
pub id: Uuid,
pub monero_receive_address: monero::Address,
pub receive_monero_address: monero::Address,
}
impl Swap {
@ -32,26 +32,21 @@ impl Swap {
monero_wallet: Arc<monero::Wallet>,
env_config: env::Config,
event_loop_handle: cli::EventLoopHandle,
monero_receive_address: monero::Address,
bitcoin_change_address: bitcoin::Address,
receive_monero_address: monero::Address,
btc_amount: bitcoin::Amount,
) -> Self {
Self {
state: BobState::Started {
btc_amount,
change_address: bitcoin_change_address,
},
state: BobState::Started { btc_amount },
event_loop_handle,
db,
bitcoin_wallet,
monero_wallet,
env_config,
id,
monero_receive_address,
receive_monero_address,
}
}
#[allow(clippy::too_many_arguments)]
pub fn from_db(
db: Database,
id: Uuid,
@ -59,7 +54,7 @@ impl Swap {
monero_wallet: Arc<monero::Wallet>,
env_config: env::Config,
event_loop_handle: cli::EventLoopHandle,
monero_receive_address: monero::Address,
receive_monero_address: monero::Address,
) -> Result<Self> {
let state = db.get_state(id)?.try_into_bob()?.into();
@ -71,7 +66,7 @@ impl Swap {
monero_wallet,
env_config,
id,
monero_receive_address,
receive_monero_address,
})
}
}

@ -25,7 +25,6 @@ use uuid::Uuid;
pub enum BobState {
Started {
btc_amount: bitcoin::Amount,
change_address: bitcoin::Address,
},
SwapSetupCompleted(State2),
BtcLocked(State3),
@ -170,14 +169,7 @@ impl State0 {
bail!("Alice's dleq proof doesn't verify")
}
let tx_lock = bitcoin::TxLock::new(
wallet,
self.btc,
msg.A,
self.b.public(),
self.refund_address.clone(),
)
.await?;
let tx_lock = bitcoin::TxLock::new(wallet, self.btc, msg.A, self.b.public()).await?;
let v = msg.v_a + self.v_b;
Ok(State1 {

@ -37,7 +37,7 @@ pub async fn run_until(
&mut swap.event_loop_handle,
swap.bitcoin_wallet.as_ref(),
swap.monero_wallet.as_ref(),
swap.monero_receive_address,
swap.receive_monero_address,
)
.await?;
@ -56,15 +56,13 @@ async fn next_state(
event_loop_handle: &mut EventLoopHandle,
bitcoin_wallet: &bitcoin::Wallet,
monero_wallet: &monero::Wallet,
monero_receive_address: monero::Address,
receive_monero_address: monero::Address,
) -> Result<BobState> {
tracing::trace!(%state, "Advancing state");
Ok(match state {
BobState::Started {
btc_amount,
change_address,
} => {
BobState::Started { btc_amount } => {
let bitcoin_refund_address = bitcoin_wallet.new_address().await?;
let tx_refund_fee = bitcoin_wallet
.estimate_fee(TxRefund::weight(), btc_amount)
.await?;
@ -78,7 +76,7 @@ async fn next_state(
btc: btc_amount,
tx_refund_fee,
tx_cancel_fee,
bitcoin_refund_address: change_address,
bitcoin_refund_address,
})
.await?;
@ -149,13 +147,11 @@ async fn next_state(
received_xmr = monero_wallet.watch_for_transfer(watch_request) => {
match received_xmr {
Ok(()) => BobState::XmrLocked(state.xmr_locked(monero_wallet_restore_blockheight)),
Err(monero::InsufficientFunds { expected, actual }) => {
tracing::warn!(%expected, %actual, "Insufficient Monero have been locked!");
tracing::info!(timelock = %state.cancel_timelock, "Waiting for cancel timelock to expire");
tx_lock_status.wait_until_confirmed_with(state.cancel_timelock).await?;
Err(e) => {
tracing::warn!("Waiting for refund because insufficient Monero have been locked! {:#}", e);
tx_lock_status.wait_until_confirmed_with(state.cancel_timelock).await?;
BobState::CancelTimelockExpired(state.cancel())
BobState::CancelTimelockExpired(state.cancel())
},
}
}
@ -228,10 +224,10 @@ async fn next_state(
// Ensure that the generated wallet is synced so we have a proper balance
monero_wallet.refresh().await?;
// Sweep (transfer all funds) to the given address
let tx_hashes = monero_wallet.sweep_all(monero_receive_address).await?;
let tx_hashes = monero_wallet.sweep_all(receive_monero_address).await?;
for tx_hash in tx_hashes {
tracing::info!(%monero_receive_address, txid=%tx_hash.0, "Successfully transferred XMR to wallet");
tracing::info!(%receive_monero_address, txid=%tx_hash.0, "Sent XMR to");
}
BobState::XmrRedeemed {

@ -64,7 +64,7 @@ impl Seed {
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());
let random_seed = Seed::random()?;
random_seed.write_to(file_path.to_path_buf())?;

@ -1,4 +1,4 @@
use anyhow::{bail, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use std::future::Future;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use tokio::net::TcpStream;
@ -50,7 +50,7 @@ impl Client {
pub async fn assert_tor_running(&self) -> Result<()> {
// Make sure you are running tor and this is your socks port
let proxy = reqwest::Proxy::all(format!("socks5h://{}", self.socks5_address).as_str())
.context("Failed to construct Tor proxy URL")?;
.map_err(|_| anyhow!("tor proxy should be there"))?;
let client = reqwest::Client::builder().proxy(proxy).build()?;
let res = client.get("https://check.torproject.org").send().await?;
@ -77,21 +77,21 @@ impl Client {
let mut uc = self
.init_unauthenticated_connection()
.await
.context("Failed to connect to Tor")?;
.map_err(|_| anyhow!("Could not connect to Tor. Tor might not be running or the control port is incorrect."))?;
let tor_info = uc
.load_protocol_info()
.await
.context("Failed to load protocol info from Tor")?;
.map_err(|_| anyhow!("Failed to load protocol info from Tor."))?;
let tor_auth_data = tor_info
.make_auth_data()?
.context("Failed to make Tor auth data")?;
.context("Failed to make Tor auth data.")?;
// Get an authenticated connection to the Tor via the Tor Controller protocol.
uc.authenticate(&tor_auth_data)
.await
.context("Failed to authenticate with Tor")?;
.map_err(|_| anyhow!("Failed to authenticate with Tor"))?;
Ok(AuthenticatedClient {
inner: uc.into_authenticated().await,
@ -123,6 +123,6 @@ impl AuthenticatedClient {
self.inner
.add_onion_v3(tor_key, false, false, false, None, &mut listeners)
.await
.context("Failed to add onion service")
.map_err(|e| anyhow!("Could not add onion service.: {:#?}", e))
}
}

@ -1,65 +0,0 @@
#![allow(clippy::unwrap_used)] // This is only meant to be used in tests.
use std::io;
use std::sync::{Arc, Mutex};
use tracing::subscriber;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt::MakeWriter;
/// Setup tracing with a capturing writer, allowing assertions on the log
/// messages.
///
/// Time and ANSI are disabled to make the output more predictable and
/// readable.
pub fn capture_logs(min_level: LevelFilter) -> MakeCapturingWriter {
let make_writer = MakeCapturingWriter::default();
let guard = subscriber::set_default(
tracing_subscriber::fmt()
.with_ansi(false)
.without_time()
.with_writer(make_writer.clone())
.with_env_filter(format!("{}", min_level))
.finish(),
);
// don't clean up guard we stay initialized
std::mem::forget(guard);
make_writer
}
#[derive(Default, Clone)]
pub struct MakeCapturingWriter {
writer: CapturingWriter,
}
impl MakeCapturingWriter {
pub fn captured(&self) -> String {
let captured = &self.writer.captured;
let cursor = captured.lock().unwrap();
String::from_utf8(cursor.clone().into_inner()).unwrap()
}
}
impl MakeWriter for MakeCapturingWriter {
type Writer = CapturingWriter;
fn make_writer(&self) -> Self::Writer {
self.writer.clone()
}
}
#[derive(Default, Clone)]
pub struct CapturingWriter {
captured: Arc<Mutex<io::Cursor<Vec<u8>>>>,
}
impl io::Write for CapturingWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.captured.lock().unwrap().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

@ -236,7 +236,6 @@ async fn start_alice(
latest_rate,
resume_only,
env_config,
None,
)
.unwrap();
swarm.listen_on(listen_address).unwrap();
@ -434,7 +433,6 @@ impl BobParams {
self.env_config,
handle,
self.monero_wallet.get_main_address(),
self.bitcoin_wallet.new_address().await?,
btc_amount,
);
@ -447,16 +445,12 @@ impl BobParams {
) -> Result<(cli::EventLoop, cli::EventLoopHandle)> {
let tor_socks5_port = get_port()
.expect("We don't care about Tor in the tests so we get a free port to disable it.");
let behaviour = cli::Behaviour::new(
let mut swarm = swarm::cli(
&self.seed,
self.alice_peer_id,
tor_socks5_port,
self.env_config,
self.bitcoin_wallet.clone(),
);
let mut swarm = swarm::cli(
self.seed.derive_libp2p_identity(),
tor_socks5_port,
behaviour,
)
.await?;
swarm

Loading…
Cancel
Save