Compare commits

...

73 Commits

Author SHA1 Message Date
bors[bot] 4212504941
Merge #668
3 years ago
dependabot[bot] ad531ef328
Bump tracing-subscriber from 0.2.19 to 0.2.20
3 years ago
bors[bot] 0d7eb669e8
Merge #665
3 years ago
dependabot[bot] 302f433416
Bump serde_json from 1.0.64 to 1.0.66
3 years ago
bors[bot] cd5d2353ae
Merge #664
3 years ago
dependabot[bot] 59b7baee2d
Bump anyhow from 1.0.42 to 1.0.43
3 years ago
bors[bot] 64b44af18e
Merge #663
3 years ago
dependabot[bot] 210913af48
Bump rust_decimal from 1.14.3 to 1.15.0
3 years ago
bors[bot] 518b812551
Merge #659
3 years ago
dependabot[bot] 3818cb4f48
Bump jsonrpc_client from 0.6.0 to 0.7.0
3 years ago
bors[bot] 04bbcb1fc9
Merge #631 #641
3 years ago
bors[bot] 8c1ebd2cc2
Merge #656
3 years ago
dependabot[bot] c01e19f432
Bump async-trait from 0.1.50 to 0.1.51
3 years ago
bors[bot] e032e30da6
Merge #655
3 years ago
dependabot[bot] 4a2bfbf9cf
Bump comfy-table from 4.0.1 to 4.1.1
3 years ago
bors[bot] c0be41f9a0
Merge #639
3 years ago
bors[bot] cd5a1376d3
Merge #649
3 years ago
Thomas Eizinger 6c446825b7
Instruct `Ping` to keep the connection alive
3 years ago
Thomas Eizinger 1af0623c85
Remove empty modules
3 years ago
bors[bot] cdb2939746
Merge #645
3 years ago
dependabot[bot] f08a4d535f
Bump hyper from 0.14.10 to 0.14.11
3 years ago
COMIT Botty McBotface 7126d77dc1 Prepare release 0.8.1
3 years ago
bors[bot] b2c377005b
Merge #643
3 years ago
Thomas Eizinger 0296509110
Upgrade to bdk 0.10
3 years ago
Thomas Eizinger 475057abda
Add proptest for max_giveable and signing PSBT
3 years ago
Thomas Eizinger e4b5e28a93
Introduce WalletBuilder for creating test instances of wallet
3 years ago
Thomas Eizinger 148fdb8d0a
Ensure the size of our locking script never changes
3 years ago
dependabot[bot] 339b1e4758
Bump thomaseizinger/create-pull-request from 1.1.0 to 1.2.1
3 years ago
bors[bot] 4405dbcf9c
Merge #634
3 years ago
bors[bot] 956e710f48
Merge #635
3 years ago
dependabot[bot] 5cfd50f50c
Bump hex-literal from 0.3.2 to 0.3.3
3 years ago
Thomas Eizinger 819feafc77
Add note about security of software
3 years ago
bors[bot] 52d5f2d83b
Merge #632
3 years ago
dependabot[bot] 0174170afe
Bump vergen from 5.1.12 to 5.1.13
3 years ago
bors[bot] 6ec5e34f80
Merge #630
3 years ago
dependabot[bot] 83c378db15
Bump tokio-tungstenite from 0.14.0 to 0.15.0
3 years ago
dependabot[bot] d25d7c7bbe
Bump anyhow from 1.0.41 to 1.0.42
3 years ago
bors[bot] d1f68f6672
Merge #623 #627
3 years ago
bors[bot] 57bdd5020f
Merge #629
3 years ago
dependabot[bot] f7ccfb96fd
Bump monero-epee-bin-serde from 1.0.0 to 1.0.1
3 years ago
dependabot[bot] 634b1d3877
Bump libp2p from `74a39f4` to `4af8af7`
3 years ago
dependabot[bot] 8ea0877dc1
Bump comfy-table from 4.0.0 to 4.0.1
3 years ago
bors[bot] 723e94622b
Merge #626
3 years ago
Thomas Eizinger 5c4aec6ae3
Rendezvous point is not defaulted
3 years ago
bors[bot] 21f4295e94
Merge #625
3 years ago
COMIT Botty McBotface 403e3d2b33 Prepare release 0.8.0
3 years ago
bors[bot] 9eb82cd0a9
Merge #624
3 years ago
Daniel Karzel 5e2e0f7dc0
No default for rendezvous-point in CLI README
3 years ago
Daniel Karzel e72922923a
Update discover-and-take script with rendezvous-point param
3 years ago
Thomas Eizinger d21bd556ec
Remove rendezvous point default
3 years ago
bors[bot] 3e3015a478
Merge #622
3 years ago
dependabot[bot] e43c9a8d6d
Bump hyper from 0.14.9 to 0.14.10
3 years ago
bors[bot] 9fc53d3f84
Merge #615
3 years ago
Daniel Karzel 0dc3943d9c
Update default Monero node URLs and info
3 years ago
bors[bot] 6208689237
Merge #620
3 years ago
bors[bot] 238e52228e
Merge #618
3 years ago
bors[bot] 00f581dee1
Merge #619
3 years ago
Daniel Karzel ab24f7bce5
Revert "Prepare release 0.8.0"
3 years ago
bors[bot] b7a832eb7a
Merge #616
3 years ago
bors[bot] 41982e0ff9
Merge #617
3 years ago
Thomas Eizinger 2c8bbe4913
Remove left-over "half" of log message
3 years ago
Thomas Eizinger 94f089f4f2
Disallow Bitcoin legacy addresses
3 years ago
Thomas Eizinger 367d75cab6
Reduce code duplication and make evaluation order determinisitic
3 years ago
Thomas Eizinger 46ffc34f40
Don't spam on transaction status change
3 years ago
Thomas Eizinger 991dbf496e
Extract `print_status_change` so it is easily testable
3 years ago
Thomas Eizinger 2eb7fab0c3
Make `capture_logs` available for the whole crate
3 years ago
Daniel Karzel 6abf83f4ad
Sort seller list inside of `list_sellers`
3 years ago
Thomas Eizinger cacfc50fb2
Don't spam the user while waiting for BTC
3 years ago
Thomas Eizinger 56a48e71ef
Add failing test that shows spamming of output
3 years ago
Thomas Eizinger 56ea23c2a3
Assert log output for `determine_btc_to_swap`
3 years ago
Thomas Eizinger a347dd8b97
Move helper structs below tests
3 years ago
bors[bot] c275e33a6c
Merge #614
3 years ago
binarybaron 357f4a0711
Fix quick start README.md list-sellers command
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.1.0
uses: thomaseizinger/create-pull-request@1.2.1
with:
GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }}
head: release/${{ github.event.inputs.version }}

@ -7,7 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.8.0] - 2021-07-07
### 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
@ -19,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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` which is used as default for discovery in the CLI.
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.
@ -30,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
@ -173,7 +188,8 @@ 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.0...HEAD
[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
[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

193
Cargo.lock generated

@ -78,9 +78,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.41"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
[[package]]
name = "arrayref"
@ -115,9 +115,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.50"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [
"proc-macro2 1.0.27",
"quote 1.0.9",
@ -235,11 +235,20 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64-compat"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7"
dependencies = [
"byteorder",
]
[[package]]
name = "bdk"
version = "0.8.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d7fee1aedf8935ba1e2c9aeee640d1b9754da1b64f30ad47e8b8e2b7904ec0"
checksum = "8f4da304c23a06c21807598a7fe3223566e84c76c6bba2cab2504370dd6f4938"
dependencies = [
"async-trait",
"bdk-macros",
@ -252,14 +261,13 @@ dependencies = [
"serde",
"serde_json",
"sled",
"tokio",
]
[[package]]
name = "bdk-macros"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b45570b78250774145859a8f85bfdb6e310663fc82640d7e159a44b1386074a2"
checksum = "c3f510015e946c5995cc169f7ed4c92ba032bbce795c0956ee0d98d82f7aff78"
dependencies = [
"proc-macro2 1.0.27",
"quote 1.0.9",
@ -331,6 +339,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6742ec672d3f12506f4ac5c0d853926ff1f94e675f60ffd3224039972bf663f1"
dependencies = [
"base64-compat",
"bech32",
"bitcoin_hashes",
"secp256k1",
@ -620,13 +629,14 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "comfy-table"
version = "4.0.0"
version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ac47a9f5b89127ada154ad504ab5f8eea37e3679ab63d52a694617d0f9e2018"
checksum = "11e95a3e867422fd8d04049041f5671f94d53c32a9dcd82e2be268714942f3f3"
dependencies = [
"crossterm",
"strum",
"strum_macros",
"unicode-width",
]
[[package]]
@ -677,6 +687,22 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
[[package]]
name = "core-foundation"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "cpufeatures"
version = "0.1.4"
@ -1443,9 +1469,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76505e26b6ca3bbdbbb360b68472abbb80998c5fa5dc43672eca34f28258e138"
checksum = "21e4590e13640f19f249fe3e4eca5113bc4289f2497710378190e7f4bd96f45b"
[[package]]
name = "hmac"
@ -1553,9 +1579,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "0.14.9"
version = "0.14.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83"
checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11"
dependencies = [
"bytes 1.0.1",
"futures-channel",
@ -1582,7 +1608,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64"
dependencies = [
"futures-util",
"hyper 0.14.9",
"hyper 0.14.11",
"log 0.4.14",
"rustls 0.19.0",
"tokio",
@ -1663,15 +1689,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "input_buffer"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413"
dependencies = [
"bytes 1.0.1",
]
[[package]]
name = "instant"
version = "0.1.9"
@ -1769,9 +1786,9 @@ dependencies = [
[[package]]
name = "jsonrpc_client"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a85cf2c5ce158eabf30b2ac4f535463d7b09ce7905502e11238b7d6048ef7d02"
checksum = "2f2e6b724d546ba449f65c42d02f97d437742962fe4a5735733a5132b3453208"
dependencies = [
"async-trait",
"jsonrpc_client_macro 0.3.0",
@ -1844,9 +1861,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.90"
version = "0.2.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
[[package]]
name = "libgit2-sys"
@ -1863,7 +1880,7 @@ dependencies = [
[[package]]
name = "libp2p"
version = "0.39.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"atomic",
"bytes 1.0.1",
@ -1891,7 +1908,7 @@ dependencies = [
[[package]]
name = "libp2p-core"
version = "0.29.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"asn1_der",
"bs58",
@ -1924,7 +1941,7 @@ dependencies = [
[[package]]
name = "libp2p-dns"
version = "0.29.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"futures",
"libp2p-core",
@ -1936,7 +1953,7 @@ dependencies = [
[[package]]
name = "libp2p-mplex"
version = "0.29.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"asynchronous-codec",
"bytes 1.0.1",
@ -1953,7 +1970,7 @@ dependencies = [
[[package]]
name = "libp2p-noise"
version = "0.32.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"bytes 1.0.1",
"curve25519-dalek",
@ -1974,7 +1991,7 @@ dependencies = [
[[package]]
name = "libp2p-ping"
version = "0.30.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"futures",
"libp2p-core",
@ -1988,7 +2005,7 @@ dependencies = [
[[package]]
name = "libp2p-rendezvous"
version = "0.1.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"asynchronous-codec",
"bimap",
@ -2010,7 +2027,7 @@ dependencies = [
[[package]]
name = "libp2p-request-response"
version = "0.12.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"async-trait",
"bytes 1.0.1",
@ -2029,7 +2046,7 @@ dependencies = [
[[package]]
name = "libp2p-swarm"
version = "0.30.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"either",
"futures",
@ -2044,7 +2061,7 @@ dependencies = [
[[package]]
name = "libp2p-swarm-derive"
version = "0.23.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"quote 1.0.9",
"syn 1.0.73",
@ -2053,7 +2070,7 @@ dependencies = [
[[package]]
name = "libp2p-tcp"
version = "0.29.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"futures",
"futures-timer",
@ -2069,7 +2086,7 @@ dependencies = [
[[package]]
name = "libp2p-websocket"
version = "0.30.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"either",
"futures",
@ -2086,7 +2103,7 @@ dependencies = [
[[package]]
name = "libp2p-yamux"
version = "0.33.0"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"futures",
"libp2p-core",
@ -2360,9 +2377,9 @@ dependencies = [
[[package]]
name = "monero-epee-bin-serde"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13be5b525af150f294b98d4291b0ec01e5bc157db740de2822827c17561d3960"
checksum = "1f8a3f7f7ef5bb1fd6c953be9187e48df8cc1a0ffc7d94f9fbabd4a23e37321e"
dependencies = [
"byteorder",
"serde",
@ -2391,7 +2408,7 @@ dependencies = [
"curve25519-dalek",
"hex 0.4.3",
"hex-literal",
"jsonrpc_client 0.6.0",
"jsonrpc_client 0.7.0",
"monero",
"monero-epee-bin-serde",
"rand 0.7.3",
@ -2471,7 +2488,7 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "multistream-select"
version = "0.10.3"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#74a39f45df6f2ea5e3e08a01fe765bd9b7128758"
source = "git+https://github.com/comit-network/rust-libp2p?branch=rendezvous#4af8af780ba3b1c8344b3d19a146d9628ad5bdb2"
dependencies = [
"bytes 1.0.1",
"futures",
@ -2624,6 +2641,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl-probe"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
[[package]]
name = "parking_lot"
version = "0.10.2"
@ -3300,7 +3323,7 @@ dependencies = [
"futures-util",
"http",
"http-body",
"hyper 0.14.9",
"hyper 0.14.11",
"hyper-rustls",
"ipnet",
"js-sys",
@ -3351,9 +3374,9 @@ dependencies = [
[[package]]
name = "rust_decimal"
version = "1.14.3"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01127cb8617e5e21bcf2e19b5eb48317735ca677f1d0a94833c21c331c446582"
checksum = "c5446d1cf2dfe2d6367c8b27f2082bdf011e60e76fa1fcd140047f535156d6e7"
dependencies = [
"arrayvec",
"num-traits",
@ -3426,6 +3449,18 @@ dependencies = [
"webpki",
]
[[package]]
name = "rustls-native-certs"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092"
dependencies = [
"openssl-probe",
"rustls 0.19.0",
"schannel",
"security-framework",
]
[[package]]
name = "rustversion"
version = "1.0.4"
@ -3467,6 +3502,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi 0.3.9",
]
[[package]]
name = "scoped-tls"
version = "1.0.0"
@ -3531,6 +3576,29 @@ dependencies = [
"subtle-ng",
]
[[package]]
name = "security-framework"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "0.9.0"
@ -3606,9 +3674,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.64"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
dependencies = [
"itoa",
"ryu",
@ -4038,7 +4106,7 @@ checksum = "8049cf85f0e715d6af38dde439cb0ccb91f67fb9f5f63c80f8b43e48356e1a3f"
[[package]]
name = "swap"
version = "0.8.0"
version = "0.8.1"
dependencies = [
"anyhow",
"async-compression",
@ -4063,7 +4131,7 @@ dependencies = [
"ed25519-dalek",
"futures",
"get-port",
"hyper 0.14.9",
"hyper 0.14.11",
"itertools 0.10.1",
"libp2p",
"miniscript",
@ -4392,9 +4460,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.14.0"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e96bb520beab540ab664bd5a9cfeaa1fcd846fa68c830b42e2c8963071251d2"
checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8"
dependencies = [
"futures-util",
"log 0.4.14",
@ -4532,9 +4600,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.2.19"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab69019741fca4d98be3c62d2b75254528b5432233fd8a4d2739fec20278de48"
checksum = "b9cbe87a2fa7e35900ce5de20220a582a9483a7063811defce79d7cbd59d4cfe"
dependencies = [
"ansi_term 0.12.1",
"chrono",
@ -4610,25 +4678,24 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "tungstenite"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8dada8c1a3aeca77d6b51a4f1314e0f4b8e438b7b1b71e3ddaca8080e4093"
checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5"
dependencies = [
"base64 0.13.0",
"byteorder",
"bytes 1.0.1",
"http",
"httparse",
"input_buffer",
"log 0.4.14",
"rand 0.8.3",
"rustls 0.19.0",
"rustls-native-certs",
"sha-1",
"thiserror",
"url 2.2.2",
"utf-8",
"webpki",
"webpki-roots 0.21.0",
]
[[package]]
@ -4800,9 +4867,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "vergen"
version = "5.1.12"
version = "5.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f710158a7c9449f400cf734e97cb417a09a7b5f0cf54b133718b6b2951c9f79"
checksum = "542f37b4798c879409865dde7908e746d836f77839c3a6bea5c8b4e4dcf6620b"
dependencies = [
"anyhow",
"cfg-if 1.0.0",

@ -15,7 +15,7 @@ You can read [this blogpost](https://comit.network/blog/2021/07/02/transaction-p
2. Find a seller to swap with:
```shell
./swap --testnet --list-sellers
./swap --testnet list-sellers
```
3. Swap with a seller:
@ -33,6 +33,12 @@ Swapping of course needs two parties - and the CLI is only one of them: The take
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).
## Safety
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).
## Contact
Feel free to reach out to us in the [COMIT-Monero Matrix channel](https://matrix.to/#/#comit-monero:matrix.org).

@ -12,10 +12,7 @@ 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 stagenet nodes for running the Monero Wallet RPC:
- `monero-stagenet.exan.tech:38081`
- `stagenet.melo.tools:38081`
Public Monero nodes for running the Monero Wallet RPC can be found [here](https://melo.tools/nodes.html).
Run `./asb --help` for more information.

@ -94,13 +94,11 @@ FLAGS:
-V, --version Prints version information
OPTIONS:
--rendezvous-point <rendezvous-point> Address of the rendezvous point you want to use to discover ASBs [default: /dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o]
--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]
```
This command only takes optional parameters and can be run as-is:
Running `swap --testnet list-sellers` will give you something like:
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 ...

@ -1,10 +1,21 @@
#!/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
YOUR_MONERO_ADDR=$2
YOUR_BITCOIN_ADDR=$3
RENDEZVOUS_POINT=$2
YOUR_MONERO_ADDR=$3
YOUR_BITCOIN_ADDR=$4
CLI_LIST_SELLERS="$CLI_PATH --testnet --json --debug list-sellers"
CLI_LIST_SELLERS="$CLI_PATH --testnet --json --debug list-sellers --rendezvous-point $RENDEZVOUS_POINT"
echo "Requesting sellers with command: $CLI_LIST_SELLERS"
echo

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

@ -1,6 +1,6 @@
[package]
name = "swap"
version = "0.8.0"
version = "0.8.1"
authors = [ "The COMIT guys <hello@comit.network>" ]
edition = "2018"
description = "XMR/BTC trustless atomic swaps."
@ -15,11 +15,11 @@ async-trait = "0.1"
atty = "0.2"
backoff = { version = "0.3", features = [ "tokio" ] }
base64 = "0.13"
bdk = "0.8"
bdk = "0.10"
big-bytes = "1"
bitcoin = { version = "0.26", features = [ "rand", "use-serde" ] }
bmrng = "0.5"
comfy-table = "4.0.0"
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" }
@ -55,7 +55,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.14", features = [ "rustls-tls" ] }
tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] }
tokio-util = { version = "0.6", features = [ "io" ] }
toml = "0.5"
torut = { version = "0.1", default-features = false, features = [ "v3", "control" ] }
@ -80,6 +80,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"

@ -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,6 +1,5 @@
use crate::env::{Mainnet, Testnet};
use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir};
use crate::network::rendezvous::DEFAULT_RENDEZVOUS_ADDRESS;
use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT};
use anyhow::{bail, Context, Result};
use config::ConfigError;
@ -249,7 +248,7 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
.map(|str| str.parse())
.collect::<Result<Vec<Multiaddr>, _>>()?;
let electrum_rpc_url: Url = Input::with_theme(&ColorfulTheme::default())
let electrum_rpc_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()?;
@ -290,24 +289,22 @@ 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_address = Input::with_theme(&ColorfulTheme::default())
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.")
.default(DEFAULT_RENDEZVOUS_ADDRESS.to_string())
.allow_empty(true)
.interact_text()?;
let rendezvous_point = if rendezvous_address.is_empty() {
None
} else {
Some(Multiaddr::from_str(&rendezvous_address)?)
};
println!();
Ok(Config {
data: Data { dir: data_dir },
network: Network {
listen: listen_addresses,
rendezvous_point,
rendezvous_point: if rendezvous_point.is_empty() {
None
} else {
Some(rendezvous_point)
},
external_addresses: vec![],
},
bitcoin: Bitcoin {
@ -358,7 +355,7 @@ mod tests {
},
network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
rendezvous_point: Some(DEFAULT_RENDEZVOUS_ADDRESS.parse().unwrap()),
rendezvous_point: None,
external_addresses: vec![],
},
@ -401,7 +398,7 @@ mod tests {
},
network: Network {
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
rendezvous_point: Some(DEFAULT_RENDEZVOUS_ADDRESS.parse().unwrap()),
rendezvous_point: None,
external_addresses: vec![],
},

@ -13,7 +13,7 @@ use libp2p::core::connection::ConnectionId;
use libp2p::core::muxing::StreamMuxerBox;
use libp2p::core::transport::Boxed;
use libp2p::dns::TokioDnsConfig;
use libp2p::ping::{Ping, PingEvent};
use libp2p::ping::{Ping, PingConfig, PingEvent};
use libp2p::request_response::{RequestId, ResponseChannel};
use libp2p::swarm::{
DialPeerCondition, IntoProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction,
@ -152,7 +152,7 @@ pub mod behaviour {
),
transfer_proof: transfer_proof::alice(),
encrypted_signature: encrypted_signature::alice(),
ping: Ping::default(),
ping: Ping::new(PingConfig::new().with_keep_alive(true)),
}
}
}

@ -285,7 +285,7 @@ async fn main() -> Result<()> {
.context("Failed to read in seed file")?;
let identity = seed.derive_libp2p_identity();
let mut sellers = list_sellers(
let sellers = list_sellers(
rendezvous_node_peer_id,
rendezvous_point,
namespace,
@ -293,7 +293,6 @@ async fn main() -> Result<()> {
identity,
)
.await?;
sellers.sort();
if json {
for seller in sellers {
@ -423,7 +422,7 @@ where
price = %bid_quote.price,
minimum_amount = %bid_quote.min_quantity,
maximum_amount = %bid_quote.max_quantity,
"Received quote: 1 XMR ~ ",
"Received quote",
);
let mut max_giveable = max_giveable_fn().await?;
@ -446,28 +445,26 @@ where
"Waiting for Bitcoin deposit",
);
sync().await?;
max_giveable = loop {
sync().await?;
let new_max_givable = max_giveable_fn().await?;
let new_max_givable = max_giveable_fn().await?;
if new_max_givable > max_giveable {
break new_max_givable;
}
if new_max_givable != max_giveable {
max_giveable = new_max_givable;
tokio::time::sleep(Duration::from_secs(1)).await;
};
let new_balance = balance().await?;
tracing::info!(
%new_balance,
%max_giveable,
"Received Bitcoin",
);
let new_balance = balance().await?;
tracing::info!(%new_balance, %max_giveable, "Received Bitcoin");
if max_giveable >= bid_quote.min_quantity {
break;
} else {
tracing::info!("Deposited amount is less than `min_quantity`",);
}
if max_giveable < bid_quote.min_quantity {
tracing::info!("Deposited amount is less than `min_quantity`");
continue;
}
tokio::time::sleep(Duration::from_secs(1)).await;
break;
}
};
@ -483,40 +480,16 @@ where
#[cfg(test)]
mod tests {
use std::sync::Mutex;
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)
}
}
use crate::determine_btc_to_swap;
use ::bitcoin::Amount;
use std::sync::Mutex;
use swap::tracing_ext::capture_logs;
use tracing::level_filters::LevelFilter;
#[tokio::test]
async fn given_no_balance_and_transfers_less_than_max_swaps_max_giveable() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.0009).unwrap(),
@ -539,11 +512,19 @@ 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!((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
"
);
}
#[tokio::test]
async fn given_no_balance_and_transfers_more_then_swaps_max_quantity_from_quote() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.1).unwrap(),
@ -566,12 +547,19 @@ 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!((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
"
);
}
#[tokio::test]
async fn given_initial_balance_below_max_quantity_swaps_max_givable() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.0049).unwrap(),
Amount::from_btc(99.9).unwrap(),
@ -594,12 +582,17 @@ 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!((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
"
);
}
#[tokio::test]
async fn given_initial_balance_above_max_quantity_swaps_max_quantity() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.1).unwrap(),
Amount::from_btc(99.9).unwrap(),
@ -622,12 +615,17 @@ 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!((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
"
);
}
#[tokio::test]
async fn given_no_initial_balance_then_min_wait_for_sufficient_deposit() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.01).unwrap(),
@ -650,12 +648,19 @@ 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!((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
"
);
}
#[tokio::test]
async fn given_balance_less_then_min_wait_for_sufficient_deposit() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::from_btc(0.0001).unwrap(),
Amount::from_btc(0.01).unwrap(),
@ -678,12 +683,19 @@ 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!((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
"
);
}
#[tokio::test]
async fn given_no_initial_balance_and_transfers_less_than_min_keep_waiting() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let writer = capture_logs(LevelFilter::INFO);
let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![
Amount::ZERO,
Amount::from_btc(0.01).unwrap(),
@ -709,7 +721,82 @@ mod tests {
.await
.unwrap_err();
assert!(matches!(error, tokio::time::error::Elapsed { .. }))
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)
}
}
fn quote_with_max(btc: f64) -> BidQuote {

@ -21,6 +21,9 @@ 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;
@ -317,8 +320,8 @@ mod tests {
#[tokio::test]
async fn calculate_transaction_weights() {
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 alice_wallet = WalletBuilder::new(Amount::ONE_BTC.as_sat()).build();
let bob_wallet = WalletBuilder::new(Amount::ONE_BTC.as_sat()).build();
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);

@ -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,
@ -112,12 +112,7 @@ impl TxLock {
/// Calculate the size of the script used by this transaction.
pub fn script_size() -> usize {
build_shared_output_descriptor(
Point::random(&mut thread_rng()),
Point::random(&mut thread_rng()),
)
.script_pubkey()
.len()
SCRIPT_SIZE
}
pub fn script_pubkey(&self) -> Script {
@ -188,11 +183,12 @@ 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 = Wallet::new_funded_default_fees(50000);
let wallet = WalletBuilder::new(50_000).build();
let agreed_amount = Amount::from_sat(10000);
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
@ -206,7 +202,7 @@ mod tests {
let (A, B) = alice_and_bob();
let fees = 610;
let agreed_amount = Amount::from_sat(10000);
let wallet = Wallet::new_funded_default_fees(agreed_amount.as_sat() + fees);
let wallet = WalletBuilder::new(agreed_amount.as_sat() + fees).build();
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
assert_eq!(
@ -222,7 +218,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 = Wallet::new_funded_default_fees(50000);
let wallet = WalletBuilder::new(50_000).build();
let agreed_amount = Amount::from_sat(10000);
let bad_amount = Amount::from_sat(5000);
@ -235,7 +231,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 = Wallet::new_funded_default_fees(50000);
let wallet = WalletBuilder::new(50_000).build();
let agreed_amount = Amount::from_sat(10000);
let E = eve();
@ -245,6 +241,17 @@ 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.

@ -149,16 +149,7 @@ impl Wallet {
}
};
match (last_status, new_status) {
(None, new_status) => {
tracing::debug!(%txid, status = %new_status, "Found relevant Bitcoin transaction");
},
(Some(old_status), new_status) => {
tracing::debug!(%txid, %new_status, %old_status, "Bitcoin transaction status changed");
}
}
last_status = Some(new_status);
last_status = Some(print_status_change(txid, last_status, new_status));
let all_receivers_gone = sender.send(new_status).is_err();
@ -182,6 +173,20 @@ 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 {
@ -301,7 +306,8 @@ where
.iter()
.find(|tx| tx.txid == txid)
.context("Could not find tx in bdk wallet when trying to determine fees")?
.fees;
.fee
.expect("fees are always present with Electrum backend");
Ok(Amount::from_sat(fees))
}
@ -389,14 +395,16 @@ where
let mut tx_builder = wallet.build_tx();
let dummy_script = Script::from(vec![0u8; locking_script_size]);
tx_builder.set_single_recipient(dummy_script);
tx_builder.drain_wallet();
tx_builder.drain_to(dummy_script);
tx_builder.fee_rate(fee_rate);
let response = tx_builder.finish();
match response {
Ok((_, details)) => {
let max_giveable = details.sent - details.fees;
let max_giveable = details.sent
- details
.fee
.expect("fees are always present with Electrum backend");
Ok(Amount::from_sat(max_giveable))
}
Err(bdk::Error::InsufficientFunds { .. }) => Ok(Amount::ZERO),
@ -542,48 +550,84 @@ impl EstimateFeeRate for StaticFeeRate {
}
#[cfg(test)]
impl Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate> {
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 {
/// 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_funded_default_fees(amount: u64) -> Self {
Self::new_funded(amount, 1.0, 1000)
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,
}
}
/// 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 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 }
}
/// 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 {
pub fn with_num_utxos(self, number: u8) -> Self {
Self {
num_utxos: number,
..self
}
}
pub fn build(self) -> Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate> {
use bdk::database::MemoryDatabase;
use bdk::{LocalUtxo, TransactionDetails};
use bdk::{ConfirmationTime, LocalUtxo, TransactionDetails};
use bitcoin::OutPoint;
use testutils::testutils;
let descriptors = testutils!(@descriptors ("wpkh(tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m/*)"));
let descriptors = testutils!(@descriptors (&format!("wpkh({}/*)", self.key)));
let mut database = MemoryDatabase::new();
bdk::populate_test_db!(
&mut database,
testutils! {
@tx ( (@external descriptors, 0) => amount ) (@confirmations 1)
},
Some(100)
);
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)
);
}
let wallet =
bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database).unwrap();
Self {
Wallet {
client: Arc::new(Mutex::new(StaticFeeRate {
fee_rate: FeeRate::from_sat_per_vb(sats_per_vb),
min_relay_fee: bitcoin::Amount::from_sat(min_relay_fee_sats),
fee_rate: FeeRate::from_sat_per_vb(self.sats_per_vb),
min_relay_fee: bitcoin::Amount::from_sat(self.min_relay_fee_sats),
})),
wallet: Arc::new(Mutex::new(wallet)),
finality_confirmations: 1,
@ -850,7 +894,9 @@ 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() {
@ -1040,7 +1086,7 @@ mod tests {
#[tokio::test]
async fn given_no_balance_returns_amount_0() {
let wallet = Wallet::new_funded(0, 1.0, 1);
let wallet = WalletBuilder::new(0).with_fees(1.0, 1).build();
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
assert_eq!(amount, Amount::ZERO);
@ -1048,7 +1094,7 @@ mod tests {
#[tokio::test]
async fn given_balance_below_min_relay_fee_returns_amount_0() {
let wallet = Wallet::new_funded(1000, 1.0, 1001);
let wallet = WalletBuilder::new(1000).with_fees(1.0, 1001).build();
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
assert_eq!(amount, Amount::ZERO);
@ -1056,7 +1102,7 @@ mod tests {
#[tokio::test]
async fn given_balance_above_relay_fee_returns_amount_greater_0() {
let wallet = Wallet::new_funded_default_fees(10_000);
let wallet = WalletBuilder::new(10_000).build();
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
assert!(amount.as_sat() > 0);
@ -1076,7 +1122,7 @@ mod tests {
let balance = 2000;
// We don't care about fees in this test, thus use a zero fee rate
let wallet = Wallet::new_funded_zero_fees(balance);
let wallet = WalletBuilder::new(balance).with_zero_fees().build();
// 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
@ -1101,7 +1147,7 @@ mod tests {
#[tokio::test]
async fn can_override_change_address() {
let wallet = Wallet::new_funded_default_fees(50_000);
let wallet = WalletBuilder::new(50_000).build();
let custom_change = "bcrt1q08pfqpsyrt7acllzyjm8q5qsz5capvyahm49rw"
.parse::<Address>()
.unwrap();
@ -1124,4 +1170,55 @@ mod tests {
_ => 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");
});
}
}
}

@ -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, PingEvent};
use libp2p::ping::{Ping, PingConfig, 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::default(),
ping: Ping::new(PingConfig::new().with_keep_alive(true)),
}
}

@ -1,8 +1,9 @@
use crate::env::GetConfig;
use crate::fs::system_data_dir;
use crate::network::rendezvous::{XmrBtcNamespace, DEFAULT_RENDEZVOUS_ADDRESS};
use crate::network::rendezvous::XmrBtcNamespace;
use crate::{env, monero};
use anyhow::{Context, Result};
use bitcoin::AddressType;
use libp2p::core::Multiaddr;
use std::ffi::OsString;
use std::path::PathBuf;
@ -12,8 +13,8 @@ use url::Url;
use uuid::Uuid;
// See: https://moneroworld.com/
pub const DEFAULT_MONERO_DAEMON_ADDRESS: &str = "node.xmr.to:18081";
pub const DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET: &str = "monero-stagenet.exan.tech:38081";
pub const DEFAULT_MONERO_DAEMON_ADDRESS: &str = "node.melo.tools:18081";
pub const DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET: &str = "stagenet.melo.tools:38081";
// See: https://1209k.com/bitcoin-eye/ele.php?chain=btc
const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:50002";
@ -69,41 +70,36 @@ where
let arguments = match args.cmd {
RawCommand::BuyXmr {
seller: Seller { seller },
bitcoin:
Bitcoin {
bitcoin_electrum_rpc_url,
bitcoin_target_block,
},
bitcoin,
bitcoin_change_address,
monero: Monero {
monero_daemon_address,
},
monero,
monero_receive_address,
tor: Tor { tor_socks5_port },
} => Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::BuyXmr {
seller,
bitcoin_electrum_rpc_url: bitcoin_electrum_rpc_url_from(
} => {
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
bitcoin.apply_defaults(is_testnet)?;
let monero_daemon_address = monero.apply_defaults(is_testnet);
let monero_receive_address =
validate_monero_address(monero_receive_address, is_testnet)?;
let bitcoin_change_address =
validate_bitcoin_address(bitcoin_change_address, is_testnet)?;
Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::BuyXmr {
seller,
bitcoin_electrum_rpc_url,
is_testnet,
)?,
bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet),
bitcoin_change_address,
monero_receive_address: validate_monero_address(
bitcoin_target_block,
bitcoin_change_address,
monero_receive_address,
is_testnet,
)?,
monero_daemon_address: monero_daemon_address_from(
monero_daemon_address,
is_testnet,
),
tor_socks5_port,
},
},
tor_socks5_port,
},
}
}
RawCommand::History => Arguments {
env_config: env_config_from(is_testnet),
debug,
@ -113,80 +109,70 @@ where
},
RawCommand::Resume {
swap_id: SwapId { swap_id },
bitcoin:
Bitcoin {
bitcoin_electrum_rpc_url,
bitcoin_target_block,
},
monero: Monero {
monero_daemon_address,
},
bitcoin,
monero,
tor: Tor { tor_socks5_port },
} => Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Resume {
swap_id,
bitcoin_electrum_rpc_url: bitcoin_electrum_rpc_url_from(
} => {
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
bitcoin.apply_defaults(is_testnet)?;
let monero_daemon_address = monero.apply_defaults(is_testnet);
Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Resume {
swap_id,
bitcoin_electrum_rpc_url,
is_testnet,
)?,
bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet),
monero_daemon_address: monero_daemon_address_from(
bitcoin_target_block,
monero_daemon_address,
is_testnet,
),
tor_socks5_port,
},
},
tor_socks5_port,
},
}
}
RawCommand::Cancel {
swap_id: SwapId { swap_id },
force,
bitcoin:
Bitcoin {
bitcoin,
} => {
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
bitcoin.apply_defaults(is_testnet)?;
Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Cancel {
swap_id,
force,
bitcoin_electrum_rpc_url,
bitcoin_target_block,
},
} => Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Cancel {
swap_id,
force,
bitcoin_electrum_rpc_url: bitcoin_electrum_rpc_url_from(
bitcoin_electrum_rpc_url,
is_testnet,
)?,
bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet),
},
},
}
}
RawCommand::Refund {
swap_id: SwapId { swap_id },
force,
bitcoin:
Bitcoin {
bitcoin,
} => {
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
bitcoin.apply_defaults(is_testnet)?;
Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Refund {
swap_id,
force,
bitcoin_electrum_rpc_url,
bitcoin_target_block,
},
} => Arguments {
env_config: env_config_from(is_testnet),
debug,
json,
data_dir: data::data_dir_from(data, is_testnet)?,
cmd: Command::Refund {
swap_id,
force,
bitcoin_electrum_rpc_url: bitcoin_electrum_rpc_url_from(
bitcoin_electrum_rpc_url,
is_testnet,
)?,
bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet),
},
},
}
}
RawCommand::ListSellers {
rendezvous_point,
tor: Tor { tor_socks5_port },
@ -350,8 +336,7 @@ enum RawCommand {
ListSellers {
#[structopt(
long,
help = "Address of the rendezvous point you want to use to discover ASBs",
default_value = DEFAULT_RENDEZVOUS_ADDRESS
help = "Address of the rendezvous point you want to use to discover ASBs"
)]
rendezvous_point: Multiaddr,
@ -369,6 +354,18 @@ struct Monero {
monero_daemon_address: Option<String>,
}
impl Monero {
fn apply_defaults(self, testnet: bool) -> String {
if let Some(address) = self.monero_daemon_address {
address
} else if testnet {
DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string()
} else {
DEFAULT_MONERO_DAEMON_ADDRESS.to_string()
}
}
}
#[derive(structopt::StructOpt, Debug)]
struct Bitcoin {
#[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")]
@ -381,6 +378,28 @@ struct Bitcoin {
bitcoin_target_block: Option<usize>,
}
impl Bitcoin {
fn apply_defaults(self, testnet: bool) -> Result<(Url, usize)> {
let bitcoin_electrum_rpc_url = if let Some(url) = self.bitcoin_electrum_rpc_url {
url
} else if testnet {
Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET)?
} else {
Url::from_str(DEFAULT_ELECTRUM_RPC_URL)?
};
let bitcoin_target_block = if let Some(target_block) = self.bitcoin_target_block {
target_block
} else if testnet {
DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET
} else {
DEFAULT_BITCOIN_CONFIRMATION_TARGET
};
Ok((bitcoin_electrum_rpc_url, bitcoin_target_block))
}
}
#[derive(structopt::StructOpt, Debug)]
struct Tor {
#[structopt(
@ -428,16 +447,6 @@ mod data {
}
}
fn bitcoin_electrum_rpc_url_from(url: Option<Url>, testnet: bool) -> Result<Url> {
if let Some(url) = url {
Ok(url)
} else if testnet {
Ok(Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET)?)
} else {
Ok(Url::from_str(DEFAULT_ELECTRUM_RPC_URL)?)
}
}
fn rendezvous_namespace_from(is_testnet: bool) -> XmrBtcNamespace {
if is_testnet {
XmrBtcNamespace::Testnet
@ -446,26 +455,6 @@ fn rendezvous_namespace_from(is_testnet: bool) -> XmrBtcNamespace {
}
}
fn bitcoin_target_block_from(target_block: Option<usize>, testnet: bool) -> usize {
if let Some(target_block) = target_block {
target_block
} else if testnet {
DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET
} else {
DEFAULT_BITCOIN_CONFIRMATION_TARGET
}
}
fn monero_daemon_address_from(address: Option<String>, testnet: bool) -> String {
if let Some(address) = address {
address
} else if testnet {
DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string()
} else {
DEFAULT_MONERO_DAEMON_ADDRESS.to_string()
}
}
fn env_config_from(testnet: bool) -> env::Config {
if testnet {
env::Testnet::get_config()
@ -494,6 +483,28 @@ fn validate_monero_address(
Ok(address)
}
fn validate_bitcoin_address(address: bitcoin::Address, testnet: bool) -> Result<bitcoin::Address> {
let expected_network = if testnet {
bitcoin::Network::Testnet
} else {
bitcoin::Network::Bitcoin
};
if address.network != expected_network {
anyhow::bail!(
"Invalid Bitcoin address provided; expected network {} but provided address is for {}",
expected_network,
address.network
);
}
if address.address_type() != Some(AddressType::P2wpkh) {
anyhow::bail!("Invalid Bitcoin address provided, only bech32 format is supported!")
}
Ok(address)
}
fn parse_monero_address(s: &str) -> Result<monero::Address> {
monero::Address::from_str(s).with_context(|| {
format!(
@ -504,7 +515,7 @@ fn parse_monero_address(s: &str) -> Result<monero::Address> {
}
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
#[error("Invalid monero address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
#[error("Invalid monero address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
pub struct MoneroAddressNetworkMismatch {
expected: monero::Network,
actual: monero::Network,
@ -907,6 +918,105 @@ mod tests {
);
}
#[test]
fn only_bech32_addresses_mainnet_are_allowed() {
let raw_ars = vec![
BINARY_NAME,
"buy-xmr",
"--change-address",
"1A5btpLKZjgYm8R22rJAhdbTFVXgSRA2Mp",
"--receive-address",
MONERO_MAINNET_ADDRESS,
"--seller",
MULTI_ADDRESS,
];
let result = parse_args_and_apply_defaults(raw_ars);
assert_eq!(
result.unwrap_err().to_string(),
"Invalid Bitcoin address provided, only bech32 format is supported!"
);
let raw_ars = vec![
BINARY_NAME,
"buy-xmr",
"--change-address",
"36vn4mFhmTXn7YcNwELFPxTXhjorw2ppu2",
"--receive-address",
MONERO_MAINNET_ADDRESS,
"--seller",
MULTI_ADDRESS,
];
let result = parse_args_and_apply_defaults(raw_ars);
assert_eq!(
result.unwrap_err().to_string(),
"Invalid Bitcoin address provided, only bech32 format is supported!"
);
let raw_ars = vec![
BINARY_NAME,
"buy-xmr",
"--change-address",
"bc1qh4zjxrqe3trzg7s6m7y67q2jzrw3ru5mx3z7j3",
"--receive-address",
MONERO_MAINNET_ADDRESS,
"--seller",
MULTI_ADDRESS,
];
let result = parse_args_and_apply_defaults(raw_ars).unwrap();
assert!(matches!(result, ParseResult::Arguments(_)));
}
#[test]
fn only_bech32_addresses_testnet_are_allowed() {
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"buy-xmr",
"--change-address",
"n2czxyeFCQp9e8WRyGpy4oL4YfQAeKkkUH",
"--receive-address",
MONERO_STAGENET_ADDRESS,
"--seller",
MULTI_ADDRESS,
];
let result = parse_args_and_apply_defaults(raw_ars);
assert_eq!(
result.unwrap_err().to_string(),
"Invalid Bitcoin address provided, only bech32 format is supported!"
);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"buy-xmr",
"--change-address",
"2ND9a4xmQG89qEWG3ETRuytjKpLmGrW7Jvf",
"--receive-address",
MONERO_STAGENET_ADDRESS,
"--seller",
MULTI_ADDRESS,
];
let result = parse_args_and_apply_defaults(raw_ars);
assert_eq!(
result.unwrap_err().to_string(),
"Invalid Bitcoin address provided, only bech32 format is supported!"
);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"buy-xmr",
"--change-address",
"tb1q958vfh3wkdp232pktq8zzvmttyxeqnj80zkz3v",
"--receive-address",
MONERO_STAGENET_ADDRESS,
"--seller",
MULTI_ADDRESS,
];
let result = parse_args_and_apply_defaults(raw_ars).unwrap();
assert!(matches!(result, ParseResult::Arguments(_)));
}
impl Arguments {
pub fn buy_xmr_testnet_defaults() -> Self {
Self {

@ -15,6 +15,12 @@ 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,
@ -289,7 +295,10 @@ impl EventLoop {
.collect::<Result<Vec<_>, _>>();
match all_quotes_fetched {
Ok(sellers) => break sellers,
Ok(mut sellers) => {
sellers.sort();
break sellers;
}
Err(StillPending {}) => continue,
}
}

@ -29,5 +29,9 @@ pub mod network;
pub mod protocol;
pub mod seed;
pub mod tor;
pub mod tracing_ext;
mod monero_ext;
#[cfg(test)]
mod proptest;

@ -1,7 +1,5 @@
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;

@ -1,9 +1,6 @@
use libp2p::rendezvous::Namespace;
use std::fmt;
pub const DEFAULT_RENDEZVOUS_ADDRESS: &str =
"/dnsaddr/rendezvous.coblox.tech/p2p/12D3KooWQUt9DkNZxEn2R5ymJzWj15MpG6mTW84kyd8vDaRZi46o";
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum XmrBtcNamespace {
Mainnet,

@ -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…
Cancel
Save