Compare commits
126 Commits
rendezvous
...
master
Author | SHA1 | Date |
---|---|---|
bors[bot] | 4212504941 | 3 years ago |
dependabot[bot] | ad531ef328 | 3 years ago |
bors[bot] | 0d7eb669e8 | 3 years ago |
dependabot[bot] | 302f433416 | 3 years ago |
bors[bot] | cd5d2353ae | 3 years ago |
dependabot[bot] | 59b7baee2d | 3 years ago |
bors[bot] | 64b44af18e | 3 years ago |
dependabot[bot] | 210913af48 | 3 years ago |
bors[bot] | 518b812551 | 3 years ago |
dependabot[bot] | 3818cb4f48 | 3 years ago |
bors[bot] | 04bbcb1fc9 | 3 years ago |
bors[bot] | 8c1ebd2cc2 | 3 years ago |
dependabot[bot] | c01e19f432 | 3 years ago |
bors[bot] | e032e30da6 | 3 years ago |
dependabot[bot] | 4a2bfbf9cf | 3 years ago |
bors[bot] | c0be41f9a0 | 3 years ago |
bors[bot] | cd5a1376d3 | 3 years ago |
Thomas Eizinger | 6c446825b7 | 3 years ago |
Thomas Eizinger | 1af0623c85 | 3 years ago |
bors[bot] | cdb2939746 | 3 years ago |
dependabot[bot] | f08a4d535f | 3 years ago |
COMIT Botty McBotface | 7126d77dc1 | 3 years ago |
bors[bot] | b2c377005b | 3 years ago |
Thomas Eizinger | 0296509110 | 3 years ago |
Thomas Eizinger | 475057abda | 3 years ago |
Thomas Eizinger | e4b5e28a93 | 3 years ago |
Thomas Eizinger | 148fdb8d0a | 3 years ago |
dependabot[bot] | 339b1e4758 | 3 years ago |
bors[bot] | 4405dbcf9c | 3 years ago |
bors[bot] | 956e710f48 | 3 years ago |
dependabot[bot] | 5cfd50f50c | 3 years ago |
Thomas Eizinger | 819feafc77 | 3 years ago |
bors[bot] | 52d5f2d83b | 3 years ago |
dependabot[bot] | 0174170afe | 3 years ago |
bors[bot] | 6ec5e34f80 | 3 years ago |
dependabot[bot] | 83c378db15 | 3 years ago |
dependabot[bot] | d25d7c7bbe | 3 years ago |
bors[bot] | d1f68f6672 | 3 years ago |
bors[bot] | 57bdd5020f | 3 years ago |
dependabot[bot] | f7ccfb96fd | 3 years ago |
dependabot[bot] | 634b1d3877 | 3 years ago |
dependabot[bot] | 8ea0877dc1 | 3 years ago |
bors[bot] | 723e94622b | 3 years ago |
Thomas Eizinger | 5c4aec6ae3 | 3 years ago |
bors[bot] | 21f4295e94 | 3 years ago |
COMIT Botty McBotface | 403e3d2b33 | 3 years ago |
bors[bot] | 9eb82cd0a9 | 3 years ago |
Daniel Karzel | 5e2e0f7dc0 | 3 years ago |
Daniel Karzel | e72922923a | 3 years ago |
Thomas Eizinger | d21bd556ec | 3 years ago |
bors[bot] | 3e3015a478 | 3 years ago |
dependabot[bot] | e43c9a8d6d | 3 years ago |
bors[bot] | 9fc53d3f84 | 3 years ago |
Daniel Karzel | 0dc3943d9c | 3 years ago |
bors[bot] | 6208689237 | 3 years ago |
bors[bot] | 238e52228e | 3 years ago |
bors[bot] | 00f581dee1 | 3 years ago |
Daniel Karzel | ab24f7bce5 | 3 years ago |
bors[bot] | b7a832eb7a | 3 years ago |
bors[bot] | 41982e0ff9 | 3 years ago |
Thomas Eizinger | 2c8bbe4913 | 3 years ago |
Thomas Eizinger | 94f089f4f2 | 3 years ago |
Thomas Eizinger | 367d75cab6 | 3 years ago |
Thomas Eizinger | 46ffc34f40 | 3 years ago |
Thomas Eizinger | 991dbf496e | 3 years ago |
Thomas Eizinger | 2eb7fab0c3 | 3 years ago |
Daniel Karzel | 6abf83f4ad | 3 years ago |
Thomas Eizinger | cacfc50fb2 | 3 years ago |
Thomas Eizinger | 56a48e71ef | 3 years ago |
Thomas Eizinger | 56ea23c2a3 | 3 years ago |
Thomas Eizinger | a347dd8b97 | 3 years ago |
bors[bot] | c275e33a6c | 3 years ago |
binarybaron | 357f4a0711 | 3 years ago |
bors[bot] | 7ff57ff0d4 | 3 years ago |
dependabot[bot] | e90ceb392e | 3 years ago |
bors[bot] | ab0f429eea | 3 years ago |
COMIT Botty McBotface | 50da958078 | 3 years ago |
bors[bot] | 668a41e6e2 | 3 years ago |
Thomas Eizinger | 714514edbc | 3 years ago |
bors[bot] | 93a69563a9 | 3 years ago |
Daniel Karzel | ffad47d515 | 3 years ago |
Thomas Eizinger | 5c37fe6733 | 3 years ago |
Thomas Eizinger | bbc3a49f41 | 3 years ago |
Thomas Eizinger | 987f8abb9d | 3 years ago |
Thomas Eizinger | 09f395a26b | 3 years ago |
Thomas Eizinger | 40eccd089f | 3 years ago |
Thomas Eizinger | 3b1789fe07 | 3 years ago |
Daniel Karzel | 91b0a0863b | 3 years ago |
bors[bot] | 15751f8a0e | 3 years ago |
Thomas Eizinger | 8f50eb2f34 | 3 years ago |
Thomas Eizinger | 9119ce5cc4 | 3 years ago |
Thomas Eizinger | 78480547d5 | 3 years ago |
bors[bot] | fa1a5e6efb | 3 years ago |
Thomas Eizinger | 1d0d38cd48 | 3 years ago |
bors[bot] | c8b29aecd1 | 3 years ago |
binarybaron | bdfa6e1f9f | 3 years ago |
bors[bot] | 3a99b753ed | 3 years ago |
Thomas Eizinger | acfd2dd6bb | 3 years ago |
Thomas Eizinger | 5463bde4f8 | 3 years ago |
Thomas Eizinger | 683d565679 | 3 years ago |
Thomas Eizinger | 8b59ac26ba | 3 years ago |
Daniel Karzel | 625ff4868a | 3 years ago |
Thomas Eizinger | 348fca0827 | 3 years ago |
Thomas Eizinger | e642f5c148 | 3 years ago |
rishflab | 93a0692998 | 3 years ago |
Daniel Karzel | ff10edd8a4 | 3 years ago |
Daniel Karzel | f45cde84ab | 3 years ago |
Thomas Eizinger | b4fafeba6b | 3 years ago |
Thomas Eizinger | e163942850 | 3 years ago |
Daniel Karzel | ff8cca2e27 | 3 years ago |
bors[bot] | 4cd27e372c | 3 years ago |
bors[bot] | 72673fa166 | 3 years ago |
Thomas Eizinger | d49f4ea60d | 3 years ago |
Thomas Eizinger | ec4234fbb9 | 3 years ago |
Thomas Eizinger | c2daf7a11e | 3 years ago |
bors[bot] | 962b648911 | 3 years ago |
bors[bot] | 206c98d71b | 3 years ago |
bors[bot] | f89bc701a5 | 3 years ago |
dependabot[bot] | dcd854e697 | 3 years ago |
dependabot[bot] | f33e50ab19 | 3 years ago |
dependabot[bot] | 35f481f4ae | 3 years ago |
dependabot[bot] | 15cf4ff638 | 3 years ago |
dependabot[bot] | 31be076fd1 | 3 years ago |
Thomas Eizinger | 8057b45e17 | 3 years ago |
Thomas Eizinger | 92ed8d9c04 | 3 years ago |
Thomas Eizinger | ec59184e85 | 3 years ago |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
||||
# Documentation
|
||||
|
||||
This directory hosts various pieces of documentation.
|
||||
|
||||
- [`swap` CLI](./cli/README.md)
|
||||
- [`asb` service](./asb/README.md)
|
@ -0,0 +1,139 @@
|
||||
# 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.
|
@ -0,0 +1,39 @@
|
||||
#!/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
|
@ -0,0 +1,9 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
# 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,116 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,441 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
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)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,365 @@
|
||||
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
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
|
@ -1 +0,0 @@
|
||||
|
@ -0,0 +1,29 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
#![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(())
|
||||
}
|
||||
}
|
Loading…
Reference in new issue