From 870911bd5837470f7ec518d1ad319815c5e70420 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 09:06:19 +0000 Subject: [PATCH 01/55] build(deps): bump async-trait from 0.1.78 to 0.1.79 Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.78 to 0.1.79. - [Release notes](https://github.com/dtolnay/async-trait/releases) - [Commits](https://github.com/dtolnay/async-trait/compare/0.1.78...0.1.79) --- updated-dependencies: - dependency-name: async-trait dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c58895b..48ca6b71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.78" +version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", From f575a93bbb191cc0b2aa7bf358328b0f799110b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:06:45 +0000 Subject: [PATCH 02/55] build(deps): bump serde_json from 1.0.114 to 1.0.115 Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.114 to 1.0.115. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.114...v1.0.115) --- updated-dependencies: - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c58895b..972b4fb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3765,9 +3765,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", From be8b3c1ddec88f3a27cd4ac8a2b6b1b0a785cc1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:37:49 +0000 Subject: [PATCH 03/55] build(deps): bump tokio from 1.36.0 to 1.37.0 Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.36.0 to 1.37.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.36.0...tokio-1.37.0) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c58895b..e1f60e88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4553,9 +4553,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", From c231a343995eb7747c3212f6618c32d54d70995d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:28:21 +0000 Subject: [PATCH 04/55] build(deps): bump port_check from 0.1.5 to 0.2.1 Bumps [port_check](https://github.com/ufoscout/port-check-rs) from 0.1.5 to 0.2.1. - [Commits](https://github.com/ufoscout/port-check-rs/commits/v0.2.1) --- updated-dependencies: - dependency-name: port_check dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- swap/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1f60e88..909bef52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2925,9 +2925,9 @@ dependencies = [ [[package]] name = "port_check" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6519412c9e0d4be579b9f0618364d19cb434b324fc6ddb1b27b1e682c7105ed" +checksum = "2110609fb863cdb367d4e69d6c43c81ba6a8c7d18e80082fe9f3ef16b23afeed" [[package]] name = "powerfmt" diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 8b60517a..b0c31790 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -80,7 +80,7 @@ get-port = "3" hyper = "1.2" mockito = "1.3.0" monero-harness = { path = "../monero-harness" } -port_check = "0.1" +port_check = "0.2" proptest = "1" serde_cbor = "0.11" serial_test = "3.0" From 73370ce7d922f409bff1a177ef0e1148f5cbdfdf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:42:05 +0000 Subject: [PATCH 05/55] build(deps): bump comfy-table from 7.1.0 to 7.1.1 Bumps [comfy-table](https://github.com/nukesor/comfy-table) from 7.1.0 to 7.1.1. - [Release notes](https://github.com/nukesor/comfy-table/releases) - [Changelog](https://github.com/Nukesor/comfy-table/blob/main/CHANGELOG.md) - [Commits](https://github.com/nukesor/comfy-table/compare/v7.1.0...v7.1.1) --- updated-dependencies: - dependency-name: comfy-table dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 909bef52..f37addc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -683,13 +683,13 @@ dependencies = [ [[package]] name = "comfy-table" -version = "7.1.0" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" dependencies = [ "crossterm", - "strum 0.25.0", - "strum_macros 0.25.3", + "strum", + "strum_macros", "unicode-width", ] @@ -4216,32 +4216,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" - [[package]] name = "strum" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" dependencies = [ - "strum_macros 0.26.1", -] - -[[package]] -name = "strum_macros" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.46", + "strum_macros", ] [[package]] @@ -4323,7 +4304,7 @@ dependencies = [ "spectral", "sqlx", "structopt", - "strum 0.26.2", + "strum", "tempfile", "testcontainers", "thiserror", From a87ffaa63172befb5a174277f5d6380b87c658b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:25:28 +0000 Subject: [PATCH 06/55] build(deps): bump thomaseizinger/keep-a-changelog-new-release Bumps [thomaseizinger/keep-a-changelog-new-release](https://github.com/thomaseizinger/keep-a-changelog-new-release) from 2.0.0 to 3.0.0. - [Release notes](https://github.com/thomaseizinger/keep-a-changelog-new-release/releases) - [Changelog](https://github.com/thomaseizinger/keep-a-changelog-new-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/thomaseizinger/keep-a-changelog-new-release/compare/2.0.0...3.0.0) --- updated-dependencies: - dependency-name: thomaseizinger/keep-a-changelog-new-release dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/draft-new-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 5188be0a..fe857dc9 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -20,7 +20,7 @@ jobs: run: git checkout -b release/${{ github.event.inputs.version }} - name: Update changelog - uses: thomaseizinger/keep-a-changelog-new-release@2.0.0 + uses: thomaseizinger/keep-a-changelog-new-release@3.0.0 with: version: ${{ github.event.inputs.version }} changelogPath: CHANGELOG.md From cf0b9aa601ee64ad94dd05366b224cad1d014803 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:55:30 +0000 Subject: [PATCH 07/55] build(deps): bump pem from 3.0.3 to 3.0.4 Bumps [pem](https://github.com/jcreekmore/pem-rs) from 3.0.3 to 3.0.4. - [Changelog](https://github.com/jcreekmore/pem-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/jcreekmore/pem-rs/compare/v3.0.3...v3.0.4) --- updated-dependencies: - dependency-name: pem dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41d94424..c51e72b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2804,11 +2804,11 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pem" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.21.7", + "base64 0.22.0", "serde", ] From 0d63087ab905edda2f0830ef58d26bfb901714dc Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Wed, 10 Apr 2024 09:53:32 +0200 Subject: [PATCH 08/55] ci: add cargo check job on stable rust --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14c5a835..e2f869f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,3 +172,16 @@ jobs: - name: Run test ${{ matrix.test_name }} run: cargo test --package swap --all-features --test ${{ matrix.test_name }} -- --nocapture + + check_stable: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4.1.2 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2.7.3 + + - name: Run cargo check on stable rust + run: cargo check --all-targets From 4767c684674f6be01f7d21f2adc5109ed2db71f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:34:44 +0000 Subject: [PATCH 09/55] build(deps): bump anyhow from 1.0.81 to 1.0.82 Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.81 to 1.0.82. - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.81...1.0.82) --- updated-dependencies: - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c51e72b2..26e3d2d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arrayref" From a449d9b60061d602e377165d174ab3653a88c2bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 11:48:20 +0000 Subject: [PATCH 10/55] build(deps): bump time from 0.3.34 to 0.3.36 Bumps [time](https://github.com/time-rs/time) from 0.3.34 to 0.3.36. - [Release notes](https://github.com/time-rs/time/releases) - [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) - [Commits](https://github.com/time-rs/time/compare/v0.3.34...v0.3.36) --- updated-dependencies: - dependency-name: time dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26e3d2d5..8619a4d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4308,7 +4308,7 @@ dependencies = [ "tempfile", "testcontainers", "thiserror", - "time 0.3.34", + "time 0.3.36", "tokio", "tokio-socks", "tokio-tar", @@ -4477,9 +4477,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -4500,9 +4500,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -4749,7 +4749,7 @@ checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", "thiserror", - "time 0.3.34", + "time 0.3.36", "tracing-subscriber 0.3.18", ] @@ -4850,7 +4850,7 @@ dependencies = [ "serde_json", "sharded-slab", "thread_local", - "time 0.3.34", + "time 0.3.36", "tracing", "tracing-core", "tracing-log 0.2.0", @@ -5095,7 +5095,7 @@ dependencies = [ "cfg-if 1.0.0", "git2", "rustversion", - "time 0.3.34", + "time 0.3.36", ] [[package]] From 23216dea07d09e9e3a26740a77d7291be790fa48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:00:31 +0000 Subject: [PATCH 11/55] build(deps): bump async-trait from 0.1.79 to 0.1.80 Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.79 to 0.1.80. - [Release notes](https://github.com/dtolnay/async-trait/releases) - [Commits](https://github.com/dtolnay/async-trait/compare/0.1.79...0.1.80) --- updated-dependencies: - dependency-name: async-trait dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8619a4d0..1b03e2ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", From f0f197de20347e4fd682b3a438db4b3799dead6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:24:28 +0000 Subject: [PATCH 12/55] build(deps): bump hyper from 1.2.0 to 1.3.0 Bumps [hyper](https://github.com/hyperium/hyper) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/hyperium/hyper/releases) - [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md) - [Commits](https://github.com/hyperium/hyper/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: hyper dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- swap/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b03e2ba..c25d2c1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1698,9 +1698,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "9f24ce812868d86d19daa79bf3bf9175bc44ea323391147a5e3abde2a283871b" dependencies = [ "bytes", "http 1.0.0", @@ -4278,7 +4278,7 @@ dependencies = [ "futures", "get-port", "hex", - "hyper 1.2.0", + "hyper 1.3.0", "itertools 0.12.1", "libp2p", "mockito", diff --git a/swap/Cargo.toml b/swap/Cargo.toml index b0c31790..68c3dfdb 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -77,7 +77,7 @@ zip = "0.5" [dev-dependencies] bitcoin-harness = "0.2.2" get-port = "3" -hyper = "1.2" +hyper = "1.3" mockito = "1.3.0" monero-harness = { path = "../monero-harness" } port_check = "0.2" From 76c72cceb2c463be5abbea121fcf1b41b6726d2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:27:29 +0000 Subject: [PATCH 13/55] build(deps): bump serde_json from 1.0.115 to 1.0.116 Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.115 to 1.0.116. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.115...v1.0.116) --- updated-dependencies: - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b03e2ba..324ddcf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3765,9 +3765,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", From 6ba11b1738e2b900f99e1c95da50277019acea57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:50:10 +0000 Subject: [PATCH 14/55] build(deps): bump hyper from 1.3.0 to 1.3.1 Bumps [hyper](https://github.com/hyperium/hyper) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/hyperium/hyper/releases) - [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md) - [Commits](https://github.com/hyperium/hyper/compare/v1.3.0...v1.3.1) --- updated-dependencies: - dependency-name: hyper dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f02a73b3..b8cc035c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1698,9 +1698,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f24ce812868d86d19daa79bf3bf9175bc44ea323391147a5e3abde2a283871b" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "http 1.0.0", @@ -4278,7 +4278,7 @@ dependencies = [ "futures", "get-port", "hex", - "hyper 1.3.0", + "hyper 1.3.1", "itertools 0.12.1", "libp2p", "mockito", From 1ff67ec94348b004ea9223a29a5a32f6c1f7c24c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:50:26 +0000 Subject: [PATCH 15/55] build(deps): bump serde from 1.0.197 to 1.0.198 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.197 to 1.0.198. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.197...v1.0.198) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f02a73b3..669fa369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3725,9 +3725,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] @@ -3754,9 +3754,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", From e389027439ce09f0f20779924a73830b73c4c821 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:17:01 +0000 Subject: [PATCH 16/55] build(deps): bump thiserror from 1.0.58 to 1.0.59 Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.58 to 1.0.59. - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.58...1.0.59) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52b0edd2..b1f3d9f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4438,18 +4438,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", From 59974535258c97949d5cf44f31f6eeafa2566f2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:18:11 +0000 Subject: [PATCH 17/55] build(deps): bump actions/checkout from 4.1.2 to 4.1.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.2 to 4.1.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.2...v4.1.3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build-release-binaries.yml | 2 +- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/create-release.yml | 2 +- .github/workflows/draft-new-release.yml | 2 +- .github/workflows/preview-release.yml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index c18c3124..44754dbb 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -45,7 +45,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout tagged commit - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 with: ref: ${{ github.event.release.target_commitish }} token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2f869f3..e9f2ca70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - uses: dtolnay/rust-toolchain@master with: @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - uses: Swatinem/rust-cache@v2.7.3 @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - uses: Swatinem/rust-cache@v2.7.3 @@ -76,7 +76,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout sources - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - uses: Swatinem/rust-cache@v2.7.3 @@ -129,7 +129,7 @@ jobs: tool-cache: false - name: Checkout sources - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - uses: Swatinem/rust-cache@v2.7.3 @@ -166,7 +166,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - uses: Swatinem/rust-cache@v2.7.3 @@ -177,7 +177,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index c62d47eb..a793b43e 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -11,7 +11,7 @@ jobs: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.3 - name: Extract version from branch name id: extract-version diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index fe857dc9..0d066fcf 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -12,7 +12,7 @@ jobs: name: "Draft a new release" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.3 with: token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index f45ff1b4..1e03123a 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -10,7 +10,7 @@ jobs: name: Create preview release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.2 + - uses: actions/checkout@v4.1.3 - name: Delete 'preview' release uses: larryjoelane/delete-release-action@v1.0.24 From 8a88bde4f5f04bab8b199a835435fc51fdbcc036 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:42:46 +0000 Subject: [PATCH 18/55] build(deps): bump actions/checkout from 4.1.3 to 4.1.4 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.3 to 4.1.4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.3...v4.1.4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build-release-binaries.yml | 2 +- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/create-release.yml | 2 +- .github/workflows/draft-new-release.yml | 2 +- .github/workflows/preview-release.yml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 44754dbb..3ee02709 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -45,7 +45,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout tagged commit - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 with: ref: ${{ github.event.release.target_commitish }} token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9f2ca70..1c740eb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - uses: dtolnay/rust-toolchain@master with: @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - uses: Swatinem/rust-cache@v2.7.3 @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - uses: Swatinem/rust-cache@v2.7.3 @@ -76,7 +76,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout sources - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - uses: Swatinem/rust-cache@v2.7.3 @@ -129,7 +129,7 @@ jobs: tool-cache: false - name: Checkout sources - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - uses: Swatinem/rust-cache@v2.7.3 @@ -166,7 +166,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - uses: Swatinem/rust-cache@v2.7.3 @@ -177,7 +177,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index a793b43e..28640153 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -11,7 +11,7 @@ jobs: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.3 + - uses: actions/checkout@v4.1.4 - name: Extract version from branch name id: extract-version diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 0d066fcf..8ff8b6e4 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -12,7 +12,7 @@ jobs: name: "Draft a new release" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.3 + - uses: actions/checkout@v4.1.4 with: token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 1e03123a..7d7be25c 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -10,7 +10,7 @@ jobs: name: Create preview release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.3 + - uses: actions/checkout@v4.1.4 - name: Delete 'preview' release uses: larryjoelane/delete-release-action@v1.0.24 From 24466c0234f02fef1e32a06018dbb3e3d97f66f3 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 30 Apr 2024 10:17:15 +0200 Subject: [PATCH 19/55] ci: fix macos-latest and add macos-12 macos-latest is now aarch64, add macos-12 for x64_64 --- .github/workflows/build-release-binaries.yml | 7 +++++++ .github/workflows/ci.yml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 3ee02709..91c2788f 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -20,6 +20,9 @@ jobs: archive_ext: tar - bin: swap target: x86_64-apple-darwin + os: macos-12 + - bin: swap + target: aarch64-apple-darwin os: macos-latest archive_ext: tar - bin: swap @@ -36,6 +39,10 @@ jobs: archive_ext: tar - bin: asb target: x86_64-apple-darwin + os: macos-12 + archive_ext: tar + - bin: asb + target: aarch64-apple-darwin os: macos-latest archive_ext: tar - bin: asb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c740eb1..8b4f9964 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,8 @@ jobs: - target: armv7-unknown-linux-gnueabihf os: ubuntu-latest - target: x86_64-apple-darwin + os: macos-12 + - target: aarch64-apple-darwin os: macos-latest - target: x86_64-pc-windows-msvc os: windows-latest From 79b4d8efbcdbb160e6e609ec150c8b2f54d7f213 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:28:44 +0000 Subject: [PATCH 20/55] build(deps): bump data-encoding from 2.5.0 to 2.6.0 Bumps [data-encoding](https://github.com/ia0/data-encoding) from 2.5.0 to 2.6.0. - [Commits](https://github.com/ia0/data-encoding/commits) --- updated-dependencies: - dependency-name: data-encoding dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- swap/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1f3d9f9..a205210a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -990,9 +990,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "deranged" diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 68c3dfdb..4cf0e05f 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -23,7 +23,7 @@ comfy-table = "7.1" config = { version = "0.14", default-features = false, features = [ "toml" ] } conquer-once = "0.4" curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" } -data-encoding = "2.5" +data-encoding = "2.6" dialoguer = "0.11" digest = "0.10.7" directories-next = "2" From 1aca4462e7347b2cc036c44a792ad2156a2221e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:29:01 +0000 Subject: [PATCH 21/55] build(deps): bump serde from 1.0.198 to 1.0.199 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.198 to 1.0.199. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.198...v1.0.199) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1f3d9f9..019d8825 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3725,9 +3725,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] @@ -3754,9 +3754,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", From ad6b00beecb1d22db8d81d1c7e047c2c0fab1709 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 11:36:56 +0000 Subject: [PATCH 22/55] build(deps): bump base64 from 0.22.0 to 0.22.1 Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.22.0 to 0.22.1. - [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md) - [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.22.0...v0.22.1) --- updated-dependencies: - dependency-name: base64 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bc6c8aa..2f274631 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,9 +266,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bdk" @@ -2808,7 +2808,7 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "serde", ] @@ -4259,7 +4259,7 @@ dependencies = [ "async-trait", "atty", "backoff", - "base64 0.22.0", + "base64 0.22.1", "bdk", "big-bytes", "bitcoin", From cc3abf647c9296a1a8bccfe6bbea64b36bfcb86e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 11:31:44 +0000 Subject: [PATCH 23/55] build(deps): bump serde from 1.0.199 to 1.0.200 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.199 to 1.0.200. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.199...v1.0.200) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f274631..3a22e80e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3725,9 +3725,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] @@ -3754,9 +3754,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", From ca5bfed3923aee4b0dc65d71b3a4f759b2a82a9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 11:05:47 +0000 Subject: [PATCH 24/55] build(deps): bump thomaseizinger/create-pull-request from 1.3.1 to 1.4.0 Bumps [thomaseizinger/create-pull-request](https://github.com/thomaseizinger/create-pull-request) from 1.3.1 to 1.4.0. - [Release notes](https://github.com/thomaseizinger/create-pull-request/releases) - [Changelog](https://github.com/thomaseizinger/create-pull-request/blob/master/CHANGELOG.md) - [Commits](https://github.com/thomaseizinger/create-pull-request/compare/1.3.1...1.4.0) --- updated-dependencies: - dependency-name: thomaseizinger/create-pull-request dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/draft-new-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 8ff8b6e4..1dd94d10 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -58,7 +58,7 @@ jobs: run: git push origin release/${{ github.event.inputs.version }} --force - name: Create pull request - uses: thomaseizinger/create-pull-request@1.3.1 + uses: thomaseizinger/create-pull-request@1.4.0 with: GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }} head: release/${{ github.event.inputs.version }} From 7661b19300230d795621960ef951d4113752350e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 11:47:38 +0000 Subject: [PATCH 25/55] build(deps): bump tokio-util from 0.7.10 to 0.7.11 Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.10 to 0.7.11. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.10...tokio-util-0.7.11) --- updated-dependencies: - dependency-name: tokio-util dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a22e80e..c22abe2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4651,16 +4651,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite 0.2.13", "tokio", - "tracing", ] [[package]] From ed8deeff57828cbf0e9ae2f8df51e769d66f3754 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 11:26:23 +0000 Subject: [PATCH 26/55] build(deps): bump actions/checkout from 4.1.4 to 4.1.5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.4 to 4.1.5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.4...v4.1.5) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build-release-binaries.yml | 2 +- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/create-release.yml | 2 +- .github/workflows/draft-new-release.yml | 2 +- .github/workflows/preview-release.yml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 91c2788f..a0812e98 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -52,7 +52,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout tagged commit - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 with: ref: ${{ github.event.release.target_commitish }} token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b4f9964..5dd8fd3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - uses: dtolnay/rust-toolchain@master with: @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - uses: Swatinem/rust-cache@v2.7.3 @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - uses: Swatinem/rust-cache@v2.7.3 @@ -78,7 +78,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout sources - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - uses: Swatinem/rust-cache@v2.7.3 @@ -131,7 +131,7 @@ jobs: tool-cache: false - name: Checkout sources - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - uses: Swatinem/rust-cache@v2.7.3 @@ -168,7 +168,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - uses: Swatinem/rust-cache@v2.7.3 @@ -179,7 +179,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 28640153..fc79af38 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -11,7 +11,7 @@ jobs: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - name: Extract version from branch name id: extract-version diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 1dd94d10..9982f130 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -12,7 +12,7 @@ jobs: name: "Draft a new release" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 with: token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 7d7be25c..d654bfc6 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -10,7 +10,7 @@ jobs: name: Create preview release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4.1.5 - name: Delete 'preview' release uses: larryjoelane/delete-release-action@v1.0.24 From d40059192a2bc1c837f7cf1787041cd881210393 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 11:59:43 +0000 Subject: [PATCH 27/55] build(deps): bump anyhow from 1.0.82 to 1.0.83 Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.82 to 1.0.83. - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.82...1.0.83) --- updated-dependencies: - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c22abe2b..e7473808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "arrayref" From 252c394ef616305fd6b3480160fdd124d7bf4b1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 12:01:20 +0000 Subject: [PATCH 28/55] build(deps): bump thiserror from 1.0.59 to 1.0.60 Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.59 to 1.0.60. - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.59...1.0.60) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c22abe2b..7812407d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4438,18 +4438,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", From f1d78593342d70e8a975a6d36183747220a3369b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 11:58:50 +0000 Subject: [PATCH 29/55] build(deps): bump serde_json from 1.0.116 to 1.0.117 Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.116 to 1.0.117. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.116...v1.0.117) --- updated-dependencies: - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7644ba9c..2cd36698 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3765,9 +3765,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", From 081bb965eec98b062268ddd1a32e457f2c1fc81b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 11:59:51 +0000 Subject: [PATCH 30/55] build(deps): bump serde from 1.0.200 to 1.0.201 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.200 to 1.0.201. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.200...v1.0.201) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7644ba9c..3161afd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3725,9 +3725,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] @@ -3754,9 +3754,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", From d9de1b6cf36fe449feddb44ab5420cde9f9b28fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 11:33:15 +0000 Subject: [PATCH 31/55] build(deps): bump serde from 1.0.201 to 1.0.202 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.201 to 1.0.202. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.201...v1.0.202) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e85e364..34fdc50b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3725,9 +3725,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.201" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] @@ -3754,9 +3754,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", From 6edcc28b8def128b952644a33f1ded66690fcea4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 11:18:16 +0000 Subject: [PATCH 32/55] build(deps): bump toml from 0.8.12 to 0.8.13 Bumps [toml](https://github.com/toml-rs/toml) from 0.8.12 to 0.8.13. - [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.12...toml-v0.8.13) --- updated-dependencies: - dependency-name: toml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34fdc50b..12c5b916 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -703,7 +703,7 @@ dependencies = [ "nom", "pathdiff", "serde", - "toml 0.8.12", + "toml 0.8.13", ] [[package]] @@ -3776,9 +3776,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -4314,7 +4314,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite", "tokio-util", - "toml 0.8.12", + "toml 0.8.13", "torut", "tracing", "tracing-appender", @@ -4673,9 +4673,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "serde", "serde_spanned", @@ -4685,18 +4685,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.8" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap 2.1.0", "serde", From c8e5768955645dfe275c70745b0854ac4caf103b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 11:12:37 +0000 Subject: [PATCH 33/55] build(deps): bump actions/checkout from 4.1.5 to 4.1.6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.5 to 4.1.6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.5...v4.1.6) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build-release-binaries.yml | 2 +- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/create-release.yml | 2 +- .github/workflows/draft-new-release.yml | 2 +- .github/workflows/preview-release.yml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index a0812e98..ecfde0c8 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -52,7 +52,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout tagged commit - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 with: ref: ${{ github.event.release.target_commitish }} token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dd8fd3d..2a41dd6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - uses: dtolnay/rust-toolchain@master with: @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - uses: Swatinem/rust-cache@v2.7.3 @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - uses: Swatinem/rust-cache@v2.7.3 @@ -78,7 +78,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout sources - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - uses: Swatinem/rust-cache@v2.7.3 @@ -131,7 +131,7 @@ jobs: tool-cache: false - name: Checkout sources - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - uses: Swatinem/rust-cache@v2.7.3 @@ -168,7 +168,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - uses: Swatinem/rust-cache@v2.7.3 @@ -179,7 +179,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index fc79af38..2e52519a 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -11,7 +11,7 @@ jobs: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.5 + - uses: actions/checkout@v4.1.6 - name: Extract version from branch name id: extract-version diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 9982f130..48fb2d3a 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -12,7 +12,7 @@ jobs: name: "Draft a new release" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.5 + - uses: actions/checkout@v4.1.6 with: token: ${{ secrets.BOTTY_GITHUB_TOKEN }} diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index d654bfc6..63ecfe6f 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -10,7 +10,7 @@ jobs: name: Create preview release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.5 + - uses: actions/checkout@v4.1.6 - name: Delete 'preview' release uses: larryjoelane/delete-release-action@v1.0.24 From 0e5241787fbac75ac92bc3ab48617c6ab4109efa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 May 2024 11:58:52 +0000 Subject: [PATCH 34/55] build(deps): bump itertools from 0.12.1 to 0.13.0 Bumps [itertools](https://github.com/rust-itertools/itertools) from 0.12.1 to 0.13.0. - [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-itertools/itertools/compare/v0.12.1...v0.13.0) --- updated-dependencies: - dependency-name: itertools dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- swap/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34fdc50b..ed8efa66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1838,9 +1838,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -4279,7 +4279,7 @@ dependencies = [ "get-port", "hex", "hyper 1.3.1", - "itertools 0.12.1", + "itertools 0.13.0", "libp2p", "mockito", "monero", diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 4cf0e05f..22705715 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -31,7 +31,7 @@ ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = ed25519-dalek = "1" futures = { version = "0.3", default-features = false } hex = "0.4" -itertools = "0.12" +itertools = "0.13" libp2p = { version = "0.42.2", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous", "identify" ] } monero = { version = "0.12", features = [ "serde_support" ] } monero-rpc = { path = "../monero-rpc" } From 8d8c938a940df44daed83bef64a75ec5cca0cd65 Mon Sep 17 00:00:00 2001 From: icy-ux <63157298+icy-ux@users.noreply.github.com> Date: Mon, 20 May 2024 08:28:44 +0000 Subject: [PATCH 35/55] document use of asb export-bitcoin-wallet (#1280) * document use of asb export-bitcoin-wallet Add to the `asb` document @delta1's text on exporting the Bitcoin wallet descriptor and importing the descriptor into Sparrow Wallet. TODO: maybe it would make sense to add the images to Git, instead of linking to files on Github? * bring asb/README.md into compliance with dprint * update bullet point formatting in asb/README.md * run * add images to git --------- Co-authored-by: icyfestive <> Co-authored-by: Byron Hambly --- docs/asb/README.md | 35 ++++++++++++++++++++++++++ docs/asb/enter-master-private-key.png | Bin 0 -> 35936 bytes docs/asb/import-keystore.png | Bin 0 -> 186436 bytes docs/asb/transactions-tab.png | Bin 0 -> 111456 bytes 4 files changed, 35 insertions(+) create mode 100644 docs/asb/enter-master-private-key.png create mode 100644 docs/asb/import-keystore.png create mode 100644 docs/asb/transactions-tab.png diff --git a/docs/asb/README.md b/docs/asb/README.md index 16e0a602..b1acc877 100644 --- a/docs/asb/README.md +++ b/docs/asb/README.md @@ -164,3 +164,38 @@ May 01 01:32:05.018 INFO Tor found. Setting up hidden service. May 01 01:32:07.475 INFO /onion3/z4findrdwtfbpoq64ayjtmxvr52vvxnsynerlenlfkmm52dqxsl4deyd:9939 May 01 01:32:07.476 INFO /onion3/z4findrdwtfbpoq64ayjtmxvr52vvxnsynerlenlfkmm52dqxsl4deyd:9940 ``` + +### Exporting the Bitcoin wallet descriptor + +First use `swap` or `asb` with the `export-bitcoin-wallet` subcommand. + +Output example: + +```json +{"descriptor":"wpkh(tprv8Zgredacted.../84'/1'/0'/0/*)","blockheight":2415616,"label":"asb-testnet"} +``` + +The wallet can theoretically be directly imported into +[bdk-cli](https://bitcoindevkit.org/bdk-cli/installation/) but it is easier to +use Sparrow Wallet. + +Sparrow wallet import works as follows: + +- File -> New wallet -> Give it a name +- Select "New or Imported Software Wallet" +- Click "Enter Private Key" for "Master Private Key (BIP32)" +- Enter the `xprv...` or `tprv...` part of the descriptor (example above is `tprv8Zgredacted...`: + +![image](enter-master-private-key.png) + +- Click "Import" +- Leave the derivation path as `m/84'/0'/0'` and click "Import Keystore" button +- Click "Apply" and then supply password + +![image](import-keystore.png) + +- Click Transactions tab +- ??? +- Profit! + +![image](transactions-tab.png) diff --git a/docs/asb/enter-master-private-key.png b/docs/asb/enter-master-private-key.png new file mode 100644 index 0000000000000000000000000000000000000000..7b69b5502d87a47c220458e3f71cbe4c3d46519b GIT binary patch literal 35936 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

juV7!m>J$0I2f#*5zabva!qa3& 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

+YjgTPXg*kerFDhTDlE%r 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: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%0A;Zu{5ku- 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 literal 0 HcmV?d00001 diff --git a/docs/asb/transactions-tab.png b/docs/asb/transactions-tab.png new file mode 100644 index 0000000000000000000000000000000000000000..7b0532aff6b4580ca2b1952931f293e8f5019103 GIT binary patch literal 111456 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 literal 0 HcmV?d00001 From 3bb6f060a049520b8d7d6fad48f54a9f5eb0fc85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 11:30:15 +0000 Subject: [PATCH 36/55] build(deps): bump thiserror from 1.0.60 to 1.0.61 Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.60 to 1.0.61. - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.60...1.0.61) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed8efa66..e46ceb7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4438,18 +4438,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", From 928fe33532aa93b740e942663a827ca39871665c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 11:31:28 +0000 Subject: [PATCH 37/55] build(deps): bump anyhow from 1.0.83 to 1.0.86 Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.83 to 1.0.86. - [Release notes](https://github.com/dtolnay/anyhow/releases) - [Commits](https://github.com/dtolnay/anyhow/compare/1.0.83...1.0.86) --- updated-dependencies: - dependency-name: anyhow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed8efa66..85f1523c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arrayref" From 5ff46be2793bc62278ded2a4c02a4502ea6e99ba Mon Sep 17 00:00:00 2001 From: yamabiiko Date: Wed, 22 May 2024 16:12:58 +0300 Subject: [PATCH 38/55] RPC server for API Interface (#1276) * saving: implementing internal api shared by cli and rpc server * writing async rpc methods and using arc for shared struct references * cleaning up, renamed Init to Context * saving: cleaning up and initial work for tests * Respond with bitcoin withdraw txid * Print RPC server address * Cleanup, formatting, add `get_seller`, `get_swap_start_date` RPC endpoints * fixing tests in cli module * uncommenting and fixing more tests * split api module and propagate errors with rpc server * moving methods to api and validating addresses for rpc * add broadcast channel to handle shutdowns gracefully and prepare for RPC server test * added files * Update rpc.rs * adding new unfinished RPC tests * updating rpc-server tests * fixing warnings * fixing formatting and cargo clippy warnings * fix missing import in test * fix: add data_dir to config to make config command work * set server listen address manually and return file locations in JSON on Config * Add called api method and swap_id to tracing for context, reduced boilerplate * Pass server_address properly to RpcServer * Update Cargo.lock * dprint fmt * Add cancel_refund RPC endpoint * Combine Cmd and Params * Disallow concurrent swaps * Use RwLock instead of Mutex to allow for parallel reads and add get_current_swap endpoint * Return wallet descriptor to RPC API caller * Append all cli logs to single log file After careful consideration, I've concluded that it's not practical/possible to ensure that the previous behaviour (one log file per swap) is preserved due to limitations of the tracing-subscriber crate and a big in the built in JSON formatter * Add get_swap_expired_timelock timelock, other small refactoring - Add get_swap_expired_timelock endpoint to return expired timelock if one exists. Fails if bitcoin lock tx has not yet published or if swap is already finished. - Rename current_epoch to expired_timelock to enforce consistent method names - Add blocks left until current expired timelock expires (next timelock expires) to ExpiredTimelock struct - Change .expect() to .unwrap() in rpc server method register because those will only fail if we register the same method twice which will never happen * initiating swaps in a separate task and handling shutdown signals with broadcast queues * Replace get_swap_start_date, get_seller, get_expired_timelock with one get_swap_info rpc method * WIP: Struct for concurrent swaps manager * Ensure correct tracing spans * Add note regarding Request, Method structs * Update request.rs * Add tracing span attribute log_reference_id to logs caused by rpc call * Sync bitcoin wallet before initial max_giveable call * use Span::current() to pass down to tracing span to spawned tasks * Remove unused shutdown channel * Add `get_monero_recovery_info` RPC endpoint - Add `get_monero_recovery_info` RPC endpoint - format PrivateViewKey using Display * Rename `Method::RawHistory` to `Method::GetRawStates` * Wait for swap to be suspended after sending signal * Remove notes * Add tracing span attribute log_reference_id to logs caused by rpc call * Sync bitcoin wallet before initial max_giveable call * use Span::current() to pass down to tracing span to spawned tasks * Remove unused shutdown channel * Add `get_monero_recovery_info` RPC endpoint - Add `get_monero_recovery_info` RPC endpoint - format PrivateViewKey using Display * Rename `Method::RawHistory` to `Method::GetRawStates` * Wait for swap to be suspended after sending signal * Return additonal info on GetSwapInfo * Update wallet.rs * fix compile issues for tests and use serial_test crate * fix rpc tests, only check for RPC errors and not returned values * Rename `get_raw_history` tp `get_raw_states` * Fix typo in rpc server stopped tracing log * Remove unnecessary success property on suspend_current_swap response * fixing test_cli_arguments and other tests * WIP: RPC server integration tests * WIP: Integration tests for RPC server * Update rpc tests * fix compile and warnings in tests/rpc.rs * test: fix assert * clippy --fix * remove otp file * cargo clippy fixes * move resume swap initialization code out of spawned task * Use `in_current_span` to pass down tracing span to spawned tasks * moving buy_xmr initialization code out of spawned tasks * cargo fmt * Moving swap initialization code inside tokio select block to handle swap lock release logic * Remove unnecessary swap suspension listener from determine_btc_to_swap call in BuyXmr * Spawn event loop before requesting quote * Release swap lock after receiving shutdown signal * Remove inner tokio::select in BuyXmr and Resume * Improve debug text for swap resume * Return error to API caller if bid quote request fails * Print error if one occurs during process invoked by API call * Return bid quote to API caller * Use type safe query! macro for database retrieval of states * Return tx_lock_fee to API caller on GetSwapInfo call Update request.rs * Allow API caller to retrieve last synced bitcoin balane and avoid costly sync * Return restore height on MoneroRecovery command to API Caller * Include entire error cause-chain in API response * Add span to bitcoin wallet logs * Log event loop connection properties as tracing fields * Wait for background tasks to complete before exiting CLI * clippy * specify sqlx patch version explicitly * remove mem::forget and replace with _guard * ci: add rpc test job * test: wrap rpc test in #[cfg(test)] * add missing tokio::test attribute * fix and merge rpc tests, parse uuuid and multiaddr from serde_json value * default Tor socks port to 9050, Cargo fmt * Update swap/sqlite_dev_setup.sh: add version Co-authored-by: Byron Hambly * ci: free up space on ubuntu test job * Update swap/src/bitcoin/wallet.rs Co-authored-by: Byron Hambly * Update swap/src/bitcoin/wallet.rs Co-authored-by: Byron Hambly * fmt --------- Co-authored-by: binarybaron <86064887+binarybaron@users.noreply.github.com> Co-authored-by: Byron Hambly --- .github/workflows/ci.yml | 11 + Cargo.lock | 230 ++++- swap/Cargo.toml | 8 +- swap/sqlite_dev_setup.sh | 3 +- swap/sqlx-data.json | 96 +- swap/src/api.rs | 460 ++++++++++ swap/src/api/request.rs | 930 ++++++++++++++++++++ swap/src/bin/swap.rs | 706 +-------------- swap/src/bitcoin.rs | 66 +- swap/src/bitcoin/cancel.rs | 12 + swap/src/bitcoin/lock.rs | 12 +- swap/src/bitcoin/timelocks.rs | 6 +- swap/src/bitcoin/wallet.rs | 64 +- swap/src/cli/cancel_and_refund.rs | 6 +- swap/src/cli/command.rs | 1347 ++++++++++++++--------------- swap/src/cli/event_loop.rs | 13 +- swap/src/cli/tracing.rs | 82 +- swap/src/database/sqlite.rs | 87 +- swap/src/lib.rs | 2 + swap/src/monero.rs | 53 ++ swap/src/protocol.rs | 4 + swap/src/protocol/alice/swap.rs | 6 +- swap/src/protocol/bob/state.rs | 19 +- swap/src/protocol/bob/swap.rs | 12 +- swap/src/rpc.rs | 31 + swap/src/rpc/methods.rs | 236 +++++ swap/src/seed.rs | 2 +- swap/tests/harness/mod.rs | 36 +- swap/tests/rpc.rs | 436 ++++++++++ 29 files changed, 3471 insertions(+), 1505 deletions(-) create mode 100644 swap/src/api.rs create mode 100644 swap/src/api/request.rs create mode 100644 swap/src/rpc.rs create mode 100644 swap/src/rpc/methods.rs create mode 100644 swap/tests/rpc.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a41dd6d..da82b266 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,6 +175,17 @@ jobs: - name: Run test ${{ matrix.test_name }} run: cargo test --package swap --all-features --test ${{ matrix.test_name }} -- --nocapture + rpc_tests: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4.1.1 + + - uses: Swatinem/rust-cache@v2.7.1 + + - name: Run RPC server tests + run: cargo test --package swap --all-features --test rpc -- --nocapture + check_stable: runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 0767d24f..1c9b370c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-lock" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +dependencies = [ + "event-listener", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -308,6 +317,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +dependencies = [ + "serde", +] + [[package]] name = "big-bytes" version = "1.0.0" @@ -519,6 +537,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.6.1" @@ -1479,6 +1507,19 @@ dependencies = [ "url", ] +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + [[package]] name = "h2" version = "0.3.18" @@ -1566,9 +1607,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" [[package]] name = "hex" @@ -1818,13 +1859,13 @@ checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.1", - "rustix", - "windows-sys 0.48.0", + "hermit-abi 0.3.8", + "libc", + "windows-sys 0.52.0", ] [[package]] @@ -1893,6 +1934,115 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "jsonrpsee" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d291e3a5818a2384645fd9756362e6d89cf0541b0b916fa7702ea4a9833608e" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-server", + "jsonrpsee-types", + "jsonrpsee-ws-client", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965de52763f2004bc91ac5bcec504192440f0b568a5d621c59d9dbd6f886c3fb" +dependencies = [ + "futures-util", + "http 0.2.11", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project 1.0.5", + "rustls-native-certs 0.6.3", + "soketto", + "thiserror", + "tokio", + "tokio-rustls 0.23.1", + "tokio-util", + "tracing", + "webpki-roots 0.22.2", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e70b4439a751a5de7dd5ed55eacff78ebf4ffe0fc009cb1ebb11417f5b536b" +dependencies = [ + "anyhow", + "arrayvec", + "async-lock", + "async-trait", + "beef", + "futures-channel", + "futures-timer", + "futures-util", + "globset", + "hyper 0.14.28", + "jsonrpsee-types", + "parking_lot 0.12.0", + "rand 0.8.3", + "rustc-hash", + "serde", + "serde_json", + "soketto", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb69dad85df79527c019659a992498d03f8495390496da2f07e6c24c2b356fc" +dependencies = [ + "futures-channel", + "futures-util", + "http 0.2.11", + "hyper 0.14.28", + "jsonrpsee-core", + "jsonrpsee-types", + "serde", + "serde_json", + "soketto", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd522fe1ce3702fd94812965d7bb7a3364b1c9aba743944c5a00529aae80f8c" +dependencies = [ + "anyhow", + "beef", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b83daeecfc6517cfe210df24e570fb06213533dfb990318fae781f4c7119dd9" +dependencies = [ + "http 0.2.11", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + [[package]] name = "keccak" version = "0.1.0" @@ -3470,6 +3620,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -3553,6 +3709,18 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.0" @@ -3723,6 +3891,21 @@ dependencies = [ "pest", ] +[[package]] +name = "sequential-macro" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5facc5f409a55d25bf271c853402a00e1187097d326757043f5dd711944d07" + +[[package]] +name = "sequential-test" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d9c0d773bc7e7733264f460e5dfa00b2510421ddd6284db0749eef8dfb79e9" +dependencies = [ + "sequential-macro", +] + [[package]] name = "serde" version = "1.0.202" @@ -3936,9 +4119,9 @@ checksum = "0f0242b8e50dd9accdd56170e94ca1ebd223b098eb9c83539a6e367d0f36ae68" [[package]] name = "similar" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" [[package]] name = "slab" @@ -4019,14 +4202,15 @@ dependencies = [ [[package]] name = "soketto" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "083624472e8817d44d02c0e55df043737ff11f279af924abdf93845717c2b75c" +checksum = "41d1c5305e39e09653383c2c7244f2f78b3bcae37cf50c64cb4789c9f5096ec2" dependencies = [ "base64 0.13.1", "bytes", "flate2", "futures", + "http 0.2.11", "httparse", "log", "rand 0.8.3", @@ -4280,6 +4464,8 @@ dependencies = [ "hex", "hyper 1.3.1", "itertools 0.13.0", + "jsonrpsee", + "jsonrpsee-core", "libp2p", "mockito", "monero", @@ -4294,6 +4480,7 @@ dependencies = [ "reqwest", "rust_decimal", "rust_decimal_macros", + "sequential-test", "serde", "serde_cbor", "serde_json", @@ -4657,6 +4844,7 @@ checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite 0.2.13", "tokio", @@ -4723,6 +4911,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.1" @@ -4735,6 +4940,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite 0.2.13", "tracing-attributes", "tracing-core", @@ -4921,7 +5127,7 @@ dependencies = [ "log", "rand 0.8.3", "rustls 0.19.0", - "rustls-native-certs", + "rustls-native-certs 0.5.0", "sha-1", "thiserror", "url", diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 22705715..b73d61bb 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -32,6 +32,8 @@ ed25519-dalek = "1" futures = { version = "0.3", default-features = false } hex = "0.4" itertools = "0.13" +jsonrpsee = { version = "0.16.2", features = [ "server" ] } +jsonrpsee-core = "0.16.2" libp2p = { version = "0.42.2", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous", "identify" ] } monero = { version = "0.12", features = [ "serde_support" ] } monero-rpc = { path = "../monero-rpc" } @@ -49,12 +51,12 @@ serde_json = "1" serde_with = { version = "1", features = [ "macros" ] } sha2 = "0.10" sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde", "secp256k1", "alloc" ] } -sqlx = { version = "0.6", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] } +sqlx = { version = "0.6.3", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] } structopt = "0.3" strum = { version = "0.26", features = [ "derive" ] } thiserror = "1" time = "0.3" -tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs", "net" ] } +tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "parking_lot" ] } tokio-socks = "0.5" tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] } tokio-util = { version = "0.7", features = [ "io", "codec" ] } @@ -78,10 +80,12 @@ zip = "0.5" bitcoin-harness = "0.2.2" get-port = "3" hyper = "1.3" +jsonrpsee = { version = "0.16.2", features = [ "ws-client" ] } mockito = "1.3.0" monero-harness = { path = "../monero-harness" } port_check = "0.2" proptest = "1" +sequential-test = "0.2.4" serde_cbor = "0.11" serial_test = "3.0" spectral = "0.6" diff --git a/swap/sqlite_dev_setup.sh b/swap/sqlite_dev_setup.sh index 67d2c9da..a30adaff 100755 --- a/swap/sqlite_dev_setup.sh +++ b/swap/sqlite_dev_setup.sh @@ -1,7 +1,8 @@ #!/bin/bash # run this script from the swap dir -# make sure you have sqlx-cli installed: cargo install sqlx-cli +# make sure you have sqlx-cli installed: cargo install --version 0.6.3 sqlx-cli +# it's advised for the sqlx-cli to be the same version as specified in cargo.toml # this script creates a temporary sqlite database # then runs the migration scripts to create the tables (migrations folder) diff --git a/swap/sqlx-data.json b/swap/sqlx-data.json index fd50cd87..f24a50e6 100644 --- a/swap/sqlx-data.json +++ b/swap/sqlx-data.json @@ -28,6 +28,24 @@ }, "query": "\n insert into peer_addresses (\n peer_id,\n address\n ) values (?, ?);\n " }, + "0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c": { + "describe": { + "columns": [ + { + "name": "start_date", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT min(entered_at) as start_date\n FROM swap_states\n WHERE swap_id = ?\n " + }, "1ec38c85e7679b2eb42b3df75d9098772ce44fdb8db3012d3c2410d828b74157": { "describe": { "columns": [ @@ -62,6 +80,30 @@ }, "query": "\n insert into peers (\n swap_id,\n peer_id\n ) values (?, ?);\n " }, + "3f2bfdd2d134586ccad22171cd85a465800fc5c4fdaf191d206974e530240c87": { + "describe": { + "columns": [ + { + "name": "swap_id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "state", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Right": 0 + } + }, + "query": "\n SELECT swap_id, state\n FROM swap_states\n " + }, "50a5764546f69c118fa0b64120da50f51073d36257d49768de99ff863e3511e0": { "describe": { "columns": [], @@ -90,24 +132,6 @@ }, "query": "\n SELECT state\n FROM swap_states\n WHERE swap_id = ?\n ORDER BY id desc\n LIMIT 1;\n\n " }, - "a0eb85d04ee3842c52291dad4d225941d1141af735922fcbc665868997fce304": { - "describe": { - "columns": [ - { - "name": "address", - "ordinal": 0, - "type_info": "Text" - } - ], - "nullable": [ - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT address\n FROM peer_addresses\n WHERE peer_id = ?\n " - }, "b703032b4ddc627a1124817477e7a8e5014bdc694c36a14053ef3bb2fc0c69b0": { "describe": { "columns": [], @@ -135,5 +159,41 @@ } }, "query": "\n SELECT address\n FROM monero_addresses\n WHERE swap_id = ?\n " + }, + "d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2": { + "describe": { + "columns": [ + { + "name": "address", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT DISTINCT address\n FROM peer_addresses\n WHERE peer_id = ?\n " + }, + "e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646": { + "describe": { + "columns": [ + { + "name": "state", + "ordinal": 0, + "type_info": "Text" + } + ], + "nullable": [ + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT state\n FROM swap_states\n WHERE swap_id = ?\n " } } \ No newline at end of file diff --git a/swap/src/api.rs b/swap/src/api.rs new file mode 100644 index 00000000..ca3ae21d --- /dev/null +++ b/swap/src/api.rs @@ -0,0 +1,460 @@ +pub mod request; +use crate::cli::command::{Bitcoin, Monero, Tor}; +use crate::database::open_db; +use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; +use crate::fs::system_data_dir; +use crate::network::rendezvous::XmrBtcNamespace; +use crate::protocol::Database; +use crate::seed::Seed; +use crate::{bitcoin, cli, monero}; +use anyhow::{bail, Context as AnyContext, Error, Result}; +use futures::future::try_join_all; +use std::fmt; +use std::future::Future; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::{Arc, Once}; +use tokio::sync::{broadcast, broadcast::Sender, Mutex, RwLock}; +use tokio::task::JoinHandle; +use url::Url; + +static START: Once = Once::new(); + +#[derive(Clone, PartialEq, Debug)] +pub struct Config { + tor_socks5_port: u16, + namespace: XmrBtcNamespace, + server_address: Option, + pub env_config: EnvConfig, + seed: Option, + debug: bool, + json: bool, + data_dir: PathBuf, + is_testnet: bool, +} + +use uuid::Uuid; + +#[derive(Default)] +pub struct PendingTaskList(Mutex>>); + +impl PendingTaskList { + pub async fn spawn(&self, future: F) + where + F: Future + Send + 'static, + T: Send + 'static, + { + let handle = tokio::spawn(async move { + let _ = future.await; + }); + + self.0.lock().await.push(handle); + } + + pub async fn wait_for_tasks(&self) -> Result<()> { + let tasks = { + // Scope for the lock, to avoid holding it for the entire duration of the async block + let mut guard = self.0.lock().await; + guard.drain(..).collect::>() + }; + + try_join_all(tasks).await?; + + Ok(()) + } +} + +pub struct SwapLock { + current_swap: RwLock>, + suspension_trigger: Sender<()>, +} + +impl SwapLock { + pub fn new() -> Self { + let (suspension_trigger, _) = broadcast::channel(10); + SwapLock { + current_swap: RwLock::new(None), + suspension_trigger, + } + } + + pub async fn listen_for_swap_force_suspension(&self) -> Result<(), Error> { + let mut listener = self.suspension_trigger.subscribe(); + let event = listener.recv().await; + match event { + Ok(_) => Ok(()), + Err(e) => { + tracing::error!("Error receiving swap suspension signal: {}", e); + bail!(e) + } + } + } + + pub async fn acquire_swap_lock(&self, swap_id: Uuid) -> Result<(), Error> { + let mut current_swap = self.current_swap.write().await; + if current_swap.is_some() { + bail!("There already exists an active swap lock"); + } + + tracing::debug!(swap_id = %swap_id, "Acquiring swap lock"); + *current_swap = Some(swap_id); + Ok(()) + } + + pub async fn get_current_swap_id(&self) -> Option { + *self.current_swap.read().await + } + + /// Sends a signal to suspend all ongoing swap processes. + /// + /// This function performs the following steps: + /// 1. Triggers the suspension by sending a unit `()` signal to all listeners via `self.suspension_trigger`. + /// 2. Polls the `current_swap` state every 50 milliseconds to check if it has been set to `None`, indicating that the swap processes have been suspended and the lock released. + /// 3. If the lock is not released within 10 seconds, the function returns an error. + /// + /// If we send a suspend signal while no swap is in progress, the function will not fail, but will return immediately. + /// + /// # Returns + /// - `Ok(())` if the swap lock is successfully released. + /// - `Err(Error)` if the function times out waiting for the swap lock to be released. + /// + /// # Notes + /// The 50ms polling interval is considered negligible overhead compared to the typical time required to suspend ongoing swap processes. + pub async fn send_suspend_signal(&self) -> Result<(), Error> { + const TIMEOUT: u64 = 10_000; + const INTERVAL: u64 = 50; + + let _ = self.suspension_trigger.send(())?; + + for _ in 0..(TIMEOUT / INTERVAL) { + if self.get_current_swap_id().await.is_none() { + return Ok(()); + } + tokio::time::sleep(tokio::time::Duration::from_millis(INTERVAL)).await; + } + + bail!("Timed out waiting for swap lock to be released"); + } + + pub async fn release_swap_lock(&self) -> Result { + let mut current_swap = self.current_swap.write().await; + if let Some(swap_id) = current_swap.as_ref() { + tracing::debug!(swap_id = %swap_id, "Releasing swap lock"); + + let prev_swap_id = *swap_id; + *current_swap = None; + drop(current_swap); + Ok(prev_swap_id) + } else { + bail!("There is no current swap lock to release"); + } + } +} + +impl Default for SwapLock { + fn default() -> Self { + Self::new() + } +} + +// workaround for warning over monero_rpc_process which we must own but not read +#[allow(dead_code)] +pub struct Context { + pub db: Arc, + bitcoin_wallet: Option>, + monero_wallet: Option>, + monero_rpc_process: Option, + pub swap_lock: Arc, + pub config: Config, + pub tasks: Arc, +} + +#[allow(clippy::too_many_arguments)] +impl Context { + pub async fn build( + bitcoin: Option, + monero: Option, + tor: Option, + data: Option, + is_testnet: bool, + debug: bool, + json: bool, + server_address: Option, + ) -> Result { + let data_dir = data::data_dir_from(data, is_testnet)?; + let env_config = env_config_from(is_testnet); + + let seed = Seed::from_file_or_generate(data_dir.as_path()) + .context("Failed to read seed in file")?; + + let bitcoin_wallet = { + if let Some(bitcoin) = bitcoin { + let (bitcoin_electrum_rpc_url, bitcoin_target_block) = + bitcoin.apply_defaults(is_testnet)?; + Some(Arc::new( + init_bitcoin_wallet( + bitcoin_electrum_rpc_url, + &seed, + data_dir.clone(), + env_config, + bitcoin_target_block, + ) + .await?, + )) + } else { + None + } + }; + + let (monero_wallet, monero_rpc_process) = { + if let Some(monero) = monero { + let monero_daemon_address = monero.apply_defaults(is_testnet); + let (wlt, prc) = + init_monero_wallet(data_dir.clone(), monero_daemon_address, env_config).await?; + (Some(Arc::new(wlt)), Some(prc)) + } else { + (None, None) + } + }; + + let tor_socks5_port = tor.map_or(9050, |tor| tor.tor_socks5_port); + + START.call_once(|| { + let _ = cli::tracing::init(debug, json, data_dir.join("logs")); + }); + + let context = Context { + db: open_db(data_dir.join("sqlite")).await?, + bitcoin_wallet, + monero_wallet, + monero_rpc_process, + config: Config { + tor_socks5_port, + namespace: XmrBtcNamespace::from_is_testnet(is_testnet), + env_config, + seed: Some(seed), + server_address, + debug, + json, + is_testnet, + data_dir, + }, + swap_lock: Arc::new(SwapLock::new()), + tasks: Arc::new(PendingTaskList::default()), + }; + + Ok(context) + } + + pub async fn for_harness( + seed: Seed, + env_config: EnvConfig, + db_path: PathBuf, + bob_bitcoin_wallet: Arc, + bob_monero_wallet: Arc, + ) -> Self { + let config = Config::for_harness(seed, env_config); + + Self { + bitcoin_wallet: Some(bob_bitcoin_wallet), + monero_wallet: Some(bob_monero_wallet), + config, + db: open_db(db_path) + .await + .expect("Could not open sqlite database"), + monero_rpc_process: None, + swap_lock: Arc::new(SwapLock::new()), + tasks: Arc::new(PendingTaskList::default()), + } + } +} + +impl fmt::Debug for Context { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "") + } +} + +async fn init_bitcoin_wallet( + electrum_rpc_url: Url, + seed: &Seed, + data_dir: PathBuf, + env_config: EnvConfig, + bitcoin_target_block: usize, +) -> Result { + let wallet_dir = data_dir.join("wallet"); + + let wallet = bitcoin::Wallet::new( + electrum_rpc_url.clone(), + &wallet_dir, + seed.derive_extended_private_key(env_config.bitcoin_network)?, + env_config, + bitcoin_target_block, + ) + .await + .context("Failed to initialize Bitcoin wallet")?; + + wallet.sync().await?; + + Ok(wallet) +} + +async fn init_monero_wallet( + data_dir: PathBuf, + monero_daemon_address: String, + env_config: EnvConfig, +) -> Result<(monero::Wallet, monero::WalletRpcProcess)> { + let network = env_config.monero_network; + + const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet"; + + let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?; + + let monero_wallet_rpc_process = monero_wallet_rpc + .run(network, Some(monero_daemon_address)) + .await?; + + let monero_wallet = monero::Wallet::open_or_create( + monero_wallet_rpc_process.endpoint(), + MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(), + env_config, + ) + .await?; + + Ok((monero_wallet, monero_wallet_rpc_process)) +} + +mod data { + use super::*; + + pub fn data_dir_from(arg_dir: Option, testnet: bool) -> Result { + let base_dir = match arg_dir { + Some(custom_base_dir) => custom_base_dir, + None => os_default()?, + }; + + let sub_directory = if testnet { "testnet" } else { "mainnet" }; + + Ok(base_dir.join(sub_directory)) + } + + fn os_default() -> Result { + Ok(system_data_dir()?.join("cli")) + } +} + +fn env_config_from(testnet: bool) -> EnvConfig { + if testnet { + Testnet::get_config() + } else { + Mainnet::get_config() + } +} + +impl Config { + pub fn for_harness(seed: Seed, env_config: EnvConfig) -> Self { + let data_dir = data::data_dir_from(None, false).expect("Could not find data directory"); + + Self { + tor_socks5_port: 9050, + namespace: XmrBtcNamespace::from_is_testnet(false), + server_address: None, + env_config, + seed: Some(seed), + debug: false, + json: false, + is_testnet: false, + data_dir, + } + } +} + +#[cfg(test)] +pub mod api_test { + use super::*; + use crate::api::request::{Method, Request}; + + use libp2p::Multiaddr; + use std::str::FromStr; + use uuid::Uuid; + + pub const MULTI_ADDRESS: &str = + "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; + pub const MONERO_STAGENET_ADDRESS: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a"; + pub const BITCOIN_TESTNET_ADDRESS: &str = "tb1qr3em6k3gfnyl8r7q0v7t4tlnyxzgxma3lressv"; + pub const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa"; + pub const BITCOIN_MAINNET_ADDRESS: &str = "bc1qe4epnfklcaa0mun26yz5g8k24em5u9f92hy325"; + pub const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b"; + + impl Config { + pub fn default( + is_testnet: bool, + data_dir: Option, + debug: bool, + json: bool, + ) -> Self { + let data_dir = data::data_dir_from(data_dir, is_testnet).unwrap(); + + let seed = Seed::from_file_or_generate(data_dir.as_path()).unwrap(); + + let env_config = env_config_from(is_testnet); + Self { + tor_socks5_port: 9050, + namespace: XmrBtcNamespace::from_is_testnet(is_testnet), + server_address: None, + env_config, + seed: Some(seed), + debug, + json, + is_testnet, + data_dir, + } + } + } + + impl Request { + pub fn buy_xmr(is_testnet: bool) -> Request { + let seller = Multiaddr::from_str(MULTI_ADDRESS).unwrap(); + let bitcoin_change_address = { + if is_testnet { + bitcoin::Address::from_str(BITCOIN_TESTNET_ADDRESS).unwrap() + } else { + bitcoin::Address::from_str(BITCOIN_MAINNET_ADDRESS).unwrap() + } + }; + + let monero_receive_address = { + if is_testnet { + monero::Address::from_str(MONERO_STAGENET_ADDRESS).unwrap() + } else { + monero::Address::from_str(MONERO_MAINNET_ADDRESS).unwrap() + } + }; + + Request::new(Method::BuyXmr { + seller, + bitcoin_change_address, + monero_receive_address, + swap_id: Uuid::new_v4(), + }) + } + + pub fn resume() -> Request { + Request::new(Method::Resume { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + }) + } + + pub fn cancel() -> Request { + Request::new(Method::CancelAndRefund { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + }) + } + + pub fn refund() -> Request { + Request::new(Method::CancelAndRefund { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + }) + } + } +} diff --git a/swap/src/api/request.rs b/swap/src/api/request.rs new file mode 100644 index 00000000..02bf27e1 --- /dev/null +++ b/swap/src/api/request.rs @@ -0,0 +1,930 @@ +use crate::api::Context; +use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock}; +use crate::cli::{list_sellers, EventLoop, SellerStatus}; +use crate::libp2p_ext::MultiAddrExt; +use crate::network::quote::{BidQuote, ZeroQuoteReceived}; +use crate::network::swarm; +use crate::protocol::bob::{BobState, Swap}; +use crate::protocol::{bob, State}; +use crate::{bitcoin, cli, monero, rpc}; +use anyhow::{bail, Context as AnyContext, Result}; +use libp2p::core::Multiaddr; +use qrcode::render::unicode; +use qrcode::QrCode; +use serde_json::json; +use std::cmp::min; +use std::convert::TryInto; +use std::future::Future; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; +use tracing::{debug_span, field, Instrument, Span}; +use uuid::Uuid; + +#[derive(PartialEq, Debug)] +pub struct Request { + pub cmd: Method, + pub log_reference: Option, +} + +#[derive(Debug, PartialEq)] +pub enum Method { + BuyXmr { + seller: Multiaddr, + bitcoin_change_address: bitcoin::Address, + monero_receive_address: monero::Address, + swap_id: Uuid, + }, + Resume { + swap_id: Uuid, + }, + CancelAndRefund { + swap_id: Uuid, + }, + MoneroRecovery { + swap_id: Uuid, + }, + History, + Config, + WithdrawBtc { + amount: Option, + address: bitcoin::Address, + }, + Balance { + force_refresh: bool, + }, + ListSellers { + rendezvous_point: Multiaddr, + }, + ExportBitcoinWallet, + SuspendCurrentSwap, + StartDaemon { + server_address: Option, + }, + GetCurrentSwap, + GetSwapInfo { + swap_id: Uuid, + }, + GetRawStates, +} + +impl Method { + fn get_tracing_span(&self, log_reference_id: Option) -> Span { + let span = match self { + Method::Balance { .. } => { + debug_span!( + "method", + method_name = "Balance", + log_reference_id = field::Empty + ) + } + Method::BuyXmr { swap_id, .. } => { + debug_span!("method", method_name="BuyXmr", swap_id=%swap_id, log_reference_id=field::Empty) + } + Method::CancelAndRefund { swap_id } => { + debug_span!("method", method_name="CancelAndRefund", swap_id=%swap_id, log_reference_id=field::Empty) + } + Method::Resume { swap_id } => { + debug_span!("method", method_name="Resume", swap_id=%swap_id, log_reference_id=field::Empty) + } + Method::Config => { + debug_span!( + "method", + method_name = "Config", + log_reference_id = field::Empty + ) + } + Method::ExportBitcoinWallet => { + debug_span!( + "method", + method_name = "ExportBitcoinWallet", + log_reference_id = field::Empty + ) + } + Method::GetCurrentSwap => { + debug_span!( + "method", + method_name = "GetCurrentSwap", + log_reference_id = field::Empty + ) + } + Method::GetSwapInfo { .. } => { + debug_span!( + "method", + method_name = "GetSwapInfo", + log_reference_id = field::Empty + ) + } + Method::History => { + debug_span!( + "method", + method_name = "History", + log_reference_id = field::Empty + ) + } + Method::ListSellers { .. } => { + debug_span!( + "method", + method_name = "ListSellers", + log_reference_id = field::Empty + ) + } + Method::MoneroRecovery { .. } => { + debug_span!( + "method", + method_name = "MoneroRecovery", + log_reference_id = field::Empty + ) + } + Method::GetRawStates => debug_span!( + "method", + method_name = "RawHistory", + log_reference_id = field::Empty + ), + Method::StartDaemon { .. } => { + debug_span!( + "method", + method_name = "StartDaemon", + log_reference_id = field::Empty + ) + } + Method::SuspendCurrentSwap => { + debug_span!( + "method", + method_name = "SuspendCurrentSwap", + log_reference_id = field::Empty + ) + } + Method::WithdrawBtc { .. } => { + debug_span!( + "method", + method_name = "WithdrawBtc", + log_reference_id = field::Empty + ) + } + }; + if let Some(log_reference_id) = log_reference_id { + span.record("log_reference_id", log_reference_id.as_str()); + } + span + } +} + +impl Request { + pub fn new(cmd: Method) -> Request { + Request { + cmd, + log_reference: None, + } + } + + pub fn with_id(cmd: Method, id: Option) -> Request { + Request { + cmd, + log_reference: id, + } + } + + async fn handle_cmd(self, context: Arc) -> Result { + match self.cmd { + Method::SuspendCurrentSwap => { + let swap_id = context.swap_lock.get_current_swap_id().await; + + if let Some(id_value) = swap_id { + context.swap_lock.send_suspend_signal().await?; + + Ok(json!({ "swapId": id_value })) + } else { + bail!("No swap is currently running") + } + } + Method::GetSwapInfo { swap_id } => { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let state = context.db.get_state(swap_id).await?; + let is_completed = state.swap_finished(); + + let peerId = context + .db + .get_peer_id(swap_id) + .await + .with_context(|| "Could not get PeerID")?; + + let addresses = context + .db + .get_addresses(peerId) + .await + .with_context(|| "Could not get addressess")?; + + let start_date = context.db.get_swap_start_date(swap_id).await?; + + let swap_state: BobState = state.try_into()?; + let state_name = format!("{}", swap_state); + + let ( + xmr_amount, + btc_amount, + tx_lock_id, + tx_cancel_fee, + tx_refund_fee, + tx_lock_fee, + btc_refund_address, + cancel_timelock, + punish_timelock, + ) = context + .db + .get_states(swap_id) + .await? + .iter() + .find_map(|state| { + if let State::Bob(BobState::SwapSetupCompleted(state2)) = state { + let xmr_amount = state2.xmr; + let btc_amount = state2.tx_lock.lock_amount().to_sat(); + let tx_cancel_fee = state2.tx_cancel_fee.to_sat(); + let tx_refund_fee = state2.tx_refund_fee.to_sat(); + let tx_lock_id = state2.tx_lock.txid(); + let btc_refund_address = state2.refund_address.to_string(); + + if let Ok(tx_lock_fee) = state2.tx_lock.fee() { + let tx_lock_fee = tx_lock_fee.to_sat(); + + Some(( + xmr_amount, + btc_amount, + tx_lock_id, + tx_cancel_fee, + tx_refund_fee, + tx_lock_fee, + btc_refund_address, + state2.cancel_timelock, + state2.punish_timelock, + )) + } else { + None + } + } else { + None + } + }) + .with_context(|| "Did not find SwapSetupCompleted state for swap")?; + + let timelock = match swap_state { + BobState::Started { .. } + | BobState::SafelyAborted + | BobState::SwapSetupCompleted(_) => None, + BobState::BtcLocked { state3: state, .. } + | BobState::XmrLockProofReceived { state, .. } => { + Some(state.expired_timelock(bitcoin_wallet).await) + } + BobState::XmrLocked(state) | BobState::EncSigSent(state) => { + Some(state.expired_timelock(bitcoin_wallet).await) + } + BobState::CancelTimelockExpired(state) | BobState::BtcCancelled(state) => { + Some(state.expired_timelock(bitcoin_wallet).await) + } + BobState::BtcPunished { .. } => Some(Ok(ExpiredTimelocks::Punish)), + BobState::BtcRefunded(_) + | BobState::BtcRedeemed(_) + | BobState::XmrRedeemed { .. } => None, + }; + + Ok(json!({ + "swapId": swap_id, + "seller": { + "peerId": peerId.to_string(), + "addresses": addresses + }, + "completed": is_completed, + "startDate": start_date, + "stateName": state_name, + "xmrAmount": xmr_amount, + "btcAmount": btc_amount, + "txLockId": tx_lock_id, + "txCancelFee": tx_cancel_fee, + "txRefundFee": tx_refund_fee, + "txLockFee": tx_lock_fee, + "btcRefundAddress": btc_refund_address.to_string(), + "cancelTimelock": cancel_timelock, + "punishTimelock": punish_timelock, + // If the timelock is None, it means that the swap is in a state where the timelock is not accessible to us. + // If that is the case, we return null. Otherwise, we return the timelock. + "timelock": timelock.map(|tl| tl.map(|tl| json!(tl)).unwrap_or(json!(null))).unwrap_or(json!(null)), + })) + } + Method::BuyXmr { + seller, + bitcoin_change_address, + monero_receive_address, + swap_id, + } => { + let bitcoin_wallet = Arc::clone( + context + .bitcoin_wallet + .as_ref() + .expect("Could not find Bitcoin wallet"), + ); + let monero_wallet = Arc::clone( + context + .monero_wallet + .as_ref() + .context("Could not get Monero wallet")?, + ); + let env_config = context.config.env_config; + let seed = context.config.seed.clone().context("Could not get seed")?; + + let seller_peer_id = seller + .extract_peer_id() + .context("Seller address must contain peer ID")?; + context + .db + .insert_address(seller_peer_id, seller.clone()) + .await?; + + let behaviour = cli::Behaviour::new( + seller_peer_id, + env_config, + bitcoin_wallet.clone(), + (seed.derive_libp2p_identity(), context.config.namespace), + ); + let mut swarm = swarm::cli( + seed.derive_libp2p_identity(), + context.config.tor_socks5_port, + behaviour, + ) + .await?; + + swarm.behaviour_mut().add_address(seller_peer_id, seller); + + context + .db + .insert_monero_address(swap_id, monero_receive_address) + .await?; + + tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized"); + + context.swap_lock.acquire_swap_lock(swap_id).await?; + + let initialize_swap = tokio::select! { + biased; + _ = context.swap_lock.listen_for_swap_force_suspension() => { + tracing::debug!("Shutdown signal received, exiting"); + context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); + bail!("Shutdown signal received"); + }, + result = async { + let (event_loop, mut event_loop_handle) = + EventLoop::new(swap_id, swarm, seller_peer_id)?; + let event_loop = tokio::spawn(event_loop.run().in_current_span()); + + let bid_quote = event_loop_handle.request_quote().await?; + + Ok::<_, anyhow::Error>((event_loop, event_loop_handle, bid_quote)) + } => { + result + }, + }; + + let (event_loop, event_loop_handle, bid_quote) = match initialize_swap { + Ok(result) => result, + Err(error) => { + tracing::error!(%swap_id, "Swap initialization failed: {:#}", error); + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + bail!(error); + } + }; + + context.tasks.clone().spawn(async move { + tokio::select! { + biased; + _ = context.swap_lock.listen_for_swap_force_suspension() => { + tracing::debug!("Shutdown signal received, exiting"); + context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); + bail!("Shutdown signal received"); + }, + event_loop_result = event_loop => { + match event_loop_result { + Ok(_) => { + tracing::debug!(%swap_id, "EventLoop completed") + } + Err(error) => { + tracing::error!(%swap_id, "EventLoop failed: {:#}", error) + } + } + }, + swap_result = async { + let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); + let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount); + + let determine_amount = determine_btc_to_swap( + context.config.json, + bid_quote, + bitcoin_wallet.new_address(), + || bitcoin_wallet.balance(), + max_givable, + || bitcoin_wallet.sync(), + estimate_fee, + ); + + let (amount, fees) = match determine_amount.await { + Ok(val) => val, + Err(error) => match error.downcast::() { + Ok(_) => { + bail!("Seller's XMR balance is currently too low to initiate a swap, please try again later") + } + Err(other) => bail!(other), + }, + }; + + tracing::info!(%amount, %fees, "Determined swap amount"); + + context.db.insert_peer_id(swap_id, seller_peer_id).await?; + + let swap = Swap::new( + Arc::clone(&context.db), + swap_id, + Arc::clone(&bitcoin_wallet), + monero_wallet, + env_config, + event_loop_handle, + monero_receive_address, + bitcoin_change_address, + amount, + ); + + bob::run(swap).await + } => { + match swap_result { + Ok(state) => { + tracing::debug!(%swap_id, state=%state, "Swap completed") + } + Err(error) => { + tracing::error!(%swap_id, "Failed to complete swap: {:#}", error) + } + } + }, + }; + tracing::debug!(%swap_id, "Swap completed"); + + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + Ok::<_, anyhow::Error>(()) + }.in_current_span()).await; + + Ok(json!({ + "swapId": swap_id.to_string(), + "quote": bid_quote, + })) + } + Method::Resume { swap_id } => { + context.swap_lock.acquire_swap_lock(swap_id).await?; + + let seller_peer_id = context.db.get_peer_id(swap_id).await?; + let seller_addresses = context.db.get_addresses(seller_peer_id).await?; + + let seed = context + .config + .seed + .as_ref() + .context("Could not get seed")? + .derive_libp2p_identity(); + + let behaviour = cli::Behaviour::new( + seller_peer_id, + context.config.env_config, + Arc::clone( + context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?, + ), + (seed.clone(), context.config.namespace), + ); + let mut swarm = + swarm::cli(seed.clone(), context.config.tor_socks5_port, behaviour).await?; + let our_peer_id = swarm.local_peer_id(); + + tracing::debug!(peer_id = %our_peer_id, "Network layer initialized"); + + for seller_address in seller_addresses { + swarm + .behaviour_mut() + .add_address(seller_peer_id, seller_address); + } + + let (event_loop, event_loop_handle) = + EventLoop::new(swap_id, swarm, seller_peer_id)?; + let monero_receive_address = context.db.get_monero_address(swap_id).await?; + let swap = Swap::from_db( + Arc::clone(&context.db), + swap_id, + Arc::clone( + context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?, + ), + Arc::clone( + context + .monero_wallet + .as_ref() + .context("Could not get Monero wallet")?, + ), + context.config.env_config, + event_loop_handle, + monero_receive_address, + ) + .await?; + + context.tasks.clone().spawn( + async move { + let handle = tokio::spawn(event_loop.run().in_current_span()); + tokio::select! { + biased; + _ = context.swap_lock.listen_for_swap_force_suspension() => { + tracing::debug!("Shutdown signal received, exiting"); + context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); + bail!("Shutdown signal received"); + }, + + event_loop_result = handle => { + match event_loop_result { + Ok(_) => { + tracing::debug!(%swap_id, "EventLoop completed during swap resume") + } + Err(error) => { + tracing::error!(%swap_id, "EventLoop failed during swap resume: {:#}", error) + } + } + }, + swap_result = bob::run(swap) => { + match swap_result { + Ok(state) => { + tracing::debug!(%swap_id, state=%state, "Swap completed after resuming") + } + Err(error) => { + tracing::error!(%swap_id, "Failed to resume swap: {:#}", error) + } + } + + } + } + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + Ok::<(), anyhow::Error>(()) + } + .in_current_span(), + ).await; + Ok(json!({ + "result": "ok", + })) + } + Method::CancelAndRefund { swap_id } => { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + context.swap_lock.acquire_swap_lock(swap_id).await?; + + let state = cli::cancel_and_refund( + swap_id, + Arc::clone(bitcoin_wallet), + Arc::clone(&context.db), + ) + .await; + + context + .swap_lock + .release_swap_lock() + .await + .expect("Could not release swap lock"); + + state.map(|state| { + json!({ + "result": state, + }) + }) + } + Method::History => { + let swaps = context.db.all().await?; + let mut vec: Vec<(Uuid, String)> = Vec::new(); + for (swap_id, state) in swaps { + let state: BobState = state.try_into()?; + vec.push((swap_id, state.to_string())); + } + + Ok(json!({ "swaps": vec })) + } + Method::GetRawStates => { + let raw_history = context.db.raw_all().await?; + + Ok(json!({ "raw_states": raw_history })) + } + Method::Config => { + let data_dir_display = context.config.data_dir.display(); + tracing::info!(path=%data_dir_display, "Data directory"); + tracing::info!(path=%format!("{}/logs", data_dir_display), "Log files directory"); + tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location"); + tracing::info!(path=%format!("{}/seed.pem", data_dir_display), "Seed file location"); + tracing::info!(path=%format!("{}/monero", data_dir_display), "Monero-wallet-rpc directory"); + tracing::info!(path=%format!("{}/wallet", data_dir_display), "Internal bitcoin wallet directory"); + + Ok(json!({ + "log_files": format!("{}/logs", data_dir_display), + "sqlite": format!("{}/sqlite", data_dir_display), + "seed": format!("{}/seed.pem", data_dir_display), + "monero-wallet-rpc": format!("{}/monero", data_dir_display), + "bitcoin_wallet": format!("{}/wallet", data_dir_display), + })) + } + Method::WithdrawBtc { address, amount } => { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let amount = match amount { + Some(amount) => amount, + None => { + bitcoin_wallet + .max_giveable(address.script_pubkey().len()) + .await? + } + }; + let psbt = bitcoin_wallet + .send_to_address(address, amount, None) + .await?; + let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?; + + bitcoin_wallet + .broadcast(signed_tx.clone(), "withdraw") + .await?; + + Ok(json!({ + "signed_tx": signed_tx, + "amount": amount.to_sat(), + "txid": signed_tx.txid(), + })) + } + Method::StartDaemon { server_address } => { + // Default to 127.0.0.1:1234 + let server_address = server_address.unwrap_or("127.0.0.1:1234".parse()?); + + let (addr, server_handle) = + rpc::run_server(server_address, Arc::clone(&context)).await?; + + tracing::info!(%addr, "Started RPC server"); + + server_handle.stopped().await; + + tracing::info!("Stopped RPC server"); + + Ok(json!({})) + } + Method::Balance { force_refresh } => { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + if force_refresh { + bitcoin_wallet.sync().await?; + } + + let bitcoin_balance = bitcoin_wallet.balance().await?; + + if force_refresh { + tracing::info!( + balance = %bitcoin_balance, + "Checked Bitcoin balance", + ); + } else { + tracing::debug!( + balance = %bitcoin_balance, + "Current Bitcoin balance as of last sync", + ); + } + + Ok(json!({ + "balance": bitcoin_balance.to_sat() + })) + } + Method::ListSellers { rendezvous_point } => { + let rendezvous_node_peer_id = rendezvous_point + .extract_peer_id() + .context("Rendezvous node address must contain peer ID")?; + + let identity = context + .config + .seed + .as_ref() + .context("Cannot extract seed")? + .derive_libp2p_identity(); + + let sellers = list_sellers( + rendezvous_node_peer_id, + rendezvous_point, + context.config.namespace, + context.config.tor_socks5_port, + identity, + ) + .await?; + + for seller in &sellers { + match seller.status { + SellerStatus::Online(quote) => { + tracing::info!( + price = %quote.price.to_string(), + min_quantity = %quote.min_quantity.to_string(), + max_quantity = %quote.max_quantity.to_string(), + status = "Online", + address = %seller.multiaddr.to_string(), + "Fetched peer status" + ); + } + SellerStatus::Unreachable => { + tracing::info!( + status = "Unreachable", + address = %seller.multiaddr.to_string(), + "Fetched peer status" + ); + } + } + } + + Ok(json!({ "sellers": sellers })) + } + Method::ExportBitcoinWallet => { + let bitcoin_wallet = context + .bitcoin_wallet + .as_ref() + .context("Could not get Bitcoin wallet")?; + + let wallet_export = bitcoin_wallet.wallet_export("cli").await?; + tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet"); + Ok(json!({ + "descriptor": wallet_export.to_string(), + })) + } + Method::MoneroRecovery { swap_id } => { + let swap_state: BobState = context.db.get_state(swap_id).await?.try_into()?; + + if let BobState::BtcRedeemed(state5) = swap_state { + let (spend_key, view_key) = state5.xmr_keys(); + let restore_height = state5.monero_wallet_restore_blockheight.height; + + let address = monero::Address::standard( + context.config.env_config.monero_network, + monero::PublicKey::from_private_key(&spend_key), + monero::PublicKey::from(view_key.public()), + ); + + tracing::info!(restore_height=%restore_height, address=%address, spend_key=%spend_key, view_key=%view_key, "Monero recovery information"); + + Ok(json!({ + "address": address, + "spend_key": spend_key.to_string(), + "view_key": view_key.to_string(), + "restore_height": state5.monero_wallet_restore_blockheight.height, + })) + } else { + bail!( + "Cannot print monero recovery information in state {}, only possible for BtcRedeemed", + swap_state + ) + } + } + Method::GetCurrentSwap => Ok(json!({ + "swap_id": context.swap_lock.get_current_swap_id().await + })), + } + } + + pub async fn call(self, context: Arc) -> Result { + let method_span = self.cmd.get_tracing_span(self.log_reference.clone()); + + self.handle_cmd(context) + .instrument(method_span.clone()) + .await + .map_err(|err| { + method_span.in_scope(|| { + tracing::debug!(err = format!("{:?}", err), "API call resulted in an error"); + }); + err + }) + } +} + +fn qr_code(value: &impl ToString) -> Result { + let code = QrCode::new(value.to_string())?; + let qr_code = code + .render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + Ok(qr_code) +} + +pub async fn determine_btc_to_swap( + json: bool, + bid_quote: BidQuote, + get_new_address: impl Future>, + balance: FB, + max_giveable_fn: FMG, + sync: FS, + estimate_fee: FFE, +) -> Result<(Amount, Amount)> +where + TB: Future>, + FB: Fn() -> TB, + TMG: Future>, + FMG: Fn() -> TMG, + TS: Future>, + FS: Fn() -> TS, + FFE: Fn(Amount) -> TFE, + TFE: Future>, +{ + if bid_quote.max_quantity == Amount::ZERO { + bail!(ZeroQuoteReceived) + } + + tracing::info!( + price = %bid_quote.price, + minimum_amount = %bid_quote.min_quantity, + maximum_amount = %bid_quote.max_quantity, + "Received quote", + ); + + sync().await?; + let mut max_giveable = max_giveable_fn().await?; + + if max_giveable == Amount::ZERO || max_giveable < bid_quote.min_quantity { + let deposit_address = get_new_address.await?; + let minimum_amount = bid_quote.min_quantity; + let maximum_amount = bid_quote.max_quantity; + + if !json { + eprintln!("{}", qr_code(&deposit_address)?); + } + + loop { + let min_outstanding = bid_quote.min_quantity - max_giveable; + let min_fee = estimate_fee(min_outstanding).await?; + let min_deposit = min_outstanding + min_fee; + + tracing::info!( + "Deposit at least {} to cover the min quantity with fee!", + min_deposit + ); + tracing::info!( + %deposit_address, + %min_deposit, + %max_giveable, + %minimum_amount, + %maximum_amount, + "Waiting for Bitcoin deposit", + ); + + max_giveable = loop { + sync().await?; + let new_max_givable = max_giveable_fn().await?; + + if new_max_givable > max_giveable { + break new_max_givable; + } + + tokio::time::sleep(Duration::from_secs(1)).await; + }; + + let new_balance = balance().await?; + tracing::info!(%new_balance, %max_giveable, "Received Bitcoin"); + + if max_giveable < bid_quote.min_quantity { + tracing::info!("Deposited amount is less than `min_quantity`"); + continue; + } + + break; + } + }; + + let balance = balance().await?; + let fees = balance - max_giveable; + let max_accepted = bid_quote.max_quantity; + let btc_swap_amount = min(max_giveable, max_accepted); + + Ok((btc_swap_amount, fees)) +} diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 292a4586..06be7b40 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -12,43 +12,15 @@ #![forbid(unsafe_code)] #![allow(non_snake_case)] -use anyhow::{bail, Context, Result}; -use comfy_table::Table; -use qrcode::render::unicode; -use qrcode::QrCode; -use std::cmp::min; -use std::convert::TryInto; +use anyhow::Result; use std::env; -use std::future::Future; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; -use swap::bitcoin::TxLock; -use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command, ParseResult}; -use swap::cli::{list_sellers, EventLoop, SellerStatus}; +use swap::cli::command::{parse_args_and_apply_defaults, ParseResult}; use swap::common::check_latest_version; -use swap::database::open_db; -use swap::env::Config; -use swap::libp2p_ext::MultiAddrExt; -use swap::network::quote::{BidQuote, ZeroQuoteReceived}; -use swap::network::swarm; -use swap::protocol::bob; -use swap::protocol::bob::{BobState, Swap}; -use swap::seed::Seed; -use swap::{bitcoin, cli, monero}; -use url::Url; -use uuid::Uuid; #[tokio::main] async fn main() -> Result<()> { - let Arguments { - env_config, - data_dir, - debug, - json, - cmd, - } = match parse_args_and_apply_defaults(env::args_os())? { - ParseResult::Arguments(args) => *args, + let (context, request) = match parse_args_and_apply_defaults(env::args_os()).await? { + ParseResult::Context(context, request) => (context, request), ParseResult::PrintAndExitZero { message } => { println!("{}", message); std::process::exit(0); @@ -58,601 +30,19 @@ async fn main() -> Result<()> { if let Err(e) = check_latest_version(env!("CARGO_PKG_VERSION")).await { eprintln!("{}", e); } - - match cmd { - Command::BuyXmr { - seller, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - bitcoin_change_address, - monero_receive_address, - monero_daemon_address, - tor_socks5_port, - namespace, - } => { - let swap_id = Uuid::new_v4(); - - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - let (monero_wallet, _process) = - init_monero_wallet(data_dir, monero_daemon_address, env_config).await?; - let bitcoin_wallet = Arc::new(bitcoin_wallet); - let seller_peer_id = seller - .extract_peer_id() - .context("Seller address must contain peer ID")?; - db.insert_address(seller_peer_id, seller.clone()).await?; - - let behaviour = cli::Behaviour::new( - seller_peer_id, - env_config, - bitcoin_wallet.clone(), - (seed.derive_libp2p_identity(), namespace), - ); - let mut swarm = - swarm::cli(seed.derive_libp2p_identity(), tor_socks5_port, behaviour).await?; - swarm.behaviour_mut().add_address(seller_peer_id, seller); - - tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized"); - - let (event_loop, mut event_loop_handle) = - EventLoop::new(swap_id, swarm, seller_peer_id)?; - let event_loop = tokio::spawn(event_loop.run()); - - let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); - let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount); - - let (amount, fees) = match determine_btc_to_swap( - json, - event_loop_handle.request_quote(), - bitcoin_wallet.new_address(), - || bitcoin_wallet.balance(), - max_givable, - || bitcoin_wallet.sync(), - estimate_fee, - ) - .await - { - Ok(val) => val, - Err(error) => match error.downcast::() { - Ok(_) => { - bail!("Seller's XMR balance is currently too low to initiate a swap, please try again later") - } - Err(other) => bail!(other), - }, - }; - - tracing::info!(%amount, %fees, "Determined swap amount"); - - db.insert_peer_id(swap_id, seller_peer_id).await?; - db.insert_monero_address(swap_id, monero_receive_address) - .await?; - - let swap = Swap::new( - db, - swap_id, - bitcoin_wallet, - Arc::new(monero_wallet), - env_config, - event_loop_handle, - monero_receive_address, - bitcoin_change_address, - amount, - ); - - tokio::select! { - result = event_loop => { - result - .context("EventLoop panicked")?; - }, - result = bob::run(swap) => { - result.context("Failed to complete swap")?; - } - } - } - Command::History => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let db = open_db(data_dir.join("sqlite")).await?; - let swaps = db.all().await?; - - if json { - for (swap_id, state) in swaps { - let state: BobState = state.try_into()?; - tracing::info!(swap_id=%swap_id.to_string(), state=%state.to_string(), "Read swap state from database"); - } - } else { - let mut table = Table::new(); - - table.set_header(vec!["SWAP ID", "STATE"]); - - for (swap_id, state) in swaps { - let state: BobState = state.try_into()?; - table.add_row(vec![swap_id.to_string(), state.to_string()]); - } - - println!("{}", table); - } - } - Command::Config => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - tracing::info!(path=%data_dir.display(), "Data directory"); - tracing::info!(path=%format!("{}/logs", data_dir.display()), "Log files directory"); - tracing::info!(path=%format!("{}/sqlite", data_dir.display()), "Sqlite file location"); - tracing::info!(path=%format!("{}/seed.pem", data_dir.display()), "Seed file location"); - tracing::info!(path=%format!("{}/monero", data_dir.display()), "Monero-wallet-rpc directory"); - tracing::info!(path=%format!("{}/wallet", data_dir.display()), "Internal bitcoin wallet directory"); - } - Command::WithdrawBtc { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - amount, - address, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - - let amount = match amount { - Some(amount) => amount, - None => { - bitcoin_wallet - .max_giveable(address.script_pubkey().len()) - .await? - } - }; - - let psbt = bitcoin_wallet - .send_to_address(address, amount, None) - .await?; - let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?; - - bitcoin_wallet.broadcast(signed_tx, "withdraw").await?; - } - - Command::Balance { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - - let bitcoin_balance = bitcoin_wallet.balance().await?; - tracing::info!( - balance = %bitcoin_balance, - "Checked Bitcoin balance", - ); - } - Command::Resume { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - monero_daemon_address, - tor_socks5_port, - namespace, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - let (monero_wallet, _process) = - init_monero_wallet(data_dir, monero_daemon_address, env_config).await?; - let bitcoin_wallet = Arc::new(bitcoin_wallet); - - let seller_peer_id = db.get_peer_id(swap_id).await?; - let seller_addresses = db.get_addresses(seller_peer_id).await?; - - let behaviour = cli::Behaviour::new( - seller_peer_id, - env_config, - bitcoin_wallet.clone(), - (seed.derive_libp2p_identity(), namespace), - ); - let mut swarm = - swarm::cli(seed.derive_libp2p_identity(), tor_socks5_port, behaviour).await?; - let our_peer_id = swarm.local_peer_id(); - tracing::debug!(peer_id = %our_peer_id, "Network layer initialized"); - - for seller_address in seller_addresses { - swarm - .behaviour_mut() - .add_address(seller_peer_id, seller_address); - } - - let (event_loop, event_loop_handle) = EventLoop::new(swap_id, swarm, seller_peer_id)?; - let handle = tokio::spawn(event_loop.run()); - - let monero_receive_address = db.get_monero_address(swap_id).await?; - let swap = Swap::from_db( - db, - swap_id, - bitcoin_wallet, - Arc::new(monero_wallet), - env_config, - event_loop_handle, - monero_receive_address, - ) - .await?; - - tokio::select! { - event_loop_result = handle => { - event_loop_result?; - }, - swap_result = bob::run(swap) => { - swap_result?; - } - } - } - Command::CancelAndRefund { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir, - env_config, - bitcoin_target_block, - ) - .await?; - - cli::cancel_and_refund(swap_id, Arc::new(bitcoin_wallet), db).await?; - } - Command::ListSellers { - rendezvous_point, - namespace, - tor_socks5_port, - } => { - let rendezvous_node_peer_id = rendezvous_point - .extract_peer_id() - .context("Rendezvous node address must contain peer ID")?; - - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - let identity = seed.derive_libp2p_identity(); - - let sellers = list_sellers( - rendezvous_node_peer_id, - rendezvous_point, - namespace, - tor_socks5_port, - identity, - ) - .await?; - - if json { - for seller in sellers { - match seller.status { - SellerStatus::Online(quote) => { - tracing::info!( - price = %quote.price.to_string(), - min_quantity = %quote.min_quantity.to_string(), - max_quantity = %quote.max_quantity.to_string(), - status = "Online", - address = %seller.multiaddr.to_string(), - "Fetched peer status" - ); - } - SellerStatus::Unreachable => { - tracing::info!( - status = "Unreachable", - address = %seller.multiaddr.to_string(), - "Fetched peer status" - ); - } - } - } - } else { - let mut table = Table::new(); - - table.set_header(vec![ - "PRICE", - "MIN_QUANTITY", - "MAX_QUANTITY", - "STATUS", - "ADDRESS", - ]); - - for seller in sellers { - let row = match seller.status { - SellerStatus::Online(quote) => { - vec![ - quote.price.to_string(), - quote.min_quantity.to_string(), - quote.max_quantity.to_string(), - "Online".to_owned(), - seller.multiaddr.to_string(), - ] - } - SellerStatus::Unreachable => { - vec![ - "???".to_owned(), - "???".to_owned(), - "???".to_owned(), - "Unreachable".to_owned(), - seller.multiaddr.to_string(), - ] - } - }; - - table.add_row(row); - } - - println!("{}", table); - } - } - Command::ExportBitcoinWallet { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - } => { - cli::tracing::init(debug, json, data_dir.join("logs"), None)?; - - let seed = Seed::from_file_or_generate(data_dir.as_path()) - .context("Failed to read in seed file")?; - let bitcoin_wallet = init_bitcoin_wallet( - bitcoin_electrum_rpc_url, - &seed, - data_dir.clone(), - env_config, - bitcoin_target_block, - ) - .await?; - let wallet_export = bitcoin_wallet.wallet_export("cli").await?; - tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet"); - } - Command::MoneroRecovery { swap_id } => { - cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?; - - let db = open_db(data_dir.join("sqlite")).await?; - - let swap_state: BobState = db.get_state(swap_id).await?.try_into()?; - - match swap_state { - BobState::Started { .. } - | BobState::SwapSetupCompleted(_) - | BobState::BtcLocked { .. } - | BobState::XmrLockProofReceived { .. } - | BobState::XmrLocked(_) - | BobState::EncSigSent(_) - | BobState::CancelTimelockExpired(_) - | BobState::BtcCancelled(_) - | BobState::BtcRefunded(_) - | BobState::BtcPunished { .. } - | BobState::SafelyAborted - | BobState::XmrRedeemed { .. } => { - bail!("Cannot print monero recovery information in state {}, only possible for BtcRedeemed", swap_state) - } - BobState::BtcRedeemed(state5) => { - let (spend_key, view_key) = state5.xmr_keys(); - - let address = monero::Address::standard( - env_config.monero_network, - monero::PublicKey::from_private_key(&spend_key), - monero::PublicKey::from(view_key.public()), - ); - tracing::info!("Wallet address: {}", address.to_string()); - - let view_key = serde_json::to_string(&view_key)?; - println!("View key: {}", view_key); - - println!("Spend key: {}", spend_key); - } - } - } - }; + request.call(context.clone()).await?; + context.tasks.wait_for_tasks().await?; Ok(()) } -async fn init_bitcoin_wallet( - electrum_rpc_url: Url, - seed: &Seed, - data_dir: PathBuf, - env_config: Config, - bitcoin_target_block: usize, -) -> Result { - tracing::debug!("Initializing bitcoin wallet"); - let xprivkey = seed.derive_extended_private_key(env_config.bitcoin_network)?; - - let wallet = bitcoin::Wallet::new( - electrum_rpc_url.clone(), - data_dir, - xprivkey, - env_config, - bitcoin_target_block, - ) - .await - .context("Failed to initialize Bitcoin wallet")?; - - tracing::debug!("Syncing bitcoin wallet"); - wallet.sync().await?; - - Ok(wallet) -} - -async fn init_monero_wallet( - data_dir: PathBuf, - monero_daemon_address: Option, - env_config: Config, -) -> Result<(monero::Wallet, monero::WalletRpcProcess)> { - let network = env_config.monero_network; - - const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet"; - - let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?; - - let monero_wallet_rpc_process = monero_wallet_rpc - .run(network, monero_daemon_address) - .await?; - - let monero_wallet = monero::Wallet::open_or_create( - monero_wallet_rpc_process.endpoint(), - MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(), - env_config, - ) - .await?; - - Ok((monero_wallet, monero_wallet_rpc_process)) -} - -fn qr_code(value: &impl ToString) -> Result { - let code = QrCode::new(value.to_string())?; - let qr_code = code - .render::() - .dark_color(unicode::Dense1x2::Light) - .light_color(unicode::Dense1x2::Dark) - .build(); - Ok(qr_code) -} - -async fn determine_btc_to_swap( - json: bool, - bid_quote: impl Future>, - get_new_address: impl Future>, - balance: FB, - max_giveable_fn: FMG, - sync: FS, - estimate_fee: FFE, -) -> Result<(bitcoin::Amount, bitcoin::Amount)> -where - TB: Future>, - FB: Fn() -> TB, - TMG: Future>, - FMG: Fn() -> TMG, - TS: Future>, - FS: Fn() -> TS, - FFE: Fn(bitcoin::Amount) -> TFE, - TFE: Future>, -{ - tracing::debug!("Requesting quote"); - let bid_quote = bid_quote.await?; - - if bid_quote.max_quantity == bitcoin::Amount::ZERO { - bail!(ZeroQuoteReceived) - } - - tracing::info!( - price = %bid_quote.price, - minimum_amount = %bid_quote.min_quantity, - maximum_amount = %bid_quote.max_quantity, - "Received quote", - ); - - let mut max_giveable = max_giveable_fn().await?; - - if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity { - let deposit_address = get_new_address.await?; - let minimum_amount = bid_quote.min_quantity; - let maximum_amount = bid_quote.max_quantity; - - if !json { - eprintln!("{}", qr_code(&deposit_address)?); - } - - loop { - let min_outstanding = bid_quote.min_quantity - max_giveable; - let min_fee = estimate_fee(min_outstanding).await?; - let min_deposit = min_outstanding + min_fee; - - tracing::info!( - "Deposit at least {} to cover the min quantity with fee!", - min_deposit - ); - tracing::info!( - %deposit_address, - %min_deposit, - %max_giveable, - %minimum_amount, - %maximum_amount, - "Waiting for Bitcoin deposit", - ); - - max_giveable = loop { - sync().await?; - let new_max_givable = max_giveable_fn().await?; - - if new_max_givable > max_giveable { - break new_max_givable; - } - - tokio::time::sleep(Duration::from_secs(1)).await; - }; - - let new_balance = balance().await?; - tracing::info!(%new_balance, %max_giveable, "Received Bitcoin"); - - if max_giveable < bid_quote.min_quantity { - tracing::info!("Deposited amount is less than `min_quantity`"); - continue; - } - - break; - } - }; - - let balance = balance().await?; - let fees = balance - max_giveable; - let max_accepted = bid_quote.max_quantity; - let btc_swap_amount = min(max_giveable, max_accepted); - - Ok((btc_swap_amount, fees)) -} - #[cfg(test)] mod tests { use super::*; - use crate::determine_btc_to_swap; use ::bitcoin::Amount; - use std::sync::Mutex; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + use swap::api::request::determine_btc_to_swap; + use swap::network::quote::BidQuote; use swap::tracing_ext::capture_logs; use tracing::level_filters::LevelFilter; @@ -666,7 +56,7 @@ mod tests { let (amount, fees) = determine_btc_to_swap( true, - async { Ok(quote_with_max(0.01)) }, + quote_with_max(0.01), get_dummy_address(), || async { Ok(Amount::from_btc(0.001)?) }, || async { @@ -685,10 +75,10 @@ mod tests { 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: Deposit at least 0.00001000 BTC to cover the min quantity with fee! - INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001000 BTC 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 + r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC + INFO swap::api::request: Deposit at least 0.00001 BTC to cover the min quantity with fee! + INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001 BTC max_giveable=0 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC + INFO swap::api::request: Received Bitcoin new_balance=0.001 BTC max_giveable=0.0009 BTC " ); } @@ -703,7 +93,7 @@ mod tests { let (amount, fees) = determine_btc_to_swap( true, - async { Ok(quote_with_max(0.01)) }, + quote_with_max(0.01), get_dummy_address(), || async { Ok(Amount::from_btc(0.1001)?) }, || async { @@ -722,10 +112,10 @@ mod tests { 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: Deposit at least 0.00001000 BTC to cover the min quantity with fee! - INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001000 BTC 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 + r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC + INFO swap::api::request: Deposit at least 0.00001 BTC to cover the min quantity with fee! + INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001 BTC max_giveable=0 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC + INFO swap::api::request: Received Bitcoin new_balance=0.1001 BTC max_giveable=0.1 BTC " ); } @@ -740,7 +130,7 @@ mod tests { let (amount, fees) = determine_btc_to_swap( true, - async { Ok(quote_with_max(0.01)) }, + quote_with_max(0.01), async { panic!("should not request new address when initial balance is > 0") }, || async { Ok(Amount::from_btc(0.005)?) }, || async { @@ -759,7 +149,7 @@ mod tests { assert_eq!((amount, fees), (expected_amount, expected_fees)); assert_eq!( writer.captured(), - " INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC\n" + " INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC\n" ); } @@ -773,7 +163,7 @@ mod tests { let (amount, fees) = determine_btc_to_swap( true, - async { Ok(quote_with_max(0.01)) }, + quote_with_max(0.01), async { panic!("should not request new address when initial balance is > 0") }, || async { Ok(Amount::from_btc(0.1001)?) }, || async { @@ -792,7 +182,7 @@ mod tests { assert_eq!((amount, fees), (expected_amount, expected_fees)); assert_eq!( writer.captured(), - " INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC\n" + " INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC\n" ); } @@ -806,7 +196,7 @@ mod tests { let (amount, fees) = determine_btc_to_swap( true, - async { Ok(quote_with_min(0.01)) }, + quote_with_min(0.01), get_dummy_address(), || async { Ok(Amount::from_btc(0.0101)?) }, || async { @@ -825,10 +215,10 @@ mod tests { 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: Deposit at least 0.01001000 BTC to cover the min quantity with fee! - INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.01001000 BTC 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 + r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC + INFO swap::api::request: Deposit at least 0.01001 BTC to cover the min quantity with fee! + INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.01001 BTC max_giveable=0 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC + INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC " ); } @@ -843,7 +233,7 @@ mod tests { let (amount, fees) = determine_btc_to_swap( true, - async { Ok(quote_with_min(0.01)) }, + quote_with_min(0.01), get_dummy_address(), || async { Ok(Amount::from_btc(0.0101)?) }, || async { @@ -862,10 +252,10 @@ mod tests { 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: Deposit at least 0.00991000 BTC to cover the min quantity with fee! - INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00991000 BTC 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 + r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC + INFO swap::api::request: Deposit at least 0.00991 BTC to cover the min quantity with fee! + INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00991 BTC max_giveable=0.0001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC + INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC " ); } @@ -885,7 +275,7 @@ mod tests { Duration::from_secs(1), determine_btc_to_swap( true, - async { Ok(quote_with_min(0.1)) }, + quote_with_min(0.1), get_dummy_address(), || async { Ok(Amount::from_btc(0.0101)?) }, || async { @@ -902,13 +292,13 @@ mod tests { 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: Deposit at least 0.10001000 BTC to cover the min quantity with fee! - INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001000 BTC 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: Deposit at least 0.09001000 BTC to cover the min quantity with fee! - INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.09001000 BTC max_giveable=0.01000000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC + r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC + INFO swap::api::request: Deposit at least 0.10001 BTC to cover the min quantity with fee! + INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001 BTC max_giveable=0 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC + INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC + INFO swap::api::request: Deposited amount is less than `min_quantity` + INFO swap::api::request: Deposit at least 0.09001 BTC to cover the min quantity with fee! + INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.09001 BTC max_giveable=0.01 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC " ); } @@ -933,7 +323,7 @@ mod tests { Duration::from_secs(10), determine_btc_to_swap( true, - async { Ok(quote_with_min(0.1)) }, + quote_with_min(0.1), get_dummy_address(), || async { Ok(Amount::from_btc(0.21)?) }, || async { @@ -951,10 +341,10 @@ mod tests { 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: Deposit at least 0.10001000 BTC to cover the min quantity with fee! - INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001000 BTC 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 + r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC + INFO swap::api::request: Deposit at least 0.10001 BTC to cover the min quantity with fee! + INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001 BTC max_giveable=0 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC + INFO swap::api::request: Received Bitcoin new_balance=0.21 BTC max_giveable=0.2 BTC " ); } @@ -968,7 +358,7 @@ mod tests { let determination_error = determine_btc_to_swap( true, - async { Ok(quote_with_max(0.00)) }, + quote_with_max(0.00), get_dummy_address(), || async { Ok(Amount::from_btc(0.0101)?) }, || async { diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 925f48e6..556d8453 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -15,7 +15,7 @@ pub use crate::bitcoin::refund::TxRefund; pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks}; pub use ::bitcoin::util::amount::Amount; pub use ::bitcoin::util::psbt::PartiallySignedTransaction; -pub use ::bitcoin::{Address, Network, Transaction, Txid}; +pub use ::bitcoin::{Address, AddressType, Network, Transaction, Txid}; pub use ecdsa_fun::adaptor::EncryptedSignature; pub use ecdsa_fun::fun::Scalar; pub use ecdsa_fun::Signature; @@ -244,10 +244,65 @@ pub fn current_epoch( } if tx_lock_status.is_confirmed_with(cancel_timelock) { - return ExpiredTimelocks::Cancel; + return ExpiredTimelocks::Cancel { + blocks_left: tx_cancel_status.blocks_left_until(punish_timelock), + }; } - ExpiredTimelocks::None + ExpiredTimelocks::None { + blocks_left: tx_lock_status.blocks_left_until(cancel_timelock), + } +} + +pub mod bitcoin_address { + use anyhow::{bail, Result}; + use serde::Serialize; + use std::str::FromStr; + + #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)] + #[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")] + pub struct BitcoinAddressNetworkMismatch { + #[serde(with = "crate::bitcoin::network")] + expected: bitcoin::Network, + #[serde(with = "crate::bitcoin::network")] + actual: bitcoin::Network, + } + + pub fn parse(addr_str: &str) -> Result { + let address = bitcoin::Address::from_str(addr_str)?; + + if address.address_type() != Some(bitcoin::AddressType::P2wpkh) { + anyhow::bail!("Invalid Bitcoin address provided, only bech32 format is supported!") + } + + Ok(address) + } + + pub fn validate( + address: bitcoin::Address, + expected_network: bitcoin::Network, + ) -> Result { + if address.network != expected_network { + bail!(BitcoinAddressNetworkMismatch { + expected: expected_network, + actual: address.network + }); + } + + Ok(address) + } + + pub fn validate_is_testnet( + address: bitcoin::Address, + is_testnet: bool, + ) -> Result { + let expected_network = if is_testnet { + bitcoin::Network::Testnet + } else { + bitcoin::Network::Bitcoin + }; + validate(address, expected_network) + } } /// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23 @@ -324,6 +379,7 @@ mod tests { use crate::env::{GetConfig, Regtest}; use crate::protocol::{alice, bob}; use rand::rngs::OsRng; + use std::matches; use uuid::Uuid; #[test] @@ -338,7 +394,7 @@ mod tests { tx_cancel_status, ); - assert_eq!(expired_timelock, ExpiredTimelocks::None) + assert!(matches!(expired_timelock, ExpiredTimelocks::None { .. })); } #[test] @@ -353,7 +409,7 @@ mod tests { tx_cancel_status, ); - assert_eq!(expired_timelock, ExpiredTimelocks::Cancel) + assert!(matches!(expired_timelock, ExpiredTimelocks::Cancel { .. })); } #[test] diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index 35b6b197..aec3fe38 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -24,6 +24,12 @@ use std::ops::Add; #[serde(transparent)] pub struct CancelTimelock(u32); +impl From for u32 { + fn from(cancel_timelock: CancelTimelock) -> Self { + cancel_timelock.0 + } +} + impl CancelTimelock { pub const fn new(number_of_blocks: u32) -> Self { Self(number_of_blocks) @@ -64,6 +70,12 @@ impl fmt::Display for CancelTimelock { #[serde(transparent)] pub struct PunishTimelock(u32); +impl From for u32 { + fn from(punish_timelock: PunishTimelock) -> Self { + punish_timelock.0 + } +} + impl PunishTimelock { pub const fn new(number_of_blocks: u32) -> Self { Self(number_of_blocks) diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index 42819ad5..f8aa9a39 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -4,9 +4,10 @@ use crate::bitcoin::{ }; use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::{OutPoint, TxIn, TxOut, Txid}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use bdk::database::BatchDatabase; use bdk::miniscript::Descriptor; +use bdk::psbt::PsbtUtils; use bitcoin::{PackedLockTime, Script, Sequence}; use serde::{Deserialize, Serialize}; @@ -100,6 +101,15 @@ impl TxLock { Amount::from_sat(self.inner.clone().extract_tx().output[self.lock_output_vout()].value) } + pub fn fee(&self) -> Result { + Ok(Amount::from_sat( + self.inner + .clone() + .fee_amount() + .context("The PSBT is missing a TxOut for an input")?, + )) + } + pub fn txid(&self) -> Txid { self.inner.clone().extract_tx().txid() } diff --git a/swap/src/bitcoin/timelocks.rs b/swap/src/bitcoin/timelocks.rs index e8b72ea6..427bef80 100644 --- a/swap/src/bitcoin/timelocks.rs +++ b/swap/src/bitcoin/timelocks.rs @@ -37,9 +37,9 @@ impl Add for BlockHeight { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)] pub enum ExpiredTimelocks { - None, - Cancel, + None { blocks_left: u32 }, + Cancel { blocks_left: u32 }, Punish, } diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 2e5732f8..5d8685f3 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -24,6 +24,7 @@ use std::path::Path; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{watch, Mutex}; +use tracing::{debug_span, Instrument}; const SLED_TREE_NAME: &str = "default_tree"; @@ -192,7 +193,7 @@ impl Wallet { tokio::time::sleep(Duration::from_secs(5)).await; } - }); + }.instrument(debug_span!("BitcoinWalletSubscription"))); Subscription { receiver, @@ -274,7 +275,7 @@ impl Subscription { pub async fn wait_until_confirmed_with(&self, target: T) -> Result<()> where - u32: PartialOrd, + T: Into, T: Copy, { self.wait_until(|status| status.is_confirmed_with(target)) @@ -933,9 +934,20 @@ impl Confirmed { pub fn meets_target(&self, target: T) -> bool where - u32: PartialOrd, + T: Into, { - self.confirmations() >= target + self.confirmations() >= target.into() + } + + pub fn blocks_left_until(&self, target: T) -> u32 + where + T: Into + Copy, + { + if self.meets_target(target) { + 0 + } else { + target.into() - self.confirmations() + } } } @@ -948,7 +960,7 @@ impl ScriptStatus { /// Check if the script has met the given confirmation target. pub fn is_confirmed_with(&self, target: T) -> bool where - u32: PartialOrd, + T: Into, { match self { ScriptStatus::Confirmed(inner) => inner.meets_target(target), @@ -956,6 +968,17 @@ impl ScriptStatus { } } + // Calculate the number of blocks left until the target is met. + pub fn blocks_left_until(&self, target: T) -> u32 + where + T: Into + Copy, + { + match self { + ScriptStatus::Confirmed(inner) => inner.blocks_left_until(target), + _ => target.into(), + } + } + pub fn has_been_seen(&self) -> bool { matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_)) } @@ -987,7 +1010,7 @@ mod tests { fn given_depth_0_should_meet_confirmation_target_one() { let script = ScriptStatus::Confirmed(Confirmed { depth: 0 }); - let confirmed = script.is_confirmed_with(1); + let confirmed = script.is_confirmed_with(1_u32); assert!(confirmed) } @@ -996,7 +1019,7 @@ mod tests { fn given_confirmations_1_should_meet_confirmation_target_one() { let script = ScriptStatus::from_confirmations(1); - let confirmed = script.is_confirmed_with(1); + let confirmed = script.is_confirmed_with(1_u32); assert!(confirmed) } @@ -1011,6 +1034,33 @@ mod tests { assert_eq!(confirmed.depth, 0) } + #[test] + fn given_depth_0_should_return_0_blocks_left_until_1() { + let script = ScriptStatus::Confirmed(Confirmed { depth: 0 }); + + let blocks_left = script.blocks_left_until(1_u32); + + assert_eq!(blocks_left, 0) + } + + #[test] + fn given_depth_1_should_return_0_blocks_left_until_1() { + let script = ScriptStatus::Confirmed(Confirmed { depth: 1 }); + + let blocks_left = script.blocks_left_until(1_u32); + + assert_eq!(blocks_left, 0) + } + + #[test] + fn given_depth_0_should_return_1_blocks_left_until_2() { + let script = ScriptStatus::Confirmed(Confirmed { depth: 0 }); + + let blocks_left = script.blocks_left_until(2_u32); + + assert_eq!(blocks_left, 1) + } + #[test] fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() { // 400 weight = 100 vbyte diff --git a/swap/src/cli/cancel_and_refund.rs b/swap/src/cli/cancel_and_refund.rs index b42c124c..d542b7ed 100644 --- a/swap/src/cli/cancel_and_refund.rs +++ b/swap/src/cli/cancel_and_refund.rs @@ -10,7 +10,7 @@ use uuid::Uuid; pub async fn cancel_and_refund( swap_id: Uuid, bitcoin_wallet: Arc, - db: Arc, + db: Arc, ) -> Result { if let Err(err) = cancel(swap_id, bitcoin_wallet.clone(), db.clone()).await { tracing::info!(%err, "Could not submit cancel transaction"); @@ -28,7 +28,7 @@ pub async fn cancel_and_refund( pub async fn cancel( swap_id: Uuid, bitcoin_wallet: Arc, - db: Arc, + db: Arc, ) -> Result<(Txid, Subscription, BobState)> { let state = db.get_state(swap_id).await?.try_into()?; @@ -80,7 +80,7 @@ pub async fn cancel( pub async fn refund( swap_id: Uuid, bitcoin_wallet: Arc, - db: Arc, + db: Arc, ) -> Result { let state = db.get_state(swap_id).await?.try_into()?; diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 5c3b2827..affd6a19 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,43 +1,39 @@ -use crate::bitcoin::Amount; -use crate::env::GetConfig; -use crate::fs::system_data_dir; -use crate::network::rendezvous::XmrBtcNamespace; -use crate::{env, monero}; -use anyhow::{bail, Context, Result}; -use bitcoin::{Address, AddressType}; +use crate::api::request::{Method, Request}; +use crate::api::Context; +use crate::bitcoin::{bitcoin_address, Amount}; +use crate::monero; +use crate::monero::monero_address; +use anyhow::Result; use libp2p::core::Multiaddr; -use serde::Serialize; use std::ffi::OsString; +use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; +use std::sync::Arc; use structopt::{clap, StructOpt}; use url::Url; use uuid::Uuid; +// See: https://moneroworld.com/ +pub const DEFAULT_MONERO_DAEMON_ADDRESS: &str = "node.community.rino.io:18081"; +pub const DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET: &str = "stagenet.community.rino.io:38081"; + // See: https://1209k.com/bitcoin-eye/ele.php?chain=btc const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://blockstream.info:700"; // See: https://1209k.com/bitcoin-eye/ele.php?chain=tbtc pub const DEFAULT_ELECTRUM_RPC_URL_TESTNET: &str = "ssl://electrum.blockstream.info:60002"; const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 3; -const DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET: usize = 1; +pub const DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET: usize = 1; const DEFAULT_TOR_SOCKS5_PORT: &str = "9050"; -#[derive(Debug, PartialEq, Eq)] -pub struct Arguments { - pub env_config: env::Config, - pub debug: bool, - pub json: bool, - pub data_dir: PathBuf, - pub cmd: Command, -} - /// Represents the result of parsing the command-line parameters. -#[derive(Debug, PartialEq, Eq)] + +#[derive(Debug)] pub enum ParseResult { /// The arguments we were invoked in. - Arguments(Box), + Context(Arc, Box), /// A flag or command was given that does not need further processing other /// than printing the provided message. /// @@ -45,13 +41,13 @@ pub enum ParseResult { PrintAndExitZero { message: String }, } -pub fn parse_args_and_apply_defaults(raw_args: I) -> Result +pub async fn parse_args_and_apply_defaults(raw_args: I) -> Result where I: IntoIterator, T: Into + Clone, { - let args = match RawArguments::clap().get_matches_from_safe(raw_args) { - Ok(matches) => RawArguments::from_clap(&matches), + let args = match Arguments::clap().get_matches_from_safe(raw_args) { + Ok(matches) => Arguments::from_clap(&matches), Err(clap::Error { message, kind: clap::ErrorKind::HelpDisplayed | clap::ErrorKind::VersionDisplayed, @@ -64,233 +60,195 @@ where let json = args.json; let is_testnet = args.testnet; let data = args.data; - - let arguments = match args.cmd { - RawCommand::BuyXmr { + let (context, request) = match args.cmd { + CliCommand::BuyXmr { seller: Seller { seller }, bitcoin, bitcoin_change_address, monero, monero_receive_address, - tor: Tor { tor_socks5_port }, + tor, } => { - let (bitcoin_electrum_rpc_url, bitcoin_target_block) = - bitcoin.apply_defaults(is_testnet)?; let monero_receive_address = - validate_monero_address(monero_receive_address, is_testnet)?; + monero_address::validate_is_testnet(monero_receive_address, is_testnet)?; let bitcoin_change_address = - validate_bitcoin_address(bitcoin_change_address, is_testnet)?; - let monero_daemon_address = monero.monero_daemon_address; + bitcoin_address::validate_is_testnet(bitcoin_change_address, is_testnet)?; - Arguments { - env_config: env_config_from(is_testnet), + let request = Request::new(Method::BuyXmr { + seller, + bitcoin_change_address, + monero_receive_address, + swap_id: Uuid::new_v4(), + }); + + let context = Context::build( + Some(bitcoin), + Some(monero), + Some(tor), + data, + is_testnet, debug, json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::BuyXmr { - seller, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - bitcoin_change_address, - monero_receive_address, - monero_daemon_address, - tor_socks5_port, - namespace: XmrBtcNamespace::from_is_testnet(is_testnet), - }, - } + None, + ) + .await?; + (context, request) } - RawCommand::History => Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::History, - }, - RawCommand::Config => Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::Config, - }, - RawCommand::Balance { - bitcoin_electrum_rpc_url, + CliCommand::History => { + let request = Request::new(Method::History); + + let context = + Context::build(None, None, None, data, is_testnet, debug, json, None).await?; + (context, request) + } + CliCommand::Config => { + let request = Request::new(Method::Config); + + let context = + Context::build(None, None, None, data, is_testnet, debug, json, None).await?; + (context, request) + } + CliCommand::Balance { bitcoin } => { + let request = Request::new(Method::Balance { + force_refresh: true, + }); + + let context = Context::build( + Some(bitcoin), + None, + None, + data, + is_testnet, + debug, + json, + None, + ) + .await?; + (context, request) + } + CliCommand::StartDaemon { + server_address, + bitcoin, + monero, + tor, } => { - let bitcoin = Bitcoin { - bitcoin_electrum_rpc_url, - bitcoin_target_block: None, - }; - let (bitcoin_electrum_rpc_url, bitcoin_target_block) = - bitcoin.apply_defaults(is_testnet)?; + let request = Request::new(Method::StartDaemon { server_address }); - Arguments { - env_config: env_config_from(is_testnet), + let context = Context::build( + Some(bitcoin), + Some(monero), + Some(tor), + data, + is_testnet, debug, json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::Balance { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - }, - } + server_address, + ) + .await?; + (context, request) } - RawCommand::WithdrawBtc { + CliCommand::WithdrawBtc { bitcoin, amount, address, } => { - let (bitcoin_electrum_rpc_url, bitcoin_target_block) = - bitcoin.apply_defaults(is_testnet)?; + let address = bitcoin_address::validate_is_testnet(address, is_testnet)?; + let request = Request::new(Method::WithdrawBtc { amount, address }); - Arguments { - env_config: env_config_from(is_testnet), + let context = Context::build( + Some(bitcoin), + None, + None, + data, + is_testnet, debug, json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::WithdrawBtc { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - amount, - address: bitcoin_address(address, is_testnet)?, - }, - } + None, + ) + .await?; + (context, request) } - RawCommand::Resume { + CliCommand::Resume { swap_id: SwapId { swap_id }, bitcoin, monero, - tor: Tor { tor_socks5_port }, + tor, } => { - let (bitcoin_electrum_rpc_url, bitcoin_target_block) = - bitcoin.apply_defaults(is_testnet)?; - let monero_daemon_address = monero.monero_daemon_address; + let request = Request::new(Method::Resume { swap_id }); - Arguments { - env_config: env_config_from(is_testnet), + let context = Context::build( + Some(bitcoin), + Some(monero), + Some(tor), + data, + is_testnet, debug, json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::Resume { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - monero_daemon_address, - tor_socks5_port, - namespace: XmrBtcNamespace::from_is_testnet(is_testnet), - }, - } + None, + ) + .await?; + (context, request) } - RawCommand::CancelAndRefund { + CliCommand::CancelAndRefund { swap_id: SwapId { swap_id }, bitcoin, + tor, } => { - let (bitcoin_electrum_rpc_url, bitcoin_target_block) = - bitcoin.apply_defaults(is_testnet)?; + let request = Request::new(Method::CancelAndRefund { swap_id }); - Arguments { - env_config: env_config_from(is_testnet), + let context = Context::build( + Some(bitcoin), + None, + Some(tor), + data, + is_testnet, debug, json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::CancelAndRefund { - swap_id, - bitcoin_electrum_rpc_url, - bitcoin_target_block, - }, - } + None, + ) + .await?; + (context, request) } - RawCommand::ListSellers { + CliCommand::ListSellers { rendezvous_point, - 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::ListSellers { - rendezvous_point, - tor_socks5_port, - namespace: XmrBtcNamespace::from_is_testnet(is_testnet), - }, - }, - RawCommand::ExportBitcoinWallet { bitcoin } => { - let (bitcoin_electrum_rpc_url, bitcoin_target_block) = - bitcoin.apply_defaults(is_testnet)?; + tor, + } => { + let request = Request::new(Method::ListSellers { rendezvous_point }); - Arguments { - env_config: env_config_from(is_testnet), + let context = + Context::build(None, None, Some(tor), data, is_testnet, debug, json, None).await?; + + (context, request) + } + CliCommand::ExportBitcoinWallet { bitcoin } => { + let request = Request::new(Method::ExportBitcoinWallet); + + let context = Context::build( + Some(bitcoin), + None, + None, + data, + is_testnet, debug, json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::ExportBitcoinWallet { - bitcoin_electrum_rpc_url, - bitcoin_target_block, - }, - } + None, + ) + .await?; + (context, request) + } + CliCommand::MoneroRecovery { + swap_id: SwapId { swap_id }, + } => { + let request = Request::new(Method::MoneroRecovery { swap_id }); + + let context = + Context::build(None, None, None, data, is_testnet, debug, json, None).await?; + + (context, request) } - RawCommand::MoneroRecovery { swap_id } => Arguments { - env_config: env_config_from(is_testnet), - debug, - json, - data_dir: data::data_dir_from(data, is_testnet)?, - cmd: Command::MoneroRecovery { - swap_id: swap_id.swap_id, - }, - }, }; - Ok(ParseResult::Arguments(Box::new(arguments))) -} - -#[derive(Debug, PartialEq, Eq)] -pub enum Command { - BuyXmr { - seller: Multiaddr, - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - bitcoin_change_address: bitcoin::Address, - monero_receive_address: monero::Address, - monero_daemon_address: Option, - tor_socks5_port: u16, - namespace: XmrBtcNamespace, - }, - History, - Config, - WithdrawBtc { - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - amount: Option, - address: Address, - }, - Balance { - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - }, - Resume { - swap_id: Uuid, - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - monero_daemon_address: Option, - tor_socks5_port: u16, - namespace: XmrBtcNamespace, - }, - CancelAndRefund { - swap_id: Uuid, - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - }, - ListSellers { - rendezvous_point: Multiaddr, - namespace: XmrBtcNamespace, - tor_socks5_port: u16, - }, - ExportBitcoinWallet { - bitcoin_electrum_rpc_url: Url, - bitcoin_target_block: usize, - }, - MoneroRecovery { - swap_id: Uuid, - }, + Ok(ParseResult::Context(Arc::new(context), Box::new(request))) } #[derive(structopt::StructOpt, Debug)] @@ -300,7 +258,7 @@ pub enum Command { author, version = env!("VERGEN_GIT_DESCRIBE") )] -struct RawArguments { +struct Arguments { // global is necessary to ensure that clap can match against testnet in subcommands #[structopt( long, @@ -327,11 +285,11 @@ struct RawArguments { json: bool, #[structopt(subcommand)] - cmd: RawCommand, + cmd: CliCommand, } #[derive(structopt::StructOpt, Debug)] -enum RawCommand { +enum CliCommand { /// Start a BTC for XMR swap BuyXmr { #[structopt(flatten)] @@ -342,7 +300,8 @@ enum RawCommand { #[structopt( long = "change-address", - help = "The bitcoin address where any form of change or excess funds should be sent to" + help = "The bitcoin address where any form of change or excess funds should be sent to", + parse(try_from_str = bitcoin_address::parse) )] bitcoin_change_address: bitcoin::Address, @@ -351,7 +310,7 @@ enum RawCommand { #[structopt(long = "receive-address", help = "The monero address where you would like to receive monero", - parse(try_from_str = parse_monero_address) + parse(try_from_str = monero_address::parse) )] monero_receive_address: monero::Address, @@ -372,13 +331,34 @@ enum RawCommand { help = "Optionally specify the amount of Bitcoin to be withdrawn. If not specified the wallet will be drained." )] amount: Option, - #[structopt(long = "address", help = "The address to receive the Bitcoin.")] - address: Address, + + #[structopt(long = "address", + help = "The address to receive the Bitcoin.", + parse(try_from_str = bitcoin_address::parse) + )] + address: bitcoin::Address, }, #[structopt(about = "Prints the Bitcoin balance.")] Balance { - #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")] - bitcoin_electrum_rpc_url: Option, + #[structopt(flatten)] + bitcoin: Bitcoin, + }, + #[structopt(about = "Starts a JSON-RPC server")] + StartDaemon { + #[structopt(flatten)] + bitcoin: Bitcoin, + + #[structopt(flatten)] + monero: Monero, + + #[structopt( + long = "server-address", + help = "The socket address the server should use" + )] + server_address: Option, + + #[structopt(flatten)] + tor: Tor, }, /// Resume a swap Resume { @@ -402,6 +382,9 @@ enum RawCommand { #[structopt(flatten)] bitcoin: Bitcoin, + + #[structopt(flatten)] + tor: Tor, }, /// Discover and list sellers (i.e. ASB providers) ListSellers { @@ -429,28 +412,40 @@ enum RawCommand { } #[derive(structopt::StructOpt, Debug)] -struct Monero { +pub struct Monero { #[structopt( long = "monero-daemon-address", - help = "Specify to connect to a monero daemon of your choice: :. If none is specified, we will connect to a public node." + help = "Specify to connect to a monero daemon of your choice: :" )] - monero_daemon_address: Option, + pub monero_daemon_address: Option, +} + +impl Monero { + pub 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 { +pub struct Bitcoin { #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")] - bitcoin_electrum_rpc_url: Option, + pub bitcoin_electrum_rpc_url: Option, #[structopt( long = "bitcoin-target-block", help = "Estimate Bitcoin fees such that transactions are confirmed within the specified number of blocks" )] - bitcoin_target_block: Option, + pub bitcoin_target_block: Option, } impl Bitcoin { - fn apply_defaults(self, testnet: bool) -> Result<(Url, usize)> { + pub 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 { @@ -472,13 +467,13 @@ impl Bitcoin { } #[derive(structopt::StructOpt, Debug)] -struct Tor { +pub struct Tor { #[structopt( long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT )] - tor_socks5_port: u16, + pub tor_socks5_port: u16, } #[derive(structopt::StructOpt, Debug)] @@ -499,137 +494,26 @@ struct Seller { seller: Multiaddr, } -mod data { - use super::*; - - pub fn data_dir_from(arg_dir: Option, testnet: bool) -> Result { - let base_dir = match arg_dir { - Some(custom_base_dir) => custom_base_dir, - None => os_default()?, - }; - - let sub_directory = if testnet { "testnet" } else { "mainnet" }; - - Ok(base_dir.join(sub_directory)) - } - - fn os_default() -> Result { - Ok(system_data_dir()?.join("cli")) - } -} - -fn env_config_from(testnet: bool) -> env::Config { - if testnet { - env::Testnet::get_config() - } else { - env::Mainnet::get_config() - } -} - -fn bitcoin_address(address: Address, is_testnet: bool) -> Result

{ - let network = if is_testnet { - bitcoin::Network::Testnet - } else { - bitcoin::Network::Bitcoin - }; - - if address.network != network { - bail!(BitcoinAddressNetworkMismatch { - expected: network, - actual: address.network - }); - } - - Ok(address) -} - -fn validate_monero_address( - address: monero::Address, - testnet: bool, -) -> Result { - let expected_network = if testnet { - monero::Network::Stagenet - } else { - monero::Network::Mainnet - }; - - if address.network != expected_network { - return Err(MoneroAddressNetworkMismatch { - expected: expected_network, - actual: address.network, - }); - } - - Ok(address) -} - -fn validate_bitcoin_address(address: bitcoin::Address, testnet: bool) -> Result { - 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::from_str(s).with_context(|| { - format!( - "Failed to parse {} as a monero address, please make sure it is a valid address", - s - ) - }) -} - -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] -#[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, -} - -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq, Serialize)] -#[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")] -pub struct BitcoinAddressNetworkMismatch { - #[serde(with = "crate::bitcoin::network")] - expected: bitcoin::Network, - #[serde(with = "crate::bitcoin::network")] - actual: bitcoin::Network, -} - #[cfg(test)] mod tests { use super::*; - use crate::tor::DEFAULT_SOCKS5_PORT; + + use crate::api::api_test::*; + use crate::api::Config; + use crate::monero::monero_address::MoneroAddressNetworkMismatch; const BINARY_NAME: &str = "swap"; + const ARGS_DATA_DIR: &str = "/tmp/dir/"; - const TESTNET: &str = "testnet"; - const MAINNET: &str = "mainnet"; + #[tokio::test] - const MONERO_STAGENET_ADDRESS: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a"; - const BITCOIN_TESTNET_ADDRESS: &str = "tb1qr3em6k3gfnyl8r7q0v7t4tlnyxzgxma3lressv"; - const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa"; - const BITCOIN_MAINNET_ADDRESS: &str = "bc1qe4epnfklcaa0mun26yz5g8k24em5u9f92hy325"; - const MULTI_ADDRESS: &str = - "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; - const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b"; + // this test is very long, however it just checks that various CLI arguments sets the + // internal Context and Request properly. It is unlikely to fail and splitting it in various + // tests would require to run the tests sequantially which is very slow (due to the context + // need to access files like the Bitcoin wallet). + async fn test_cli_arguments() { + // given_buy_xmr_on_mainnet_then_defaults_to_mainnet - #[test] - fn given_buy_xmr_on_mainnet_then_defaults_to_mainnet() { let raw_ars = vec![ BINARY_NAME, "buy-xmr", @@ -641,15 +525,34 @@ mod tests { MULTI_ADDRESS, ]; - let expected_args = - ParseResult::Arguments(Arguments::buy_xmr_mainnet_defaults().into_boxed()); - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, false, false); - assert_eq!(expected_args, args); - } + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; - #[test] - fn given_buy_xmr_on_testnet_then_defaults_to_testnet() { + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::buy_xmr(is_testnet), + ); + + // since Uuid is random, copy before comparing requests + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_buy_xmr_on_testnet_then_defaults_to_testnet let raw_ars = vec![ BINARY_NAME, "--testnet", @@ -662,16 +565,33 @@ mod tests { MULTI_ADDRESS, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, false, false); - assert_eq!( - args, - ParseResult::Arguments(Arguments::buy_xmr_testnet_defaults().into_boxed()) + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::buy_xmr(is_testnet), ); - } - #[test] - fn given_buy_xmr_on_mainnet_with_testnet_address_then_fails() { + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_buy_xmr_on_mainnet_with_testnet_address_then_fails let raw_ars = vec![ BINARY_NAME, "buy-xmr", @@ -683,7 +603,7 @@ mod tests { MULTI_ADDRESS, ]; - let err = parse_args_and_apply_defaults(raw_ars).unwrap_err(); + let err = parse_args_and_apply_defaults(raw_ars).await.unwrap_err(); assert_eq!( err.downcast_ref::().unwrap(), @@ -692,10 +612,8 @@ mod tests { actual: monero::Network::Stagenet } ); - } - #[test] - fn given_buy_xmr_on_testnet_with_mainnet_address_then_fails() { + // given_buy_xmr_on_testnet_with_mainnet_address_then_fails let raw_ars = vec![ BINARY_NAME, "--testnet", @@ -708,7 +626,7 @@ mod tests { MULTI_ADDRESS, ]; - let err = parse_args_and_apply_defaults(raw_ars).unwrap_err(); + let err = parse_args_and_apply_defaults(raw_ars).await.unwrap_err(); assert_eq!( err.downcast_ref::().unwrap(), @@ -717,88 +635,126 @@ mod tests { actual: monero::Network::Mainnet } ); - } - #[test] - fn given_resume_on_mainnet_then_defaults_to_mainnet() { + // given_resume_on_mainnet_then_defaults_to_mainnet let raw_ars = vec![BINARY_NAME, "resume", "--swap-id", SWAP_ID]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, false, false); - assert_eq!( - args, - ParseResult::Arguments(Arguments::resume_mainnet_defaults().into_boxed()) + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::resume(), ); - } - #[test] - fn given_resume_on_testnet_then_defaults_to_testnet() { + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_resume_on_testnet_then_defaults_to_testnet let raw_ars = vec![BINARY_NAME, "--testnet", "resume", "--swap-id", SWAP_ID]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, false, false); - assert_eq!( - args, - ParseResult::Arguments(Arguments::resume_testnet_defaults().into_boxed()) + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::resume(), ); - } - #[test] - fn given_cancel_on_mainnet_then_defaults_to_mainnet() { + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_cancel_on_mainnet_then_defaults_to_mainnet let raw_ars = vec![BINARY_NAME, "cancel", "--swap-id", SWAP_ID]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); - assert_eq!( - args, - ParseResult::Arguments(Arguments::cancel_mainnet_defaults().into_boxed()) + let (is_testnet, debug, json) = (false, false, false); + + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::cancel(), ); - } - #[test] - fn given_cancel_on_testnet_then_defaults_to_testnet() { + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_cancel_on_testnet_then_defaults_to_testnet let raw_ars = vec![BINARY_NAME, "--testnet", "cancel", "--swap-id", SWAP_ID]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, false, false); - assert_eq!( - args, - ParseResult::Arguments(Arguments::cancel_testnet_defaults().into_boxed()) + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::cancel(), ); - } - #[test] - fn given_refund_on_mainnet_then_defaults_to_mainnet() { + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + let raw_ars = vec![BINARY_NAME, "refund", "--swap-id", SWAP_ID]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, false, false); - assert_eq!( - args, - ParseResult::Arguments(Arguments::refund_mainnet_defaults().into_boxed()) + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::refund(), ); - } - #[test] - fn given_refund_on_testnet_then_defaults_to_testnet() { + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_refund_on_testnet_then_defaults_to_testnet let raw_ars = vec![BINARY_NAME, "--testnet", "refund", "--swap-id", SWAP_ID]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, false, false); - assert_eq!( - args, - ParseResult::Arguments(Arguments::refund_testnet_defaults().into_boxed()) + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::refund(), ); - } - #[test] - fn given_with_data_dir_then_data_dir_set() { - let data_dir = "/some/path/to/dir"; + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_buy_xmr_on_mainnet_with_data_dir_then_data_dir_set let raw_ars = vec![ BINARY_NAME, "--data-base-dir", - data_dir, + ARGS_DATA_DIR, "buy-xmr", "--change-address", BITCOIN_MAINNET_ADDRESS, @@ -808,22 +764,39 @@ mod tests { MULTI_ADDRESS, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, false, false); + let data_dir = PathBuf::from_str(ARGS_DATA_DIR).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::buy_xmr_mainnet_defaults() - .with_data_dir(PathBuf::from_str(data_dir).unwrap().join("mainnet")) - .into_boxed() - ) + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, Some(data_dir.clone()), debug, json), + Request::buy_xmr(is_testnet), ); + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_buy_xmr_on_testnet_with_data_dir_then_data_dir_set let raw_ars = vec![ BINARY_NAME, "--testnet", "--data-base-dir", - data_dir, + ARGS_DATA_DIR, "buy-xmr", "--change-address", BITCOIN_TESTNET_ADDRESS, @@ -833,61 +806,99 @@ mod tests { MULTI_ADDRESS, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let data_dir = PathBuf::from_str(ARGS_DATA_DIR).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, false, false); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::buy_xmr_testnet_defaults() - .with_data_dir(PathBuf::from_str(data_dir).unwrap().join("testnet")) - .into_boxed() - ) + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, Some(data_dir.clone()), debug, json), + Request::buy_xmr(is_testnet), ); + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_resume_on_mainnet_with_data_dir_then_data_dir_set let raw_ars = vec![ BINARY_NAME, "--data-base-dir", - data_dir, + ARGS_DATA_DIR, "resume", "--swap-id", SWAP_ID, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let data_dir = PathBuf::from_str(ARGS_DATA_DIR).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, false, false); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::resume_mainnet_defaults() - .with_data_dir(PathBuf::from_str(data_dir).unwrap().join("mainnet")) - .into_boxed() - ) + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, Some(data_dir.clone()), debug, json), + Request::resume(), ); + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_resume_on_testnet_with_data_dir_then_data_dir_set let raw_ars = vec![ BINARY_NAME, "--testnet", "--data-base-dir", - data_dir, + ARGS_DATA_DIR, "resume", "--swap-id", SWAP_ID, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + let data_dir = PathBuf::from_str(ARGS_DATA_DIR).unwrap(); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, false, false); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::resume_testnet_defaults() - .with_data_dir(PathBuf::from_str(data_dir).unwrap().join("testnet")) - .into_boxed() - ) + let (expected_config, expected_request) = ( + Config::default(is_testnet, Some(data_dir.clone()), debug, json), + Request::resume(), ); - } - #[test] - fn given_with_debug_then_debug_set() { + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_buy_xmr_on_mainnet_with_debug_then_debug_set let raw_ars = vec![ BINARY_NAME, "--debug", @@ -900,16 +911,33 @@ mod tests { MULTI_ADDRESS, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::buy_xmr_mainnet_defaults() - .with_debug() - .into_boxed() - ) + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, true, false); + + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::buy_xmr(is_testnet), ); + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_buy_xmr_on_testnet_with_debug_then_debug_set let raw_ars = vec![ BINARY_NAME, "--testnet", @@ -923,28 +951,62 @@ mod tests { MULTI_ADDRESS, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::buy_xmr_testnet_defaults() - .with_debug() - .into_boxed() - ) + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, true, false); + + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::buy_xmr(is_testnet), ); + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_resume_on_mainnet_with_debug_then_debug_set let raw_ars = vec![BINARY_NAME, "--debug", "resume", "--swap-id", SWAP_ID]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::resume_mainnet_defaults() - .with_debug() - .into_boxed() - ) + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, true, false); + + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::resume(), ); + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_resume_on_testnet_with_debug_then_debug_set let raw_ars = vec![ BINARY_NAME, "--testnet", @@ -954,19 +1016,23 @@ mod tests { SWAP_ID, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::resume_testnet_defaults() - .with_debug() - .into_boxed() - ) - ); - } + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, true, false); - #[test] - fn given_with_json_then_json_set() { + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::resume(), + ); + + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_buy_xmr_on_mainnet_with_json_then_json_set let raw_ars = vec![ BINARY_NAME, "--json", @@ -979,16 +1045,33 @@ mod tests { MULTI_ADDRESS, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::buy_xmr_mainnet_defaults() - .with_json() - .into_boxed() - ) + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, false, true); + + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::buy_xmr(is_testnet), ); + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_buy_xmr_on_testnet_with_json_then_json_set let raw_ars = vec![ BINARY_NAME, "--testnet", @@ -1002,28 +1085,51 @@ mod tests { MULTI_ADDRESS, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::buy_xmr_testnet_defaults() - .with_json() - .into_boxed() - ) - ); + let (is_testnet, debug, json) = (true, false, true); + let (expected_config, mut expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::buy_xmr(is_testnet), + ); + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + if let Method::BuyXmr { + ref mut swap_id, .. + } = expected_request.cmd + { + *swap_id = match actual_request.cmd { + Method::BuyXmr { swap_id, .. } => swap_id, + _ => panic!("Not the Method we expected"), + } + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_resume_on_mainnet_with_json_then_json_set let raw_ars = vec![BINARY_NAME, "--json", "resume", "--swap-id", SWAP_ID]; + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (false, false, true); - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::resume_mainnet_defaults() - .with_json() - .into_boxed() - ) + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::resume(), ); + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // given_resume_on_testnet_with_json_then_json_set let raw_ars = vec![ BINARY_NAME, "--testnet", @@ -1033,19 +1139,23 @@ mod tests { SWAP_ID, ]; - let args = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert_eq!( - args, - ParseResult::Arguments( - Arguments::resume_testnet_defaults() - .with_json() - .into_boxed() - ) - ); - } + let args = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + let (is_testnet, debug, json) = (true, false, true); - #[test] - fn only_bech32_addresses_mainnet_are_allowed() { + let (expected_config, expected_request) = ( + Config::default(is_testnet, None, debug, json), + Request::resume(), + ); + + let (actual_config, actual_request) = match args { + ParseResult::Context(context, request) => (context.config.clone(), request), + _ => panic!("Couldn't parse result"), + }; + + assert_eq!(actual_config, expected_config); + assert_eq!(actual_request, Box::new(expected_request)); + + // only_bech32_addresses_mainnet_are_allowed let raw_ars = vec![ BINARY_NAME, "buy-xmr", @@ -1056,11 +1166,7 @@ mod tests { "--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!" - ); + parse_args_and_apply_defaults(raw_ars).await.unwrap_err(); let raw_ars = vec![ BINARY_NAME, @@ -1072,11 +1178,7 @@ mod tests { "--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!" - ); + parse_args_and_apply_defaults(raw_ars).await.unwrap_err(); let raw_ars = vec![ BINARY_NAME, @@ -1088,12 +1190,10 @@ mod tests { "--seller", MULTI_ADDRESS, ]; - let result = parse_args_and_apply_defaults(raw_ars).unwrap(); - assert!(matches!(result, ParseResult::Arguments(_))); - } + let result = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + assert!(matches!(result, ParseResult::Context(_, _))); - #[test] - fn only_bech32_addresses_testnet_are_allowed() { + // only_bech32_addresses_testnet_are_allowed let raw_ars = vec![ BINARY_NAME, "--testnet", @@ -1105,11 +1205,7 @@ mod tests { "--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!" - ); + parse_args_and_apply_defaults(raw_ars).await.unwrap_err(); let raw_ars = vec![ BINARY_NAME, @@ -1122,11 +1218,7 @@ mod tests { "--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!" - ); + parse_args_and_apply_defaults(raw_ars).await.unwrap_err(); let raw_ars = vec![ BINARY_NAME, @@ -1139,166 +1231,7 @@ mod tests { "--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 { - env_config: env::Testnet::get_config(), - debug: false, - json: false, - data_dir: data_dir_path_cli().join(TESTNET), - cmd: Command::BuyXmr { - seller: Multiaddr::from_str(MULTI_ADDRESS).unwrap(), - bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) - .unwrap(), - bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, - bitcoin_change_address: BITCOIN_TESTNET_ADDRESS.parse().unwrap(), - monero_receive_address: monero::Address::from_str(MONERO_STAGENET_ADDRESS) - .unwrap(), - monero_daemon_address: None, - tor_socks5_port: DEFAULT_SOCKS5_PORT, - namespace: XmrBtcNamespace::Testnet, - }, - } - } - - pub fn buy_xmr_mainnet_defaults() -> Self { - Self { - env_config: env::Mainnet::get_config(), - debug: false, - json: false, - data_dir: data_dir_path_cli().join(MAINNET), - cmd: Command::BuyXmr { - seller: Multiaddr::from_str(MULTI_ADDRESS).unwrap(), - bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), - bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, - bitcoin_change_address: BITCOIN_MAINNET_ADDRESS.parse().unwrap(), - monero_receive_address: monero::Address::from_str(MONERO_MAINNET_ADDRESS) - .unwrap(), - monero_daemon_address: None, - tor_socks5_port: DEFAULT_SOCKS5_PORT, - namespace: XmrBtcNamespace::Mainnet, - }, - } - } - - pub fn resume_testnet_defaults() -> Self { - Self { - env_config: env::Testnet::get_config(), - debug: false, - json: false, - data_dir: data_dir_path_cli().join(TESTNET), - cmd: Command::Resume { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) - .unwrap(), - bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, - monero_daemon_address: None, - tor_socks5_port: DEFAULT_SOCKS5_PORT, - namespace: XmrBtcNamespace::Testnet, - }, - } - } - - pub fn resume_mainnet_defaults() -> Self { - Self { - env_config: env::Mainnet::get_config(), - debug: false, - json: false, - data_dir: data_dir_path_cli().join(MAINNET), - cmd: Command::Resume { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), - bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, - monero_daemon_address: None, - tor_socks5_port: DEFAULT_SOCKS5_PORT, - namespace: XmrBtcNamespace::Mainnet, - }, - } - } - - pub fn cancel_testnet_defaults() -> Self { - Self { - env_config: env::Testnet::get_config(), - debug: false, - json: false, - data_dir: data_dir_path_cli().join(TESTNET), - cmd: Command::CancelAndRefund { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) - .unwrap(), - bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, - }, - } - } - - pub fn cancel_mainnet_defaults() -> Self { - Self { - env_config: env::Mainnet::get_config(), - debug: false, - json: false, - data_dir: data_dir_path_cli().join(MAINNET), - cmd: Command::CancelAndRefund { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), - bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, - }, - } - } - - pub fn refund_testnet_defaults() -> Self { - Self { - env_config: env::Testnet::get_config(), - debug: false, - json: false, - data_dir: data_dir_path_cli().join(TESTNET), - cmd: Command::CancelAndRefund { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) - .unwrap(), - bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, - }, - } - } - - pub fn refund_mainnet_defaults() -> Self { - Self { - env_config: env::Mainnet::get_config(), - debug: false, - json: false, - data_dir: data_dir_path_cli().join(MAINNET), - cmd: Command::CancelAndRefund { - swap_id: Uuid::from_str(SWAP_ID).unwrap(), - bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), - bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, - }, - } - } - - pub fn with_data_dir(mut self, data_dir: PathBuf) -> Self { - self.data_dir = data_dir; - self - } - - pub fn with_debug(mut self) -> Self { - self.debug = true; - self - } - - pub fn with_json(mut self) -> Self { - self.json = true; - self - } - - pub fn into_boxed(self) -> Box { - Box::new(self) - } - } - - fn data_dir_path_cli() -> PathBuf { - system_data_dir().unwrap().join("cli") + let result = parse_args_and_apply_defaults(raw_ars).await.unwrap(); + assert!(matches!(result, ParseResult::Context(_, _))); } } diff --git a/swap/src/cli/event_loop.rs b/swap/src/cli/event_loop.rs index 1799d20b..23aa0f38 100644 --- a/swap/src/cli/event_loop.rs +++ b/swap/src/cli/event_loop.rs @@ -151,17 +151,17 @@ impl EventLoop { return; } SwarmEvent::Behaviour(OutEvent::Failure { peer, error }) => { - tracing::warn!(%peer, "Communication error: {:#}", error); + tracing::warn!(%peer, err = %error, "Communication error"); return; } SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } if peer_id == self.alice_peer_id => { - tracing::info!("Connected to Alice at {}", endpoint.get_remote_address()); + tracing::info!(peer_id = %endpoint.get_remote_address(), "Connected to Alice"); } SwarmEvent::Dialing(peer_id) if peer_id == self.alice_peer_id => { - tracing::debug!("Dialling Alice at {}", peer_id); + tracing::debug!(%peer_id, "Dialling Alice"); } SwarmEvent::ConnectionClosed { peer_id, endpoint, num_established, cause: Some(error) } if peer_id == self.alice_peer_id && num_established == 0 => { - tracing::warn!("Lost connection to Alice at {}, cause: {}", endpoint.get_remote_address(), error); + tracing::warn!(peer_id = %endpoint.get_remote_address(), cause = %error, "Lost connection to Alice"); } SwarmEvent::ConnectionClosed { peer_id, num_established, cause: None, .. } if peer_id == self.alice_peer_id && num_established == 0 => { // no error means the disconnection was requested @@ -169,10 +169,10 @@ impl EventLoop { return; } SwarmEvent::OutgoingConnectionError { peer_id, error } if matches!(peer_id, Some(alice_peer_id) if alice_peer_id == self.alice_peer_id) => { - tracing::warn!( "Failed to dial Alice: {}", error); + tracing::warn!(%error, "Failed to dial Alice"); if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() { - tracing::info!("Next redial attempt in {}s", duration.as_secs()); + tracing::info!(seconds_until_next_redial = %duration.as_secs(), "Waiting for next redial attempt"); } } @@ -241,6 +241,7 @@ impl EventLoopHandle { } pub async fn request_quote(&mut self) -> Result { + tracing::debug!("Requesting quote"); Ok(self.quote.send_receive(()).await?) } diff --git a/swap/src/cli/tracing.rs b/swap/src/cli/tracing.rs index a1cd77fb..7cb0721b 100644 --- a/swap/src/cli/tracing.rs +++ b/swap/src/cli/tracing.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use std::option::Option::Some; use std::path::Path; use time::format_description::well_known::Rfc3339; use tracing::subscriber::set_global_default; @@ -7,55 +6,32 @@ use tracing::{Event, Level, Subscriber}; use tracing_subscriber::fmt::format::{DefaultFields, Format, JsonFields}; use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::layer::{Context, SubscriberExt}; -use tracing_subscriber::{fmt, EnvFilter, FmtSubscriber, Layer, Registry}; -use uuid::Uuid; +use tracing_subscriber::{fmt, EnvFilter, Layer, Registry}; -pub fn init(debug: bool, json: bool, dir: impl AsRef, swap_id: Option) -> Result<()> { - if let Some(swap_id) = swap_id { - let level_filter = EnvFilter::try_new("swap=debug")?; +pub fn init(debug: bool, json: bool, dir: impl AsRef) -> Result<()> { + let level_filter = EnvFilter::try_new("swap=debug")?; + let registry = Registry::default().with(level_filter); - let registry = Registry::default().with(level_filter); + let appender = tracing_appender::rolling::never(dir.as_ref(), "swap-all.log"); + let (appender, _guard) = tracing_appender::non_blocking(appender); - let appender = - tracing_appender::rolling::never(dir.as_ref(), format!("swap-{}.log", swap_id)); - let (appender, guard) = tracing_appender::non_blocking(appender); + let file_logger = registry.with( + fmt::layer() + .with_ansi(false) + .with_target(false) + .json() + .with_writer(appender), + ); - std::mem::forget(guard); - - let file_logger = registry.with( - fmt::layer() - .with_ansi(false) - .with_target(false) - .json() - .with_writer(appender), - ); - - if json && debug { - set_global_default(file_logger.with(debug_json_terminal_printer()))?; - } else if json && !debug { - set_global_default(file_logger.with(info_json_terminal_printer()))?; - } else if !json && debug { - set_global_default(file_logger.with(debug_terminal_printer()))?; - } else { - set_global_default(file_logger.with(info_terminal_printer()))?; - } + if json && debug { + set_global_default(file_logger.with(debug_json_terminal_printer()))?; + } else if json && !debug { + set_global_default(file_logger.with(info_json_terminal_printer()))?; + } else if !json && debug { + set_global_default(file_logger.with(debug_terminal_printer()))?; } else { - let level = if debug { Level::DEBUG } else { Level::INFO }; - let is_terminal = atty::is(atty::Stream::Stderr); - - let builder = FmtSubscriber::builder() - .with_env_filter(format!("swap={}", level)) - .with_writer(std::io::stderr) - .with_ansi(is_terminal) - .with_timer(UtcTime::rfc_3339()) - .with_target(false); - - if json { - builder.json().init(); - } else { - builder.init(); - } - }; + set_global_default(file_logger.with(info_terminal_printer()))?; + } tracing::info!("Logging initialized to {}", dir.as_ref().display()); Ok(()) @@ -66,19 +42,11 @@ pub struct StdErrPrinter { level: Level, } -type StdErrLayer = tracing_subscriber::fmt::Layer< - S, - DefaultFields, - Format, - fn() -> std::io::Stderr, ->; +type StdErrLayer = + fmt::Layer, fn() -> std::io::Stderr>; -type StdErrJsonLayer = tracing_subscriber::fmt::Layer< - S, - JsonFields, - Format, - fn() -> std::io::Stderr, ->; +type StdErrJsonLayer = + fmt::Layer, fn() -> std::io::Stderr>; fn debug_terminal_printer() -> StdErrPrinter>> { let is_terminal = atty::is(atty::Stream::Stderr); diff --git a/swap/src/database/sqlite.rs b/swap/src/database/sqlite.rs index d39e3f87..751f76aa 100644 --- a/swap/src/database/sqlite.rs +++ b/swap/src/database/sqlite.rs @@ -1,11 +1,12 @@ use crate::database::Swap; use crate::monero::Address; use crate::protocol::{Database, State}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use libp2p::{Multiaddr, PeerId}; use sqlx::sqlite::Sqlite; use sqlx::{Pool, SqlitePool}; +use std::collections::HashMap; use std::path::Path; use std::str::FromStr; use time::OffsetDateTime; @@ -149,7 +150,7 @@ impl Database for SqliteDatabase { let rows = sqlx::query!( r#" - SELECT address + SELECT DISTINCT address FROM peer_addresses WHERE peer_id = ? "#, @@ -169,6 +170,25 @@ impl Database for SqliteDatabase { addresses } + async fn get_swap_start_date(&self, swap_id: Uuid) -> Result { + let mut conn = self.pool.acquire().await?; + let swap_id = swap_id.to_string(); + + let row = sqlx::query!( + r#" + SELECT min(entered_at) as start_date + FROM swap_states + WHERE swap_id = ? + "#, + swap_id + ) + .fetch_one(&mut conn) + .await?; + + row.start_date + .ok_or_else(|| anyhow!("Could not get swap start date")) + } + async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()> { let mut conn = self.pool.acquire().await?; let entered_at = OffsetDateTime::now_utc(); @@ -249,6 +269,69 @@ impl Database for SqliteDatabase { result } + + async fn get_states(&self, swap_id: Uuid) -> Result> { + let mut conn = self.pool.acquire().await?; + let swap_id = swap_id.to_string(); + + // TODO: We should use query! instead of query here to allow for at-compile-time validation + // I didn't manage to generate the mappings for the query! macro because of problems with sqlx-cli + let rows = sqlx::query!( + r#" + SELECT state + FROM swap_states + WHERE swap_id = ? + "#, + swap_id + ) + .fetch_all(&mut conn) + .await?; + + let result = rows + .iter() + .map(|row| { + let state_str: &str = &row.state; + + let state = match serde_json::from_str::(state_str) { + Ok(a) => Ok(State::from(a)), + Err(e) => Err(e), + }?; + Ok(state) + }) + .collect::>>(); + + result + } + + async fn raw_all(&self) -> Result>> { + let mut conn = self.pool.acquire().await?; + let rows = sqlx::query!( + r#" + SELECT swap_id, state + FROM swap_states + "# + ) + .fetch_all(&mut conn) + .await?; + + let mut swaps: HashMap> = HashMap::new(); + + for row in &rows { + let swap_id = Uuid::from_str(&row.swap_id)?; + let state = serde_json::from_str(&row.state)?; + + if let std::collections::hash_map::Entry::Vacant(e) = swaps.entry(swap_id) { + e.insert(vec![state]); + } else { + swaps + .get_mut(&swap_id) + .ok_or_else(|| anyhow!("Error while retrieving the swap"))? + .push(state); + } + } + + Ok(swaps) + } } #[cfg(test)] diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 4865187b..84a39dcc 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -16,6 +16,7 @@ missing_copy_implementations )] +pub mod api; pub mod asb; pub mod bitcoin; pub mod cli; @@ -28,6 +29,7 @@ pub mod libp2p_ext; pub mod monero; pub mod network; pub mod protocol; +pub mod rpc; pub mod seed; pub mod tor; pub mod tracing_ext; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index d46fc157..8205e75f 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -42,6 +42,13 @@ pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey); +impl fmt::Display for PrivateViewKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Delegate to the Display implementation of PrivateKey + write!(f, "{}", self.0) + } +} + impl PrivateViewKey { pub fn new_random(rng: &mut R) -> Self { let scalar = Scalar::random(rng); @@ -320,6 +327,52 @@ pub mod monero_amount { } } +pub mod monero_address { + use anyhow::{bail, Context, Result}; + use std::str::FromStr; + + #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)] + #[error("Invalid monero address provided, expected address on network {expected:?} but address provided is on {actual:?}")] + pub struct MoneroAddressNetworkMismatch { + pub expected: monero::Network, + pub actual: monero::Network, + } + + pub fn parse(s: &str) -> Result { + monero::Address::from_str(s).with_context(|| { + format!( + "Failed to parse {} as a monero address, please make sure it is a valid address", + s + ) + }) + } + + pub fn validate( + address: monero::Address, + expected_network: monero::Network, + ) -> Result { + if address.network != expected_network { + bail!(MoneroAddressNetworkMismatch { + expected: expected_network, + actual: address.network, + }); + } + Ok(address) + } + + pub fn validate_is_testnet( + address: monero::Address, + is_testnet: bool, + ) -> Result { + let expected_network = if is_testnet { + monero::Network::Stagenet + } else { + monero::Network::Mainnet + }; + validate(address, expected_network) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/swap/src/protocol.rs b/swap/src/protocol.rs index 9389e3ae..0e15f89a 100644 --- a/swap/src/protocol.rs +++ b/swap/src/protocol.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof}; use sigma_fun::HashTranscript; +use std::collections::HashMap; use std::convert::TryInto; use uuid::Uuid; @@ -139,7 +140,10 @@ pub trait Database { async fn get_monero_address(&self, swap_id: Uuid) -> Result; async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>; async fn get_addresses(&self, peer_id: PeerId) -> Result>; + async fn get_swap_start_date(&self, swap_id: Uuid) -> Result; async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>; async fn get_state(&self, swap_id: Uuid) -> Result; + async fn get_states(&self, swap_id: Uuid) -> Result>; async fn all(&self) -> Result>; + async fn raw_all(&self) -> Result>>; } diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 941e5a43..0eef7dcd 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -112,7 +112,7 @@ where } AliceState::BtcLocked { state3 } => { match state3.expired_timelocks(bitcoin_wallet).await? { - ExpiredTimelocks::None => { + ExpiredTimelocks::None { .. } => { // Record the current monero wallet block height so we don't have to scan from // block 0 for scenarios where we create a refund wallet. let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; @@ -135,7 +135,7 @@ where transfer_proof, state3, } => match state3.expired_timelocks(bitcoin_wallet).await? { - ExpiredTimelocks::None => { + ExpiredTimelocks::None { .. } => { monero_wallet .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1)) .await @@ -221,7 +221,7 @@ where encrypted_signature, state3, } => match state3.expired_timelocks(bitcoin_wallet).await? { - ExpiredTimelocks::None => { + ExpiredTimelocks::None { .. } => { let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; match state3.signed_redeem_transaction(*encrypted_signature) { Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await { diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 4d0e5a31..edf902b6 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -21,9 +21,10 @@ use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof; use std::fmt; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum BobState { Started { + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] btc_amount: bitcoin::Amount, change_address: bitcoin::Address, }, @@ -287,13 +288,13 @@ pub struct State2 { S_a_monero: monero::PublicKey, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, - xmr: monero::Amount, - cancel_timelock: CancelTimelock, - punish_timelock: PunishTimelock, - refund_address: bitcoin::Address, + pub xmr: monero::Amount, + pub cancel_timelock: CancelTimelock, + pub punish_timelock: PunishTimelock, + pub refund_address: bitcoin::Address, redeem_address: bitcoin::Address, punish_address: bitcoin::Address, - tx_lock: bitcoin::TxLock, + pub tx_lock: bitcoin::TxLock, tx_cancel_sig_a: Signature, tx_refund_encsig: bitcoin::EncryptedSignature, min_monero_confirmations: u64, @@ -302,9 +303,9 @@ pub struct State2 { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] tx_punish_fee: bitcoin::Amount, #[serde(with = "::bitcoin::util::amount::serde::as_sat")] - tx_refund_fee: bitcoin::Amount, + pub tx_refund_fee: bitcoin::Amount, #[serde(with = "::bitcoin::util::amount::serde::as_sat")] - tx_cancel_fee: bitcoin::Amount, + pub tx_cancel_fee: bitcoin::Amount, } impl State2 { @@ -439,7 +440,7 @@ impl State3 { self.tx_lock.txid() } - pub async fn current_epoch( + pub async fn expired_timelock( &self, bitcoin_wallet: &bitcoin::Wallet, ) -> Result { diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 66933a87..6f5de482 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -117,7 +117,7 @@ async fn next_state( } => { let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; - if let ExpiredTimelocks::None = state3.current_epoch(bitcoin_wallet).await? { + if let ExpiredTimelocks::None { .. } = state3.expired_timelock(bitcoin_wallet).await? { let transfer_proof_watcher = event_loop_handle.recv_transfer_proof(); let cancel_timelock_expires = tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock); @@ -156,7 +156,7 @@ async fn next_state( } => { let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; - if let ExpiredTimelocks::None = state.current_epoch(bitcoin_wallet).await? { + if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? { let watch_request = state.lock_xmr_watch_request(lock_transfer_proof); select! { @@ -185,7 +185,7 @@ async fn next_state( BobState::XmrLocked(state) => { let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; - if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet).await? { + if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? { // Alice has locked Xmr // Bob sends Alice his key @@ -209,7 +209,7 @@ async fn next_state( BobState::EncSigSent(state) => { let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await; - if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet).await? { + if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? { select! { state5 = state.watch_for_redeem_btc(bitcoin_wallet) => { BobState::BtcRedeemed(state5?) @@ -269,12 +269,12 @@ async fn next_state( BobState::BtcCancelled(state) => { // Bob has cancelled the swap match state.expired_timelock(bitcoin_wallet).await? { - ExpiredTimelocks::None => { + ExpiredTimelocks::None { .. } => { bail!( "Internal error: canceled state reached before cancel timelock was expired" ); } - ExpiredTimelocks::Cancel => { + ExpiredTimelocks::Cancel { .. } => { state.publish_refund_btc(bitcoin_wallet).await?; BobState::BtcRefunded(state) } diff --git a/swap/src/rpc.rs b/swap/src/rpc.rs new file mode 100644 index 00000000..6e759057 --- /dev/null +++ b/swap/src/rpc.rs @@ -0,0 +1,31 @@ +use crate::api::Context; +use jsonrpsee::server::{RpcModule, ServerBuilder, ServerHandle}; +use std::net::SocketAddr; +use std::sync::Arc; +use thiserror::Error; + +pub mod methods; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Could not parse key value from params")] + ParseError, +} + +pub async fn run_server( + server_address: SocketAddr, + context: Arc, +) -> anyhow::Result<(SocketAddr, ServerHandle)> { + let server = ServerBuilder::default().build(server_address).await?; + let mut modules = RpcModule::new(()); + { + modules + .merge(methods::register_modules(Arc::clone(&context))?) + .expect("Could not register RPC modules") + } + + let addr = server.local_addr()?; + let server_handle = server.start(modules)?; + + Ok((addr, server_handle)) +} diff --git a/swap/src/rpc/methods.rs b/swap/src/rpc/methods.rs new file mode 100644 index 00000000..83100d4a --- /dev/null +++ b/swap/src/rpc/methods.rs @@ -0,0 +1,236 @@ +use crate::api::request::{Method, Request}; +use crate::api::Context; +use crate::bitcoin::bitcoin_address; +use crate::monero::monero_address; +use crate::{bitcoin, monero}; +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use jsonrpsee::types::Params; +use libp2p::core::Multiaddr; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; +use uuid::Uuid; + +pub fn register_modules(context: Arc) -> Result>> { + let mut module = RpcModule::new(context); + + module.register_async_method("suspend_current_swap", |params, context| async move { + execute_request(params, Method::SuspendCurrentSwap, &context).await + })?; + + module.register_async_method("get_swap_info", |params_raw, context| async move { + let params: HashMap = params_raw.parse()?; + + let swap_id = params + .get("swap_id") + .ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?; + + let swap_id = as_uuid(swap_id) + .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; + + execute_request(params_raw, Method::GetSwapInfo { swap_id }, &context).await + })?; + + module.register_async_method("get_bitcoin_balance", |params_raw, context| async move { + let params: HashMap = params_raw.parse()?; + + let force_refresh = params + .get("force_refresh") + .ok_or_else(|| { + jsonrpsee_core::Error::Custom("Does not contain force_refresh".to_string()) + })? + .as_bool() + .ok_or_else(|| { + jsonrpsee_core::Error::Custom("force_refesh is not a boolean".to_string()) + })?; + + execute_request(params_raw, Method::Balance { force_refresh }, &context).await + })?; + + module.register_async_method("get_history", |params, context| async move { + execute_request(params, Method::History, &context).await + })?; + + module.register_async_method("get_raw_states", |params, context| async move { + execute_request(params, Method::GetRawStates, &context).await + })?; + + module.register_async_method("resume_swap", |params_raw, context| async move { + let params: HashMap = params_raw.parse()?; + + let swap_id = params + .get("swap_id") + .ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?; + + let swap_id = as_uuid(swap_id) + .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; + + execute_request(params_raw, Method::Resume { swap_id }, &context).await + })?; + + module.register_async_method("cancel_refund_swap", |params_raw, context| async move { + let params: HashMap = params_raw.parse()?; + + let swap_id = params + .get("swap_id") + .ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?; + + let swap_id = as_uuid(swap_id) + .ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?; + + execute_request(params_raw, Method::CancelAndRefund { swap_id }, &context).await + })?; + + module.register_async_method( + "get_monero_recovery_info", + |params_raw, context| async move { + let params: HashMap = params_raw.parse()?; + + let swap_id = params.get("swap_id").ok_or_else(|| { + jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()) + })?; + + let swap_id = as_uuid(swap_id).ok_or_else(|| { + jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()) + })?; + + execute_request(params_raw, Method::MoneroRecovery { swap_id }, &context).await + }, + )?; + + module.register_async_method("withdraw_btc", |params_raw, context| async move { + let params: HashMap = params_raw.parse()?; + + let amount = if let Some(amount_str) = params.get("amount") { + Some( + ::bitcoin::Amount::from_str_in(amount_str, ::bitcoin::Denomination::Bitcoin) + .map_err(|_| { + jsonrpsee_core::Error::Custom("Unable to parse amount".to_string()) + })?, + ) + } else { + None + }; + + let withdraw_address = + bitcoin::Address::from_str(params.get("address").ok_or_else(|| { + jsonrpsee_core::Error::Custom("Does not contain address".to_string()) + })?) + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + let withdraw_address = + bitcoin_address::validate(withdraw_address, context.config.env_config.bitcoin_network)?; + + execute_request( + params_raw, + Method::WithdrawBtc { + amount, + address: withdraw_address, + }, + &context, + ) + .await + })?; + + module.register_async_method("buy_xmr", |params_raw, context| async move { + let params: HashMap = params_raw.parse()?; + + let bitcoin_change_address = + bitcoin::Address::from_str(params.get("bitcoin_change_address").ok_or_else(|| { + jsonrpsee_core::Error::Custom("Does not contain bitcoin_change_address".to_string()) + })?) + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + + let bitcoin_change_address = bitcoin_address::validate( + bitcoin_change_address, + context.config.env_config.bitcoin_network, + )?; + + let monero_receive_address = + monero::Address::from_str(params.get("monero_receive_address").ok_or_else(|| { + jsonrpsee_core::Error::Custom("Does not contain monero_receiveaddress".to_string()) + })?) + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + + let monero_receive_address = monero_address::validate( + monero_receive_address, + context.config.env_config.monero_network, + )?; + + let seller = + Multiaddr::from_str(params.get("seller").ok_or_else(|| { + jsonrpsee_core::Error::Custom("Does not contain seller".to_string()) + })?) + .map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?; + + execute_request( + params_raw, + Method::BuyXmr { + bitcoin_change_address, + monero_receive_address, + seller, + swap_id: Uuid::new_v4(), + }, + &context, + ) + .await + })?; + + module.register_async_method("list_sellers", |params_raw, context| async move { + let params: HashMap = params_raw.parse()?; + + let rendezvous_point = params.get("rendezvous_point").ok_or_else(|| { + jsonrpsee_core::Error::Custom("Does not contain rendezvous_point".to_string()) + })?; + + let rendezvous_point = rendezvous_point + .as_str() + .and_then(|addr_str| Multiaddr::from_str(addr_str).ok()) + .ok_or_else(|| { + jsonrpsee_core::Error::Custom("Could not parse valid multiaddr".to_string()) + })?; + + execute_request( + params_raw, + Method::ListSellers { + rendezvous_point: rendezvous_point.clone(), + }, + &context, + ) + .await + })?; + + module.register_async_method("get_current_swap", |params, context| async move { + execute_request(params, Method::GetCurrentSwap, &context).await + })?; + + Ok(module) +} + +fn as_uuid(json_value: &serde_json::Value) -> Option { + if let Some(uuid_str) = json_value.as_str() { + Uuid::parse_str(uuid_str).ok() + } else { + None + } +} + +async fn execute_request( + params: Params<'static>, + cmd: Method, + context: &Arc, +) -> Result { + // If we fail to parse the params as a String HashMap, it's most likely because its an empty object + // In that case, we want to make sure not to fail the request, so we set the log_reference_id to None + // and swallow the error + let reference_id = params + .parse::>() + .ok() + .and_then(|params_parsed| params_parsed.get("log_reference_id").cloned()); + + let request = Request::with_id(cmd, reference_id.map(|log_ref| log_ref.to_string())); + request + .call(Arc::clone(context)) + .await + .map_err(|err| jsonrpsee_core::Error::Custom(format!("{:#}", err))) +} diff --git a/swap/src/seed.rs b/swap/src/seed.rs index 5ffe9124..aa363905 100644 --- a/swap/src/seed.rs +++ b/swap/src/seed.rs @@ -16,7 +16,7 @@ use torut::onion::TorSecretKeyV3; pub const SEED_LENGTH: usize = 32; -#[derive(Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq)] pub struct Seed([u8; SEED_LENGTH]); impl Seed { diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index bd039477..c099376d 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -23,9 +23,9 @@ use swap::network::rendezvous::XmrBtcNamespace; use swap::network::swarm; use swap::protocol::alice::{AliceState, Swap}; use swap::protocol::bob::BobState; -use swap::protocol::{alice, bob}; +use swap::protocol::{alice, bob, Database}; use swap::seed::Seed; -use swap::{asb, bitcoin, cli, env, monero}; +use swap::{api, asb, bitcoin, cli, env, monero}; use tempfile::{tempdir, NamedTempFile}; use testcontainers::clients::Cli; use testcontainers::{Container, RunnableImage}; @@ -400,7 +400,7 @@ impl StartingBalances { } } -struct BobParams { +pub struct BobParams { seed: Seed, db_path: PathBuf, bitcoin_wallet: Arc, @@ -411,6 +411,21 @@ struct BobParams { } impl BobParams { + pub fn get_concentenated_alice_address(&self) -> String { + format!( + "{}/p2p/{}", + self.alice_address.clone(), + self.alice_peer_id.clone().to_base58() + ) + } + + pub async fn get_change_receive_addresses(&self) -> (bitcoin::Address, monero::Address) { + ( + self.bitcoin_wallet.new_address().await.unwrap(), + self.monero_wallet.get_main_address(), + ) + } + pub async fn new_swap_from_db(&self, swap_id: Uuid) -> Result<(bob::Swap, cli::EventLoop)> { let (event_loop, handle) = self.new_eventloop(swap_id).await?; @@ -452,6 +467,8 @@ impl BobParams { } let db = Arc::new(SqliteDatabase::open(&self.db_path).await?); + db.insert_peer_id(swap_id, self.alice_peer_id).await?; + let swap = bob::Swap::new( db, swap_id, @@ -525,13 +542,24 @@ pub struct TestContext { alice_swap_handle: mpsc::Receiver, alice_handle: AliceApplicationHandle, - bob_params: BobParams, + pub bob_params: BobParams, bob_starting_balances: StartingBalances, bob_bitcoin_wallet: Arc, bob_monero_wallet: Arc, } impl TestContext { + pub async fn get_bob_context(self) -> api::Context { + api::Context::for_harness( + self.bob_params.seed, + self.env_config, + self.bob_params.db_path, + self.bob_bitcoin_wallet, + self.bob_monero_wallet, + ) + .await + } + pub async fn restart_alice(&mut self) { self.alice_handle.abort(); diff --git a/swap/tests/rpc.rs b/swap/tests/rpc.rs new file mode 100644 index 00000000..5dc640d4 --- /dev/null +++ b/swap/tests/rpc.rs @@ -0,0 +1,436 @@ +pub mod harness; +#[cfg(test)] +mod test { + + use anyhow::Result; + + use jsonrpsee::ws_client::WsClientBuilder; + use jsonrpsee_core::client::{Client, ClientT}; + use jsonrpsee_core::params::ObjectParams; + + use serial_test::serial; + + use serde_json::Value; + use std::collections::HashMap; + use std::net::SocketAddr; + use std::sync::Arc; + use std::time::Duration; + use swap::api::request::{Method, Request}; + use swap::api::Context; + + use crate::harness::alice_run_until::is_xmr_lock_transaction_sent; + use crate::harness::bob_run_until::is_btc_locked; + use crate::harness::{setup_test, SlowCancelConfig, TestContext}; + use swap::asb::FixedRate; + use swap::protocol::{alice, bob}; + use swap::tracing_ext::{capture_logs, MakeCapturingWriter}; + use tracing_subscriber::filter::LevelFilter; + use uuid::Uuid; + + const SERVER_ADDRESS: &str = "127.0.0.1:1234"; + const SERVER_START_TIMEOUT_SECS: u64 = 50; + const BITCOIN_ADDR: &str = "bcrt1qahvhjfc7vx5857zf8knxs8yp5lkm26jgyt0k76"; + const MONERO_ADDR: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a"; + const SELLER: &str = + "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; + const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b"; + + pub async fn setup_daemon( + harness_ctx: TestContext, + ) -> (Client, MakeCapturingWriter, Arc) { + let writer = capture_logs(LevelFilter::DEBUG); + let server_address: SocketAddr = SERVER_ADDRESS.parse().unwrap(); + + let request = Request::new(Method::StartDaemon { + server_address: Some(server_address), + }); + + let context = Arc::new(harness_ctx.get_bob_context().await); + + let context_clone = context.clone(); + + tokio::spawn(async move { + if let Err(err) = request.call(context_clone).await { + println!("Failed to initialize daemon for testing: {}", err); + } + }); + + for _ in 0..SERVER_START_TIMEOUT_SECS { + if writer.captured().contains("Started RPC server") { + let url = format!("ws://{}", SERVER_ADDRESS); + let client = WsClientBuilder::default().build(&url).await.unwrap(); + + return (client, writer, context); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + } + + panic!( + "Failed to start RPC server after {} seconds", + SERVER_START_TIMEOUT_SECS + ); + } + + fn assert_has_keys_serde(map: &serde_json::Map, keys: &[&str]) { + for &key in keys { + assert!(map.contains_key(key), "Key {} is missing", key); + } + } + + // Helper function for HashMap + fn assert_has_keys_hashmap(map: &HashMap, keys: &[&str]) { + for &key in keys { + assert!(map.contains_key(key), "Key {} is missing", key); + } + } + + #[tokio::test] + #[serial] + pub async fn get_swap_info() { + setup_test(SlowCancelConfig, |mut harness_ctx| async move { + // Start a swap and wait for xmr lock transaction to be published (XmrLockTransactionSent) + let (bob_swap, _) = harness_ctx.bob_swap().await; + let bob_swap_id = bob_swap.id; + tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); + let alice_swap = harness_ctx.alice_next_swap().await; + alice::run_until( + alice_swap, + is_xmr_lock_transaction_sent, + FixedRate::default(), + ) + .await?; + + let (client, _, _) = setup_daemon(harness_ctx).await; + + let response: HashMap> = client + .request("get_history", ObjectParams::new()) + .await + .unwrap(); + let swaps: Vec<(Uuid, String)> = vec![(bob_swap_id, "btc is locked".to_string())]; + + assert_eq!(response, HashMap::from([("swaps".to_string(), swaps)])); + + let response: HashMap>> = client + .request("get_raw_states", ObjectParams::new()) + .await + .unwrap(); + + let response_raw_states = response.get("raw_states").unwrap(); + + assert!(response_raw_states.contains_key(&bob_swap_id)); + assert_eq!(response_raw_states.get(&bob_swap_id).unwrap().len(), 2); + + let mut params = ObjectParams::new(); + params.insert("swap_id", bob_swap_id).unwrap(); + let response: HashMap = + client.request("get_swap_info", params).await.unwrap(); + + // Check primary keys in response + assert_has_keys_hashmap( + &response, + &[ + "txRefundFee", + "swapId", + "cancelTimelock", + "timelock", + "punishTimelock", + "stateName", + "btcAmount", + "startDate", + "btcRefundAddress", + "txCancelFee", + "xmrAmount", + "completed", + "txLockId", + "seller", + ], + ); + + // Assert specific fields + assert_eq!(response.get("swapId").unwrap(), &bob_swap_id.to_string()); + assert_eq!( + response.get("stateName").unwrap(), + &"btc is locked".to_string() + ); + assert_eq!(response.get("completed").unwrap(), &Value::Bool(false)); + + // Check seller object and its keys + let seller = response + .get("seller") + .expect("Field 'seller' is missing from response") + .as_object() + .expect("'seller' is not an object"); + assert_has_keys_serde(seller, &["peerId"]); + + // Check timelock object, nested 'None' object, and blocks_left + let timelock = response + .get("timelock") + .expect("Field 'timelock' is missing from response") + .as_object() + .expect("'timelock' is not an object"); + let none_obj = timelock + .get("None") + .expect("Field 'None' is missing from 'timelock'") + .as_object() + .expect("'None' is not an object in 'timelock'"); + let blocks_left = none_obj + .get("blocks_left") + .expect("Field 'blocks_left' is missing from 'None'") + .as_i64() + .expect("'blocks_left' is not an integer"); + + // Validate blocks_left + assert!( + blocks_left > 0 && blocks_left <= 180, + "Field 'blocks_left' should be > 0 and <= 180 but got {}", + blocks_left + ); + + Ok(()) + }) + .await; + } + + #[tokio::test] + #[serial] + pub async fn test_rpc_calls() { + setup_test(SlowCancelConfig, |harness_ctx| async move { + let alice_addr = harness_ctx.bob_params.get_concentenated_alice_address(); + let (change_address, receive_address) = + harness_ctx.bob_params.get_change_receive_addresses().await; + + let (client, writer, _) = setup_daemon(harness_ctx).await; + assert!(client.is_connected()); + + let mut params = ObjectParams::new(); + + params.insert("force_refresh", false).unwrap(); + let response: HashMap = client + .request("get_bitcoin_balance", params) + .await + .unwrap(); + + assert_eq!(response, HashMap::from([("balance".to_string(), 10000000)])); + + + let mut params = ObjectParams::new(); + params.insert("log_reference_id", "test_ref_id").unwrap(); + params.insert("force_refresh", false).unwrap(); + + let _: HashMap = client.request("get_bitcoin_balance", params).await.unwrap(); + + assert!(writer.captured().contains( + r#"method{method_name="Balance" log_reference_id="\"test_ref_id\""}: swap::api::request: Current Bitcoin balance as of last sync balance=0.1 BTC"# + )); + + for method in ["get_swap_info", "resume_swap", "cancel_refund_swap"].iter() { + let mut params = ObjectParams::new(); + params.insert("swap_id", "invalid_swap").unwrap(); + + let response: Result, _> = + client.request(method, params).await; + response.expect_err(&format!( + "Expected an error when swap_id is invalid for method {}", + method + )); + + let params = ObjectParams::new(); + + let response: Result, _> = + client.request(method, params).await; + response.expect_err(&format!( + "Expected an error when swap_id is missing for method {}", + method + )); + } + + let params = ObjectParams::new(); + let result: Result, _> = + client.request("list_sellers", params).await; + + result.expect_err("Expected an error when rendezvous_point is missing"); + + let params = ObjectParams::new(); + let result: Result, _> = + client.request("list_sellers", params).await; + + result.expect_err("Expected an error when rendezvous_point is missing"); + + let params = ObjectParams::new(); + let response: Result, _> = + client.request("withdraw_btc", params).await; + response.expect_err("Expected an error when withdraw_address is missing"); + + let mut params = ObjectParams::new(); + params.insert("address", "invalid_address").unwrap(); + let response: Result, _> = + client.request("withdraw_btc", params).await; + response.expect_err("Expected an error when withdraw_address is malformed"); + + let mut params = ObjectParams::new(); + params.insert("address", BITCOIN_ADDR).unwrap(); + params.insert("amount", "0").unwrap(); + let response: Result, _> = + client.request("withdraw_btc", params).await; + response.expect_err("Expected an error when amount is 0"); + + let mut params = ObjectParams::new(); + params + .insert("address", BITCOIN_ADDR) + .unwrap(); + params.insert("amount", "0.01").unwrap(); + let response: HashMap = client + .request("withdraw_btc", params) + .await + .expect("Expected a valid response"); + + assert_has_keys_hashmap(&response, &["signed_tx", "amount", "txid"]); + assert_eq!( + response.get("amount").unwrap().as_u64().unwrap(), + 1_000_000 + ); + + let params = ObjectParams::new(); + let response: Result, _> = + client.request("buy_xmr", params).await; + response.expect_err("Expected an error when no params are given"); + + let mut params = ObjectParams::new(); + params + .insert("bitcoin_change_address", BITCOIN_ADDR) + .unwrap(); + params + .insert("monero_receive_address", MONERO_ADDR) + .unwrap(); + let response: Result, _> = + client.request("buy_xmr", params).await; + response.expect_err("Expected an error when seller is missing"); + + let mut params = ObjectParams::new(); + params + .insert("bitcoin_change_address", BITCOIN_ADDR) + .unwrap(); + params.insert("seller", SELLER).unwrap(); + let response: Result, _> = + client.request("buy_xmr", params).await; + response.expect_err("Expected an error when monero_receive_address is missing"); + + let mut params = ObjectParams::new(); + params + .insert("monero_receive_address", MONERO_ADDR) + .unwrap(); + params.insert("seller", SELLER).unwrap(); + let response: Result, _> = + client.request("buy_xmr", params).await; + response.expect_err("Expected an error when bitcoin_change_address is missing"); + + let mut params = ObjectParams::new(); + params + .insert("bitcoin_change_address", "invalid_address") + .unwrap(); + params + .insert("monero_receive_address", MONERO_ADDR) + .unwrap(); + params.insert("seller", SELLER).unwrap(); + let response: Result, _> = + client.request("buy_xmr", params).await; + response.expect_err("Expected an error when bitcoin_change_address is malformed"); + + let mut params = ObjectParams::new(); + params + .insert("bitcoin_change_address", BITCOIN_ADDR) + .unwrap(); + params + .insert("monero_receive_address", "invalid_address") + .unwrap(); + params.insert("seller", SELLER).unwrap(); + let response: Result, _> = + client.request("buy_xmr", params).await; + response.expect_err("Expected an error when monero_receive_address is malformed"); + + + let mut params = ObjectParams::new(); + params + .insert("bitcoin_change_address", BITCOIN_ADDR) + .unwrap(); + params + .insert("monero_receive_address", MONERO_ADDR) + .unwrap(); + params.insert("seller", "invalid_seller").unwrap(); + let response: Result, _> = + client.request("buy_xmr", params).await; + response.expect_err("Expected an error when seller is malformed"); + + let response: Result, _> = client + .request("suspend_current_swap", ObjectParams::new()) + .await; + response.expect_err("Expected an error when no swap is running"); + + let mut params = ObjectParams::new(); + params + .insert("bitcoin_change_address", change_address) + .unwrap(); + params + .insert("monero_receive_address", receive_address) + .unwrap(); + + params.insert("seller", alice_addr).unwrap(); + let response: HashMap = client + .request("buy_xmr", params) + .await + .expect("Expected a HashMap, got an error"); + + assert_has_keys_hashmap(&response, &["swapId"]); + + Ok(()) + }) + .await; + } + + #[tokio::test] + #[serial] + pub async fn suspend_current_swap_swap_running() { + setup_test(SlowCancelConfig, |harness_ctx| async move { + let (client, _, ctx) = setup_daemon(harness_ctx).await; + + ctx.swap_lock + .acquire_swap_lock(Uuid::parse_str(SWAP_ID).unwrap()) + .await + .unwrap(); + let cloned_ctx = ctx.clone(); + + tokio::spawn(async move { + // Immediately release lock when suspend signal is received. Mocks a running swap that is then cancelled. + ctx.swap_lock + .listen_for_swap_force_suspension() + .await + .unwrap(); + ctx.swap_lock.release_swap_lock().await.unwrap(); + }); + + let response: HashMap = client + .request("suspend_current_swap", ObjectParams::new()) + .await + .unwrap(); + assert_eq!( + response, + HashMap::from([("swapId".to_string(), SWAP_ID.to_string())]) + ); + + cloned_ctx + .swap_lock + .acquire_swap_lock(Uuid::parse_str(SWAP_ID).unwrap()) + .await + .unwrap(); + + let response: Result, _> = client + .request("suspend_current_swap", ObjectParams::new()) + .await; + response.expect_err("Expected an error when suspend signal times out"); + + Ok(()) + }) + .await; + } +} From 97210739a177d27c12f1056ec8d2c89b77abf4f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 May 2024 11:10:26 +0000 Subject: [PATCH 39/55] build(deps): bump Swatinem/rust-cache from 2.7.1 to 2.7.3 Bumps [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) from 2.7.1 to 2.7.3. - [Release notes](https://github.com/swatinem/rust-cache/releases) - [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md) - [Commits](https://github.com/swatinem/rust-cache/compare/v2.7.1...v2.7.3) --- updated-dependencies: - dependency-name: Swatinem/rust-cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da82b266..5711b8f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,7 +181,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v4.1.1 - - uses: Swatinem/rust-cache@v2.7.1 + - uses: Swatinem/rust-cache@v2.7.3 - name: Run RPC server tests run: cargo test --package swap --all-features --test rpc -- --nocapture From b443a964695bcc0cd707b6fdedbca6f3f1a5c476 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 May 2024 11:10:29 +0000 Subject: [PATCH 40/55] build(deps): bump actions/checkout from 4.1.1 to 4.1.6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.1 to 4.1.6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.1...v4.1.6) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da82b266..23072bf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,7 +179,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.6 - uses: Swatinem/rust-cache@v2.7.1 From 0c4b7d50c267ff0c94157a7ad225e19e5b5e724f Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Fri, 24 May 2024 18:02:29 +0200 Subject: [PATCH 41/55] Lower DEFAULT_BITCOIN_CONFIRMATION_TARGET to 1 to ensure timely confirmation of bitcoin transactions (#1640) --- swap/src/cli/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index affd6a19..af72df1b 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -23,7 +23,7 @@ const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://blockstream.info:700"; // See: https://1209k.com/bitcoin-eye/ele.php?chain=tbtc pub const DEFAULT_ELECTRUM_RPC_URL_TESTNET: &str = "ssl://electrum.blockstream.info:60002"; -const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 3; +const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 1; pub const DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET: usize = 1; const DEFAULT_TOR_SOCKS5_PORT: &str = "9050"; From 2f28ef940183d8272244c2a81ec4295873569a49 Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Fri, 24 May 2024 18:06:21 +0200 Subject: [PATCH 42/55] Add missing changelog entries (#1641) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3ad105..2eaf409c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Minimum Supported Rust Version (MSRV) bumped to 1.70 +- Lowered default Bitcoin confirmation target for Bob to 1 to make sure Bitcoin transactions get confirmed in time +- Added support for starting the CLI (using the `start-daemon` subcommand) as a Daemon that accepts JSON-RPC requests ## [0.12.3] - 2023-09-20 From 0ca98cd0b78aff95a38021e9574fe3c28c968963 Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Sat, 25 May 2024 14:43:22 +0200 Subject: [PATCH 43/55] Make tracing file appender blocking (#1643) --- swap/src/cli/tracing.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/swap/src/cli/tracing.rs b/swap/src/cli/tracing.rs index 7cb0721b..6b6de60f 100644 --- a/swap/src/cli/tracing.rs +++ b/swap/src/cli/tracing.rs @@ -13,7 +13,6 @@ pub fn init(debug: bool, json: bool, dir: impl AsRef) -> Result<()> { let registry = Registry::default().with(level_filter); let appender = tracing_appender::rolling::never(dir.as_ref(), "swap-all.log"); - let (appender, _guard) = tracing_appender::non_blocking(appender); let file_logger = registry.with( fmt::layer() From c433bd2389300f05f167462a8fe45a66e1040915 Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Sat, 25 May 2024 17:59:12 +0200 Subject: [PATCH 44/55] CLI: Initiate tracing earlier to avoid lost logs (#1646) --- swap/src/api.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/swap/src/api.rs b/swap/src/api.rs index ca3ae21d..6940b8b1 100644 --- a/swap/src/api.rs +++ b/swap/src/api.rs @@ -184,6 +184,10 @@ impl Context { let data_dir = data::data_dir_from(data, is_testnet)?; let env_config = env_config_from(is_testnet); + START.call_once(|| { + let _ = cli::tracing::init(debug, json, data_dir.join("logs")); + }); + let seed = Seed::from_file_or_generate(data_dir.as_path()) .context("Failed to read seed in file")?; @@ -219,10 +223,6 @@ impl Context { let tor_socks5_port = tor.map_or(9050, |tor| tor.tor_socks5_port); - START.call_once(|| { - let _ = cli::tracing::init(debug, json, data_dir.join("logs")); - }); - let context = Context { db: open_db(data_dir.join("sqlite")).await?, bitcoin_wallet, From 32ca0b1a4ac868505f1a6522c16fd3690c73c649 Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Sat, 25 May 2024 18:08:17 +0200 Subject: [PATCH 45/55] CLI: Upgrade monero-wallet-rpc to 0.18.3.1, clarify a few log. messages --- CHANGELOG.md | 1 + swap/src/api.rs | 2 +- swap/src/monero/wallet_rpc.rs | 43 +++++++++++++++++++++++------------ swap/src/protocol/bob/swap.rs | 3 +++ 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eaf409c..0a299af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Minimum Supported Rust Version (MSRV) bumped to 1.70 - Lowered default Bitcoin confirmation target for Bob to 1 to make sure Bitcoin transactions get confirmed in time - Added support for starting the CLI (using the `start-daemon` subcommand) as a Daemon that accepts JSON-RPC requests +- Update monero-wallet-rpc version to v0.18.3.1 ## [0.12.3] - 2023-09-20 diff --git a/swap/src/api.rs b/swap/src/api.rs index 6940b8b1..340c2e39 100644 --- a/swap/src/api.rs +++ b/swap/src/api.rs @@ -187,7 +187,7 @@ impl Context { START.call_once(|| { let _ = cli::tracing::init(debug, json, data_dir.join("logs")); }); - + let seed = Seed::from_file_or_generate(data_dir.as_path()) .context("Failed to read seed in file")?; diff --git a/swap/src/monero/wallet_rpc.rs b/swap/src/monero/wallet_rpc.rs index e227c3f3..8af1167e 100644 --- a/swap/src/monero/wallet_rpc.rs +++ b/swap/src/monero/wallet_rpc.rs @@ -46,30 +46,30 @@ const MONERO_DAEMONS: [MoneroDaemon; 17] = [ compile_error!("unsupported operating system"); #[cfg(all(target_os = "macos", target_arch = "x86_64"))] -const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-mac-x64-v0.18.1.2.tar.bz2"; +const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-mac-x64-v0.18.3.1.tar.bz2"; #[cfg(all(target_os = "macos", target_arch = "x86_64"))] -const DOWNLOAD_HASH: &str = "ba1108c7a5e5efe15b6a628fb007c50f01c231f61137bba7427605286dbc6f01"; +const DOWNLOAD_HASH: &str = "7f8bd9364ef16482b418aa802a65be0e4cc660c794bb5d77b2d17bc84427883a"; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] -const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-mac-armv8-v0.18.1.2.tar.bz2"; +const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-mac-armv8-v0.18.3.1.tar.bz2"; #[cfg(all(target_os = "macos", target_arch = "aarch64"))] -const DOWNLOAD_HASH: &str = "620b825c04f84845ed09de03b207a3230a34f74b30a8a07dde504a7d376ee4b9"; +const DOWNLOAD_HASH: &str = "915288b023cb5811e626e10052adc6ac5323dd283c5a25b91059b0fb86a21fb6"; #[cfg(all(target_os = "linux", target_arch = "x86_64"))] -const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.1.2.tar.bz2"; +const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.1.tar.bz2"; #[cfg(all(target_os = "linux", target_arch = "x86_64"))] -const DOWNLOAD_HASH: &str = "7d51e7072351f65d0c7909e745827cfd3b00abe5e7c4cc4c104a3c9b526da07e"; +const DOWNLOAD_HASH: &str = "23af572fdfe3459b9ab97e2e9aa7e3c11021c955d6064b801a27d7e8c21ae09d"; #[cfg(all(target_os = "linux", target_arch = "arm"))] const DOWNLOAD_URL: &str = - "https://downloads.getmonero.org/cli/monero-linux-armv7-v0.18.1.2.tar.bz2"; + "https://downloads.getmonero.org/cli/monero-linux-armv7-v0.18.3.1.tar.bz2"; #[cfg(all(target_os = "linux", target_arch = "arm"))] -const DOWNLOAD_HASH: &str = "94ece435ed60f85904114643482c2b6716f74bf97040a7af237450574a9cf06d"; +const DOWNLOAD_HASH: &str = "2ea2c8898cbab88f49423f4f6c15f2a94046cb4bbe827493dd061edc0fd5f1ca"; #[cfg(target_os = "windows")] -const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-win-x64-v0.18.1.2.zip"; +const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-win-x64-v0.18.3.1.zip"; #[cfg(target_os = "windows")] -const DOWNLOAD_HASH: &str = "0a3d4d1af7e094c05352c31b2dafcc6ccbc80edc195ca9eaedc919c36accd05a"; +const DOWNLOAD_HASH: &str = "35dcc4bee4caad3442659d37837e0119e4649a77f2e3b5e80dd6d9b8fc4fb6ad"; #[cfg(any(target_os = "macos", target_os = "linux"))] const PACKED_FILE: &str = "monero-wallet-rpc"; @@ -77,7 +77,7 @@ const PACKED_FILE: &str = "monero-wallet-rpc"; #[cfg(target_os = "windows")] const PACKED_FILE: &str = "monero-wallet-rpc.exe"; -const WALLET_RPC_VERSION: &str = "v0.18.1.2"; +const WALLET_RPC_VERSION: &str = "v0.18.3.1"; #[derive(Debug, Clone, Copy, thiserror::Error)] #[error("monero wallet rpc executable not found in downloaded archive")] @@ -233,9 +233,10 @@ impl WalletRpc { .parse::()?; tracing::info!( - "Downloading monero-wallet-rpc ({}) from {}", - content_length.big_byte(2), - DOWNLOAD_URL + progress="0%", + size=%content_length.big_byte(2), + download_url=DOWNLOAD_URL, + "Downloading monero-wallet-rpc", ); let mut hasher = Sha256::new(); @@ -268,12 +269,24 @@ impl WalletRpc { let total = 3 * content_length; let percent = 100 * received as u64 / total; if percent != notified && percent % 10 == 0 { - tracing::debug!("{}%", percent); + tracing::info!( + progress=format!("{}%", percent), + size=%content_length.big_byte(2), + download_url=DOWNLOAD_URL, + "Downloading monero-wallet-rpc", + ); notified = percent; } file.write_all(&bytes).await?; } + tracing::info!( + progress="100%", + size=%content_length.big_byte(2), + download_url=DOWNLOAD_URL, + "Downloading monero-wallet-rpc", + ); + let result = hasher.finalize(); let result_hash = HEXLOWER.encode(result.as_ref()); if result_hash != DOWNLOAD_HASH { diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 6f5de482..37a6b65a 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -227,6 +227,9 @@ async fn next_state( let (spend_key, view_key) = state.xmr_keys(); let wallet_file_name = swap_id.to_string(); + + tracing::info!(%wallet_file_name, "Generating and opening Monero wallet from the extracted keys to redeem the Monero"); + if let Err(e) = monero_wallet .create_from_and_load( wallet_file_name.clone(), From 796863359f23190c55aaa4564a505b7c0cee37d5 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Mon, 27 May 2024 11:03:20 +0200 Subject: [PATCH 46/55] upgrade secp256kfun (#1466) * ci: add cargo check on rust stable * refactor: upgrade secp256kfun and fix resulting issues * build(deps): update sigma_fun and ecdsa_fun to a52142cf7f #1520 #1521 * chore: fix clippy issue * update to 91112f80b24 * bump to 294de1721add * chore(deps): remove spectral spectral fails to compile on rust stable 1.76 due to dep on deprecated rustc-serialize * secp256kfun: update to 7da9d277 and set rev in manifest * update to 6fdc5d8 * switch to crates.io versions of ecdsa_fun and sigma_fun * ci: update toolchain to 1.74 and fix draft action * clippy fixes --------- Co-authored-by: binarybaron <86064887+binarybaron@users.noreply.github.com> --- .github/workflows/build-release-binaries.yml | 2 +- .github/workflows/ci.yml | 20 +- .github/workflows/draft-new-release.yml | 4 +- CHANGELOG.md | 2 +- Cargo.lock | 220 ++++++++----------- Cargo.toml | 1 + README.md | 2 +- monero-harness/Cargo.toml | 1 - monero-harness/src/lib.rs | 2 +- monero-harness/tests/monerod.rs | 5 +- monero-harness/tests/wallet.rs | 7 +- monero-wallet/src/lib.rs | 2 +- rust-toolchain.toml | 2 +- swap/Cargo.toml | 5 +- swap/src/asb/event_loop.rs | 6 +- swap/src/asb/network.rs | 4 +- swap/src/bitcoin.rs | 40 +++- swap/src/bitcoin/cancel.rs | 21 +- swap/src/bitcoin/lock.rs | 6 +- swap/src/bitcoin/punish.rs | 8 +- swap/src/bitcoin/redeem.rs | 28 ++- swap/src/bitcoin/refund.rs | 26 ++- swap/src/bitcoin/wallet.rs | 4 +- swap/src/cli/event_loop.rs | 2 +- swap/src/network/cbor_request_response.rs | 2 +- swap/src/network/json_pull_codec.rs | 2 +- swap/src/network/test.rs | 2 +- swap/src/protocol/alice/state.rs | 6 +- swap/src/protocol/bob/state.rs | 17 +- 29 files changed, 229 insertions(+), 220 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index ecfde0c8..bcd2c806 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -61,7 +61,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.70" + toolchain: "1.74" targets: armv7-unknown-linux-gnueabihf - name: Build ${{ matrix.target }} ${{ matrix.bin }} release binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b92d60de..7619754b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.70" + toolchain: "1.74" components: clippy,rustfmt - uses: Swatinem/rust-cache@v2.7.3 @@ -31,6 +31,22 @@ jobs: - name: Run clippy with all features enabled run: cargo clippy --workspace --all-targets --all-features -- -D warnings + check_stable: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4.1.1 + + - uses: dtolnay/rust-toolchain@stable + + - name: Override rust stable + run: | + rustup override set stable + + - name: Run cargo check on rust stable + run: cargo check --all-targets + + bdk_test: runs-on: ubuntu-latest steps: @@ -84,7 +100,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.70" + toolchain: "1.74" targets: armv7-unknown-linux-gnueabihf - name: Build binary diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 48fb2d3a..09df0bd7 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -42,8 +42,8 @@ jobs: - name: Commit changelog and manifest files id: make-commit env: - DPRINT_VERSION: 0.39.1 - RUST_TOOLCHAIN: 1.70 + DPRINT_VERSION: "0.39.1" + RUST_TOOLCHAIN: "1.74" run: | rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu" curl -fsSL https://dprint.dev/install.sh | sh -s $DPRINT_VERSION diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a299af8..e3e510a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Minimum Supported Rust Version (MSRV) bumped to 1.70 +- Minimum Supported Rust Version (MSRV) bumped to 1.74 - Lowered default Bitcoin confirmation target for Bob to 1 to make sure Bitcoin transactions get confirmed in time - Added support for starting the CLI (using the `start-daemon` subcommand) as a Daemon that accepts JSON-RPC requests - Update monero-wallet-rpc version to v0.18.3.1 diff --git a/Cargo.lock b/Cargo.lock index 34ced0d5..435a86bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,6 +347,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + [[package]] name = "bit-set" version = "0.5.2" @@ -371,7 +390,7 @@ dependencies = [ "base64 0.13.1", "bech32", "bitcoin_hashes", - "secp256k1", + "secp256k1 0.24.1", "serde", ] @@ -1112,10 +1131,11 @@ checksum = "5caaa75cbd2b960ff1e5392d2cfb1f44717fffe12fc1f32b7b5d1267f99732a6" [[package]] name = "ecdsa_fun" -version = "0.7.1" -source = "git+https://github.com/LLFourn/secp256kfun#9657d8c12fd26df5e57254a0063eaf41082a38ca" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd850b7ece4e4ddaa1478d5de36b6d4d599f2d521f73456ca706b4e2b32a4ec" dependencies = [ - "bincode", + "bincode 1.3.3", "rand_chacha 0.3.1", "secp256kfun", "sigma_fun", @@ -1306,12 +1326,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "futures" version = "0.3.30" @@ -2589,7 +2603,6 @@ dependencies = [ "futures", "monero-rpc", "rand 0.7.3", - "spectral", "testcontainers", "tokio", "tracing", @@ -2723,42 +2736,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" -dependencies = [ - "num-integer", - "num-traits", - "rand 0.4.6", - "rustc-serialize", -] - -[[package]] -name = "num-complex" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" -dependencies = [ - "num-traits", - "rustc-serialize", -] - [[package]] name = "num-conv" version = "0.1.0" @@ -2775,29 +2752,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", - "rustc-serialize", -] - [[package]] name = "num-traits" version = "0.2.15" @@ -3281,19 +3235,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.7.3" @@ -3339,21 +3280,6 @@ dependencies = [ "rand_core 0.6.2", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" @@ -3399,15 +3325,6 @@ dependencies = [ "rand_core 0.6.2", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.2.10" @@ -3632,12 +3549,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - [[package]] name = "rustc_version" version = "0.3.3" @@ -3825,7 +3736,27 @@ checksum = "ff55dc09d460954e9ef2fa8a7ced735a964be9981fd50e870b2b3b0705e14964" dependencies = [ "bitcoin_hashes", "rand 0.8.3", - "secp256k1-sys", + "secp256k1-sys 0.6.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys 0.8.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "secp256k1-sys 0.9.2", "serde", ] @@ -3839,17 +3770,49 @@ dependencies = [ ] [[package]] -name = "secp256kfun" -version = "0.7.1" -source = "git+https://github.com/LLFourn/secp256kfun#9657d8c12fd26df5e57254a0063eaf41082a38ca" +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256kfun" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecc2adce3ef929c5dc7dacdd612d65ab98002ee18119215ce25d8054ed53c1a" +dependencies = [ + "bincode 2.0.0-rc.3", "digest 0.10.7", "rand_core 0.6.2", - "secp256k1", + "secp256k1 0.27.0", + "secp256k1 0.28.2", + "secp256kfun_arithmetic_macros", "serde", "subtle-ng", ] +[[package]] +name = "secp256kfun_arithmetic_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91b7c385a72530ebfe6010ff476ad9e235743fb33408a360052bb706f1481e1e" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "security-framework" version = "2.3.1" @@ -4091,8 +4054,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "sigma_fun" -version = "0.4.1" -source = "git+https://github.com/LLFourn/secp256kfun#9657d8c12fd26df5e57254a0063eaf41082a38ca" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8e9462de42c6f14c7e20154d18d8e9e8683750798885e76f06973317b1cb1d" dependencies = [ "curve25519-dalek-ng", "digest 0.10.7", @@ -4217,15 +4181,6 @@ dependencies = [ "sha-1", ] -[[package]] -name = "spectral" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae3c15181f4b14e52eeaac3efaeec4d2764716ce9c86da0c934c3e318649c5ba" -dependencies = [ - "num", -] - [[package]] name = "spin" version = "0.5.2" @@ -4488,7 +4443,6 @@ dependencies = [ "serial_test", "sha2 0.10.8", "sigma_fun", - "spectral", "sqlx", "structopt", "strum", @@ -5309,6 +5263,12 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + [[package]] name = "void" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index 0f654d91..ab29e919 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet" ] [patch.crates-io] diff --git a/README.md b/README.md index 71a37896..35adc5f0 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Please have a look at the [contribution guidelines](./CONTRIBUTING.md). ## Rust Version Support Please note that only the latest stable Rust toolchain is supported. -All stable toolchains since 1.70 _should_ work. +All stable toolchains since 1.74 _should_ work. ## Contact diff --git a/monero-harness/Cargo.toml b/monero-harness/Cargo.toml index 6780dbd8..a4c9ce72 100644 --- a/monero-harness/Cargo.toml +++ b/monero-harness/Cargo.toml @@ -10,7 +10,6 @@ anyhow = "1" futures = "0.3" monero-rpc = { path = "../monero-rpc" } rand = "0.7" -spectral = "0.6" testcontainers = "0.14" tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "macros" ] } tracing = "0.1" diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index d5f8a513..61c12a27 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -225,7 +225,7 @@ impl<'c> Monerod { name: String, network: String, ) -> Result<(Self, Container<'c, image::Monerod>)> { - let image = image::Monerod::default(); + let image = image::Monerod; let image: RunnableImage = RunnableImage::from(image) .with_container_name(name.clone()) .with_network(network.clone()); diff --git a/monero-harness/tests/monerod.rs b/monero-harness/tests/monerod.rs index d218167b..f454fb74 100644 --- a/monero-harness/tests/monerod.rs +++ b/monero-harness/tests/monerod.rs @@ -1,6 +1,5 @@ use monero_harness::Monero; use monero_rpc::monerod::MonerodRpc as _; -use spectral::prelude::*; use std::time::Duration; use testcontainers::clients::Cli; use tokio::time; @@ -21,12 +20,12 @@ async fn init_miner_and_mine_to_miner_address() { let miner_wallet = monero.wallet("miner").unwrap(); let got_miner_balance = miner_wallet.balance().await.unwrap(); - assert_that!(got_miner_balance).is_greater_than(0); + assert!(got_miner_balance > 0); time::sleep(Duration::from_millis(1010)).await; // after a bit more than 1 sec another block should have been mined let block_height = monerod.client().get_block_count().await.unwrap().count; - assert_that(&block_height).is_greater_than(70); + assert!(block_height > 70); } diff --git a/monero-harness/tests/wallet.rs b/monero-harness/tests/wallet.rs index b25683ad..67d11645 100644 --- a/monero-harness/tests/wallet.rs +++ b/monero-harness/tests/wallet.rs @@ -1,6 +1,5 @@ use monero_harness::{Monero, MoneroWalletRpc}; use monero_rpc::wallet::MoneroWalletRpc as _; -use spectral::prelude::*; use std::time::Duration; use testcontainers::clients::Cli; use tokio::time::sleep; @@ -29,7 +28,7 @@ async fn fund_transfer_and_check_tx_key() { // check alice balance let got_alice_balance = alice_wallet.balance().await.unwrap(); - assert_that(&got_alice_balance).is_equal_to(fund_alice); + assert_eq!(got_alice_balance, fund_alice); // transfer from alice to bob let bob_address = bob_wallet.address().await.unwrap().address; @@ -41,7 +40,7 @@ async fn fund_transfer_and_check_tx_key() { wait_for_wallet_to_catch_up(bob_wallet, send_to_bob).await; let got_bob_balance = bob_wallet.balance().await.unwrap(); - assert_that(&got_bob_balance).is_equal_to(send_to_bob); + assert_eq!(got_bob_balance, send_to_bob); // check if tx was actually seen let tx_id = transfer.tx_hash; @@ -52,7 +51,7 @@ async fn fund_transfer_and_check_tx_key() { .await .expect("failed to check tx by key"); - assert_that!(res.received).is_equal_to(send_to_bob); + assert_eq!(res.received, send_to_bob); } async fn wait_for_wallet_to_catch_up(wallet: &MoneroWalletRpc, expected_balance: u64) { diff --git a/monero-wallet/src/lib.rs b/monero-wallet/src/lib.rs index 080c2599..7f2928c3 100644 --- a/monero-wallet/src/lib.rs +++ b/monero-wallet/src/lib.rs @@ -65,7 +65,7 @@ mod tests { #[tokio::test] async fn get_outs_for_key_offsets() { let cli = Cli::default(); - let container = cli.run(Monerod::default()); + let container = cli.run(Monerod); let rpc_client = Client::localhost(container.get_host_port_ipv4(18081)).unwrap(); rpc_client.generateblocks(150, "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".to_owned()).await.unwrap(); let wallet = Wallet { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d03c251b..cc47d97a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.70" # also update this in the readme, changelog, and github actions +channel = "1.74" # also update this in the readme, changelog, and github actions components = ["clippy"] targets = ["armv7-unknown-linux-gnueabihf"] diff --git a/swap/Cargo.toml b/swap/Cargo.toml index b73d61bb..3f9349d6 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -27,7 +27,7 @@ data-encoding = "2.6" dialoguer = "0.11" digest = "0.10.7" directories-next = "2" -ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "libsecp_compat", "serde", "adaptor" ] } +ecdsa_fun = { version = "0.10", default-features = false, features = [ "libsecp_compat", "serde", "adaptor" ] } ed25519-dalek = "1" futures = { version = "0.3", default-features = false } hex = "0.4" @@ -50,7 +50,7 @@ serde_cbor = "0.11" serde_json = "1" serde_with = { version = "1", features = [ "macros" ] } sha2 = "0.10" -sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde", "secp256k1", "alloc" ] } +sigma_fun = { version = "0.7", default-features = false, features = [ "ed25519", "serde", "secp256k1", "alloc" ] } sqlx = { version = "0.6.3", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] } structopt = "0.3" strum = { version = "0.26", features = [ "derive" ] } @@ -88,7 +88,6 @@ proptest = "1" sequential-test = "0.2.4" serde_cbor = "0.11" serial_test = "3.0" -spectral = "0.6" tempfile = "3" testcontainers = "0.14" diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 4083f130..0d6de9a7 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -279,10 +279,10 @@ where SwarmEvent::IncomingConnectionError { send_back_addr: address, error, .. } => { tracing::warn!(%address, "Failed to set up connection with peer: {:#}", error); } - SwarmEvent::ConnectionClosed { peer_id: peer, num_established, endpoint, cause: Some(error) } if num_established == 0 => { + SwarmEvent::ConnectionClosed { peer_id: peer, num_established: 0, endpoint, cause: Some(error) } => { tracing::debug!(%peer, address = %endpoint.get_remote_address(), "Lost connection to peer: {:#}", error); } - SwarmEvent::ConnectionClosed { peer_id: peer, num_established, endpoint, cause: None } if num_established == 0 => { + SwarmEvent::ConnectionClosed { peer_id: peer, num_established: 0, endpoint, cause: None } => { tracing::info!(%peer, address = %endpoint.get_remote_address(), "Successfully closed connection"); } SwarmEvent::NewListenAddr{address, ..} => { @@ -296,7 +296,7 @@ where Some(Ok((peer, transfer_proof, responder))) => { if !self.swarm.behaviour_mut().transfer_proof.is_connected(&peer) { tracing::warn!(%peer, "No active connection to peer, buffering transfer proof"); - self.buffered_transfer_proofs.entry(peer).or_insert_with(Vec::new).push((transfer_proof, responder)); + self.buffered_transfer_proofs.entry(peer).or_default().push((transfer_proof, responder)); continue; } diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 181ec9bc..7b85f8fb 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -302,7 +302,7 @@ pub mod rendezvous { fn inject_disconnected(&mut self, peer_id: &PeerId) { for i in 0..self.rendezvous_nodes.len() { - let mut node = &mut self.rendezvous_nodes[i]; + let node = &mut self.rendezvous_nodes[i]; if peer_id == &node.peer_id { node.connection_status = ConnectionStatus::Disconnected; } @@ -325,7 +325,7 @@ pub mod rendezvous { _error: &DialError, ) { for i in 0..self.rendezvous_nodes.len() { - let mut node = &mut self.rendezvous_nodes[i]; + let node = &mut self.rendezvous_nodes[i]; if let Some(id) = peer_id { if id == node.peer_id { node.connection_status = ConnectionStatus::Disconnected; diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 556d8453..2d86d303 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -16,6 +16,7 @@ pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks}; pub use ::bitcoin::util::amount::Amount; pub use ::bitcoin::util::psbt::PartiallySignedTransaction; pub use ::bitcoin::{Address, AddressType, Network, Transaction, Txid}; +use bitcoin::secp256k1::ecdsa; pub use ecdsa_fun::adaptor::EncryptedSignature; pub use ecdsa_fun::fun::Scalar; pub use ecdsa_fun::Signature; @@ -25,9 +26,8 @@ pub use wallet::Wallet; pub use wallet::WalletBuilder; use crate::bitcoin::wallet::ScriptStatus; -use ::bitcoin::hashes::hex::ToHex; use ::bitcoin::hashes::Hash; -use ::bitcoin::{secp256k1, Sighash}; +use ::bitcoin::Sighash; use anyhow::{bail, Context, Result}; use bdk::miniscript::descriptor::Wsh; use bdk::miniscript::{Descriptor, Segwitv0}; @@ -206,20 +206,21 @@ pub fn verify_encsig( #[error("encrypted signature is invalid")] pub struct InvalidEncryptedSignature; -pub fn build_shared_output_descriptor(A: Point, B: Point) -> Descriptor { +pub fn build_shared_output_descriptor( + A: Point, + B: Point, +) -> Result> { const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))"; - // NOTE: This shouldn't be a source of error, but maybe it is - let A = ToHex::to_hex(&secp256k1::PublicKey::from(A)); - let B = ToHex::to_hex(&secp256k1::PublicKey::from(B)); - - let miniscript = MINISCRIPT_TEMPLATE.replace('A', &A).replace('B', &B); + let miniscript = MINISCRIPT_TEMPLATE + .replace('A', &A.to_string()) + .replace('B', &B.to_string()); let miniscript = bdk::miniscript::Miniscript::::from_str(&miniscript) .expect("a valid miniscript"); - Descriptor::Wsh(Wsh::new(miniscript).expect("a valid descriptor")) + Ok(Descriptor::Wsh(Wsh::new(miniscript)?)) } pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { @@ -305,6 +306,13 @@ pub mod bitcoin_address { } } +// Transform the ecdsa der signature bytes into a secp256kfun ecdsa signature type. +pub fn extract_ecdsa_sig(sig: &[u8]) -> Result { + let data = &sig[..sig.len() - 1]; + let sig = ecdsa::Signature::from_der(data)?.serialize_compact(); + Signature::from_bytes(sig).ok_or(anyhow::anyhow!("invalid signature")) +} + /// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23 pub enum RpcErrorCode { /// Transaction or block was rejected by network rules. Error code -26. @@ -378,6 +386,8 @@ mod tests { use super::*; use crate::env::{GetConfig, Regtest}; use crate::protocol::{alice, bob}; + use bitcoin::secp256k1; + use ecdsa_fun::fun::marker::{NonZero, Public}; use rand::rngs::OsRng; use std::matches; use uuid::Uuid; @@ -524,4 +534,16 @@ mod tests { transaction ) } + + #[test] + fn compare_point_hex() { + // secp256kfun Point and secp256k1 PublicKey should have the same bytes and hex representation + let secp = secp256k1::Secp256k1::default(); + let keypair = secp256k1::KeyPair::new(&secp, &mut OsRng); + + let pubkey = keypair.public_key(); + let point: Point<_, Public, NonZero> = Point::from_bytes(pubkey.serialize()).unwrap(); + + assert_eq!(pubkey.to_string(), point.to_string()); + } } diff --git a/swap/src/bitcoin/cancel.rs b/swap/src/bitcoin/cancel.rs index aec3fe38..34612148 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap/src/bitcoin/cancel.rs @@ -5,7 +5,8 @@ use crate::bitcoin::{ }; use ::bitcoin::util::sighash::SighashCache; use ::bitcoin::{ - EcdsaSighashType, OutPoint, PackedLockTime, Script, Sequence, Sighash, TxIn, TxOut, Txid, + secp256k1, EcdsaSighashType, OutPoint, PackedLockTime, Script, Sequence, Sighash, TxIn, TxOut, + Txid, }; use anyhow::Result; use bdk::miniscript::Descriptor; @@ -117,8 +118,8 @@ impl TxCancel { A: PublicKey, B: PublicKey, spending_fee: Amount, - ) -> Self { - let cancel_output_descriptor = build_shared_output_descriptor(A.0, B.0); + ) -> Result { + let cancel_output_descriptor = build_shared_output_descriptor(A.0, B.0)?; let tx_in = TxIn { previous_output: tx_lock.as_outpoint(), @@ -148,12 +149,12 @@ impl TxCancel { ) .expect("sighash"); - Self { + Ok(Self { inner: transaction, digest, output_descriptor: cancel_output_descriptor, lock_output_descriptor: tx_lock.output_descriptor.clone(), - } + }) } pub fn txid(&self) -> Txid { @@ -214,25 +215,27 @@ impl TxCancel { let A = ::bitcoin::PublicKey { compressed: true, - inner: A.0.into(), + inner: secp256k1::PublicKey::from_slice(&A.0.to_bytes())?, }; let B = ::bitcoin::PublicKey { compressed: true, - inner: B.0.into(), + inner: secp256k1::PublicKey::from_slice(&B.0.to_bytes())?, }; // The order in which these are inserted doesn't matter + let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?; + let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?; satisfier.insert( A, ::bitcoin::EcdsaSig { - sig: sig_a.into(), + sig: sig_a, hash_ty: EcdsaSighashType::All, }, ); satisfier.insert( B, ::bitcoin::EcdsaSig { - sig: sig_b.into(), + sig: sig_b, hash_ty: EcdsaSighashType::All, }, ); diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index f8aa9a39..0a9bd8f8 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -32,7 +32,7 @@ impl TxLock { C: EstimateFeeRate, D: BatchDatabase, { - let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0); + let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0)?; let address = lock_output_descriptor .address(wallet.get_network()) .expect("can derive address from descriptor"); @@ -84,7 +84,7 @@ impl TxLock { } }; - let descriptor = build_shared_output_descriptor(A.0, B.0); + let descriptor = build_shared_output_descriptor(A.0, B.0)?; let legit_shared_output_script = descriptor.script_pubkey(); if shared_output_candidate.script_pubkey != legit_shared_output_script { @@ -263,7 +263,7 @@ mod tests { 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(); + let computed_size = build_shared_output_descriptor(a, b).unwrap().script_pubkey().len(); assert_eq!(computed_size, SCRIPT_SIZE); } diff --git a/swap/src/bitcoin/punish.rs b/swap/src/bitcoin/punish.rs index 247c904f..9d687544 100644 --- a/swap/src/bitcoin/punish.rs +++ b/swap/src/bitcoin/punish.rs @@ -1,7 +1,7 @@ use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{self, Address, Amount, PunishTimelock, Transaction, TxCancel, Txid}; use ::bitcoin::util::sighash::SighashCache; -use ::bitcoin::{EcdsaSighashType, Sighash}; +use ::bitcoin::{secp256k1, EcdsaSighashType, Sighash}; use anyhow::{Context, Result}; use bdk::bitcoin::Script; use bdk::miniscript::Descriptor; @@ -64,18 +64,20 @@ impl TxPunish { let A = a.public().try_into()?; let B = B.try_into()?; + let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?; + let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?; // The order in which these are inserted doesn't matter satisfier.insert( A, ::bitcoin::EcdsaSig { - sig: sig_a.into(), + sig: sig_a, hash_ty: EcdsaSighashType::All, }, ); satisfier.insert( B, ::bitcoin::EcdsaSig { - sig: sig_b.into(), + sig: sig_b, hash_ty: EcdsaSighashType::All, }, ); diff --git a/swap/src/bitcoin/redeem.rs b/swap/src/bitcoin/redeem.rs index e91c25ee..a6a55e4b 100644 --- a/swap/src/bitcoin/redeem.rs +++ b/swap/src/bitcoin/redeem.rs @@ -6,7 +6,7 @@ use crate::bitcoin::{ use ::bitcoin::{Sighash, Txid}; use anyhow::{bail, Context, Result}; use bdk::miniscript::Descriptor; -use bitcoin::secp256k1::ecdsa; +use bitcoin::secp256k1; use bitcoin::util::sighash::SighashCache; use bitcoin::{EcdsaSighashType, Script}; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; @@ -16,6 +16,8 @@ use ecdsa_fun::Signature; use sha2::Sha256; use std::collections::HashMap; +use super::extract_ecdsa_sig; + #[derive(Clone, Debug)] pub struct TxRedeem { inner: Transaction, @@ -64,7 +66,7 @@ impl TxRedeem { ) -> Result { verify_encsig( B, - PublicKey::from(s_a.clone()), + PublicKey::from(s_a), &self.digest(), &encrypted_signature, ) @@ -79,25 +81,27 @@ impl TxRedeem { let A = ::bitcoin::PublicKey { compressed: true, - inner: a.public.into(), + inner: secp256k1::PublicKey::from_slice(&a.public.to_bytes())?, }; let B = ::bitcoin::PublicKey { compressed: true, - inner: B.0.into(), + inner: secp256k1::PublicKey::from_slice(&B.0.to_bytes())?, }; + let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?; + let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?; // The order in which these are inserted doesn't matter satisfier.insert( A, ::bitcoin::EcdsaSig { - sig: sig_a.into(), + sig: sig_a, hash_ty: EcdsaSighashType::All, }, ); satisfier.insert( B, ::bitcoin::EcdsaSig { - sig: sig_b.into(), + sig: sig_b, hash_ty: EcdsaSighashType::All, }, ); @@ -120,16 +124,16 @@ impl TxRedeem { let input = match candidate_transaction.input.as_slice() { [input] => input, [] => bail!(NoInputs), - [inputs @ ..] => bail!(TooManyInputs(inputs.len())), + inputs => bail!(TooManyInputs(inputs.len())), }; - let sigs = match input.witness.iter().collect::>().as_slice() { + let sigs = match input.witness.to_vec().as_slice() { [sig_1, sig_2, _script] => [sig_1, sig_2] - .iter() - .map(|sig| ecdsa::Signature::from_der(&sig[..sig.len() - 1]).map(Signature::from)) - .collect::, _>>(), + .into_iter() + .map(|sig| extract_ecdsa_sig(sig)) + .collect::, _>>(), [] => bail!(EmptyWitnessStack), - [witnesses @ ..] => bail!(NotThreeWitnesses(witnesses.len())), + witnesses => bail!(NotThreeWitnesses(witnesses.len())), }?; let sig = sigs diff --git a/swap/src/bitcoin/refund.rs b/swap/src/bitcoin/refund.rs index a73dd0e3..ec9fc802 100644 --- a/swap/src/bitcoin/refund.rs +++ b/swap/src/bitcoin/refund.rs @@ -4,7 +4,7 @@ use crate::bitcoin::{ TooManyInputs, Transaction, TxCancel, }; use crate::{bitcoin, monero}; -use ::bitcoin::secp256k1::ecdsa; +use ::bitcoin::secp256k1; use ::bitcoin::util::sighash::SighashCache; use ::bitcoin::{EcdsaSighashType, Script, Sighash, Txid}; use anyhow::{bail, Context, Result}; @@ -12,6 +12,8 @@ use bdk::miniscript::Descriptor; use ecdsa_fun::Signature; use std::collections::HashMap; +use super::extract_ecdsa_sig; + #[derive(Debug)] pub struct TxRefund { inner: Transaction, @@ -62,25 +64,27 @@ impl TxRefund { let A = ::bitcoin::PublicKey { compressed: true, - inner: A.0.into(), + inner: secp256k1::PublicKey::from_slice(&A.0.to_bytes())?, }; let B = ::bitcoin::PublicKey { compressed: true, - inner: B.0.into(), + inner: secp256k1::PublicKey::from_slice(&B.0.to_bytes())?, }; + let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?; + let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?; // The order in which these are inserted doesn't matter satisfier.insert( A, ::bitcoin::EcdsaSig { - sig: sig_a.into(), + sig: sig_a, hash_ty: EcdsaSighashType::All, }, ); satisfier.insert( B, ::bitcoin::EcdsaSig { - sig: sig_b.into(), + sig: sig_b, hash_ty: EcdsaSighashType::All, }, ); @@ -127,16 +131,16 @@ impl TxRefund { let input = match candidate_transaction.input.as_slice() { [input] => input, [] => bail!(NoInputs), - [inputs @ ..] => bail!(TooManyInputs(inputs.len())), + inputs => bail!(TooManyInputs(inputs.len())), }; - let sigs = match input.witness.iter().collect::>().as_slice() { + let sigs = match input.witness.to_vec().as_slice() { [sig_1, sig_2, _script] => [sig_1, sig_2] - .iter() - .map(|sig| ecdsa::Signature::from_der(&sig[..sig.len() - 1]).map(Signature::from)) - .collect::, _>>(), + .into_iter() + .map(|sig| extract_ecdsa_sig(sig)) + .collect::, _>>(), [] => bail!(EmptyWitnessStack), - [witnesses @ ..] => bail!(NotThreeWitnesses(witnesses.len())), + witnesses => bail!(NotThreeWitnesses(witnesses.len())), }?; let sig = sigs diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 5d8685f3..9748d740 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -65,9 +65,7 @@ impl Wallet { database, ) { Ok(w) => w, - Err(e) if matches!(e, bdk::Error::ChecksumMismatch) => { - Self::migrate(data_dir, xprivkey, network)? - } + Err(bdk::Error::ChecksumMismatch) => Self::migrate(data_dir, xprivkey, network)?, err => err?, }; diff --git a/swap/src/cli/event_loop.rs b/swap/src/cli/event_loop.rs index 23aa0f38..befe1dc5 100644 --- a/swap/src/cli/event_loop.rs +++ b/swap/src/cli/event_loop.rs @@ -168,7 +168,7 @@ impl EventLoop { tracing::info!("Successfully closed connection to Alice"); return; } - SwarmEvent::OutgoingConnectionError { peer_id, error } if matches!(peer_id, Some(alice_peer_id) if alice_peer_id == self.alice_peer_id) => { + SwarmEvent::OutgoingConnectionError { peer_id: Some(alice_peer_id), error } if alice_peer_id == self.alice_peer_id => { tracing::warn!(%error, "Failed to dial Alice"); if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() { diff --git a/swap/src/network/cbor_request_response.rs b/swap/src/network/cbor_request_response.rs index 18e193ee..634dd729 100644 --- a/swap/src/network/cbor_request_response.rs +++ b/swap/src/network/cbor_request_response.rs @@ -19,7 +19,7 @@ pub struct CborCodec { impl Default for CborCodec { fn default() -> Self { Self { - phantom: PhantomData::default(), + phantom: PhantomData, } } } diff --git a/swap/src/network/json_pull_codec.rs b/swap/src/network/json_pull_codec.rs index bf473e84..01fcf494 100644 --- a/swap/src/network/json_pull_codec.rs +++ b/swap/src/network/json_pull_codec.rs @@ -25,7 +25,7 @@ pub struct JsonPullCodec { impl Default for JsonPullCodec { fn default() -> Self { Self { - phantom: PhantomData::default(), + phantom: PhantomData, } } } diff --git a/swap/src/network/test.rs b/swap/src/network/test.rs index a5c316b1..5a324385 100644 --- a/swap/src/network/test.rs +++ b/swap/src/network/test.rs @@ -40,7 +40,7 @@ where .expect("failed to create dh_keys"); let noise = NoiseConfig::xx(dh_keys).into_authenticated(); - let transport = MemoryTransport::default() + let transport = MemoryTransport .or_transport(TokioTcpConfig::new()) .upgrade(Version::V1) .authenticate(noise) diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index b34e0326..b4e155a6 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -310,7 +310,8 @@ impl State2 { self.a.public(), self.B, self.tx_cancel_fee, - ); + ) + .expect("valid cancel tx"); let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee); @@ -335,7 +336,7 @@ impl State2 { self.a.public(), self.B, self.tx_cancel_fee, - ); + )?; bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig) .context("Failed to verify cancel transaction")?; let tx_punish = bitcoin::TxPunish::new( @@ -458,6 +459,7 @@ impl State3 { self.B, self.tx_cancel_fee, ) + .expect("valid cancel tx") } pub fn tx_refund(&self) -> TxRefund { diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index edf902b6..aa0045ea 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -243,7 +243,7 @@ impl State1 { self.A, self.b.public(), self.tx_cancel_fee, - ); + )?; let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee); @@ -316,7 +316,8 @@ impl State2 { self.A, self.b.public(), self.tx_cancel_fee, - ); + ) + .expect("valid cancel tx"); let tx_cancel_sig = self.b.sign(tx_cancel.digest()); let tx_punish = bitcoin::TxPunish::new( &tx_cancel, @@ -450,7 +451,7 @@ impl State3 { self.A, self.b.public(), self.tx_cancel_fee, - ); + )?; let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?; let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?; @@ -531,7 +532,7 @@ impl State4 { self.A, self.b.public(), self.tx_cancel_fee, - ); + )?; let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?; let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?; @@ -612,7 +613,7 @@ impl State6 { self.A, self.b.public(), self.tx_cancel_fee, - ); + )?; let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?; let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?; @@ -635,7 +636,7 @@ impl State6 { self.A, self.b.public(), self.tx_cancel_fee, - ); + )?; let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?; @@ -652,7 +653,7 @@ impl State6 { self.A, self.b.public(), self.tx_cancel_fee, - ) + )? .complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone()) .context("Failed to complete Bitcoin cancel transaction")?; @@ -675,7 +676,7 @@ impl State6 { self.A, self.b.public(), self.tx_cancel_fee, - ); + )?; let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee); From e6d37c01a6bcf30774bb7df40c7babbb15b861a1 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Mon, 27 May 2024 11:06:10 +0200 Subject: [PATCH 47/55] ci: fix duplicate definition of check_stable --- .github/workflows/ci.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7619754b..cb869ccf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,22 +31,6 @@ jobs: - name: Run clippy with all features enabled run: cargo clippy --workspace --all-targets --all-features -- -D warnings - check_stable: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v4.1.1 - - - uses: dtolnay/rust-toolchain@stable - - - name: Override rust stable - run: | - rustup override set stable - - - name: Run cargo check on rust stable - run: cargo check --all-targets - - bdk_test: runs-on: ubuntu-latest steps: From bbf8f8431261500c7cabe8070269a86ea8a9846f Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Mon, 27 May 2024 12:04:04 +0200 Subject: [PATCH 48/55] ci: fix build-release-binaries --- .github/workflows/build-release-binaries.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index bcd2c806..afa4d580 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -62,17 +62,16 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: "1.74" - targets: armv7-unknown-linux-gnueabihf + + - name: install armv7 target + if: matrix.target != 'armv7-unknown-linux-gnueabihf' + run: rustup target add armv7-unknown-linux-gnueabihf - name: Build ${{ matrix.target }} ${{ matrix.bin }} release binary - uses: actions-rs/cargo@v1 - with: - command: build - args: --target=${{ matrix.target }} --release --package swap --bin ${{ matrix.bin }} - use-cross: true + run: cargo build --target=${{ matrix.target }} --release --package swap --bin ${{ matrix.bin }} - name: Smoke test the binary - if: matrix.target != 'armv7-unknown-linux-gnueabihf' # armv7-unknown-linux-gnueabihf is only cross-compiled, no smoke test + if: matrix.target != 'armv7-unknown-linux-gnueabihf' run: target/${{ matrix.target }}/release/${{ matrix.bin }} --help # Remove once python 3 is the default From 52d56ae254bc537bcf8ac66c0153e6bd0e612953 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Mon, 27 May 2024 12:19:13 +0200 Subject: [PATCH 49/55] ci: fix build-release-binaries --- .github/workflows/build-release-binaries.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index afa4d580..252003be 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -6,8 +6,9 @@ on: jobs: build_binaries: - name: Build swap and asb binaries + name: Build strategy: + fail-fast: false matrix: include: - bin: swap @@ -63,11 +64,16 @@ jobs: with: toolchain: "1.74" - - name: install armv7 target - if: matrix.target != 'armv7-unknown-linux-gnueabihf' - run: rustup target add armv7-unknown-linux-gnueabihf + - name: Cross Build ${{ matrix.target }} ${{ matrix.bin }} binary + if: matrix.target == 'armv7-unknown-linux-gnueabihf' + run: | + curl -L "https://github.com/cross-rs/cross/releases/download/v0.2.5/cross-x86_64-unknown-linux-gnu.tar.gz" | tar xzv + sudo mv cross /usr/bin + sudo mv cross-util /usr/bin + cross build --target=${{ matrix.target }} --release --package swap --bin ${{ matrix.bin }} - name: Build ${{ matrix.target }} ${{ matrix.bin }} release binary + if: matrix.target != 'armv7-unknown-linux-gnueabihf' run: cargo build --target=${{ matrix.target }} --release --package swap --bin ${{ matrix.bin }} - name: Smoke test the binary From 268a24083f3cdd2b78734e7c58bb0ea1f1d00f9c Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Mon, 27 May 2024 12:57:32 +0200 Subject: [PATCH 50/55] deps: bump miniscript 9.0.0 to 9.0.2 to fix overflow issue --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 435a86bb..982de04c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2513,9 +2513,9 @@ checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d" [[package]] name = "miniscript" -version = "9.0.0" +version = "9.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "123a10aae81d0712ecc09b780f6f0ae0b0f506a5c4c912974725760d59ba073e" +checksum = "e5b106477a0709e2da253e5559ba4ab20a272f8577f1eefff72f3a905b5d35f5" dependencies = [ "bitcoin", "serde", From 6399343de9b1a73d1aa06f8f5d8f42ed76f1afd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 11:29:09 +0000 Subject: [PATCH 51/55] build(deps): bump serde from 1.0.202 to 1.0.203 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.202 to 1.0.203. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.202...v1.0.203) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 982de04c..f665b39e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3871,9 +3871,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -3900,9 +3900,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", From 66bc59892a50d906ca38dc93b435106bc66fa3a6 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 28 May 2024 12:21:19 +0200 Subject: [PATCH 52/55] ci: wip fixing build binaries actions --- .github/workflows/build-release-binaries.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 252003be..9299cea4 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -80,11 +80,6 @@ jobs: if: matrix.target != 'armv7-unknown-linux-gnueabihf' run: target/${{ matrix.target }}/release/${{ matrix.bin }} --help - # Remove once python 3 is the default - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - id: create-archive-name shell: python # Use python to have a prettier name for the archive on Windows. run: | @@ -98,10 +93,13 @@ jobs: archive_name=f'${{ matrix.bin }}_${{ github.event.release.tag_name }}_{os_info.system}_{arch}.${{ matrix.archive_ext }}' - print(f'::set-output name=archive::{archive_name}') + print(f'archive={archive_name} >> $GITHUB_OUTPUT') + + - name: debug + run: tree ./target - name: Pack macos archive - if: matrix.os == 'macos-latest' + if: startsWith(matrix.os, 'macos') shell: bash run: gtar -C ./target/${{ matrix.target }}/release --create --file=${{ steps.create-archive-name.outputs.archive }} ${{ matrix.bin }} From e09401b9f78c856ffd3014f0b8410127b42a279e Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Tue, 28 May 2024 12:32:04 +0200 Subject: [PATCH 53/55] ci: wip fixing build binaries actions --- .github/workflows/build-release-binaries.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 9299cea4..8a35a88f 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -93,10 +93,7 @@ jobs: archive_name=f'${{ matrix.bin }}_${{ github.event.release.tag_name }}_{os_info.system}_{arch}.${{ matrix.archive_ext }}' - print(f'archive={archive_name} >> $GITHUB_OUTPUT') - - - name: debug - run: tree ./target + print(f'::set-output name=archive::{archive_name}') - name: Pack macos archive if: startsWith(matrix.os, 'macos') From 41687ffab96bc1aaa22526ed0a7c51ef9cdde1bf Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Wed, 29 May 2024 11:58:28 +0200 Subject: [PATCH 54/55] CI: Fix faulty x86-darwin release name (#1658) --- .github/workflows/build-release-binaries.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 8a35a88f..29bdde64 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -22,6 +22,7 @@ jobs: - bin: swap target: x86_64-apple-darwin os: macos-12 + archive_ext: tar - bin: swap target: aarch64-apple-darwin os: macos-latest From 2932abc9ec2475717944afa02aeaad73f3c98f16 Mon Sep 17 00:00:00 2001 From: COMIT Botty McBotface <68941619+comit-botty-mc-botface@users.noreply.github.com> Date: Thu, 30 May 2024 00:00:17 +1000 Subject: [PATCH 55/55] Prepare release 0.13.0 (#1659) --- CHANGELOG.md | 5 ++++- Cargo.lock | 2 +- swap/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e510a9..1fa3bdeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.0] - 2024-05-29 + - Minimum Supported Rust Version (MSRV) bumped to 1.74 - Lowered default Bitcoin confirmation target for Bob to 1 to make sure Bitcoin transactions get confirmed in time - Added support for starting the CLI (using the `start-daemon` subcommand) as a Daemon that accepts JSON-RPC requests @@ -354,7 +356,8 @@ It is possible to migrate critical data from the old db to the sqlite but there - Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them. Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version. -[Unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.3...HEAD +[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.0...HEAD +[0.13.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.3...0.13.0 [0.12.3]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.2...0.12.3 [0.12.2]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.1...0.12.2 [0.12.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.0...0.12.1 diff --git a/Cargo.lock b/Cargo.lock index f665b39e..522c7c75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4391,7 +4391,7 @@ checksum = "8049cf85f0e715d6af38dde439cb0ccb91f67fb9f5f63c80f8b43e48356e1a3f" [[package]] name = "swap" -version = "0.12.3" +version = "0.13.0" dependencies = [ "anyhow", "async-compression", diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 3f9349d6..b37570ba 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swap" -version = "0.12.3" +version = "0.13.0" authors = [ "The COMIT guys " ] edition = "2021" description = "XMR/BTC trustless atomic swaps."