mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-18 01:54:29 -05:00
feat: Orchestration & Controller (#492)
* fix: formatting in stress_test.rs * refactor: move asb binary into swap-asb * refactor(swap-asb): remove unused disable_timestamp argument * fix(monero-sys): Include search path for aarch64-linux-gnu * fix(swap): notpick formatting in swap.rs * feat(swap-env): Split up config wizard, add default rendezvous points * feat(swap-controller, swap-controller-api): Minimal maker shell with just a check-connection command * fix(monero-rpc-pool): Use new axum route path syntax to prevent runtime panic * feat(ci): Specify crate when building binaries; build asb-controller * fix: Add swap-controller, swap-controller-api crates and their dependencies to Cargo.toml * feat(Dockerfile): Build asb-controller; default to 1.87 rust toolchain * feat(swap-orchestrator): Compose spec generator * formatting: nitpicks * fix: add swap-orchestrator auto generated files to gitginore * refactoring(swap-orchestrator: Use Into<_> to derive asb::Network and electrs::Network from Bitcoin/Monero network, use defaults * feat(swap-env): Change default bitcoin_confirmation_target to 1 * feat: Dockerfile for asb-controller, bitcoin-balance and monero-balance controller commands * formatting: nitpicks * changelog: default bitcoin finality confirmations change * feat(ci): Build swap-orchestrator binary * disable rpc server by default, split rpc-bind into rpc-bind-port and rpc-bind-host * feat(swap-controller): Add monero-address command to print primary address of internal wallet * chore: upgrade rustyline to 17.0.0 * changelog: Document CONTROLLER, ORCHESTRATOR and JSON-RPC server * refactor: Change swap-orchestrator binary to just "orchestrator" * refactor: let RpcServer::start(...) take port and host seperately * default electrum servers in config wizard * formatting * feat(swap-orchestrator): README * feat(swap-controller): Add Multiaddresses and ActiveConnections command Signed-off-by: Binarybaron <binarybaron@protonmail.com> * refactor(asb/event_loop.rs): Move quote logic and tower service into their own modules * fix(swap): some unit tests * feat(swap-controller): redumentary repl command auto complete * formatting * feat(swap-orchestrator): Burn Git commit hash into orchestrator binary * feat(swap-orchestrator): burn git commit hash into binary when building from source * feat(Dockerfiles): Build with --locked * feat: derive ports for images from network combination add some doc into the docker compose file * small refactorings * feat(swap-controller): Add get-swaps command * feat: add more default electrum mainnet nodes * feat: build asb-controller docker image in ci, move asb Dockerfile into swap-asb * fix: do not allow pre-built docker images for now * amend changelog * remove default monero_daemon_url, default to None (Monero RPC pool) * unify asb and orchestrator wizard for monero daemon url setup --------- Signed-off-by: Binarybaron <binarybaron@protonmail.com>
This commit is contained in:
parent
7c82853050
commit
97a4a31af9
49 changed files with 2979 additions and 704 deletions
|
|
@ -1,5 +1,8 @@
|
|||
# Rust build artifacts
|
||||
target/
|
||||
target-check/
|
||||
|
||||
Dockerfile
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
|
|
|
|||
96
.github/workflows/build-release-binaries.yml
vendored
96
.github/workflows/build-release-binaries.yml
vendored
|
|
@ -19,46 +19,110 @@ jobs:
|
|||
matrix:
|
||||
include:
|
||||
- bin: swap
|
||||
crate: swap
|
||||
target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
archive_ext: tar
|
||||
# Temporarily disabled armv7 Linux builds
|
||||
# - bin: swap
|
||||
# crate: swap
|
||||
# target: armv7-unknown-linux-gnueabihf
|
||||
# os: ubuntu-latest
|
||||
# archive_ext: tar
|
||||
- bin: swap
|
||||
crate: swap
|
||||
target: x86_64-apple-darwin
|
||||
os: macos-13
|
||||
archive_ext: tar
|
||||
- bin: swap
|
||||
crate: swap
|
||||
target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
archive_ext: tar
|
||||
# Temporarily disabled Windows builds
|
||||
# - bin: swap
|
||||
# crate: swap
|
||||
# target: x86_64-pc-windows-msvc
|
||||
# os: windows-latest
|
||||
# archive_ext: zip
|
||||
- bin: asb
|
||||
crate: swap
|
||||
target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
archive_ext: tar
|
||||
# Temporarily disabled armv7 Linux builds
|
||||
# - bin: asb
|
||||
# crate: swap
|
||||
# target: armv7-unknown-linux-gnueabihf
|
||||
# os: ubuntu-latest
|
||||
# archive_ext: tar
|
||||
- bin: asb
|
||||
crate: swap
|
||||
target: x86_64-apple-darwin
|
||||
os: macos-13
|
||||
archive_ext: tar
|
||||
- bin: asb
|
||||
crate: swap
|
||||
target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
archive_ext: tar
|
||||
# Temporarily disabled Windows builds
|
||||
# - bin: asb
|
||||
# crate: swap
|
||||
# target: x86_64-pc-windows-msvc
|
||||
# os: windows-latest
|
||||
# archive_ext: zip
|
||||
- bin: asb-controller
|
||||
crate: swap-controller
|
||||
target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
archive_ext: tar
|
||||
# Temporarily disabled armv7 Linux builds
|
||||
# - bin: asb-controller
|
||||
# crate: swap-controller
|
||||
# target: armv7-unknown-linux-gnueabihf
|
||||
# os: ubuntu-latest
|
||||
# archive_ext: tar
|
||||
- bin: asb-controller
|
||||
crate: swap-controller
|
||||
target: x86_64-apple-darwin
|
||||
os: macos-13
|
||||
archive_ext: tar
|
||||
- bin: asb-controller
|
||||
crate: swap-controller
|
||||
target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
archive_ext: tar
|
||||
# Temporarily disabled Windows builds
|
||||
# - bin: asb-controller
|
||||
# crate: swap-controller
|
||||
# target: x86_64-pc-windows-msvc
|
||||
# os: windows-latest
|
||||
# archive_ext: zip
|
||||
- bin: orchestrator
|
||||
crate: swap-orchestrator
|
||||
target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
archive_ext: tar
|
||||
# Temporarily disabled armv7 Linux builds
|
||||
# - bin: orchestrator
|
||||
# crate: swap-orchestrator
|
||||
# target: armv7-unknown-linux-gnueabihf
|
||||
# os: ubuntu-latest
|
||||
# archive_ext: tar
|
||||
- bin: orchestrator
|
||||
crate: swap-orchestrator
|
||||
target: x86_64-apple-darwin
|
||||
os: macos-13
|
||||
archive_ext: tar
|
||||
- bin: orchestrator
|
||||
crate: swap-orchestrator
|
||||
target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
archive_ext: tar
|
||||
# Temporarily disabled Windows builds
|
||||
# - bin: orchestrator
|
||||
# crate: swap-orchestrator
|
||||
# target: x86_64-pc-windows-msvc
|
||||
# os: windows-latest
|
||||
# archive_ext: zip
|
||||
|
|
@ -138,11 +202,11 @@ jobs:
|
|||
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 }}
|
||||
cross build --target=${{ matrix.target }} --release --package ${{ matrix.crate }} --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 }}
|
||||
run: cargo build --target=${{ matrix.target }} --release --package ${{ matrix.crate }} --bin ${{ matrix.bin }}
|
||||
|
||||
- name: Smoke test the binary
|
||||
if: matrix.target != 'armv7-unknown-linux-gnueabihf'
|
||||
|
|
@ -255,22 +319,42 @@ jobs:
|
|||
echo "preview=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push Docker image
|
||||
- name: Build and push Docker image (asb)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
file: ./swap-asb/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
${{ env.DOCKER_IMAGE_NAME }}:latest
|
||||
if: steps.docker_tags.outputs.preview == 'false'
|
||||
|
||||
- name: Build and push Docker image without latest tag (preview release)
|
||||
- name: Build and push Docker image without latest tag (preview release) (asb)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
file: ./swap-asb/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
if: steps.docker_tags.outputs.preview == 'true'
|
||||
|
||||
- name: Build and push Docker image (asb)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./swap-controller/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
${{ env.DOCKER_IMAGE_NAME }}:latest
|
||||
if: steps.docker_tags.outputs.preview == 'false'
|
||||
|
||||
- name: Build and push Docker image without latest tag (preview release) (asb)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./swap-controller/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.DOCKER_IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
if: steps.docker_tags.outputs.preview == 'true'
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,10 +1,19 @@
|
|||
target/
|
||||
target-check/
|
||||
|
||||
.vscode
|
||||
.claude/settings.local.json
|
||||
.DS_Store
|
||||
|
||||
build/
|
||||
release-build.sh
|
||||
cn_macos
|
||||
target-check
|
||||
|
||||
monero-rpc-pool/temp_db.sqlite
|
||||
monero-rpc-pool/temp.db
|
||||
|
||||
# auto-generated files by swap-orchestrator
|
||||
swap-orchestrator/docker-compose.yml
|
||||
swap-orchestrator/config.toml
|
||||
|
||||
# release build generator scripts
|
||||
release-build.sh
|
||||
cn_macos
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -8,6 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [Unreleased]
|
||||
|
||||
- GUI + CLI + ASB: The Monero RPC pool now caches TCP and Tor streams
|
||||
- ASB: The default configuration has been adjusted to accept Bitcoin transactions as finalized after one block (one confirmation). Bitcoin double spends are essentially impossible for practical purposes. If one is swapping extremely large amounts, they can consider dialing `bitcoin.finality_confirmations` to `2` or `3`. This will however force the swap to take much longer to complete, and also increase the risk of a refund being made.
|
||||
- ASB: The `monero.monero_node_pool` flag has been removed from the config. If you want to use the Monero Node Pool, you can now do so simply by omitting `monero.daemon_url` from the config.
|
||||
- ASB: The `asb` now exposes a JSON-RPC endpoint at which it can receive commands. The `asb-controller` binary implements the client side of the JSON-RPC protocol. The JSON-RPC protocol is currently entirely read-only. This means it cannot be used to withdraw funds or change configurations. The JSON-RPC endpoint is disabled by default. It can be enabled by passing the `--rpc-bind-port 127.0.0.1:9944` and `--rpc-bind-host 127.0.0.1` flags to the `asb` binary.
|
||||
- CONTROLLER: A new experimental `asb-controller` binary is now shipped. It is a CLI and REPL tool to interact with an ASB over JSON-RPC. It still has limited functionality, but will be extended in the future. Currently, it can be used to:
|
||||
- Get the primary address of th Monero wallet
|
||||
- Get the balance of the Monero wallet
|
||||
- Get the balance of the Bitcoin wallet
|
||||
- Get the list of external multiaddresses that the ASB is listening on
|
||||
- Query the currently active peer-to-peer connections
|
||||
- Get a basic list of swaps from the database
|
||||
- ORCHESTRATOR: A new experiemental `orchestrator` binary is now shipped.
|
||||
- The `orchestrator` is a lightweight tool to generate a production grade environement for running ASBs.
|
||||
- It guides the user through a wizard and generates a custom [Docker compose](https://docs.docker.com/compose/) file which specifies a secure Docker environment for running ASBs.
|
||||
- This will continue to evolve over time, and we will document this thoroughly once it is more stable.
|
||||
|
||||
## [3.0.0-beta.5] - 2025-08-04
|
||||
|
||||
|
|
|
|||
404
Cargo.lock
generated
404
Cargo.lock
generated
|
|
@ -570,9 +570,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.0"
|
||||
version = "3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
|
||||
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
|
|
@ -1569,7 +1569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.9.4",
|
||||
"toml 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1845,6 +1845,33 @@ dependencies = [
|
|||
"unicode-width 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compose_spec"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fd9b9dc67f2b3024582ec6d861950f0af0aeaabb8350ccda1f0e51ff8e5895c"
|
||||
dependencies = [
|
||||
"compose_spec_macros",
|
||||
"indexmap 2.10.0",
|
||||
"ipnet",
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compose_spec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b77735bd89be8da01c8d7e61faec5a9ccb0e313cece3c773c6b3ae251b90c7d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
|
|
@ -3137,7 +3164,7 @@ dependencies = [
|
|||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.9.4",
|
||||
"toml 0.9.5",
|
||||
"vswhom",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
|
|
@ -3169,6 +3196,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
|
||||
|
||||
[[package]]
|
||||
name = "endian-type"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.1"
|
||||
|
|
@ -3270,9 +3303,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.0"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
|
|
@ -3307,6 +3340,17 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fd-lock"
|
||||
version = "4.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustix 1.0.8",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
|
|
@ -3628,9 +3672,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
|||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.0"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
|
|
@ -4526,6 +4570,7 @@ dependencies = [
|
|||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"rustls 0.23.31",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
|
|
@ -4892,6 +4937,9 @@ name = "ipnet"
|
|||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
|
|
@ -5098,6 +5146,121 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpsee"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16"
|
||||
dependencies = [
|
||||
"jsonrpsee-core",
|
||||
"jsonrpsee-http-client",
|
||||
"jsonrpsee-proc-macros",
|
||||
"jsonrpsee-server",
|
||||
"jsonrpsee-types",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpsee-core"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"jsonrpsee-types",
|
||||
"parking_lot 0.12.4",
|
||||
"pin-project",
|
||||
"rand 0.9.2",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tower 0.5.2",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpsee-http-client"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6962d2bd295f75e97dd328891e58fce166894b974c1f7ce2e7597f02eeceb791"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"jsonrpsee-core",
|
||||
"jsonrpsee-types",
|
||||
"rustls 0.23.31",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tower 0.5.2",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpsee-proc-macros"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro-crate 3.3.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpsee-server"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d38b0bcf407ac68d241f90e2d46041e6a06988f97fe1721fb80b91c42584fae6"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"jsonrpsee-core",
|
||||
"jsonrpsee-types",
|
||||
"pin-project",
|
||||
"route-recognizer",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"soketto",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpsee-types"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca"
|
||||
dependencies = [
|
||||
"http 1.3.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k12"
|
||||
version = "0.3.0"
|
||||
|
|
@ -6516,6 +6679,15 @@ version = "1.0.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
|
|
@ -6564,9 +6736,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.1.0"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"inotify",
|
||||
|
|
@ -7586,7 +7758,7 @@ checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
|
|||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.10.0",
|
||||
"quick-xml 0.38.0",
|
||||
"quick-xml 0.38.1",
|
||||
"serde",
|
||||
"time 0.3.41",
|
||||
]
|
||||
|
|
@ -7634,9 +7806,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.9.0"
|
||||
version = "3.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7"
|
||||
checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
|
|
@ -7950,9 +8122,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.0"
|
||||
version = "0.38.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b"
|
||||
checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
|
@ -8056,6 +8228,16 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "radix_trie"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
|
||||
dependencies = [
|
||||
"endian-type",
|
||||
"nibble_vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
|
|
@ -8559,6 +8741,12 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "route-recognizer"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.8"
|
||||
|
|
@ -8796,6 +8984,33 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.31",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.103.4",
|
||||
"security-framework 3.2.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs 0.26.11",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
|
|
@ -8835,6 +9050,28 @@ dependencies = [
|
|||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustyline"
|
||||
version = "17.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed34fbd08950d17f8297e738d5b76acd4baab50c8d45008d498b4327feb43ea1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"clipboard-win",
|
||||
"fd-lock",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
"nix 0.30.1",
|
||||
"radix_trie",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.1",
|
||||
"utf8parse",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rw-stream-sink"
|
||||
version = "0.4.0"
|
||||
|
|
@ -9390,6 +9627,19 @@ dependencies = [
|
|||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap 2.10.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "3.2.0"
|
||||
|
|
@ -9585,9 +9835,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
|
||||
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
|
@ -9759,6 +10009,7 @@ dependencies = [
|
|||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures",
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
|
|
@ -10258,6 +10509,7 @@ dependencies = [
|
|||
"futures",
|
||||
"get-port",
|
||||
"hex",
|
||||
"jsonrpsee",
|
||||
"libp2p",
|
||||
"libp2p-tor",
|
||||
"mockito",
|
||||
|
|
@ -10289,6 +10541,7 @@ dependencies = [
|
|||
"sqlx",
|
||||
"structopt",
|
||||
"strum 0.26.3",
|
||||
"swap-controller-api",
|
||||
"swap-env",
|
||||
"swap-feed",
|
||||
"swap-fs",
|
||||
|
|
@ -10319,6 +10572,60 @@ dependencies = [
|
|||
"zip 0.5.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swap-asb"
|
||||
version = "3.0.0-beta.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitcoin 0.32.6",
|
||||
"comfy-table",
|
||||
"libp2p",
|
||||
"monero-rpc-pool",
|
||||
"monero-sys",
|
||||
"rust_decimal",
|
||||
"rustls 0.23.31",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"structopt",
|
||||
"swap",
|
||||
"swap-env",
|
||||
"swap-feed",
|
||||
"swap-serde",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swap-controller"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.5.42",
|
||||
"jsonrpsee",
|
||||
"monero",
|
||||
"rustyline",
|
||||
"serde_json",
|
||||
"shell-words",
|
||||
"swap-controller-api",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swap-controller-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitcoin 0.32.6",
|
||||
"jsonrpsee",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swap-env"
|
||||
version = "0.1.0"
|
||||
|
|
@ -10335,7 +10642,7 @@ dependencies = [
|
|||
"swap-serde",
|
||||
"thiserror 1.0.69",
|
||||
"time 0.3.41",
|
||||
"toml 0.8.23",
|
||||
"toml 0.9.5",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
|
@ -10369,6 +10676,23 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swap-orchestrator"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitcoin 0.32.6",
|
||||
"chrono",
|
||||
"compose_spec",
|
||||
"dialoguer",
|
||||
"monero",
|
||||
"serde_yaml",
|
||||
"swap-env",
|
||||
"toml 0.9.5",
|
||||
"url",
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swap-serde"
|
||||
version = "0.1.0"
|
||||
|
|
@ -11001,7 +11325,7 @@ checksum = "7c6d9028d41d4de835e3c482c677a8cb88137ac435d6ff9a71f392d4421576c9"
|
|||
dependencies = [
|
||||
"embed-resource",
|
||||
"indexmap 2.10.0",
|
||||
"toml 0.9.4",
|
||||
"toml 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -11290,6 +11614,7 @@ dependencies = [
|
|||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -11339,16 +11664,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.15"
|
||||
version = "0.7.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
||||
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hashbrown 0.15.4",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
|
@ -11367,9 +11691,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
|
||||
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
|
||||
dependencies = [
|
||||
"indexmap 2.10.0",
|
||||
"serde",
|
||||
|
|
@ -11436,9 +11760,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
|
||||
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
|
||||
dependencies = [
|
||||
"winnow 0.7.12",
|
||||
]
|
||||
|
|
@ -12885,6 +13209,12 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "unsigned-varint"
|
||||
version = "0.7.2"
|
||||
|
|
@ -13410,6 +13740,24 @@ dependencies = [
|
|||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
|
||||
dependencies = [
|
||||
"webpki-root-certs 1.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.21.1"
|
||||
|
|
@ -14434,9 +14782,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
|
||||
checksum = "bdbb9122ea75b11bf96e7492afb723e8a7fbe12c67417aa95e7e3d18144d37cd"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
|
|
|||
12
Cargo.toml
12
Cargo.toml
|
|
@ -10,9 +10,13 @@ members = [
|
|||
"monero-sys",
|
||||
"src-tauri",
|
||||
"swap",
|
||||
"swap-asb",
|
||||
"swap-controller",
|
||||
"swap-controller-api",
|
||||
"swap-env",
|
||||
"swap-feed",
|
||||
"swap-fs",
|
||||
"swap-orchestrator",
|
||||
"swap-serde",
|
||||
"throttle",
|
||||
]
|
||||
|
|
@ -30,6 +34,7 @@ anyhow = "1"
|
|||
backoff = { version = "0.4", features = ["futures", "tokio"] }
|
||||
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||
hex = "0.4"
|
||||
jsonrpsee = { version = "0.25", default-features = false }
|
||||
libp2p = { version = "0.53.2" }
|
||||
monero = { version = "0.12", features = ["serde_support"] }
|
||||
once_cell = "1.19"
|
||||
|
|
@ -41,6 +46,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_json = "1"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "time", "macros", "sync"] }
|
||||
toml = "0.9.5"
|
||||
tracing = { version = "0.1", features = ["attributes"] }
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "time", "tracing-log", "json"] }
|
||||
typeshare = "1.0"
|
||||
|
|
@ -54,6 +60,12 @@ tor-hsservice = { git = "https://github.com/eigenwallet/arti", rev = "18111286b5
|
|||
tor-proto = { git = "https://github.com/eigenwallet/arti", rev = "18111286b5830cda88af5df1950b5e24ee5a8841" }
|
||||
tor-rtcompat = { git = "https://github.com/eigenwallet/arti", rev = "18111286b5830cda88af5df1950b5e24ee5a8841" }
|
||||
|
||||
# Terminal Utilities
|
||||
dialoguer = "0.11"
|
||||
|
||||
# Build dependencies
|
||||
vergen = { version = "8.3", default-features = false, features = ["build", "git", "git2"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
||||
jsonrpc_client = { git = "https://github.com/delta1/rust-jsonrpc-client.git", rev = "3b6081697cd616c952acb9c2f02d546357d35506" }
|
||||
|
|
|
|||
4
justfile
4
justfile
|
|
@ -65,11 +65,11 @@ test_monero_sys:
|
|||
|
||||
# Builds the ASB and Swap binaries
|
||||
swap:
|
||||
cd swap && cargo build --bin asb --bin=swap
|
||||
cargo build -p swap-asb --bin asb && cd swap && cargo build --bin=swap
|
||||
|
||||
# Run the asb on testnet
|
||||
asb-testnet:
|
||||
cd swap && cargo run --bin asb -- --trace --testnet start
|
||||
cargo run -p swap-asb --bin asb -- --trace --testnet start
|
||||
|
||||
# Updates our submodules (currently only Monero C++ codebase)
|
||||
update_submodules:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
use arti_client::{TorClient, TorClientConfig};
|
||||
use clap::Parser;
|
||||
use monero::Network;
|
||||
use monero_rpc_pool::{config::Config, create_app_with_receiver, database::{parse_network, network_to_string}};
|
||||
use monero_rpc_pool::{
|
||||
config::Config,
|
||||
create_app_with_receiver,
|
||||
database::{network_to_string, parse_network},
|
||||
};
|
||||
use reqwest;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ pub async fn create_app_with_receiver(
|
|||
// Build the app
|
||||
let app = Router::new()
|
||||
.route("/stats", get(stats_handler))
|
||||
.route("/*path", any(proxy_handler))
|
||||
.route("/{*path}", any(proxy_handler))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(app_state);
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ fn main() {
|
|||
monero_build_dir.join("external/randomx").display()
|
||||
);
|
||||
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu");
|
||||
println!("cargo:rustc-link-search=native=/usr/lib/aarch64-linux-gnu");
|
||||
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[toolchain]
|
||||
# also update this in the readme, changelog, and github actions
|
||||
channel = "1.87"
|
||||
channel = "1.87.0"
|
||||
components = ["clippy"]
|
||||
targets = ["armv7-unknown-linux-gnueabihf"]
|
||||
|
|
|
|||
|
|
@ -120,6 +120,10 @@ const initialState: SettingsState = {
|
|||
"ssl://bitcoin.stackwallet.com:50002",
|
||||
"ssl://b.1209k.com:50002",
|
||||
"tcp://electrum.coinucopia.io:50001",
|
||||
"ssl://mainnet.foundationdevices.com:50002",
|
||||
"tcp://bitcoin.lu.ke:50001",
|
||||
"tcp://se-mma-crypto-payments-001.mullvad.net:50001",
|
||||
"ssl://electrum.coinfinity.co:50002",
|
||||
],
|
||||
[Blockchain.Monero]: [],
|
||||
},
|
||||
|
|
|
|||
43
swap-asb/Cargo.toml
Normal file
43
swap-asb/Cargo.toml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[package]
|
||||
name = "swap-asb"
|
||||
version = "3.0.0-beta.3"
|
||||
authors = ["The eigenwallet guys <hello@eigenwallet.org>", "The COMIT guys <hello@comit.network>"]
|
||||
edition = "2021"
|
||||
description = "ASB (Automated Swap Backend) binary for XMR/BTC atomic swaps."
|
||||
|
||||
[[bin]]
|
||||
name = "asb"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
anyhow = { workspace = true }
|
||||
bitcoin = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
|
||||
# Workspace dependencies
|
||||
monero-rpc-pool = { path = "../monero-rpc-pool" }
|
||||
monero-sys = { path = "../monero-sys" }
|
||||
swap = { path = "../swap" }
|
||||
swap-env = { path = "../swap-env" }
|
||||
swap-feed = { path = "../swap-feed" }
|
||||
swap-serde = { path = "../swap-serde" }
|
||||
|
||||
# Async
|
||||
tokio = { workspace = true, features = ["process", "fs", "net", "parking_lot", "rt"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
comfy-table = "7.1"
|
||||
libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] }
|
||||
rust_decimal = { workspace = true, features = ["serde-float"] }
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring"] }
|
||||
serde_json = { workspace = true }
|
||||
structopt = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
vergen = { workspace = true }
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
# This Dockerfile builds the asb binary
|
||||
|
||||
FROM ubuntu:24.04 AS builder
|
||||
# We need to build on Ubuntu because the monero-sys crate requires a bunch of system dependencies
|
||||
# We will try to use a smaller image here at some point
|
||||
#
|
||||
# Latest Ubuntu 24.04 image as of Tue, 05 Aug 2025 15:34:08 GMT
|
||||
FROM ubuntu:24.04@sha256:a08e551cb33850e4740772b38217fc1796a66da2506d312abe51acda354ff061 AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install dependencies
|
||||
# See .github/workflows/action.yml as well
|
||||
# See .github/workflows/action.yml
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
git \
|
||||
|
|
@ -36,8 +39,9 @@ RUN apt-get update && \
|
|||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Rust 1.85
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain 1.85.0
|
||||
# Install Rust 1.87.0
|
||||
# See ./rust-toolchain.toml for the Rust version
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain 1.87.0
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
COPY . .
|
||||
|
|
@ -53,12 +57,15 @@ WORKDIR /build/swap
|
|||
# Act as if we are in a GitHub Actions environment
|
||||
ENV DOCKER_BUILD=true
|
||||
|
||||
RUN cargo build -vv --bin=asb
|
||||
RUN cargo build --locked -vv -p swap-asb --bin=asb
|
||||
RUN cargo build --locked -vv -p swap-controller --bin=asb-controller
|
||||
|
||||
FROM ubuntu:24.04
|
||||
# Latest Ubuntu 24.04 image as of Tue, 05 Aug 2025 15:34:08 GMT
|
||||
FROM ubuntu:24.04@sha256:a08e551cb33850e4740772b38217fc1796a66da2506d312abe51acda354ff061 AS runner
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
COPY --from=builder /build/target/debug/asb /bin/asb
|
||||
COPY --from=builder /build/target/debug/asb-controller /bin/asb-controller
|
||||
|
||||
ENTRYPOINT ["asb"]
|
||||
9
swap-asb/build.rs
Normal file
9
swap-asb/build.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use anyhow::Result;
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
EmitBuilder::builder()
|
||||
.git_describe(true, true, None)
|
||||
.emit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
use crate::bitcoin::{bitcoin_address, Amount};
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bitcoin::address::NetworkUnchecked;
|
||||
use bitcoin::Address;
|
||||
use serde::Serialize;
|
||||
use std::ffi::OsString;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use structopt::StructOpt;
|
||||
use swap_env::config::GetDefaults;
|
||||
use swap::bitcoin::{bitcoin_address, Amount};
|
||||
use swap_env::defaults::GetDefaults;
|
||||
use swap_env::env;
|
||||
use swap_env::env::GetConfig;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -26,14 +28,27 @@ where
|
|||
let command: RawCommand = args.cmd;
|
||||
|
||||
let arguments = match command {
|
||||
RawCommand::Start { resume_only } => Arguments {
|
||||
RawCommand::Start {
|
||||
resume_only,
|
||||
rpc_bind_host,
|
||||
rpc_bind_port,
|
||||
} => {
|
||||
// Validate RPC bind arguments early
|
||||
validate_rpc_bind_args(&rpc_bind_host, &rpc_bind_port)?;
|
||||
|
||||
Arguments {
|
||||
testnet,
|
||||
json,
|
||||
trace,
|
||||
config_path: config_path(config, testnet)?,
|
||||
env_config: env_config(testnet),
|
||||
cmd: Command::Start { resume_only },
|
||||
cmd: Command::Start {
|
||||
resume_only,
|
||||
rpc_bind_host,
|
||||
rpc_bind_port,
|
||||
},
|
||||
}
|
||||
}
|
||||
RawCommand::History { only_unfinished } => Arguments {
|
||||
testnet,
|
||||
json,
|
||||
|
|
@ -202,6 +217,8 @@ pub struct Arguments {
|
|||
pub enum Command {
|
||||
Start {
|
||||
resume_only: bool,
|
||||
rpc_bind_host: Option<String>,
|
||||
rpc_bind_port: Option<u16>,
|
||||
},
|
||||
History {
|
||||
only_unfinished: bool,
|
||||
|
|
@ -258,13 +275,6 @@ pub struct RawArguments {
|
|||
#[structopt(long = "trace", help = "Also output verbose tracing logs to stdout")]
|
||||
pub trace: bool,
|
||||
|
||||
#[structopt(
|
||||
short,
|
||||
long = "disable-timestamp",
|
||||
help = "Disable timestamping of log messages"
|
||||
)]
|
||||
pub disable_timestamp: bool,
|
||||
|
||||
#[structopt(
|
||||
long = "config",
|
||||
help = "Provide a custom path to the configuration file. The configuration file must be a toml file.",
|
||||
|
|
@ -286,6 +296,16 @@ pub enum RawCommand {
|
|||
help = "For maintenance only. When set, no new swap requests will be accepted, but existing unfinished swaps will be resumed."
|
||||
)]
|
||||
resume_only: bool,
|
||||
#[structopt(
|
||||
long = "rpc-bind-host",
|
||||
help = "Host address to bind the JSON-RPC server to (e.g., 127.0.0.1). Must be used together with --rpc-bind-port."
|
||||
)]
|
||||
rpc_bind_host: Option<String>,
|
||||
#[structopt(
|
||||
long = "rpc-bind-port",
|
||||
help = "Port to bind the JSON-RPC server to (e.g., 9944). Must be used together with --rpc-bind-host."
|
||||
)]
|
||||
rpc_bind_port: Option<u16>,
|
||||
},
|
||||
#[structopt(about = "Prints all logging messages issued in the past.")]
|
||||
Logs {
|
||||
|
|
@ -390,6 +410,24 @@ pub struct RecoverCommandParams {
|
|||
pub swap_id: Uuid,
|
||||
}
|
||||
|
||||
fn validate_rpc_bind_args(host: &Option<String>, port: &Option<u16>) -> Result<()> {
|
||||
match (host, port) {
|
||||
(Some(host_str), Some(port_val)) => {
|
||||
let socket_addr = format!("{}:{}", host_str, port_val);
|
||||
SocketAddr::from_str(&socket_addr).context("Invalid socket address")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
(None, None) => Ok(()),
|
||||
(Some(_), None) => {
|
||||
bail!("--rpc-bind-host was provided but --rpc-bind-port was not. Both must be provided together or neither.");
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
bail!("--rpc-bind-port was provided but --rpc-bind-host was not. Both must be provided together or neither.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -414,7 +452,11 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
cmd: Command::Start { resume_only: false },
|
||||
cmd: Command::Start {
|
||||
resume_only: false,
|
||||
rpc_bind_host: None,
|
||||
rpc_bind_port: None,
|
||||
},
|
||||
};
|
||||
let args = parse_args(raw_ars).unwrap();
|
||||
assert_eq!(expected_args, args);
|
||||
|
|
@ -456,6 +498,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
|
||||
cmd: Command::Balance,
|
||||
};
|
||||
let args = parse_args(raw_ars).unwrap();
|
||||
|
|
@ -480,6 +523,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
|
||||
cmd: Command::WithdrawBtc {
|
||||
amount: None,
|
||||
address: bitcoin_address::parse_and_validate(BITCOIN_MAINNET_ADDRESS, false)
|
||||
|
|
@ -510,6 +554,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
|
||||
cmd: Command::Cancel {
|
||||
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
|
||||
},
|
||||
|
|
@ -538,6 +583,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
|
||||
cmd: Command::Refund {
|
||||
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
|
||||
},
|
||||
|
|
@ -566,6 +612,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
|
||||
cmd: Command::Punish {
|
||||
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
|
||||
},
|
||||
|
|
@ -594,6 +641,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
|
||||
cmd: Command::SafelyAbort {
|
||||
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
|
||||
},
|
||||
|
|
@ -616,7 +664,12 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
cmd: Command::Start { resume_only: false },
|
||||
|
||||
cmd: Command::Start {
|
||||
resume_only: false,
|
||||
rpc_bind_host: None,
|
||||
rpc_bind_port: None,
|
||||
},
|
||||
};
|
||||
let args = parse_args(raw_ars).unwrap();
|
||||
assert_eq!(expected_args, args);
|
||||
|
|
@ -636,6 +689,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
|
||||
cmd: Command::History {
|
||||
only_unfinished: false,
|
||||
},
|
||||
|
|
@ -658,6 +712,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
|
||||
cmd: Command::Balance,
|
||||
};
|
||||
let args = parse_args(raw_ars).unwrap();
|
||||
|
|
@ -678,6 +733,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
|
||||
cmd: Command::ExportMoneroWallet,
|
||||
};
|
||||
let args = parse_args(raw_ars).unwrap();
|
||||
|
|
@ -705,6 +761,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
|
||||
cmd: Command::WithdrawBtc {
|
||||
amount: None,
|
||||
address: bitcoin_address::parse_and_validate(BITCOIN_TESTNET_ADDRESS, true)
|
||||
|
|
@ -735,6 +792,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
|
||||
cmd: Command::Cancel {
|
||||
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
|
||||
},
|
||||
|
|
@ -764,6 +822,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
|
||||
cmd: Command::Refund {
|
||||
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
|
||||
},
|
||||
|
|
@ -793,6 +852,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
|
||||
cmd: Command::Punish {
|
||||
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
|
||||
},
|
||||
|
|
@ -822,6 +882,7 @@ mod tests {
|
|||
trace: false,
|
||||
config_path: default_testnet_conf_path,
|
||||
env_config: testnet_env_config,
|
||||
|
||||
cmd: Command::SafelyAbort {
|
||||
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
|
||||
},
|
||||
|
|
@ -830,26 +891,6 @@ mod tests {
|
|||
assert_eq!(expected_args, args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_disable_timestamp_mapping() {
|
||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||
.unwrap()
|
||||
.config_path;
|
||||
let mainnet_env_config = env::Mainnet::get_config();
|
||||
|
||||
let raw_ars = vec![BINARY_NAME, "--disable-timestamp", "start"];
|
||||
let expected_args = Arguments {
|
||||
testnet: false,
|
||||
json: false,
|
||||
trace: false,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
cmd: Command::Start { resume_only: false },
|
||||
};
|
||||
let args = parse_args(raw_ars).unwrap();
|
||||
assert_eq!(expected_args, args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_trace_mapping() {
|
||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||
|
|
@ -864,7 +905,12 @@ mod tests {
|
|||
trace: true,
|
||||
config_path: default_mainnet_conf_path,
|
||||
env_config: mainnet_env_config,
|
||||
cmd: Command::Start { resume_only: false },
|
||||
|
||||
cmd: Command::Start {
|
||||
resume_only: false,
|
||||
rpc_bind_host: None,
|
||||
rpc_bind_port: None,
|
||||
},
|
||||
};
|
||||
let args = parse_args(raw_ars).unwrap();
|
||||
assert_eq!(expected_args, args);
|
||||
|
|
@ -900,4 +946,26 @@ mod tests {
|
|||
"Bitcoin address network mismatch, expected `Testnet`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_bind_validation() {
|
||||
// Both None should be valid
|
||||
assert!(validate_rpc_bind_args(&None, &None).is_ok());
|
||||
|
||||
// Both Some should be valid with valid values
|
||||
assert!(validate_rpc_bind_args(&Some("127.0.0.1".to_string()), &Some(9944)).is_ok());
|
||||
assert!(validate_rpc_bind_args(&Some("0.0.0.0".to_string()), &Some(8080)).is_ok());
|
||||
assert!(validate_rpc_bind_args(&Some("localhost".to_string()), &Some(3000)).is_ok());
|
||||
|
||||
// One Some, one None should be invalid
|
||||
assert!(validate_rpc_bind_args(&Some("127.0.0.1".to_string()), &None).is_err());
|
||||
assert!(validate_rpc_bind_args(&None, &Some(9944)).is_err());
|
||||
|
||||
// Invalid host should be invalid
|
||||
assert!(validate_rpc_bind_args(&Some("invalid@host".to_string()), &Some(9944)).is_err());
|
||||
assert!(validate_rpc_bind_args(&Some("".to_string()), &Some(9944)).is_err());
|
||||
|
||||
// Port 0 should be invalid
|
||||
assert!(validate_rpc_bind_args(&Some("127.0.0.1".to_string()), &Some(0)).is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,9 @@ use std::env;
|
|||
use std::sync::Arc;
|
||||
use structopt::clap;
|
||||
use structopt::clap::ErrorKind;
|
||||
use swap::asb::command::{parse_args, Arguments, Command};
|
||||
mod command;
|
||||
use command::{parse_args, Arguments, Command};
|
||||
use swap::asb::rpc::RpcServer;
|
||||
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
|
||||
use swap::common::tor::{bootstrap_tor_client, create_tor_client};
|
||||
use swap::common::tracing_util::Format;
|
||||
|
|
@ -45,6 +47,25 @@ use uuid::Uuid;
|
|||
|
||||
const DEFAULT_WALLET_NAME: &str = "asb-wallet";
|
||||
|
||||
/// Initialize tracing with the specified configuration
|
||||
fn initialize_tracing(json: bool, config: &Config, trace: bool) -> Result<()> {
|
||||
let format = if json { Format::Json } else { Format::Raw };
|
||||
let log_dir = config.data.dir.join("logs");
|
||||
|
||||
common::tracing_util::init(LevelFilter::DEBUG, format, log_dir, None, trace)
|
||||
.expect("initialize tracing");
|
||||
|
||||
tracing::info!(
|
||||
binary = "asb",
|
||||
version = env!("VERGEN_GIT_DESCRIBE"),
|
||||
os = std::env::consts::OS,
|
||||
arch = std::env::consts::ARCH,
|
||||
"Setting up context"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
trait IntoDaemon {
|
||||
fn into_daemon(self) -> Result<Daemon>;
|
||||
}
|
||||
|
|
@ -61,9 +82,11 @@ impl IntoDaemon for url::Url {
|
|||
impl IntoDaemon for monero_rpc_pool::ServerInfo {
|
||||
fn into_daemon(self) -> Result<Daemon> {
|
||||
let address = format!("http://{}:{}", self.host, self.port);
|
||||
let ssl = false; // Pool server always uses HTTP locally
|
||||
|
||||
Ok(Daemon { address, ssl })
|
||||
Ok(Daemon {
|
||||
address,
|
||||
ssl: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +106,6 @@ pub async fn main() -> Result<()> {
|
|||
} = match parse_args(env::args_os()) {
|
||||
Ok(args) => args,
|
||||
Err(e) => {
|
||||
// make sure to display the clap error message it exists
|
||||
if let Some(clap_err) = e.downcast_ref::<clap::Error>() {
|
||||
if let ErrorKind::HelpDisplayed | ErrorKind::VersionDisplayed = clap_err.kind {
|
||||
println!("{}", clap_err.message);
|
||||
|
|
@ -97,7 +119,7 @@ pub async fn main() -> Result<()> {
|
|||
// Check in the background if there's a new version available
|
||||
tokio::spawn(async move { warn_if_outdated(env!("CARGO_PKG_VERSION")).await });
|
||||
|
||||
// Read config from the specified path
|
||||
// Read our config
|
||||
let config = match read_config(config_path.clone())? {
|
||||
Ok(config) => config,
|
||||
Err(ConfigNotInitialized {}) => {
|
||||
|
|
@ -107,17 +129,7 @@ pub async fn main() -> Result<()> {
|
|||
};
|
||||
|
||||
// Initialize tracing
|
||||
let format = if json { Format::Json } else { Format::Raw };
|
||||
let log_dir = config.data.dir.join("logs");
|
||||
common::tracing_util::init(LevelFilter::DEBUG, format, log_dir, None, trace)
|
||||
.expect("initialize tracing");
|
||||
tracing::info!(
|
||||
binary = "asb",
|
||||
version = env!("VERGEN_GIT_DESCRIBE"),
|
||||
os = std::env::consts::OS,
|
||||
arch = std::env::consts::ARCH,
|
||||
"Setting up context"
|
||||
);
|
||||
initialize_tracing(json, &config, trace)?;
|
||||
|
||||
// Check for conflicting env / config values
|
||||
if config.monero.network != env_config.monero_network {
|
||||
|
|
@ -140,7 +152,11 @@ pub async fn main() -> Result<()> {
|
|||
let db_file = config.data.dir.join("sqlite");
|
||||
|
||||
match cmd {
|
||||
Command::Start { resume_only } => {
|
||||
Command::Start {
|
||||
resume_only,
|
||||
rpc_bind_host,
|
||||
rpc_bind_port,
|
||||
} => {
|
||||
let db = open_db(db_file, AccessMode::ReadWrite, None).await?;
|
||||
|
||||
// check and warn for duplicate rendezvous points
|
||||
|
|
@ -243,16 +259,17 @@ pub async fn main() -> Result<()> {
|
|||
|
||||
tracing::info!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
||||
|
||||
for external_address in config.network.external_addresses {
|
||||
swarm.add_external_address(external_address);
|
||||
for external_address in &config.network.external_addresses {
|
||||
swarm.add_external_address(external_address.clone());
|
||||
}
|
||||
|
||||
let (event_loop, mut swap_receiver) = EventLoop::new(
|
||||
let bitcoin_wallet = Arc::new(bitcoin_wallet);
|
||||
let (event_loop, mut swap_receiver, event_loop_service) = EventLoop::new(
|
||||
swarm,
|
||||
env_config,
|
||||
Arc::new(bitcoin_wallet),
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet.clone(),
|
||||
db,
|
||||
db.clone(),
|
||||
kraken_rate.clone(),
|
||||
config.maker.min_buy_btc,
|
||||
config.maker.max_buy_btc,
|
||||
|
|
@ -260,6 +277,23 @@ pub async fn main() -> Result<()> {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
// Start RPC server conditionally
|
||||
let _rpc_server = if let (Some(host), Some(port)) = (rpc_bind_host, rpc_bind_port) {
|
||||
let rpc_server = RpcServer::start(
|
||||
host,
|
||||
port,
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet.clone(),
|
||||
event_loop_service,
|
||||
db,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Some(rpc_server.spawn())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(swap) = swap_receiver.recv().await {
|
||||
let rate = kraken_rate.clone();
|
||||
|
|
@ -452,6 +486,7 @@ async fn init_bitcoin_wallet(
|
|||
sync: bool,
|
||||
) -> Result<bitcoin::Wallet> {
|
||||
tracing::debug!("Opening Bitcoin wallet");
|
||||
|
||||
let wallet = bitcoin::wallet::WalletBuilder::default()
|
||||
.seed(seed.clone())
|
||||
.network(env_config.bitcoin_network)
|
||||
|
|
@ -491,10 +526,17 @@ async fn init_monero_wallet(
|
|||
) -> Result<Arc<monero::Wallets>> {
|
||||
tracing::debug!("Initializing Monero wallets");
|
||||
|
||||
let daemon = if config.monero.monero_node_pool {
|
||||
// Start the monero-rpc-pool and use it
|
||||
tracing::info!("Starting Monero RPC Pool for ASB");
|
||||
let daemon = match &config.monero.daemon_url {
|
||||
// If a daemon URL is provided, use it
|
||||
Some(url) => {
|
||||
tracing::info!("Using direct Monero daemon connection: {url}");
|
||||
|
||||
url.clone()
|
||||
.into_daemon()
|
||||
.context("Failed to convert daemon URL to Daemon")?
|
||||
}
|
||||
// If no daemon URL is provided, start the monero-rpc-pool and use it
|
||||
None => {
|
||||
let (server_info, _status_receiver, _pool_handle) =
|
||||
monero_rpc_pool::start_server_with_random_port(
|
||||
monero_rpc_pool::config::Config::new_random_port(
|
||||
|
|
@ -511,18 +553,7 @@ async fn init_monero_wallet(
|
|||
server_info
|
||||
.into_daemon()
|
||||
.context("Failed to convert ServerInfo to Daemon")?
|
||||
} else {
|
||||
tracing::info!(
|
||||
"Using direct Monero daemon connection: {}",
|
||||
config.monero.daemon_url
|
||||
);
|
||||
|
||||
config
|
||||
.monero
|
||||
.daemon_url
|
||||
.clone()
|
||||
.into_daemon()
|
||||
.context("Failed to convert daemon URL to Daemon")?
|
||||
}
|
||||
};
|
||||
|
||||
let manager = monero::Wallets::new(
|
||||
16
swap-controller-api/Cargo.toml
Normal file
16
swap-controller-api/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "swap-controller-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bitcoin = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = ["macros", "server", "client-core", "http-client"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
53
swap-controller-api/src/lib.rs
Normal file
53
swap-controller-api/src/lib.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use jsonrpsee::proc_macros::rpc;
|
||||
use jsonrpsee::types::ErrorObjectOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BitcoinBalanceResponse {
|
||||
#[serde(with = "bitcoin::amount::serde::as_sat")]
|
||||
pub balance: bitcoin::Amount,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct MoneroBalanceResponse {
|
||||
pub balance: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct MoneroAddressResponse {
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct MultiaddressesResponse {
|
||||
pub multiaddresses: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ActiveConnectionsResponse {
|
||||
pub connections: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Swap {
|
||||
pub id: String,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
#[rpc(client, server)]
|
||||
pub trait AsbApi {
|
||||
#[method(name = "check_connection")]
|
||||
async fn check_connection(&self) -> Result<(), ErrorObjectOwned>;
|
||||
#[method(name = "bitcoin_balance")]
|
||||
async fn bitcoin_balance(&self) -> Result<BitcoinBalanceResponse, ErrorObjectOwned>;
|
||||
#[method(name = "monero_balance")]
|
||||
async fn monero_balance(&self) -> Result<MoneroBalanceResponse, ErrorObjectOwned>;
|
||||
#[method(name = "monero_address")]
|
||||
async fn monero_address(&self) -> Result<MoneroAddressResponse, ErrorObjectOwned>;
|
||||
#[method(name = "multiaddresses")]
|
||||
async fn multiaddresses(&self) -> Result<MultiaddressesResponse, ErrorObjectOwned>;
|
||||
#[method(name = "active_connections")]
|
||||
async fn active_connections(&self) -> Result<ActiveConnectionsResponse, ErrorObjectOwned>;
|
||||
#[method(name = "get_swaps")]
|
||||
async fn get_swaps(&self) -> Result<Vec<Swap>, ErrorObjectOwned>;
|
||||
}
|
||||
22
swap-controller/Cargo.toml
Normal file
22
swap-controller/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "swap-controller"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "asb-controller"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
jsonrpsee = { workspace = true, features = ["client-core", "http-client"] }
|
||||
monero = { workspace = true }
|
||||
rustyline = "17.0.0"
|
||||
serde_json = { workspace = true }
|
||||
shell-words = "1.1"
|
||||
swap-controller-api = { path = "../swap-controller-api" }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
17
swap-controller/Dockerfile
Normal file
17
swap-controller/Dockerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Latest Rust 1.87.0 image as of Tue, 05 Aug 2025 15:34:08 GMT
|
||||
# This needs to be built from the root of the Cargo workspace (root of git repo)
|
||||
FROM rust:1.87.0-bookworm@sha256:251cec8da4689d180f124ef00024c2f83f79d9bf984e43c180a598119e326b84 AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy the entire Cargo workspace
|
||||
COPY . .
|
||||
|
||||
RUN cargo build --locked --bin asb-controller --release
|
||||
|
||||
# Latest Debian Bookworm image as of Tue, 05 Aug 2025 15:34:08 GMT
|
||||
FROM debian:bookworm@sha256:b6507e340c43553136f5078284c8c68d86ec8262b1724dde73c325e8d3dcdeba AS runner
|
||||
|
||||
COPY --from=builder /build/target/release/asb-controller /usr/local/bin/asb-controller
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/asb-controller"]
|
||||
32
swap-controller/src/cli.rs
Normal file
32
swap-controller/src/cli.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "asb-controller")]
|
||||
#[command(about = "Control tool for ASB daemon")]
|
||||
pub struct Cli {
|
||||
/// RPC server URL
|
||||
#[arg(long, default_value = "http://127.0.0.1:9944")]
|
||||
pub url: String,
|
||||
|
||||
/// Command to execute (defaults to interactive shell if omitted)
|
||||
#[command(subcommand)]
|
||||
pub cmd: Option<Cmd>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone)]
|
||||
pub enum Cmd {
|
||||
/// Check connection to ASB server
|
||||
CheckConnection,
|
||||
/// Get Bitcoin balance
|
||||
BitcoinBalance,
|
||||
/// Get Monero balance
|
||||
MoneroBalance,
|
||||
/// Get Monero wallet address
|
||||
MoneroAddress,
|
||||
/// Get external multiaddresses
|
||||
Multiaddresses,
|
||||
/// Get active connection count
|
||||
ActiveConnections,
|
||||
/// Get list of swaps
|
||||
GetSwaps,
|
||||
}
|
||||
72
swap-controller/src/main.rs
Normal file
72
swap-controller/src/main.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
mod cli;
|
||||
mod repl;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Cmd};
|
||||
use swap_controller_api::AsbApiClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let client = jsonrpsee::http_client::HttpClientBuilder::default().build(&cli.url)?;
|
||||
|
||||
match cli.cmd {
|
||||
None => repl::run(client, dispatch).await?,
|
||||
Some(cmd) => {
|
||||
if let Err(e) = dispatch(cmd.clone(), client.clone()).await {
|
||||
eprintln!("Command failed with error: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
Cmd::CheckConnection => {
|
||||
client.check_connection().await?;
|
||||
println!("Connected");
|
||||
}
|
||||
Cmd::BitcoinBalance => {
|
||||
let response = client.bitcoin_balance().await?;
|
||||
println!("Current Bitcoin balance is {} BTC", response.balance);
|
||||
}
|
||||
Cmd::MoneroBalance => {
|
||||
let response = client.monero_balance().await?;
|
||||
let amount = monero::Amount::from_pico(response.balance);
|
||||
|
||||
println!("Current Monero balance is {:.12} XMR", amount.as_xmr());
|
||||
}
|
||||
Cmd::MoneroAddress => {
|
||||
let response = client.monero_address().await?;
|
||||
println!("The primary Monero address is {}", response.address);
|
||||
}
|
||||
Cmd::Multiaddresses => {
|
||||
let response = client.multiaddresses().await?;
|
||||
if response.multiaddresses.is_empty() {
|
||||
println!("No external multiaddresses configured");
|
||||
} else {
|
||||
for addr in response.multiaddresses {
|
||||
println!("{}", addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::ActiveConnections => {
|
||||
let response = client.active_connections().await?;
|
||||
println!("Connected to {} peers", response.connections);
|
||||
}
|
||||
Cmd::GetSwaps => {
|
||||
let swaps = client.get_swaps().await?;
|
||||
if swaps.is_empty() {
|
||||
println!("No swaps found");
|
||||
} else {
|
||||
for swap in swaps {
|
||||
println!("{}: {}", swap.id, swap.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
181
swap-controller/src/repl/mod.rs
Normal file
181
swap-controller/src/repl/mod.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
mod parse;
|
||||
|
||||
use crate::cli::Cmd;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use rustyline::completion::{Completer, Pair};
|
||||
use rustyline::highlight::Highlighter;
|
||||
use rustyline::hint::Hinter;
|
||||
use rustyline::validate::Validator;
|
||||
use rustyline::{error::ReadlineError, Editor};
|
||||
use rustyline::{Context, Helper};
|
||||
use std::future::Future;
|
||||
use swap_controller_api::AsbApiClient;
|
||||
|
||||
struct CommandCompleter {
|
||||
commands: Vec<String>,
|
||||
}
|
||||
|
||||
impl CommandCompleter {
|
||||
fn new() -> Self {
|
||||
let mut commands = Vec::new();
|
||||
|
||||
// Extract command names from the Cmd enum using clap
|
||||
#[derive(Parser)]
|
||||
#[command(name = "")]
|
||||
#[command(about = "")]
|
||||
#[command(no_binary_name = true)]
|
||||
struct TempReplCommand {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
let app = TempReplCommand::command();
|
||||
for subcmd in app.get_subcommands() {
|
||||
commands.push(subcmd.get_name().to_string());
|
||||
// Also add aliases if any
|
||||
for alias in subcmd.get_all_aliases() {
|
||||
commands.push(alias.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Add shell-specific commands
|
||||
commands.extend_from_slice(&[
|
||||
"help".to_string(),
|
||||
"quit".to_string(),
|
||||
"exit".to_string(),
|
||||
":q".to_string(),
|
||||
]);
|
||||
|
||||
commands.sort();
|
||||
|
||||
Self { commands }
|
||||
}
|
||||
}
|
||||
|
||||
impl Helper for CommandCompleter {}
|
||||
|
||||
impl Completer for CommandCompleter {
|
||||
type Candidate = Pair;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_ctx: &Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
let line = &line[..pos];
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
|
||||
if words.is_empty() || (words.len() == 1 && !line.ends_with(' ')) {
|
||||
// Complete command names
|
||||
let start_pos = line.rfind(' ').map(|i| i + 1).unwrap_or(0);
|
||||
let prefix = &line[start_pos..];
|
||||
|
||||
let matches: Vec<Pair> = self
|
||||
.commands
|
||||
.iter()
|
||||
.filter(|cmd| cmd.starts_with(prefix))
|
||||
.map(|cmd| Pair {
|
||||
display: cmd.clone(),
|
||||
replacement: cmd.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((start_pos, matches))
|
||||
} else {
|
||||
// No completion for command arguments yet
|
||||
Ok((pos, Vec::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hinter for CommandCompleter {
|
||||
type Hint = String;
|
||||
}
|
||||
|
||||
impl Highlighter for CommandCompleter {}
|
||||
|
||||
impl Validator for CommandCompleter {}
|
||||
|
||||
pub async fn run<C, F, Fut>(client: C, dispatch: F) -> anyhow::Result<()>
|
||||
where
|
||||
C: AsbApiClient + Clone + Send + 'static,
|
||||
F: Fn(Cmd, C) -> Fut + Clone + 'static,
|
||||
Fut: Future<Output = anyhow::Result<()>>,
|
||||
{
|
||||
let completer = CommandCompleter::new();
|
||||
let mut rl = Editor::new()?;
|
||||
rl.set_helper(Some(completer));
|
||||
|
||||
println!("ASB Control Shell - Type 'help' for commands, 'quit' to exit\n");
|
||||
|
||||
loop {
|
||||
let readline = rl.readline("asb> ");
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
rl.add_history_entry(line.as_str())?;
|
||||
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match line {
|
||||
"quit" | "exit" | ":q" => {
|
||||
println!("Goodbye!");
|
||||
break;
|
||||
}
|
||||
"help" => {
|
||||
print_help();
|
||||
}
|
||||
_ => {
|
||||
if let Some(cmd) = parse::parse_line(line) {
|
||||
if let Err(e) = dispatch(cmd, client.clone()).await {
|
||||
eprintln!("Command failed with error: {e:?}");
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"Unknown command: {}. Type 'help' for available commands.",
|
||||
line
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
println!("^C");
|
||||
continue;
|
||||
}
|
||||
Err(ReadlineError::Eof) => {
|
||||
println!("^D");
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
use crate::cli::Cmd;
|
||||
use clap::{CommandFactory, Parser};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "")]
|
||||
#[command(about = "")]
|
||||
#[command(no_binary_name = true)]
|
||||
struct ReplCommand {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
println!("Available commands:");
|
||||
println!("{}", ReplCommand::command().render_help());
|
||||
println!("\nAdditional shell commands:");
|
||||
println!(" help Show this help message");
|
||||
println!(" quit, exit, :q Exit the shell");
|
||||
}
|
||||
33
swap-controller/src/repl/parse.rs
Normal file
33
swap-controller/src/repl/parse.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use crate::cli::Cmd;
|
||||
use clap::Parser;
|
||||
|
||||
/// A wrapper for parsing REPL commands using clap
|
||||
#[derive(Parser)]
|
||||
#[command(name = "")]
|
||||
#[command(about = "")]
|
||||
#[command(no_binary_name = true)]
|
||||
struct ReplCommand {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
/// Parse a line from the REPL into a command using clap's parser
|
||||
pub fn parse_line(line: &str) -> Option<Cmd> {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Split the line into arguments, preserving quoted strings
|
||||
let args = shell_words::split(line).ok()?;
|
||||
|
||||
// Try to parse using clap
|
||||
match ReplCommand::try_parse_from(args) {
|
||||
Ok(repl_cmd) => Some(repl_cmd.cmd),
|
||||
Err(err) => {
|
||||
// Print clap's error message (it's quite good)
|
||||
eprintln!("{}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ edition = "2024"
|
|||
anyhow = { workspace = true }
|
||||
bitcoin = { workspace = true }
|
||||
config = { version = "0.14", default-features = false, features = ["toml"] }
|
||||
dialoguer = "0.11"
|
||||
dialoguer = { workspace = true }
|
||||
libp2p = { workspace = true, features = ["serde"] }
|
||||
monero = { workspace = true }
|
||||
rust_decimal = { workspace = true }
|
||||
|
|
@ -16,7 +16,7 @@ swap-fs = { path = "../swap-fs" }
|
|||
swap-serde = { path = "../swap-serde" }
|
||||
thiserror = { workspace = true }
|
||||
time = "0.3"
|
||||
toml = "0.8"
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,85 +1,17 @@
|
|||
use crate::defaults::GetDefaults;
|
||||
use crate::env::{Mainnet, Testnet};
|
||||
use crate::prompt;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use config::ConfigError;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use dialoguer::{Input, Select};
|
||||
use libp2p::core::Multiaddr;
|
||||
use rust_decimal::prelude::FromPrimitive;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use swap_fs::{ensure_directory_exists, system_config_dir, system_data_dir};
|
||||
use swap_fs::ensure_directory_exists;
|
||||
use url::Url;
|
||||
|
||||
pub trait GetDefaults {
|
||||
fn get_config_file_defaults() -> Result<Defaults>;
|
||||
}
|
||||
|
||||
pub struct Defaults {
|
||||
pub config_path: PathBuf,
|
||||
data_dir: PathBuf,
|
||||
listen_address_tcp: Multiaddr,
|
||||
electrum_rpc_url: Url,
|
||||
monero_daemon_address: Url,
|
||||
price_ticker_ws_url: Url,
|
||||
bitcoin_confirmation_target: u16,
|
||||
}
|
||||
|
||||
impl GetDefaults for Testnet {
|
||||
fn get_config_file_defaults() -> Result<Defaults> {
|
||||
let defaults = Defaults {
|
||||
config_path: default_asb_config_dir()?
|
||||
.join("testnet")
|
||||
.join("config.toml"),
|
||||
data_dir: default_asb_data_dir()?.join("testnet"),
|
||||
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
|
||||
electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:60002")?,
|
||||
monero_daemon_address: Url::parse("http://node.sethforprivacy.com:38089")?,
|
||||
price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?,
|
||||
bitcoin_confirmation_target: 1,
|
||||
};
|
||||
|
||||
Ok(defaults)
|
||||
}
|
||||
}
|
||||
|
||||
impl GetDefaults for Mainnet {
|
||||
fn get_config_file_defaults() -> Result<Defaults> {
|
||||
let defaults = Defaults {
|
||||
config_path: default_asb_config_dir()?
|
||||
.join("mainnet")
|
||||
.join("config.toml"),
|
||||
data_dir: default_asb_data_dir()?.join("mainnet"),
|
||||
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
|
||||
electrum_rpc_url: Url::parse("ssl://blockstream.info:700")?,
|
||||
monero_daemon_address: Url::parse("nthpyro.dev:18089")?,
|
||||
price_ticker_ws_url: Url::parse("wss://ws.kraken.com")?,
|
||||
bitcoin_confirmation_target: 3,
|
||||
};
|
||||
|
||||
Ok(defaults)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_asb_config_dir() -> Result<PathBuf> {
|
||||
system_config_dir()
|
||||
.map(|dir| Path::join(&dir, "asb"))
|
||||
.context("Could not generate default config file path")
|
||||
}
|
||||
|
||||
fn default_asb_data_dir() -> Result<PathBuf> {
|
||||
system_data_dir()
|
||||
.map(|dir| Path::join(&dir, "asb"))
|
||||
.context("Could not generate default config file path")
|
||||
}
|
||||
|
||||
const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64;
|
||||
const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
|
||||
const DEFAULT_SPREAD: f64 = 0.02f64;
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
|
|
@ -91,34 +23,6 @@ pub struct Config {
|
|||
pub maker: Maker,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn read<D>(config_file: D) -> Result<Self, ConfigError>
|
||||
where
|
||||
D: AsRef<OsStr>,
|
||||
{
|
||||
let config_file = Path::new(&config_file);
|
||||
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::from(config_file))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("ASB")
|
||||
.separator("__")
|
||||
.list_separator(","),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
config.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<config::Config> for Config {
|
||||
type Error = config::ConfigError;
|
||||
|
||||
fn try_from(value: config::Config) -> Result<Self, Self::Error> {
|
||||
value.try_deserialize()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Data {
|
||||
|
|
@ -162,16 +66,12 @@ fn default_use_mempool_space_fee_estimation() -> bool {
|
|||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Monero {
|
||||
pub daemon_url: Url,
|
||||
/// If None, we will use the Monero Remote Node Pool with built in defaults
|
||||
#[serde(default)]
|
||||
pub daemon_url: Option<Url>,
|
||||
pub finality_confirmations: Option<u64>,
|
||||
#[serde(with = "swap_serde::monero::network")]
|
||||
pub network: monero::Network,
|
||||
#[serde(default = "default_monero_node_pool")]
|
||||
pub monero_node_pool: bool,
|
||||
}
|
||||
|
||||
fn default_monero_node_pool() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
|
|
@ -181,6 +81,15 @@ pub struct TorConf {
|
|||
pub hidden_service_num_intro_points: u8,
|
||||
}
|
||||
|
||||
impl Default for TorConf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
register_hidden_service: true,
|
||||
hidden_service_num_intro_points: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Maker {
|
||||
|
|
@ -194,13 +103,32 @@ pub struct Maker {
|
|||
pub external_bitcoin_redeem_address: Option<bitcoin::Address>,
|
||||
}
|
||||
|
||||
impl Default for TorConf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
register_hidden_service: true,
|
||||
hidden_service_num_intro_points: 5,
|
||||
impl Config {
|
||||
pub fn read<D>(config_file: D) -> Result<Self, ConfigError>
|
||||
where
|
||||
D: AsRef<OsStr>,
|
||||
{
|
||||
let config_file = Path::new(&config_file);
|
||||
|
||||
let config = config::Config::builder()
|
||||
.add_source(config::File::from(config_file))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("ASB")
|
||||
.separator("__")
|
||||
.list_separator(","),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
config.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<config::Config> for Config {
|
||||
type Error = config::ConfigError;
|
||||
|
||||
fn try_from(value: config::Config) -> Result<Self, Self::Error> {
|
||||
value.try_deserialize()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, Copy)]
|
||||
|
|
@ -236,145 +164,26 @@ pub fn initial_setup(config_path: PathBuf, config: Config) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
||||
let (bitcoin_network, monero_network, defaults) = if testnet {
|
||||
tracing::info!("Running initial setup for testnet");
|
||||
|
||||
let bitcoin_network = bitcoin::Network::Testnet;
|
||||
let monero_network = monero::Network::Stagenet;
|
||||
let defaults = Testnet::get_config_file_defaults()?;
|
||||
|
||||
(bitcoin_network, monero_network, defaults)
|
||||
} else {
|
||||
tracing::info!("Running initial setup for mainnet");
|
||||
let bitcoin_network = bitcoin::Network::Bitcoin;
|
||||
let monero_network = monero::Network::Mainnet;
|
||||
let defaults = Mainnet::get_config_file_defaults()?;
|
||||
|
||||
(bitcoin_network, monero_network, defaults)
|
||||
pub fn query_user_for_initial_config_with_network(
|
||||
bitcoin_network: bitcoin::Network,
|
||||
monero_network: monero::Network,
|
||||
) -> Result<Config> {
|
||||
let defaults = match bitcoin_network {
|
||||
bitcoin::Network::Bitcoin => Mainnet::get_config_file_defaults()?,
|
||||
bitcoin::Network::Testnet => Testnet::get_config_file_defaults()?,
|
||||
_ => bail!("Unsupported bitcoin network"),
|
||||
};
|
||||
|
||||
println!();
|
||||
let data_dir = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter data directory for asb or hit return to use default")
|
||||
.default(
|
||||
defaults
|
||||
.data_dir
|
||||
.to_str()
|
||||
.context("Unsupported characters in default path")?
|
||||
.to_string(),
|
||||
)
|
||||
.interact_text()?;
|
||||
let data_dir: PathBuf = data_dir.as_str().parse()?;
|
||||
|
||||
let target_block = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("How fast should your Bitcoin transactions be confirmed? Your transaction fee will be calculated based on this target. Hit return to use default")
|
||||
.default(defaults.bitcoin_confirmation_target)
|
||||
.interact_text()?;
|
||||
|
||||
let listen_addresses = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default")
|
||||
.default( format!("{}", defaults.listen_address_tcp))
|
||||
.interact_text()?;
|
||||
let listen_addresses = listen_addresses
|
||||
.split(',')
|
||||
.map(|str| str.parse())
|
||||
.collect::<Result<Vec<Multiaddr>, _>>()?;
|
||||
|
||||
let mut electrum_rpc_urls = Vec::new();
|
||||
let mut electrum_number = 1;
|
||||
let mut electrum_done = false;
|
||||
|
||||
println!(
|
||||
"You can configure multiple Electrum servers for redundancy. At least one is required."
|
||||
);
|
||||
|
||||
// Ask for the first electrum URL with a default
|
||||
let electrum_rpc_url = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter first Electrum RPC URL or hit return to use default")
|
||||
.default(defaults.electrum_rpc_url)
|
||||
.interact_text()?;
|
||||
electrum_rpc_urls.push(electrum_rpc_url);
|
||||
electrum_number += 1;
|
||||
|
||||
// Ask for additional electrum URLs
|
||||
while !electrum_done {
|
||||
let prompt = format!(
|
||||
"Enter additional Electrum RPC URL ({electrum_number}). Or just hit Enter to continue."
|
||||
);
|
||||
let electrum_url = Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
|
||||
if electrum_url.is_empty() {
|
||||
electrum_done = true;
|
||||
} else if electrum_rpc_urls
|
||||
.iter()
|
||||
.any(|url| url.to_string() == electrum_url)
|
||||
{
|
||||
println!("That Electrum URL is already in the list.");
|
||||
} else {
|
||||
let electrum_url = Url::parse(&electrum_url).context("Invalid Electrum URL")?;
|
||||
electrum_rpc_urls.push(electrum_url);
|
||||
electrum_number += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let monero_daemon_url = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter Monero daemon url or hit enter to use default")
|
||||
.default(defaults.monero_daemon_address)
|
||||
.interact_text()?;
|
||||
|
||||
let register_hidden_service = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Do you want a Tor hidden service to be created? This will allow you to run from behind a firewall without opening ports, and hide your IP address. You do not have to run a Tor daemon yourself. We recommend this for most users. (y/n)")
|
||||
.items(&["yes", "no"])
|
||||
.default(0)
|
||||
.interact()?
|
||||
== 0;
|
||||
|
||||
let min_buy = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter minimum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
|
||||
.default(DEFAULT_MIN_BUY_AMOUNT)
|
||||
.interact_text()?;
|
||||
let min_buy = bitcoin::Amount::from_btc(min_buy)?;
|
||||
|
||||
let max_buy = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter maximum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
|
||||
.default(DEFAULT_MAX_BUY_AMOUNT)
|
||||
.interact_text()?;
|
||||
let max_buy = bitcoin::Amount::from_btc(max_buy)?;
|
||||
|
||||
let ask_spread = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter spread (in percent; value between 0.x and 1.0) to be used on top of the market rate or hit enter to use default.")
|
||||
.default(DEFAULT_SPREAD)
|
||||
.interact_text()?;
|
||||
if !(0.0..=1.0).contains(&ask_spread) {
|
||||
bail!(format!("Invalid spread {}. For the spread value floating point number in interval [0..1] are allowed.", ask_spread))
|
||||
}
|
||||
let ask_spread = Decimal::from_f64(ask_spread).context("Unable to parse spread")?;
|
||||
|
||||
let mut number = 1;
|
||||
let mut done = false;
|
||||
let mut rendezvous_points = Vec::new();
|
||||
println!("ASB can register with multiple rendezvous nodes for discoverability. This can also be edited in the config file later.");
|
||||
while !done {
|
||||
let prompt = format!(
|
||||
"Enter the address for rendezvous node ({number}). Or just hit Enter to continue."
|
||||
);
|
||||
let rendezvous_addr = Input::<Multiaddr>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
if rendezvous_addr.is_empty() {
|
||||
done = true;
|
||||
} else if rendezvous_points.contains(&rendezvous_addr) {
|
||||
println!("That rendezvous address is already in the list.");
|
||||
} else {
|
||||
rendezvous_points.push(rendezvous_addr);
|
||||
number += 1;
|
||||
}
|
||||
}
|
||||
let data_dir = prompt::data_directory(&defaults.data_dir)?;
|
||||
let target_block = prompt::bitcoin_confirmation_target(defaults.bitcoin_confirmation_target)?;
|
||||
let listen_addresses = prompt::listen_addresses(&defaults.listen_address_tcp)?;
|
||||
let electrum_rpc_urls = prompt::electrum_rpc_urls(&defaults.electrum_rpc_urls)?;
|
||||
let monero_daemon_url = prompt::monero_daemon_url()?;
|
||||
let register_hidden_service = prompt::tor_hidden_service()?;
|
||||
let min_buy = prompt::min_buy_amount()?;
|
||||
let max_buy = prompt::max_buy_amount()?;
|
||||
let ask_spread = prompt::ask_spread()?;
|
||||
let rendezvous_points = prompt::rendezvous_points()?;
|
||||
|
||||
println!();
|
||||
|
||||
|
|
@ -382,7 +191,7 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
|||
data: Data { dir: data_dir },
|
||||
network: Network {
|
||||
listen: listen_addresses,
|
||||
rendezvous_point: rendezvous_points, // keeping the singular key name for backcompat
|
||||
rendezvous_point: rendezvous_points,
|
||||
external_addresses: vec![],
|
||||
},
|
||||
bitcoin: Bitcoin {
|
||||
|
|
@ -396,7 +205,6 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
|||
daemon_url: monero_daemon_url,
|
||||
finality_confirmations: None,
|
||||
network: monero_network,
|
||||
monero_node_pool: false,
|
||||
},
|
||||
tor: TorConf {
|
||||
register_hidden_service,
|
||||
|
|
@ -412,161 +220,16 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serial_test::serial;
|
||||
use tempfile::tempdir;
|
||||
|
||||
// these tests are run serially since env vars affect the whole process
|
||||
#[test]
|
||||
#[serial]
|
||||
fn config_roundtrip_testnet() {
|
||||
let temp_dir = tempdir().unwrap().path().to_path_buf();
|
||||
let config_path = Path::join(&temp_dir, "config.toml");
|
||||
|
||||
let defaults = Testnet::get_config_file_defaults().unwrap();
|
||||
|
||||
let expected = Config {
|
||||
data: Data {
|
||||
dir: Default::default(),
|
||||
},
|
||||
bitcoin: Bitcoin {
|
||||
electrum_rpc_urls: vec![defaults.electrum_rpc_url],
|
||||
target_block: defaults.bitcoin_confirmation_target,
|
||||
finality_confirmations: None,
|
||||
network: bitcoin::Network::Testnet,
|
||||
use_mempool_space_fee_estimation: true,
|
||||
},
|
||||
network: Network {
|
||||
listen: vec![defaults.listen_address_tcp],
|
||||
rendezvous_point: vec![],
|
||||
external_addresses: vec![],
|
||||
},
|
||||
monero: Monero {
|
||||
daemon_url: defaults.monero_daemon_address,
|
||||
finality_confirmations: None,
|
||||
network: monero::Network::Stagenet,
|
||||
monero_node_pool: false,
|
||||
},
|
||||
tor: Default::default(),
|
||||
maker: Maker {
|
||||
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
|
||||
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
|
||||
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
|
||||
price_ticker_ws_url: defaults.price_ticker_ws_url,
|
||||
external_bitcoin_redeem_address: None,
|
||||
},
|
||||
pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
||||
let (bitcoin_network, monero_network) = if testnet {
|
||||
let bitcoin_network = bitcoin::Network::Testnet;
|
||||
let monero_network = monero::Network::Stagenet;
|
||||
(bitcoin_network, monero_network)
|
||||
} else {
|
||||
let bitcoin_network = bitcoin::Network::Bitcoin;
|
||||
let monero_network = monero::Network::Mainnet;
|
||||
(bitcoin_network, monero_network)
|
||||
};
|
||||
|
||||
initial_setup(config_path.clone(), expected.clone()).unwrap();
|
||||
let actual = read_config(config_path).unwrap().unwrap();
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn config_roundtrip_mainnet() {
|
||||
let temp_dir = tempdir().unwrap().path().to_path_buf();
|
||||
let config_path = Path::join(&temp_dir, "config.toml");
|
||||
|
||||
let defaults = Mainnet::get_config_file_defaults().unwrap();
|
||||
|
||||
let expected = Config {
|
||||
data: Data {
|
||||
dir: Default::default(),
|
||||
},
|
||||
bitcoin: Bitcoin {
|
||||
electrum_rpc_urls: vec![defaults.electrum_rpc_url],
|
||||
target_block: defaults.bitcoin_confirmation_target,
|
||||
finality_confirmations: None,
|
||||
network: bitcoin::Network::Bitcoin,
|
||||
use_mempool_space_fee_estimation: true,
|
||||
},
|
||||
network: Network {
|
||||
listen: vec![defaults.listen_address_tcp],
|
||||
rendezvous_point: vec![],
|
||||
external_addresses: vec![],
|
||||
},
|
||||
monero: Monero {
|
||||
daemon_url: defaults.monero_daemon_address,
|
||||
finality_confirmations: None,
|
||||
network: monero::Network::Mainnet,
|
||||
monero_node_pool: false,
|
||||
},
|
||||
tor: Default::default(),
|
||||
maker: Maker {
|
||||
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
|
||||
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
|
||||
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
|
||||
price_ticker_ws_url: defaults.price_ticker_ws_url,
|
||||
external_bitcoin_redeem_address: None,
|
||||
},
|
||||
};
|
||||
|
||||
initial_setup(config_path.clone(), expected.clone()).unwrap();
|
||||
let actual = read_config(config_path).unwrap().unwrap();
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn env_override() {
|
||||
let temp_dir = tempfile::tempdir().unwrap().path().to_path_buf();
|
||||
let config_path = Path::join(&temp_dir, "config.toml");
|
||||
|
||||
let defaults = Mainnet::get_config_file_defaults().unwrap();
|
||||
|
||||
let dir = PathBuf::from("/tmp/dir");
|
||||
std::env::set_var("ASB__DATA__DIR", dir.clone());
|
||||
let addr1 = "/dns4/example.com/tcp/9939";
|
||||
let addr2 = "/ip4/1.2.3.4/tcp/9940";
|
||||
let external_addresses = vec![addr1.parse().unwrap(), addr2.parse().unwrap()];
|
||||
let listen = external_addresses.clone();
|
||||
std::env::set_var(
|
||||
"ASB__NETWORK__EXTERNAL_ADDRESSES",
|
||||
format!("{},{}", addr1, addr2),
|
||||
);
|
||||
std::env::set_var("ASB__NETWORK__LISTEN", format!("{},{}", addr1, addr2));
|
||||
|
||||
let expected = Config {
|
||||
data: Data { dir },
|
||||
bitcoin: Bitcoin {
|
||||
electrum_rpc_urls: vec![defaults.electrum_rpc_url],
|
||||
target_block: defaults.bitcoin_confirmation_target,
|
||||
finality_confirmations: None,
|
||||
network: bitcoin::Network::Bitcoin,
|
||||
use_mempool_space_fee_estimation: true,
|
||||
},
|
||||
network: Network {
|
||||
listen,
|
||||
rendezvous_point: vec![],
|
||||
external_addresses,
|
||||
},
|
||||
monero: Monero {
|
||||
daemon_url: defaults.monero_daemon_address,
|
||||
finality_confirmations: None,
|
||||
network: monero::Network::Mainnet,
|
||||
monero_node_pool: false,
|
||||
},
|
||||
tor: Default::default(),
|
||||
maker: Maker {
|
||||
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
|
||||
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
|
||||
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
|
||||
price_ticker_ws_url: defaults.price_ticker_ws_url,
|
||||
external_bitcoin_redeem_address: None,
|
||||
},
|
||||
};
|
||||
|
||||
initial_setup(config_path.clone(), expected.clone()).unwrap();
|
||||
let actual = read_config(config_path).unwrap().unwrap();
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
std::env::remove_var("ASB__DATA__DIR");
|
||||
std::env::remove_var("ASB__NETWORK__EXTERNAL_ADDRESSES");
|
||||
std::env::remove_var("ASB__NETWORK__LISTEN");
|
||||
}
|
||||
query_user_for_initial_config_with_network(bitcoin_network, monero_network)
|
||||
}
|
||||
|
|
|
|||
139
swap-env/src/defaults.rs
Normal file
139
swap-env/src/defaults.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use crate::env::{Mainnet, Testnet};
|
||||
use anyhow::{Context, Result};
|
||||
use libp2p::Multiaddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use swap_fs::{system_config_dir, system_data_dir};
|
||||
use url::Url;
|
||||
|
||||
pub const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64;
|
||||
pub const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
|
||||
pub const DEFAULT_SPREAD: f64 = 0.02f64;
|
||||
|
||||
pub const KRAKEN_PRICE_TICKER_WS_URL: &str = "wss://ws.kraken.com";
|
||||
|
||||
pub fn default_rendezvous_points() -> Vec<Multiaddr> {
|
||||
vec![
|
||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE".parse().unwrap(),
|
||||
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw".parse().unwrap(),
|
||||
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU".parse().unwrap(),
|
||||
"/dns4/eigen.center/tcp/8888/p2p/12D3KooWS5RaYJt4ANKMH4zczGVhNcw5W214e2DDYXnjs5Mx5zAT".parse().unwrap(),
|
||||
"/dns4/swapanarchy.cfd/tcp/8888/p2p/12D3KooWRtyVpmyvwzPYXuWyakFbRKhyXGrjhq6tP7RrBofpgQGp".parse().unwrap(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn default_electrum_servers_mainnet() -> Vec<Url> {
|
||||
vec![
|
||||
Url::parse("ssl://electrum.blockstream.info:50002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://electrum.blockstream.info:50001")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://bitcoin.stackwallet.com:50002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://b.1209k.com:50002").expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://electrum.coinucopia.io:50001")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://mainnet.foundationdevices.com:50002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://bitcoin.lu.ke:50001").expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://se-mma-crypto-payments-001.mullvad.net:50001")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://electrum.coinfinity.co:50002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn default_electrum_servers_testnet() -> Vec<Url> {
|
||||
vec![
|
||||
Url::parse("ssl://ax101.blockeng.ch:60002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://blackie.c3-soft.com:57006")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://v22019051929289916.bestsrv.de:50002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://v22019051929289916.bestsrv.de:50001")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://electrum.blockstream.info:60001")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://electrum.blockstream.info:60002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://blockstream.info:993").expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://blockstream.info:143").expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://testnet.qtornado.com:51002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://testnet.qtornado.com:51001")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://testnet.aranguren.org:51001")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://testnet.aranguren.org:51002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://testnet.qtornado.com:50002")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("ssl://bitcoin.devmole.eu:5010")
|
||||
.expect("default electrum server url to be valid"),
|
||||
Url::parse("tcp://bitcoin.devmole.eu:5000")
|
||||
.expect("default electrum server url to be valid"),
|
||||
]
|
||||
}
|
||||
|
||||
pub trait GetDefaults {
|
||||
fn get_config_file_defaults() -> Result<Defaults>;
|
||||
}
|
||||
|
||||
pub struct Defaults {
|
||||
pub config_path: PathBuf,
|
||||
pub data_dir: PathBuf,
|
||||
pub listen_address_tcp: Multiaddr,
|
||||
pub electrum_rpc_urls: Vec<Url>,
|
||||
pub price_ticker_ws_url: Url,
|
||||
pub bitcoin_confirmation_target: u16,
|
||||
pub use_mempool_space_fee_estimation: bool,
|
||||
}
|
||||
|
||||
impl GetDefaults for Mainnet {
|
||||
fn get_config_file_defaults() -> Result<Defaults> {
|
||||
let defaults = Defaults {
|
||||
config_path: default_asb_config_dir()?
|
||||
.join("mainnet")
|
||||
.join("config.toml"),
|
||||
data_dir: default_asb_data_dir()?.join("mainnet"),
|
||||
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
|
||||
electrum_rpc_urls: default_electrum_servers_mainnet(),
|
||||
price_ticker_ws_url: Url::parse(KRAKEN_PRICE_TICKER_WS_URL)?,
|
||||
bitcoin_confirmation_target: 1,
|
||||
use_mempool_space_fee_estimation: true,
|
||||
};
|
||||
|
||||
Ok(defaults)
|
||||
}
|
||||
}
|
||||
|
||||
impl GetDefaults for Testnet {
|
||||
fn get_config_file_defaults() -> Result<Defaults> {
|
||||
let defaults = Defaults {
|
||||
config_path: default_asb_config_dir()?
|
||||
.join("testnet")
|
||||
.join("config.toml"),
|
||||
data_dir: default_asb_data_dir()?.join("testnet"),
|
||||
listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?,
|
||||
electrum_rpc_urls: default_electrum_servers_testnet(),
|
||||
price_ticker_ws_url: Url::parse(KRAKEN_PRICE_TICKER_WS_URL)?,
|
||||
bitcoin_confirmation_target: 1,
|
||||
use_mempool_space_fee_estimation: true,
|
||||
};
|
||||
|
||||
Ok(defaults)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_asb_config_dir() -> Result<PathBuf> {
|
||||
system_config_dir()
|
||||
.map(|dir| Path::join(&dir, "asb"))
|
||||
.context("Could not generate default config file path")
|
||||
}
|
||||
|
||||
fn default_asb_data_dir() -> Result<PathBuf> {
|
||||
system_data_dir()
|
||||
.map(|dir| Path::join(&dir, "asb"))
|
||||
.context("Could not generate default config file path")
|
||||
}
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
pub mod config;
|
||||
pub mod defaults;
|
||||
pub mod env;
|
||||
pub mod prompt;
|
||||
|
|
|
|||
235
swap-env/src/prompt.rs
Normal file
235
swap-env/src/prompt.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::defaults::{
|
||||
default_rendezvous_points, DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use dialoguer::Confirm;
|
||||
use dialoguer::{theme::ColorfulTheme, Input, Select};
|
||||
use libp2p::Multiaddr;
|
||||
use rust_decimal::prelude::FromPrimitive;
|
||||
use rust_decimal::Decimal;
|
||||
use url::Url;
|
||||
|
||||
/// Prompt user for data directory
|
||||
pub fn data_directory(default_data_dir: &Path) -> Result<PathBuf> {
|
||||
let data_dir = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter data directory for asb or hit return to use default")
|
||||
.default(
|
||||
default_data_dir
|
||||
.to_str()
|
||||
.context("Unsupported characters in default path")?
|
||||
.to_string(),
|
||||
)
|
||||
.interact_text()?;
|
||||
Ok(data_dir.as_str().parse()?)
|
||||
}
|
||||
|
||||
/// Prompt user for Bitcoin confirmation target
|
||||
pub fn bitcoin_confirmation_target(default_target: u16) -> Result<u16> {
|
||||
Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("How fast should your Bitcoin transactions be confirmed? Your transaction fee will be calculated based on this target. Hit return to use default")
|
||||
.default(default_target)
|
||||
.interact_text()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Prompt user for listen addresses
|
||||
pub fn listen_addresses(default_listen_address: &Multiaddr) -> Result<Vec<Multiaddr>> {
|
||||
let listen_addresses = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default")
|
||||
.default(format!("{}", default_listen_address))
|
||||
.interact_text()?;
|
||||
|
||||
listen_addresses
|
||||
.split(',')
|
||||
.map(|str| str.parse())
|
||||
.collect::<Result<Vec<Multiaddr>, _>>()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Prompt user for electrum RPC URLs
|
||||
pub fn electrum_rpc_urls(default_electrum_urls: &Vec<Url>) -> Result<Vec<Url>> {
|
||||
println!(
|
||||
"You can configure multiple Electrum servers for redundancy. At least one is required."
|
||||
);
|
||||
println!("The following default Electrum RPC URLs are available. We recommend using them.");
|
||||
for (i, url) in default_electrum_urls.iter().enumerate() {
|
||||
println!("{}: {}", i + 1, url);
|
||||
}
|
||||
|
||||
// Ask if the user wants to use the default Electrum RPC URLs
|
||||
let mut electrum_rpc_urls = match Confirm::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Do you want to use the default Electrum RPC URLs?")
|
||||
.default(true)
|
||||
.interact()?
|
||||
{
|
||||
true => default_electrum_urls.clone(),
|
||||
false => Vec::new(),
|
||||
};
|
||||
|
||||
let mut electrum_number = 1 + electrum_rpc_urls.len();
|
||||
let mut electrum_done = false;
|
||||
|
||||
// Ask for additional electrum URLs
|
||||
while !electrum_done {
|
||||
let prompt = format!(
|
||||
"Enter additional Electrum RPC URL ({electrum_number}). Or just hit Enter to continue."
|
||||
);
|
||||
let electrum_url = Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
|
||||
if electrum_url.is_empty() {
|
||||
electrum_done = true;
|
||||
} else if electrum_rpc_urls
|
||||
.iter()
|
||||
.any(|url| url.to_string() == electrum_url)
|
||||
{
|
||||
println!("That Electrum URL is already in the list.");
|
||||
} else {
|
||||
let electrum_url = Url::parse(&electrum_url).context("Invalid Electrum URL")?;
|
||||
electrum_rpc_urls.push(electrum_url);
|
||||
electrum_number += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(electrum_rpc_urls)
|
||||
}
|
||||
|
||||
/// Prompt user for Monero daemon URL
|
||||
/// If the user hits enter, we will use the Monero RPC pool (None)
|
||||
pub fn monero_daemon_url() -> Result<Option<Url>> {
|
||||
let type_choice = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Do you want to use the Monero RPC pool or a remote node?")
|
||||
.items(&["Use the Monero RPC pool", "Use a remote node"])
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
match type_choice {
|
||||
0 => Ok(None),
|
||||
1 => {
|
||||
let input = Input::<String>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter Monero daemon URL")
|
||||
.interact_text()?;
|
||||
|
||||
Ok(Some(Url::parse(&input)?))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompt user for Tor hidden service registration
|
||||
pub fn tor_hidden_service() -> Result<bool> {
|
||||
println!("Your ASB needs to be reachable from the outside world to provide quotes to takers.");
|
||||
println!(
|
||||
"Your ASB can run a hidden service for itself. It'll be reachable at an .onion address."
|
||||
);
|
||||
println!("You do not have to run a Tor daemon yourself. You do not have to manage anything.");
|
||||
println!("This will hide your IP address and allow you to run from behind a firewall without opening ports.");
|
||||
println!();
|
||||
|
||||
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Do you want a Tor hidden service to be created?")
|
||||
.items(&[
|
||||
"Yes, run a hidden service",
|
||||
"No, do not run a hidden service",
|
||||
])
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
Ok(selection == 0)
|
||||
}
|
||||
|
||||
/// Prompt user for minimum Bitcoin buy amount
|
||||
pub fn min_buy_amount() -> Result<bitcoin::Amount> {
|
||||
let min_buy = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter minimum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
|
||||
.default(DEFAULT_MIN_BUY_AMOUNT)
|
||||
.interact_text()?;
|
||||
bitcoin::Amount::from_btc(min_buy).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Prompt user for maximum Bitcoin buy amount
|
||||
pub fn max_buy_amount() -> Result<bitcoin::Amount> {
|
||||
let max_buy = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter maximum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
|
||||
.default(DEFAULT_MAX_BUY_AMOUNT)
|
||||
.interact_text()?;
|
||||
bitcoin::Amount::from_btc(max_buy).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Prompt user for ask spread
|
||||
pub fn ask_spread() -> Result<Decimal> {
|
||||
let ask_spread = Input::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Enter spread (in percent; value between 0.x and 1.0) to be used on top of the market rate or hit enter to use default.")
|
||||
.default(DEFAULT_SPREAD)
|
||||
.interact_text()?;
|
||||
|
||||
if !(0.0..=1.0).contains(&ask_spread) {
|
||||
bail!(format!("Invalid spread {}. For the spread value floating point number in interval [0..1] are allowed.", ask_spread))
|
||||
}
|
||||
|
||||
Decimal::from_f64(ask_spread).context("Unable to parse spread")
|
||||
}
|
||||
|
||||
/// Prompt user for rendezvous points
|
||||
pub fn rendezvous_points() -> Result<Vec<Multiaddr>> {
|
||||
println!("Your ASB can register with multiple rendezvous nodes for discoverability.");
|
||||
println!(
|
||||
"They act as sort of bootstrap nodes for peer discovery within the peer-to-peer network."
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
"The following rendezvous points are ran by community members. We recommend using them."
|
||||
);
|
||||
println!();
|
||||
|
||||
let default_rendezvous_points = default_rendezvous_points();
|
||||
for (i, point) in default_rendezvous_points.iter().enumerate() {
|
||||
println!("{}: {}", i + 1, point);
|
||||
}
|
||||
|
||||
// Ask if the user wants to use the default rendezvous points
|
||||
let use_default_rendezvous_points = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Do you want to use the default rendezvous points? (y/n)")
|
||||
.items(&[
|
||||
"Use default rendezvous points",
|
||||
"Do not use default rendezvous points",
|
||||
])
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
let mut rendezvous_points = match use_default_rendezvous_points {
|
||||
0 => {
|
||||
println!("You can now configure additional rendezvous points.");
|
||||
default_rendezvous_points
|
||||
}
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let mut number = 1 + rendezvous_points.len();
|
||||
let mut done = false;
|
||||
|
||||
while !done {
|
||||
let prompt = format!(
|
||||
"Enter the address for rendezvous node ({number}). Or just hit Enter to continue."
|
||||
);
|
||||
let rendezvous_addr = Input::<Multiaddr>::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(prompt)
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
|
||||
if rendezvous_addr.is_empty() {
|
||||
done = true;
|
||||
} else if rendezvous_points.contains(&rendezvous_addr) {
|
||||
println!("That rendezvous address is already in the list.");
|
||||
} else {
|
||||
rendezvous_points.push(rendezvous_addr);
|
||||
number += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(rendezvous_points)
|
||||
}
|
||||
27
swap-orchestrator/Cargo.toml
Normal file
27
swap-orchestrator/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "swap-orchestrator"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "orchestrator"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
bitcoin = { workspace = true }
|
||||
chrono = "0.4.41"
|
||||
compose_spec = "0.3.0"
|
||||
dialoguer = { workspace = true }
|
||||
monero = { workspace = true }
|
||||
serde_yaml = "0.9.34"
|
||||
swap-env = { path = "../swap-env" }
|
||||
toml = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
vergen = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
44
swap-orchestrator/README.md
Normal file
44
swap-orchestrator/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Orchestrator
|
||||
|
||||
The `orchestrator` tool helps you setup a secure, reliable and production environment for running an ASB.
|
||||
|
||||
The `orchestrator` guides you through a series of prompts to generate a customized [Docker](https://docs.docker.com/) environment using [Docker Compose](https://docs.docker.com/compose/).
|
||||
|
||||
## Getting started
|
||||
|
||||
To generate the `config.toml` and `docker-compose.yml` files, run:
|
||||
|
||||
```bash
|
||||
./orchestrator
|
||||
```
|
||||
|
||||
```bash
|
||||
cargo run --bin orchestrator
|
||||
```
|
||||
|
||||
To start the environment, run:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The `orchestrator` generates a `docker-compose.yml` file that includes the following containers:
|
||||
|
||||
- `asb`: Accepts connections from the outside world. Provides sell offers to the outside world. Manages your Bitcoin and Monero wallet. Controls the swap process.
|
||||
- `asb-controller`: Shell to send control commands to the `asb` container.
|
||||
- `bitcoind`: Bitcoin node.
|
||||
- `electrs`: Electrum server.
|
||||
- `monerod`: Monero node.
|
||||
|
||||
## Why Docker?
|
||||
|
||||
Your ASB will potentially be managing fairly large amounts of funds. This means you have to keep it secure and running reliably. Docker handles some of this for you.
|
||||
|
||||
Most importantly Docker provides:
|
||||
|
||||
- **System Isolation**: Containers are isolated from each other on the operating system level. If one of the nodes were to be affected by a vulnerability, your funds are still safe.
|
||||
- **Network Isolation**: Docker only exposes the peer-to-peer port of your ASB to the outside world. Bitcoin, Monero and Electrum containers are only allowed outbound connections. The RPC control port of your ASB is only accessible within Docker inside of an internal network, accessible only to the `asb-controller` container.
|
||||
- **Reproducibility**: A virtual environment is created for each container. This means that quirks on your system (e.g outdated `glibc`) will not become an issue.
|
||||
- **Building from source**: Building from source is as simple as passing a flag to Docker. You do not have to install any dependencies on your system.
|
||||
10
swap-orchestrator/build.rs
Normal file
10
swap-orchestrator/build.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use anyhow::Result;
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
EmitBuilder::builder()
|
||||
.git_describe(true, true, None)
|
||||
.git_sha(false) // false = short hash, true = full hash
|
||||
.emit()?;
|
||||
Ok(())
|
||||
}
|
||||
34
swap-orchestrator/src/asb.rs
Normal file
34
swap-orchestrator/src/asb.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use crate::{
|
||||
compose::{Flag, IntoFlag},
|
||||
flag,
|
||||
};
|
||||
|
||||
/// Wrapper around the network used for ASB
|
||||
/// There are only two combinations of networks that are supported:
|
||||
/// - Mainnet Bitcoin & Mainnet Monero
|
||||
/// - Testnet Bitcoin & Stagenet Monero
|
||||
pub struct Network((monero::Network, bitcoin::Network));
|
||||
|
||||
impl Network {
|
||||
pub fn new(monero: monero::Network, bitcoin: bitcoin::Network) -> Self {
|
||||
Self((monero, bitcoin))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFlag for Network {
|
||||
fn to_flag(self) -> Flag {
|
||||
match self.0 {
|
||||
(monero::Network::Mainnet, bitcoin::Network::Bitcoin) => flag!("--mainnet"),
|
||||
(monero::Network::Stagenet, bitcoin::Network::Testnet) => flag!("--testnet"),
|
||||
_ => panic!("Only either Mainnet Bitcoin & Mainnet Monero or Testnet Bitcoin & Stagenet Monero are supported"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_display(self) -> &'static str {
|
||||
match self.0 {
|
||||
(monero::Network::Mainnet, bitcoin::Network::Bitcoin) => "mainnet",
|
||||
(monero::Network::Stagenet, bitcoin::Network::Testnet) => "testnet",
|
||||
_ => panic!("Only either Mainnet Bitcoin & Mainnet Monero or Testnet Bitcoin & Stagenet Monero are supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
425
swap-orchestrator/src/compose.rs
Normal file
425
swap-orchestrator/src/compose.rs
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
use crate::{asb, electrs, images::PINNED_GIT_REPOSITORY};
|
||||
use compose_spec::Compose;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
pub const ASB_DATA_DIR: &str = "/asb-data";
|
||||
pub const ASB_CONFIG_FILE: &str = "config.toml";
|
||||
pub const DOCKER_COMPOSE_FILE: &str = "./docker-compose.yml";
|
||||
|
||||
pub struct OrchestratorInput {
|
||||
pub ports: OrchestratorPorts,
|
||||
pub networks: OrchestratorNetworks<monero::Network, bitcoin::Network>,
|
||||
pub images: OrchestratorImages<OrchestratorImage>,
|
||||
pub directories: OrchestratorDirectories,
|
||||
}
|
||||
|
||||
pub struct OrchestratorDirectories {
|
||||
pub asb_data_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OrchestratorNetworks<MN: IntoFlag + Clone, BN: IntoFlag + Clone> {
|
||||
pub monero: MN,
|
||||
pub bitcoin: BN,
|
||||
}
|
||||
|
||||
pub struct OrchestratorImages<T: IntoImageAttribute> {
|
||||
pub monerod: T,
|
||||
pub electrs: T,
|
||||
pub bitcoind: T,
|
||||
pub asb: T,
|
||||
pub asb_controller: T,
|
||||
}
|
||||
|
||||
pub struct OrchestratorPorts {
|
||||
pub monerod_rpc: u16,
|
||||
pub bitcoind_rpc: u16,
|
||||
pub bitcoind_p2p: u16,
|
||||
pub electrs: u16,
|
||||
pub asb_libp2p: u16,
|
||||
pub asb_rpc_port: u16,
|
||||
}
|
||||
|
||||
impl Into<OrchestratorPorts> for OrchestratorNetworks<monero::Network, bitcoin::Network> {
|
||||
fn into(self) -> OrchestratorPorts {
|
||||
match (self.monero, self.bitcoin) {
|
||||
(monero::Network::Mainnet, bitcoin::Network::Bitcoin) => OrchestratorPorts {
|
||||
monerod_rpc: 18081,
|
||||
bitcoind_rpc: 8332,
|
||||
bitcoind_p2p: 8333,
|
||||
electrs: 50001,
|
||||
asb_libp2p: 9839,
|
||||
asb_rpc_port: 9944,
|
||||
},
|
||||
(monero::Network::Stagenet, bitcoin::Network::Testnet) => OrchestratorPorts {
|
||||
monerod_rpc: 38081,
|
||||
bitcoind_rpc: 18332,
|
||||
bitcoind_p2p: 18333,
|
||||
electrs: 50001,
|
||||
asb_libp2p: 9839,
|
||||
asb_rpc_port: 9944,
|
||||
},
|
||||
_ => panic!("Unsupported Bitcoin / Monero network combination"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<asb::Network> for OrchestratorNetworks<monero::Network, bitcoin::Network> {
|
||||
fn into(self) -> asb::Network {
|
||||
asb::Network::new(self.monero, self.bitcoin)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<electrs::Network> for OrchestratorNetworks<monero::Network, bitcoin::Network> {
|
||||
fn into(self) -> electrs::Network {
|
||||
electrs::Network::new(self.bitcoin)
|
||||
}
|
||||
}
|
||||
|
||||
impl OrchestratorDirectories {
|
||||
pub fn asb_config_path_inside_container(&self) -> PathBuf {
|
||||
self.asb_data_dir.join(ASB_CONFIG_FILE)
|
||||
}
|
||||
|
||||
pub fn asb_config_path_on_host(&self) -> &'static str {
|
||||
// The config file is in the same directory as the docker-compose.yml file
|
||||
"./config.toml"
|
||||
}
|
||||
}
|
||||
|
||||
/// See: https://docs.docker.com/reference/compose-file/build/#illustrative-example
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DockerBuildInput {
|
||||
// Usually this is the root of the Cargo workspace
|
||||
pub context: &'static str,
|
||||
// Usually this is the path to the Dockerfile
|
||||
pub dockerfile: &'static str,
|
||||
}
|
||||
|
||||
/// Specified a docker image to use
|
||||
/// The image can either be pulled from a registry or built from source
|
||||
pub enum OrchestratorImage {
|
||||
Registry(String),
|
||||
Build(DockerBuildInput),
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! flag {
|
||||
($flag:expr) => {
|
||||
Flag(Some($flag.to_string()))
|
||||
};
|
||||
($flag:expr, $($args:expr),*) => {
|
||||
flag!(format!($flag, $($args),*))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! command {
|
||||
($command:expr $(, $flag:expr)* $(,)?) => {
|
||||
Flags(vec![flag!($command) $(, $flag)*])
|
||||
};
|
||||
}
|
||||
|
||||
fn build(input: OrchestratorInput) -> String {
|
||||
// Every docker compose project has a name
|
||||
// The name is prefixed to the container names
|
||||
// See: https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name
|
||||
let project_name = format!(
|
||||
"{}_monero_{}_bitcoin",
|
||||
input.networks.monero.to_display(),
|
||||
input.networks.bitcoin.to_display()
|
||||
);
|
||||
|
||||
let asb_config_path = PathBuf::from(ASB_DATA_DIR).join(ASB_CONFIG_FILE);
|
||||
let asb_network: asb::Network = input.networks.clone().into();
|
||||
|
||||
let command_asb = command![
|
||||
"asb",
|
||||
asb_network.to_flag(),
|
||||
flag!("--config={}", asb_config_path.display()),
|
||||
flag!("start"),
|
||||
flag!("--rpc-bind-port={}", input.ports.asb_rpc_port),
|
||||
flag!("--rpc-bind-host=0.0.0.0"),
|
||||
];
|
||||
|
||||
let command_monerod = command![
|
||||
"monerod",
|
||||
input.networks.monero.to_flag(),
|
||||
flag!("--rpc-bind-ip=0.0.0.0"),
|
||||
flag!("--rpc-bind-port={}", input.ports.monerod_rpc),
|
||||
flag!("--data-dir=/monerod-data/"),
|
||||
flag!("--confirm-external-bind"),
|
||||
flag!("--restricted-rpc"),
|
||||
flag!("--non-interactive"),
|
||||
flag!("--enable-dns-blocklist"),
|
||||
];
|
||||
|
||||
let command_bitcoind = command![
|
||||
"bitcoind",
|
||||
input.networks.bitcoin.to_flag(),
|
||||
flag!("-rpcallowip=0.0.0.0/0"),
|
||||
flag!("-rpcbind=0.0.0.0:{}", input.ports.bitcoind_rpc),
|
||||
flag!("-bind=0.0.0.0:{}", input.ports.bitcoind_p2p),
|
||||
flag!("-datadir=/bitcoind-data/"),
|
||||
flag!("-dbcache=16384"),
|
||||
// These are required for electrs
|
||||
// See: See: https://github.com/romanz/electrs/blob/master/doc/config.md#bitcoind-configuration
|
||||
flag!("-server=1"),
|
||||
flag!("-prune=0"),
|
||||
flag!("-txindex=1"),
|
||||
];
|
||||
|
||||
let electrs_network: electrs::Network = input.networks.clone().into();
|
||||
|
||||
let command_electrs = command![
|
||||
"electrs",
|
||||
electrs_network.to_flag(),
|
||||
flag!("--daemon-dir=/bitcoind-data/"),
|
||||
flag!("--db-dir=/electrs-data/db"),
|
||||
flag!("--daemon-rpc-addr=bitcoind:{}", input.ports.bitcoind_rpc),
|
||||
flag!("--daemon-p2p-addr=bitcoind:{}", input.ports.bitcoind_p2p),
|
||||
flag!("--electrum-rpc-addr=0.0.0.0:{}", input.ports.electrs),
|
||||
flag!("--log-filters=INFO"),
|
||||
];
|
||||
|
||||
let command_asb_controller = command![
|
||||
"asb-controller",
|
||||
flag!("--url=http://asb:{}", input.ports.asb_rpc_port),
|
||||
];
|
||||
|
||||
let date = chrono::Utc::now()
|
||||
.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
.to_string();
|
||||
|
||||
let compose_str = format!(
|
||||
"\
|
||||
# This file was auto-generated by `orchestrator` on {date}
|
||||
#
|
||||
# It is pinned to build the `asb` and `asb-controller` images from this commit:
|
||||
# {PINNED_GIT_REPOSITORY}
|
||||
#
|
||||
# If the code does not match the hash, the build will fail. This ensures that the code cannot be altered by Github.
|
||||
# The compiled `orchestrator` has this hash burned into the binary.
|
||||
#
|
||||
# To update the `asb` and `asb-controller` images, you need to either:
|
||||
# - re-compile the `orchestrator` binary from a commit from Github
|
||||
# - download a newer pre-compiled version of the `orchestrator` binary from Github.
|
||||
#
|
||||
# After updating the `orchestrator` binary, re-generate the compose file by running `orchestrator` again.
|
||||
#
|
||||
# The used images for `bitcoind`, `monerod`, `electrs` are pinned to specific hashes which prevents them from being altered by the Docker registry.
|
||||
#
|
||||
# Please check for new releases regularly. Breaking network changes are rare, but they do happen from time to time.
|
||||
name: {project_name}
|
||||
services:
|
||||
monerod:
|
||||
container_name: monerod
|
||||
{image_monerod}
|
||||
restart: unless-stopped
|
||||
user: root
|
||||
volumes:
|
||||
- 'monerod-data:/monerod-data/'
|
||||
expose:
|
||||
- {port_monerod_rpc}
|
||||
entrypoint: ''
|
||||
command: {command_monerod}
|
||||
bitcoind:
|
||||
container_name: bitcoind
|
||||
{image_bitcoind}
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- 'bitcoind-data:/bitcoind-data/'
|
||||
expose:
|
||||
- {port_bitcoind_rpc}
|
||||
- {port_bitcoind_p2p}
|
||||
user: root
|
||||
entrypoint: ''
|
||||
command: {command_bitcoind}
|
||||
electrs:
|
||||
container_name: electrs
|
||||
{image_electrs}
|
||||
restart: unless-stopped
|
||||
user: root
|
||||
depends_on:
|
||||
- bitcoind
|
||||
volumes:
|
||||
- 'bitcoind-data:/bitcoind-data'
|
||||
- 'electrs-data:/electrs-data'
|
||||
expose:
|
||||
- {electrs_port}
|
||||
entrypoint: ''
|
||||
command: {command_electrs}
|
||||
asb:
|
||||
container_name: asb
|
||||
{image_asb}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- electrs
|
||||
volumes:
|
||||
- '{asb_config_path_on_host}:{asb_config_path_inside_container}'
|
||||
- 'asb-data:{asb_data_dir}'
|
||||
ports:
|
||||
- '0.0.0.0:{asb_port}:{asb_port}'
|
||||
entrypoint: ''
|
||||
command: {command_asb}
|
||||
asb-controller:
|
||||
container_name: asb-controller
|
||||
{image_asb_controller}
|
||||
stdin_open: true
|
||||
tty: true
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- asb
|
||||
entrypoint: ''
|
||||
command: {command_asb_controller}
|
||||
volumes:
|
||||
monerod-data:
|
||||
bitcoind-data:
|
||||
electrs-data:
|
||||
asb-data:
|
||||
",
|
||||
port_monerod_rpc = input.ports.monerod_rpc,
|
||||
port_bitcoind_rpc = input.ports.bitcoind_rpc,
|
||||
port_bitcoind_p2p = input.ports.bitcoind_p2p,
|
||||
electrs_port = input.ports.electrs,
|
||||
asb_port = input.ports.asb_libp2p,
|
||||
image_monerod = input.images.monerod.to_image_attribute(),
|
||||
image_electrs = input.images.electrs.to_image_attribute(),
|
||||
image_bitcoind = input.images.bitcoind.to_image_attribute(),
|
||||
image_asb = input.images.asb.to_image_attribute(),
|
||||
image_asb_controller = input.images.asb_controller.to_image_attribute(),
|
||||
asb_data_dir = input.directories.asb_data_dir.display(),
|
||||
asb_config_path_on_host = input.directories.asb_config_path_on_host(),
|
||||
asb_config_path_inside_container = input.directories.asb_config_path_inside_container().display(),
|
||||
);
|
||||
|
||||
validate_compose(&compose_str);
|
||||
|
||||
compose_str
|
||||
}
|
||||
|
||||
pub struct Flags(Vec<Flag>);
|
||||
|
||||
/// Displays a list of flags into the "Exec form" supported by Docker
|
||||
/// This is documented here:
|
||||
/// https://docs.docker.com/reference/dockerfile/#exec-form
|
||||
///
|
||||
/// E.g ["/bin/bash", "-c", "echo hello"]
|
||||
impl Display for Flags {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Collect all non-none flags
|
||||
let flags = self
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|f| f.0.as_ref())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Put the " around each flag, join with a comma, put the whole thing in []
|
||||
write!(
|
||||
f,
|
||||
"[{}]",
|
||||
flags
|
||||
.into_iter()
|
||||
.map(|f| format!("\"{}\"", f))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Flag(pub Option<String>);
|
||||
|
||||
impl Display for Flag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(s) = &self.0 {
|
||||
return write!(f, "{}", s);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoFlag {
|
||||
/// Converts into a flag that can be used in a docker compose file
|
||||
fn to_flag(self) -> Flag;
|
||||
/// Converts into a string that can be used for display purposes
|
||||
fn to_display(self) -> &'static str;
|
||||
}
|
||||
|
||||
impl IntoFlag for monero::Network {
|
||||
/// This is documented here:
|
||||
/// https://docs.getmonero.org/interacting/monerod-reference/#pick-monero-network-blockchain
|
||||
fn to_flag(self) -> Flag {
|
||||
Flag(match self {
|
||||
monero::Network::Mainnet => None,
|
||||
monero::Network::Stagenet => Some("--stagenet".to_string()),
|
||||
monero::Network::Testnet => Some("--testnet".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn to_display(self) -> &'static str {
|
||||
match self {
|
||||
monero::Network::Mainnet => "mainnet",
|
||||
monero::Network::Stagenet => "stagenet",
|
||||
monero::Network::Testnet => "testnet",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFlag for bitcoin::Network {
|
||||
/// This is documented here:
|
||||
/// https://www.mankier.com/1/bitcoind
|
||||
fn to_flag(self) -> Flag {
|
||||
Flag(Some(match self {
|
||||
bitcoin::Network::Bitcoin => "-chain=main".to_string(),
|
||||
bitcoin::Network::Testnet => "-chain=test".to_string(),
|
||||
_ => panic!("Only Mainnet and Testnet are supported"),
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_display(self) -> &'static str {
|
||||
match self {
|
||||
bitcoin::Network::Bitcoin => "mainnet",
|
||||
bitcoin::Network::Testnet => "testnet",
|
||||
_ => panic!("Only Mainnet and Testnet are supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoSpec {
|
||||
fn to_spec(self) -> String;
|
||||
}
|
||||
|
||||
impl IntoSpec for OrchestratorInput {
|
||||
fn to_spec(self) -> String {
|
||||
build(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts something into either a:
|
||||
/// - image: <image>
|
||||
/// - build: <url to git repo>
|
||||
pub trait IntoImageAttribute {
|
||||
fn to_image_attribute(self) -> String;
|
||||
}
|
||||
|
||||
impl IntoImageAttribute for OrchestratorImage {
|
||||
fn to_image_attribute(self) -> String {
|
||||
match self {
|
||||
OrchestratorImage::Registry(image) => format!("image: {}", image),
|
||||
OrchestratorImage::Build(input) => format!(
|
||||
r#"build: {{ context: "{}", dockerfile: "{}" }}"#,
|
||||
input.context, input.dockerfile
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_compose(compose_str: &str) {
|
||||
serde_yaml::from_str::<Compose>(compose_str).expect(&format!(
|
||||
"Generated compose spec to be valid. But it was not. This is the spec: \n\n{}",
|
||||
compose_str
|
||||
));
|
||||
}
|
||||
33
swap-orchestrator/src/electrs.rs
Normal file
33
swap-orchestrator/src/electrs.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use crate::{
|
||||
compose::{Flag, IntoFlag},
|
||||
flag,
|
||||
};
|
||||
|
||||
/// Wrapper around a Bitcoin network for Electrs
|
||||
/// Electrs needs a different network flag than bitcoind
|
||||
#[derive(Clone)]
|
||||
pub struct Network(bitcoin::Network);
|
||||
|
||||
impl Network {
|
||||
pub fn new(bitcoin: bitcoin::Network) -> Self {
|
||||
Self(bitcoin)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoFlag for Network {
|
||||
fn to_flag(self) -> Flag {
|
||||
match self.0 {
|
||||
bitcoin::Network::Bitcoin => flag!("--network=mainnet"),
|
||||
bitcoin::Network::Testnet => flag!("--network=testnet"),
|
||||
_ => panic!("Only Mainnet and Testnet are supported"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_display(self) -> &'static str {
|
||||
match self.0 {
|
||||
bitcoin::Network::Bitcoin => "mainnet",
|
||||
bitcoin::Network::Testnet => "testnet",
|
||||
_ => panic!("Only Mainnet and Testnet are supported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
45
swap-orchestrator/src/images.rs
Normal file
45
swap-orchestrator/src/images.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use crate::compose::DockerBuildInput;
|
||||
|
||||
/// At compile time, we determine the git repository and commit hash
|
||||
/// This is then burned into the binary as a static string
|
||||
/// If the Git hash doesn't match, Docker will fail to build the image
|
||||
pub static PINNED_GIT_REPOSITORY: &str = concat!(
|
||||
"https://github.com/eigenwallet/core.git#",
|
||||
env!("VERGEN_GIT_SHA")
|
||||
);
|
||||
|
||||
/// All of these images are pinned to a specific commit
|
||||
/// This ensures that the images cannot be altered by the registry
|
||||
|
||||
/// monerod v0.18.4.1 (https://github.com/sethforprivacy/simple-monerod-docker/pkgs/container/simple-monerod/471968653)
|
||||
pub static MONEROD_IMAGE: &str = "ghcr.io/sethforprivacy/simple-monerod@sha256:f30e5706a335c384e4cf420215cbffd1196f0b3a11d4dd4e819fe3e0bca41ec5";
|
||||
|
||||
/// electrs v0.10.9 (https://hub.docker.com/layers/getumbrel/electrs/v0.10.9/images/sha256-738d066836953c28936eab59fd87bf5f940d457260d0d13cfc99b06513248fee)
|
||||
pub static ELECTRS_IMAGE: &str =
|
||||
"getumbrel/electrs@sha256:622657fbdc7331a69f5b3444e6f87867d51ac27d90c399c8bf25d9aab020052b";
|
||||
|
||||
/// bitcoind v28.1 (https://hub.docker.com/layers/getumbrel/bitcoind/v28.1/images/sha256-8a20dc4efd799c17fd20f27cc62a36d1ef157e2ef074a898eae88c712b8d0e24)
|
||||
pub static BITCOIND_IMAGE: &str =
|
||||
"getumbrel/bitcoind@sha256:c565266ea302c9ab2fc490f04ff14e584210cde3d0d991b8309157e5dfae9e8d";
|
||||
|
||||
/// eigenwallet asb v3.0.0-beta.5 (https://github.com/eigenwallet/core/pkgs/container/asb/477796831?tag=3.0.0-beta.5)
|
||||
pub static ASB_IMAGE: &str = "ghcr.io/eigenwallet/asb@sha256:ad0daf2ee68d05f6cb08df3d4ec856a07b0fb00df62dd5412298ecc2380f4ca6";
|
||||
|
||||
// TOOD: Add pre-built images here
|
||||
/// eigenwallet asb v3.0.0-beta.5 (https://github.com/eigenwallet/core/commit/886dbcbef2dda534d1a0763750f1e6c5e1f57564)
|
||||
// pub static ASB_IMAGE_FROM_SOURCE: &str = "https://github.com/eigenwallet/core.git#886dbcbef2dda534d1a0763750f1e6c5e1f57564";
|
||||
|
||||
// TODO: Allowing using a local git repository here
|
||||
pub static ASB_IMAGE_FROM_SOURCE: DockerBuildInput = DockerBuildInput {
|
||||
// The context is the root of the Cargo workspace
|
||||
context: PINNED_GIT_REPOSITORY,
|
||||
// The Dockerfile of the asb is in the swap-asb crate
|
||||
dockerfile: "./swap-asb/Dockerfile",
|
||||
};
|
||||
|
||||
pub static ASB_CONTROLLER_IMAGE_FROM_SOURCE: DockerBuildInput = DockerBuildInput {
|
||||
// The context is the root of the Cargo workspace
|
||||
context: PINNED_GIT_REPOSITORY,
|
||||
// The Dockerfile of the asb-controller is in the swap-controller crate
|
||||
dockerfile: "./swap-controller/Dockerfile",
|
||||
};
|
||||
4
swap-orchestrator/src/lib.rs
Normal file
4
swap-orchestrator/src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod asb;
|
||||
pub mod compose;
|
||||
pub mod electrs;
|
||||
pub mod images;
|
||||
250
swap-orchestrator/src/main.rs
Normal file
250
swap-orchestrator/src/main.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
mod asb;
|
||||
mod compose;
|
||||
mod electrs;
|
||||
mod images;
|
||||
|
||||
use crate::compose::{
|
||||
IntoSpec, OrchestratorDirectories, OrchestratorImage, OrchestratorImages, OrchestratorInput,
|
||||
OrchestratorNetworks, DOCKER_COMPOSE_FILE,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use swap_env::config::{Bitcoin, Config, Data, Maker, Monero, Network, TorConf};
|
||||
use swap_env::prompt as config_prompt;
|
||||
use swap_env::{defaults::GetDefaults, env::Mainnet, env::Testnet};
|
||||
use url::Url;
|
||||
|
||||
use crate::compose::ASB_DATA_DIR;
|
||||
|
||||
fn main() {
|
||||
let (bitcoin_network, monero_network) = prompt::network();
|
||||
|
||||
let defaults = match (bitcoin_network, monero_network) {
|
||||
(bitcoin::Network::Bitcoin, monero::Network::Mainnet) => {
|
||||
Mainnet::get_config_file_defaults().expect("defaults to be available")
|
||||
}
|
||||
(bitcoin::Network::Testnet, monero::Network::Stagenet) => {
|
||||
Testnet::get_config_file_defaults().expect("defaults to be available")
|
||||
}
|
||||
_ => panic!("Unsupported Bitcoin / Monero network combination"),
|
||||
};
|
||||
|
||||
// TOOD: Allow pre-built images here
|
||||
//let build_type = prompt::build_type();
|
||||
|
||||
let min_buy_btc = config_prompt::min_buy_amount().expect("Failed to prompt for min buy amount");
|
||||
let max_buy_btc = config_prompt::max_buy_amount().expect("Failed to prompt for max buy amount");
|
||||
let ask_spread = config_prompt::ask_spread().expect("Failed to prompt for ask spread");
|
||||
let rendezvous_points =
|
||||
config_prompt::rendezvous_points().expect("Failed to prompt for rendezvous points");
|
||||
let tor_hidden_service =
|
||||
config_prompt::tor_hidden_service().expect("Failed to prompt for tor hidden service");
|
||||
let listen_addresses = config_prompt::listen_addresses(&defaults.listen_address_tcp)
|
||||
.expect("Failed to prompt for listen addresses");
|
||||
let monero_node_type = prompt::monero_node_type();
|
||||
let electrum_server_type = prompt::electrum_server_type(&defaults.electrum_rpc_urls);
|
||||
|
||||
let recipe = OrchestratorInput {
|
||||
ports: OrchestratorNetworks {
|
||||
monero: monero_network,
|
||||
bitcoin: bitcoin_network,
|
||||
}
|
||||
.into(),
|
||||
networks: OrchestratorNetworks {
|
||||
monero: monero_network,
|
||||
bitcoin: bitcoin_network,
|
||||
},
|
||||
images: OrchestratorImages {
|
||||
monerod: OrchestratorImage::Registry(images::MONEROD_IMAGE.to_string()),
|
||||
electrs: OrchestratorImage::Registry(images::ELECTRS_IMAGE.to_string()),
|
||||
bitcoind: OrchestratorImage::Registry(images::BITCOIND_IMAGE.to_string()),
|
||||
// TODO: Allow pre-built images here
|
||||
asb: OrchestratorImage::Build(images::ASB_IMAGE_FROM_SOURCE.clone()),
|
||||
// TODO: Allow pre-built images here
|
||||
asb_controller: OrchestratorImage::Build(
|
||||
images::ASB_CONTROLLER_IMAGE_FROM_SOURCE.clone(),
|
||||
),
|
||||
},
|
||||
directories: OrchestratorDirectories {
|
||||
asb_data_dir: PathBuf::from(ASB_DATA_DIR),
|
||||
},
|
||||
};
|
||||
|
||||
let electrs_url = Url::parse(&format!("tcp://electrs:{}", recipe.ports.electrs))
|
||||
.expect("electrs url to be convertible to a valid url");
|
||||
|
||||
let monerod_daemon_url = Url::parse(&format!("http://monerod:{}", recipe.ports.monerod_rpc))
|
||||
.expect("monerod daemon url to be convertible to a valid url");
|
||||
|
||||
let config = Config {
|
||||
data: Data {
|
||||
dir: recipe.directories.asb_data_dir.clone(),
|
||||
},
|
||||
network: Network {
|
||||
listen: listen_addresses,
|
||||
rendezvous_point: rendezvous_points,
|
||||
external_addresses: vec![],
|
||||
},
|
||||
bitcoin: Bitcoin {
|
||||
electrum_rpc_urls: match electrum_server_type {
|
||||
// If user chose the included option, we will use the electrs url from the container
|
||||
ElectrumServerType::Included => vec![electrs_url],
|
||||
ElectrumServerType::Remote(electrum_servers) => electrum_servers,
|
||||
},
|
||||
network: bitcoin_network,
|
||||
target_block: defaults.bitcoin_confirmation_target,
|
||||
use_mempool_space_fee_estimation: defaults.use_mempool_space_fee_estimation,
|
||||
// This means that we will use the default set in swap-env/src/env.rs
|
||||
finality_confirmations: None,
|
||||
},
|
||||
monero: Monero {
|
||||
daemon_url: match monero_node_type.clone() {
|
||||
MoneroNodeType::Included => Some(monerod_daemon_url),
|
||||
MoneroNodeType::Pool => None,
|
||||
MoneroNodeType::Remote(url) => Some(url),
|
||||
},
|
||||
network: monero_network,
|
||||
// This means that we will use the default set in swap-env/src/env.rs
|
||||
finality_confirmations: None,
|
||||
},
|
||||
tor: TorConf {
|
||||
register_hidden_service: tor_hidden_service,
|
||||
..Default::default()
|
||||
},
|
||||
maker: Maker {
|
||||
min_buy_btc,
|
||||
max_buy_btc,
|
||||
ask_spread,
|
||||
price_ticker_ws_url: defaults.price_ticker_ws_url,
|
||||
external_bitcoin_redeem_address: None,
|
||||
},
|
||||
};
|
||||
|
||||
// Write the compose to ./docker-compose.yml and the config to ./config.toml
|
||||
let asb_config_path = recipe.directories.asb_config_path_on_host();
|
||||
let compose = recipe.to_spec();
|
||||
|
||||
std::fs::write(DOCKER_COMPOSE_FILE, compose).expect("Failed to write docker-compose.yml");
|
||||
std::fs::write(
|
||||
asb_config_path,
|
||||
toml::to_string(&config).expect("Failed to write config.toml"),
|
||||
)
|
||||
.expect("Failed to write config.toml");
|
||||
|
||||
println!();
|
||||
println!("Run `docker compose up -d` to start the services.");
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum BuildType {
|
||||
Source,
|
||||
Prebuilt,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum MoneroNodeType {
|
||||
Included, // Run a Monero node
|
||||
Pool, // Use the Monero Remote Node Pool with built in defaults
|
||||
Remote(Url), // Use a specific remote Monero node
|
||||
}
|
||||
|
||||
enum ElectrumServerType {
|
||||
Included, // Run a Bitcoin node and Electrum server
|
||||
Remote(Vec<Url>), // Use a specific remote Electrum server
|
||||
}
|
||||
|
||||
mod prompt {
|
||||
use dialoguer::{theme::ColorfulTheme, Input, Select};
|
||||
use swap_env::prompt as config_prompt;
|
||||
use url::Url;
|
||||
|
||||
use crate::{BuildType, ElectrumServerType, MoneroNodeType};
|
||||
|
||||
pub fn network() -> (bitcoin::Network, monero::Network) {
|
||||
let network = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("Which network do you want to run on?")
|
||||
.items(&[
|
||||
"Mainnet Bitcoin & Mainnet Monero",
|
||||
"Testnet Bitcoin & Stagenet Monero",
|
||||
])
|
||||
.default(0)
|
||||
.interact()
|
||||
.expect("Failed to select network");
|
||||
|
||||
match network {
|
||||
0 => (bitcoin::Network::Bitcoin, monero::Network::Mainnet),
|
||||
1 => (bitcoin::Network::Testnet, monero::Network::Stagenet),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // will be used in the future
|
||||
pub fn build_type() -> BuildType {
|
||||
let build_type = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("How do you want to build the Docker image for the ASB?")
|
||||
.items(&[
|
||||
"Build Docker image from source (can take >1h)",
|
||||
"Prebuild Docker image (pinned to a specific commit with SHA256 hash)",
|
||||
])
|
||||
.default(0)
|
||||
.interact()
|
||||
.expect("Failed to select build type");
|
||||
|
||||
match build_type {
|
||||
0 => BuildType::Source,
|
||||
1 => BuildType::Prebuilt,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn monero_node_type() -> MoneroNodeType {
|
||||
let node_choice = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt(
|
||||
"Do you want to include a Monero node or use an existing node/remote node?",
|
||||
)
|
||||
.items(&[
|
||||
"Include a full Monero node",
|
||||
"Use an existing node or remote node",
|
||||
])
|
||||
.default(0)
|
||||
.interact()
|
||||
.expect("Failed to select node choice");
|
||||
|
||||
match node_choice {
|
||||
0 => MoneroNodeType::Included,
|
||||
1 => {
|
||||
match config_prompt::monero_daemon_url()
|
||||
.expect("Failed to prompt for Monero daemon URL")
|
||||
{
|
||||
Some(url) => MoneroNodeType::Remote(url),
|
||||
None => MoneroNodeType::Pool,
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn electrum_server_type(default_electrum_urls: &Vec<Url>) -> ElectrumServerType {
|
||||
let electrum_server_type = Select::with_theme(&ColorfulTheme::default())
|
||||
.with_prompt("How do you want to connect to the Bitcoin network?")
|
||||
.items(&[
|
||||
"Run a full Bitcoin node & Electrum server",
|
||||
"List of remote Electrum servers",
|
||||
])
|
||||
.default(0)
|
||||
.interact()
|
||||
.expect("Failed to select electrum server type");
|
||||
|
||||
match electrum_server_type {
|
||||
0 => ElectrumServerType::Included,
|
||||
1 => {
|
||||
println!("Okay, let's use remote Electrum servers!");
|
||||
|
||||
let electrum_servers = config_prompt::electrum_rpc_urls(default_electrum_urls)
|
||||
.expect("Failed to prompt for electrum servers");
|
||||
|
||||
ElectrumServerType::Remote(electrum_servers)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
39
swap-orchestrator/tests/spec.rs
Normal file
39
swap-orchestrator/tests/spec.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use swap_orchestrator::compose::{
|
||||
IntoSpec, OrchestratorImage, OrchestratorImages, OrchestratorInput, OrchestratorNetworks,
|
||||
OrchestratorPorts,
|
||||
};
|
||||
use swap_orchestrator::{asb, electrs, images};
|
||||
|
||||
#[test]
|
||||
fn test_orchestrator_spec_generation() {
|
||||
let input = OrchestratorInput {
|
||||
ports: OrchestratorPorts {
|
||||
monerod_rpc: 38081,
|
||||
bitcoind_rpc: 18332,
|
||||
bitcoind_p2p: 18333,
|
||||
electrs: 60001,
|
||||
asb_libp2p: 9839,
|
||||
asb_rpc_port: 9944,
|
||||
},
|
||||
networks: OrchestratorNetworks {
|
||||
monero: monero::Network::Stagenet,
|
||||
bitcoin: bitcoin::Network::Testnet,
|
||||
electrs: electrs::Network::Testnet,
|
||||
asb: asb::Network::Testnet,
|
||||
},
|
||||
images: OrchestratorImages {
|
||||
monerod: OrchestratorImage::Registry(images::MONEROD_IMAGE.to_string()),
|
||||
electrs: OrchestratorImage::Registry(images::ELECTRS_IMAGE.to_string()),
|
||||
bitcoind: OrchestratorImage::Registry(images::BITCOIND_IMAGE.to_string()),
|
||||
asb: OrchestratorImage::Build(images::ASB_IMAGE.to_string()),
|
||||
asb_controller: OrchestratorImage::Build(images::ASB_CONTROLLER_IMAGE.to_string()),
|
||||
},
|
||||
directories: OrchestratorDirectories {
|
||||
asb_data_dir: PathBuf::from(ASB_DATA_DIR),
|
||||
},
|
||||
};
|
||||
|
||||
let spec = input.to_spec();
|
||||
|
||||
println!("{}", spec);
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ electrum-pool = { path = "../electrum-pool" }
|
|||
fns = "0.0.7"
|
||||
futures = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = ["server"] }
|
||||
libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] }
|
||||
libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] }
|
||||
moka = { version = "0.12", features = ["sync", "future"] }
|
||||
|
|
@ -72,6 +73,7 @@ sigma_fun = { version = "0.7", default-features = false, features = ["ed25519",
|
|||
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||
structopt = "0.3"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
swap-controller-api = { path = "../swap-controller-api" }
|
||||
swap-env = { path = "../swap-env" }
|
||||
swap-feed = { path = "../swap-feed" }
|
||||
swap-fs = { path = "../swap-fs" }
|
||||
|
|
@ -115,4 +117,4 @@ testcontainers = "0.15"
|
|||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
vergen = { version = "8.3", default-features = false, features = ["build", "git", "git2"] }
|
||||
vergen = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
pub mod command;
|
||||
mod event_loop;
|
||||
mod network;
|
||||
mod recovery;
|
||||
pub mod rpc;
|
||||
|
||||
pub use event_loop::{EventLoop, EventLoopHandle};
|
||||
pub use network::behaviour::{Behaviour, OutEvent};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
use self::quote::{
|
||||
make_quote, unlocked_monero_balance_with_timeout, QuoteCacheKey, QUOTE_CACHE_TTL,
|
||||
};
|
||||
use crate::asb::{Behaviour, OutEvent};
|
||||
use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason;
|
||||
use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected};
|
||||
|
|
@ -5,7 +8,7 @@ use crate::network::quote::BidQuote;
|
|||
use crate::network::swap_setup::alice::WalletSnapshot;
|
||||
use crate::network::transfer_proof;
|
||||
use crate::protocol::alice::swap::has_already_processed_enc_sig;
|
||||
use crate::protocol::alice::{AliceState, ReservesMonero, State3, Swap};
|
||||
use crate::protocol::alice::{AliceState, State3, Swap};
|
||||
use crate::protocol::{Database, State};
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
|
@ -16,7 +19,6 @@ use libp2p::request_response::{OutboundFailure, OutboundRequestId, ResponseChann
|
|||
use libp2p::swarm::SwarmEvent;
|
||||
use libp2p::{PeerId, Swarm};
|
||||
use moka::future::Cache;
|
||||
use monero::Amount;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::Debug;
|
||||
|
|
@ -25,18 +27,9 @@ use std::time::Duration;
|
|||
use swap_env::env;
|
||||
use swap_feed::LatestRate;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// The time-to-live for quotes in the cache
|
||||
const QUOTE_CACHE_TTL: Duration = Duration::from_secs(120);
|
||||
|
||||
/// The key for the quote cache
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct QuoteCacheKey {
|
||||
min_buy: bitcoin::Amount,
|
||||
max_buy: bitcoin::Amount,
|
||||
}
|
||||
pub use service::{EventLoopRequest, EventLoopService};
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct EventLoop<LR>
|
||||
|
|
@ -98,6 +91,9 @@ where
|
|||
oneshot::Sender<Result<(), OutboundFailure>>,
|
||||
)>,
|
||||
|
||||
/// Channel for service requests
|
||||
service_requests: mpsc::UnboundedReceiver<EventLoopRequest>,
|
||||
|
||||
/// Temporarily stores transfer proof requests for peers that are currently disconnected.
|
||||
///
|
||||
/// When a transfer proof cannot be sent because there's no connection to the peer:
|
||||
|
|
@ -139,10 +135,11 @@ where
|
|||
min_buy: bitcoin::Amount,
|
||||
max_buy: bitcoin::Amount,
|
||||
external_redeem_address: Option<bitcoin::Address>,
|
||||
) -> Result<(Self, mpsc::Receiver<Swap>)> {
|
||||
) -> Result<(Self, mpsc::Receiver<Swap>, EventLoopService)> {
|
||||
let swap_channel = MpscChannels::default();
|
||||
let (outgoing_transfer_proofs_sender, outgoing_transfer_proofs_requests) =
|
||||
tokio::sync::mpsc::unbounded_channel();
|
||||
let (service_sender, service_requests) = mpsc::unbounded_channel();
|
||||
|
||||
let quote_cache = Cache::builder().time_to_live(QUOTE_CACHE_TTL).build();
|
||||
|
||||
|
|
@ -162,16 +159,24 @@ where
|
|||
inflight_encrypted_signatures: Default::default(),
|
||||
outgoing_transfer_proofs_requests,
|
||||
outgoing_transfer_proofs_sender,
|
||||
service_requests,
|
||||
buffered_transfer_proofs: Default::default(),
|
||||
inflight_transfer_proofs: Default::default(),
|
||||
};
|
||||
Ok((event_loop, swap_channel.receiver))
|
||||
|
||||
let service = EventLoopService::new(service_sender);
|
||||
|
||||
Ok((event_loop, swap_channel.receiver, service))
|
||||
}
|
||||
|
||||
pub fn peer_id(&self) -> PeerId {
|
||||
*Swarm::local_peer_id(&self.swarm)
|
||||
}
|
||||
|
||||
pub fn external_addresses(&self) -> Vec<libp2p::Multiaddr> {
|
||||
self.swarm.external_addresses().cloned().collect()
|
||||
}
|
||||
|
||||
pub async fn run(mut self) {
|
||||
// ensure that these streams are NEVER empty, otherwise it will
|
||||
// terminate forever.
|
||||
|
|
@ -488,6 +493,19 @@ where
|
|||
},
|
||||
Some(response_channel) = self.inflight_encrypted_signatures.next() => {
|
||||
let _ = self.swarm.behaviour_mut().encrypted_signature.send_response(response_channel, ());
|
||||
},
|
||||
Some(request) = self.service_requests.recv() => {
|
||||
match request {
|
||||
EventLoopRequest::GetMultiaddresses { respond_to } => {
|
||||
let peer_id = *self.swarm.local_peer_id();
|
||||
let addresses = self.swarm.external_addresses().cloned().collect();
|
||||
let _ = respond_to.send((peer_id, addresses));
|
||||
}
|
||||
EventLoopRequest::GetActiveConnections { respond_to } => {
|
||||
let count = self.swarm.connected_peers().count();
|
||||
let _ = respond_to.send(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -726,6 +744,72 @@ impl EventLoopHandle {
|
|||
}
|
||||
}
|
||||
|
||||
mod service {
|
||||
use super::*;
|
||||
|
||||
/// Request types for the EventLoop service with typed responders
|
||||
#[derive(Debug)]
|
||||
pub enum EventLoopRequest {
|
||||
GetMultiaddresses {
|
||||
respond_to: oneshot::Sender<(PeerId, Vec<libp2p::Multiaddr>)>,
|
||||
},
|
||||
GetActiveConnections {
|
||||
respond_to: oneshot::Sender<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Tower service for communicating with the EventLoop
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventLoopService {
|
||||
sender: mpsc::UnboundedSender<EventLoopRequest>,
|
||||
}
|
||||
|
||||
impl EventLoopService {
|
||||
pub fn new(sender: mpsc::UnboundedSender<EventLoopRequest>) -> Self {
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
/// Get multiaddresses and peer ID from the event loop
|
||||
pub async fn get_multiaddresses(&self) -> anyhow::Result<(PeerId, Vec<libp2p::Multiaddr>)> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.sender
|
||||
.send(EventLoopRequest::GetMultiaddresses { respond_to: tx })
|
||||
.map_err(|_| anyhow::anyhow!("EventLoop service is down"))?;
|
||||
rx.await
|
||||
.map_err(|_| anyhow::anyhow!("EventLoop service did not respond"))
|
||||
}
|
||||
|
||||
/// Get the number of active connections from the event loop
|
||||
pub async fn get_active_connections(&self) -> anyhow::Result<usize> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.sender
|
||||
.send(EventLoopRequest::GetActiveConnections { respond_to: tx })
|
||||
.map_err(|_| anyhow::anyhow!("EventLoop service is down"))?;
|
||||
rx.await
|
||||
.map_err(|_| anyhow::anyhow!("EventLoop service did not respond"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod quote {
|
||||
use crate::monero::Amount;
|
||||
use anyhow::{anyhow, Context};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use swap_feed::LatestRate;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::{network::quote::BidQuote, protocol::alice::ReservesMonero};
|
||||
|
||||
/// The time-to-live for quotes in the cache
|
||||
pub const QUOTE_CACHE_TTL: Duration = Duration::from_secs(120);
|
||||
|
||||
/// The key for the quote cache
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct QuoteCacheKey {
|
||||
pub min_buy: bitcoin::Amount,
|
||||
pub max_buy: bitcoin::Amount,
|
||||
}
|
||||
|
||||
/// Computes a quote given the provided dependencies
|
||||
pub async fn make_quote<LR, F, Fut, I, Fut2, T>(
|
||||
min_buy: bitcoin::Amount,
|
||||
|
|
@ -755,7 +839,7 @@ where
|
|||
.map_err(Arc::new)?;
|
||||
|
||||
// Get the reserved amounts
|
||||
let reserved_amounts: Vec<Amount> = get_reserved_items()
|
||||
let reserved_amounts: Vec<_> = get_reserved_items()
|
||||
.await
|
||||
.context("Failed to get reserved items")
|
||||
.map_err(Arc::new)?
|
||||
|
|
@ -827,8 +911,8 @@ pub fn unreserved_monero_balance(
|
|||
}
|
||||
|
||||
/// Returns the unlocked Monero balance from the wallet
|
||||
async fn unlocked_monero_balance_with_timeout(
|
||||
wallet: Arc<monero::Wallet>,
|
||||
pub async fn unlocked_monero_balance_with_timeout(
|
||||
wallet: Arc<crate::monero::Wallet>,
|
||||
) -> Result<Amount, anyhow::Error> {
|
||||
/// This is how long we maximally wait for the wallet operation
|
||||
const MONERO_WALLET_OPERATION_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
|
@ -839,6 +923,7 @@ async fn unlocked_monero_balance_with_timeout(
|
|||
|
||||
Ok(balance.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
struct MpscChannels<T> {
|
||||
|
|
@ -855,8 +940,17 @@ impl<T> Default for MpscChannels<T> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use swap_feed::FixedRate;
|
||||
|
||||
use crate::{
|
||||
asb::event_loop::quote::{make_quote, unreserved_monero_balance},
|
||||
protocol::alice::ReservesMonero,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::monero::Amount;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unreserved_monero_balance_with_no_reserved_amounts() {
|
||||
let balance = Amount::from_monero(10.0).unwrap();
|
||||
|
|
@ -869,59 +963,59 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_unreserved_monero_balance_with_reserved_amounts() {
|
||||
let balance = Amount::from_monero(10.0).unwrap();
|
||||
let balance = monero::Amount::from_monero(10.0).unwrap();
|
||||
let reserved_amounts = vec![
|
||||
Amount::from_monero(2.0).unwrap(),
|
||||
Amount::from_monero(3.0).unwrap(),
|
||||
monero::Amount::from_monero(2.0).unwrap(),
|
||||
monero::Amount::from_monero(3.0).unwrap(),
|
||||
];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
|
||||
let expected = Amount::from_monero(5.0).unwrap();
|
||||
let expected = monero::Amount::from_monero(5.0).unwrap();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unreserved_monero_balance_insufficient_balance() {
|
||||
let balance = Amount::from_monero(5.0).unwrap();
|
||||
let balance = monero::Amount::from_monero(5.0).unwrap();
|
||||
let reserved_amounts = vec![
|
||||
Amount::from_monero(3.0).unwrap(),
|
||||
Amount::from_monero(4.0).unwrap(), // Total reserved > balance
|
||||
monero::Amount::from_monero(3.0).unwrap(),
|
||||
monero::Amount::from_monero(4.0).unwrap(), // Total reserved > balance
|
||||
];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
|
||||
// Should return zero when reserved > balance
|
||||
assert_eq!(result, Amount::ZERO);
|
||||
assert_eq!(result, monero::Amount::ZERO);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unreserved_monero_balance_exact_match() {
|
||||
let balance = Amount::from_monero(10.0).unwrap();
|
||||
let balance = monero::Amount::from_monero(10.0).unwrap();
|
||||
let reserved_amounts = vec![
|
||||
Amount::from_monero(4.0).unwrap(),
|
||||
Amount::from_monero(6.0).unwrap(), // Exactly equals balance
|
||||
monero::Amount::from_monero(4.0).unwrap(),
|
||||
monero::Amount::from_monero(6.0).unwrap(), // Exactly equals balance
|
||||
];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
|
||||
assert_eq!(result, Amount::ZERO);
|
||||
assert_eq!(result, monero::Amount::ZERO);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unreserved_monero_balance_zero_balance() {
|
||||
let balance = Amount::ZERO;
|
||||
let reserved_amounts = vec![Amount::from_monero(1.0).unwrap()];
|
||||
let balance = monero::Amount::ZERO;
|
||||
let reserved_amounts = vec![monero::Amount::from_monero(1.0).unwrap()];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
|
||||
assert_eq!(result, Amount::ZERO);
|
||||
assert_eq!(result, monero::Amount::ZERO);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unreserved_monero_balance_empty_reserved_amounts() {
|
||||
let balance = Amount::from_monero(5.0).unwrap();
|
||||
let reserved_amounts: Vec<Amount> = vec![];
|
||||
let balance = monero::Amount::from_monero(5.0).unwrap();
|
||||
let reserved_amounts: Vec<MockReservedItem> = vec![];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
|
||||
|
|
@ -930,12 +1024,12 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_unreserved_monero_balance_large_amounts() {
|
||||
let balance = Amount::from_piconero(1_000_000_000);
|
||||
let reserved_amounts = vec![Amount::from_piconero(300_000_000)];
|
||||
let balance = monero::Amount::from_piconero(1_000_000_000);
|
||||
let reserved_amounts = vec![monero::Amount::from_piconero(300_000_000)];
|
||||
|
||||
let result = unreserved_monero_balance(balance, reserved_amounts.into_iter());
|
||||
|
||||
let expected = Amount::from_piconero(700_000_000);
|
||||
let expected = monero::Amount::from_piconero(700_000_000);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
|
|
@ -944,7 +1038,7 @@ mod tests {
|
|||
let min_buy = bitcoin::Amount::from_sat(100_000);
|
||||
let max_buy = bitcoin::Amount::from_sat(500_000);
|
||||
let rate = FixedRate::default();
|
||||
let balance = Amount::from_monero(1.0).unwrap();
|
||||
let balance = monero::Amount::from_monero(1.0).unwrap();
|
||||
let reserved_items: Vec<MockReservedItem> = vec![];
|
||||
|
||||
let result = make_quote(
|
||||
|
|
@ -967,13 +1061,13 @@ mod tests {
|
|||
let min_buy = bitcoin::Amount::from_sat(50_000);
|
||||
let max_buy = bitcoin::Amount::from_sat(300_000);
|
||||
let rate = FixedRate::default();
|
||||
let balance = Amount::from_monero(1.0).unwrap();
|
||||
let balance = monero::Amount::from_monero(1.0).unwrap();
|
||||
let reserved_items = vec![
|
||||
MockReservedItem {
|
||||
reserved: Amount::from_monero(0.2).unwrap(),
|
||||
reserved: monero::Amount::from_monero(0.2).unwrap(),
|
||||
},
|
||||
MockReservedItem {
|
||||
reserved: Amount::from_monero(0.3).unwrap(),
|
||||
reserved: monero::Amount::from_monero(0.3).unwrap(),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -999,7 +1093,7 @@ mod tests {
|
|||
let min_buy = bitcoin::Amount::from_sat(600_000); // More than available
|
||||
let max_buy = bitcoin::Amount::from_sat(800_000);
|
||||
let rate = FixedRate::default();
|
||||
let balance = Amount::from_monero(0.5).unwrap(); // Only 0.005 BTC worth at rate 0.01
|
||||
let balance = monero::Amount::from_monero(0.5).unwrap(); // Only 0.005 BTC worth at rate 0.01
|
||||
let reserved_items: Vec<MockReservedItem> = vec![];
|
||||
|
||||
let result = make_quote(
|
||||
|
|
@ -1022,7 +1116,7 @@ mod tests {
|
|||
let min_buy = bitcoin::Amount::from_sat(100_000);
|
||||
let max_buy = bitcoin::Amount::from_sat(800_000); // More than available
|
||||
let rate = FixedRate::default();
|
||||
let balance = Amount::from_monero(0.6).unwrap(); // 0.006 BTC worth at rate 0.01
|
||||
let balance = monero::Amount::from_monero(0.6).unwrap(); // 0.006 BTC worth at rate 0.01
|
||||
let reserved_items: Vec<MockReservedItem> = vec![];
|
||||
|
||||
let result = make_quote(
|
||||
|
|
@ -1048,9 +1142,9 @@ mod tests {
|
|||
let min_buy = bitcoin::Amount::from_sat(100_000);
|
||||
let max_buy = bitcoin::Amount::from_sat(500_000);
|
||||
let rate = FixedRate::default();
|
||||
let balance = Amount::from_monero(1.0).unwrap();
|
||||
let balance = monero::Amount::from_monero(1.0).unwrap();
|
||||
let reserved_items = vec![MockReservedItem {
|
||||
reserved: Amount::from_monero(1.0).unwrap(), // All balance reserved
|
||||
reserved: monero::Amount::from_monero(1.0).unwrap(), // All balance reserved
|
||||
}];
|
||||
|
||||
let result = make_quote(
|
||||
|
|
@ -1096,7 +1190,7 @@ mod tests {
|
|||
let min_buy = bitcoin::Amount::from_sat(100_000);
|
||||
let max_buy = bitcoin::Amount::from_sat(500_000);
|
||||
let rate = FixedRate::default();
|
||||
let balance = Amount::from_monero(1.0).unwrap();
|
||||
let balance = monero::Amount::from_monero(1.0).unwrap();
|
||||
let reserved_items: Vec<MockReservedItem> = vec![];
|
||||
|
||||
let result = make_quote(
|
||||
|
|
|
|||
3
swap/src/asb/rpc/mod.rs
Normal file
3
swap/src/asb/rpc/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod server;
|
||||
|
||||
pub use server::RpcServer;
|
||||
160
swap/src/asb/rpc/server.rs
Normal file
160
swap/src/asb/rpc/server.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
use crate::asb::event_loop::EventLoopService;
|
||||
use crate::protocol::Database;
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{Context, Result};
|
||||
use jsonrpsee::server::{ServerBuilder, ServerHandle};
|
||||
use jsonrpsee::types::error::ErrorCode;
|
||||
use jsonrpsee::types::ErrorObjectOwned;
|
||||
use std::sync::Arc;
|
||||
use swap_controller_api::{
|
||||
ActiveConnectionsResponse, AsbApiServer, BitcoinBalanceResponse, MoneroAddressResponse,
|
||||
MoneroBalanceResponse, MultiaddressesResponse, Swap,
|
||||
};
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
pub struct RpcServer {
|
||||
handle: ServerHandle,
|
||||
}
|
||||
|
||||
impl RpcServer {
|
||||
pub async fn start(
|
||||
host: String,
|
||||
port: u16,
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
event_loop_service: EventLoopService,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
) -> Result<Self> {
|
||||
let server = ServerBuilder::default()
|
||||
.build((host, port))
|
||||
.await
|
||||
.context("Failed to build RPC server")?;
|
||||
|
||||
let addr = server.local_addr()?;
|
||||
|
||||
let rpc_impl = RpcImpl {
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
event_loop_service,
|
||||
db,
|
||||
};
|
||||
let handle = server.start(rpc_impl.into_rpc());
|
||||
|
||||
tracing::info!("JSON-RPC server listening on {}", addr);
|
||||
|
||||
Ok(Self { handle })
|
||||
}
|
||||
|
||||
/// Spawn the server in a new tokio task
|
||||
pub fn spawn(self) -> AbortOnDropHandle<()> {
|
||||
AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
self.handle.stopped().await;
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RpcImpl {
|
||||
bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
monero_wallet: Arc<monero::Wallets>,
|
||||
event_loop_service: EventLoopService,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsbApiServer for RpcImpl {
|
||||
async fn check_connection(&self) -> Result<(), ErrorObjectOwned> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn bitcoin_balance(&self) -> Result<BitcoinBalanceResponse, ErrorObjectOwned> {
|
||||
let balance = self.bitcoin_wallet.balance().await.into_json_rpc_result()?;
|
||||
|
||||
Ok(BitcoinBalanceResponse { balance })
|
||||
}
|
||||
|
||||
async fn monero_balance(&self) -> Result<MoneroBalanceResponse, ErrorObjectOwned> {
|
||||
let wallet = self.monero_wallet.main_wallet().await;
|
||||
let balance = wallet.total_balance().await;
|
||||
|
||||
Ok(MoneroBalanceResponse {
|
||||
balance: balance.as_pico(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn monero_address(&self) -> Result<MoneroAddressResponse, ErrorObjectOwned> {
|
||||
let wallet = self.monero_wallet.main_wallet().await;
|
||||
let address = wallet.main_address().await;
|
||||
|
||||
Ok(MoneroAddressResponse {
|
||||
address: address.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn multiaddresses(&self) -> Result<MultiaddressesResponse, ErrorObjectOwned> {
|
||||
let (_, addresses) = self
|
||||
.event_loop_service
|
||||
.get_multiaddresses()
|
||||
.await
|
||||
.into_json_rpc_result()?;
|
||||
|
||||
// TODO: Concenate peer id to the multiaddresses
|
||||
let multiaddresses = addresses.iter().map(|addr| addr.to_string()).collect();
|
||||
|
||||
Ok(MultiaddressesResponse { multiaddresses })
|
||||
}
|
||||
|
||||
async fn active_connections(&self) -> Result<ActiveConnectionsResponse, ErrorObjectOwned> {
|
||||
let connections = self
|
||||
.event_loop_service
|
||||
.get_active_connections()
|
||||
.await
|
||||
.into_json_rpc_result()?;
|
||||
|
||||
Ok(ActiveConnectionsResponse { connections })
|
||||
}
|
||||
|
||||
async fn get_swaps(&self) -> Result<Vec<Swap>, ErrorObjectOwned> {
|
||||
let swaps = self.db.all().await.into_json_rpc_result()?;
|
||||
|
||||
let swaps = swaps
|
||||
.into_iter()
|
||||
.map(|(swap_id, state)| {
|
||||
let state_str = match state {
|
||||
crate::protocol::State::Alice(state) => format!("{state}"),
|
||||
crate::protocol::State::Bob(state) => format!("{state}"),
|
||||
};
|
||||
|
||||
Swap {
|
||||
id: swap_id.to_string(),
|
||||
state: state_str,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(swaps)
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoJsonRpcResult<T> {
|
||||
fn into_json_rpc_result(self) -> Result<T, ErrorObjectOwned>;
|
||||
}
|
||||
|
||||
impl<T> IntoJsonRpcResult<T> for anyhow::Result<T> {
|
||||
fn into_json_rpc_result(self) -> Result<T, ErrorObjectOwned> {
|
||||
self.map_err(|e| e.into_json_rpc_error())
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoJsonRpcError {
|
||||
fn into_json_rpc_error(self) -> ErrorObjectOwned;
|
||||
}
|
||||
|
||||
impl IntoJsonRpcError for anyhow::Error {
|
||||
fn into_json_rpc_error(self) -> ErrorObjectOwned {
|
||||
ErrorObjectOwned::owned(
|
||||
ErrorCode::InternalError.code(),
|
||||
format!("{self:?}"),
|
||||
None::<()>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -582,8 +582,8 @@ mod tests {
|
|||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config.bitcoin_cancel_timelock,
|
||||
config.bitcoin_punish_timelock,
|
||||
config.bitcoin_cancel_timelock.into(),
|
||||
config.bitcoin_punish_timelock.into(),
|
||||
bob_wallet.new_address().await.unwrap(),
|
||||
config.monero_finality_confirmations,
|
||||
spending_fee,
|
||||
|
|
@ -687,8 +687,8 @@ mod tests {
|
|||
&mut OsRng,
|
||||
btc_amount,
|
||||
xmr_amount,
|
||||
config.bitcoin_cancel_timelock,
|
||||
config.bitcoin_punish_timelock,
|
||||
config.bitcoin_cancel_timelock.into(),
|
||||
config.bitcoin_punish_timelock.into(),
|
||||
bob_wallet.new_address().await.unwrap(),
|
||||
config.monero_finality_confirmations,
|
||||
spending_fee,
|
||||
|
|
|
|||
|
|
@ -2966,7 +2966,7 @@ impl TestWalletBuilder {
|
|||
|
||||
// Fund the wallet with fake utxos
|
||||
for _ in 0..self.num_utxos {
|
||||
receive_output_in_latest_block(&mut locked_wallet, self.utxo_amount);
|
||||
receive_output_in_latest_block(&mut locked_wallet, Amount::from_sat(self.utxo_amount));
|
||||
}
|
||||
|
||||
// Create another block to confirm the utxos
|
||||
|
|
|
|||
|
|
@ -240,7 +240,10 @@ async fn next_state(
|
|||
// Check if the transaction has already been broadcasted
|
||||
// It could be that the operation was aborted after the transaction reached the Electrum server
|
||||
// but before we transitioned to the BtcLocked state
|
||||
if let Ok(Some(_)) = bitcoin_wallet.get_raw_transaction(state3.tx_lock_id()).await {
|
||||
if let Ok(Some(_)) = bitcoin_wallet
|
||||
.get_raw_transaction(state3.tx_lock_id())
|
||||
.await
|
||||
{
|
||||
tracing::info!(txid = %state3.tx_lock_id(), "Bitcoin lock transaction already published, skipping publish");
|
||||
} else {
|
||||
// Publish the signed Bitcoin lock transaction
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ async fn start_alice(
|
|||
.unwrap();
|
||||
swarm.listen_on(listen_address).unwrap();
|
||||
|
||||
let (event_loop, swap_handle) = asb::EventLoop::new(
|
||||
let (event_loop, swap_handle, _service) = asb::EventLoop::new(
|
||||
swarm,
|
||||
env_config,
|
||||
bitcoin_wallet,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue