mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-04-25 10:19:18 -04:00
Compare commits
No commits in common. "master" and "0.12.1" have entirely different histories.
@ -1,6 +1,2 @@
|
|||||||
[target.armv7-unknown-linux-gnueabihf]
|
[target.armv7-unknown-linux-gnueabihf]
|
||||||
linker = "arm-linux-gnueabihf-gcc"
|
linker = "arm-linux-gnueabihf-gcc"
|
||||||
|
|
||||||
# windows defaults to smaller stack sizes which isn't enough
|
|
||||||
[target.'cfg(windows)']
|
|
||||||
rustflags = ["-C", "link-args=/STACK:8388608"]
|
|
||||||
|
43
.github/workflows/build-release-binaries.yml
vendored
43
.github/workflows/build-release-binaries.yml
vendored
@ -6,9 +6,8 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_binaries:
|
build_binaries:
|
||||||
name: Build
|
name: Build swap and asb binaries
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- bin: swap
|
- bin: swap
|
||||||
@ -21,10 +20,6 @@ jobs:
|
|||||||
archive_ext: tar
|
archive_ext: tar
|
||||||
- bin: swap
|
- bin: swap
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
os: macos-12
|
|
||||||
archive_ext: tar
|
|
||||||
- bin: swap
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
archive_ext: tar
|
archive_ext: tar
|
||||||
- bin: swap
|
- bin: swap
|
||||||
@ -41,10 +36,6 @@ jobs:
|
|||||||
archive_ext: tar
|
archive_ext: tar
|
||||||
- bin: asb
|
- bin: asb
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
os: macos-12
|
|
||||||
archive_ext: tar
|
|
||||||
- bin: asb
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
archive_ext: tar
|
archive_ext: tar
|
||||||
- bin: asb
|
- bin: asb
|
||||||
@ -54,33 +45,33 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout tagged commit
|
- name: Checkout tagged commit
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.release.target_commitish }}
|
ref: ${{ github.event.release.target_commitish }}
|
||||||
token: ${{ secrets.BOTTY_GITHUB_TOKEN }}
|
token: ${{ secrets.BOTTY_GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
- uses: Swatinem/rust-cache@v2.2.0
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: "1.74"
|
targets: armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
- name: Cross Build ${{ matrix.target }} ${{ matrix.bin }} binary
|
|
||||||
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
|
|
||||||
run: |
|
|
||||||
curl -L "https://github.com/cross-rs/cross/releases/download/v0.2.5/cross-x86_64-unknown-linux-gnu.tar.gz" | tar xzv
|
|
||||||
sudo mv cross /usr/bin
|
|
||||||
sudo mv cross-util /usr/bin
|
|
||||||
cross build --target=${{ matrix.target }} --release --package swap --bin ${{ matrix.bin }}
|
|
||||||
|
|
||||||
- name: Build ${{ matrix.target }} ${{ matrix.bin }} release binary
|
- name: Build ${{ matrix.target }} ${{ matrix.bin }} release binary
|
||||||
if: matrix.target != 'armv7-unknown-linux-gnueabihf'
|
uses: actions-rs/cargo@v1
|
||||||
run: cargo build --target=${{ matrix.target }} --release --package swap --bin ${{ matrix.bin }}
|
with:
|
||||||
|
command: build
|
||||||
|
args: --target=${{ matrix.target }} --release --package swap --bin ${{ matrix.bin }}
|
||||||
|
use-cross: true
|
||||||
|
|
||||||
- name: Smoke test the binary
|
- name: Smoke test the binary
|
||||||
if: matrix.target != 'armv7-unknown-linux-gnueabihf'
|
if: matrix.target != 'armv7-unknown-linux-gnueabihf' # armv7-unknown-linux-gnueabihf is only cross-compiled, no smoke test
|
||||||
run: target/${{ matrix.target }}/release/${{ matrix.bin }} --help
|
run: target/${{ matrix.target }}/release/${{ matrix.bin }} --help
|
||||||
|
|
||||||
|
# Remove once python 3 is the default
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
- id: create-archive-name
|
- id: create-archive-name
|
||||||
shell: python # Use python to have a prettier name for the archive on Windows.
|
shell: python # Use python to have a prettier name for the archive on Windows.
|
||||||
run: |
|
run: |
|
||||||
@ -97,7 +88,7 @@ jobs:
|
|||||||
print(f'::set-output name=archive::{archive_name}')
|
print(f'::set-output name=archive::{archive_name}')
|
||||||
|
|
||||||
- name: Pack macos archive
|
- name: Pack macos archive
|
||||||
if: startsWith(matrix.os, 'macos')
|
if: matrix.os == 'macos-latest'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: gtar -C ./target/${{ matrix.target }}/release --create --file=${{ steps.create-archive-name.outputs.archive }} ${{ matrix.bin }}
|
run: gtar -C ./target/${{ matrix.target }}/release --create --file=${{ steps.create-archive-name.outputs.archive }} ${{ matrix.bin }}
|
||||||
|
|
||||||
|
86
.github/workflows/ci.yml
vendored
86
.github/workflows/ci.yml
vendored
@ -4,6 +4,8 @@ on:
|
|||||||
pull_request: # Need to run on pull-requests, otherwise PRs from forks don't run
|
pull_request: # Need to run on pull-requests, otherwise PRs from forks don't run
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
- "staging" # Bors uses this branch
|
||||||
|
- "trying" # Bors uses this branch
|
||||||
- "master" # Always build head of master for the badge in the README
|
- "master" # Always build head of master for the badge in the README
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -11,19 +13,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: Swatinem/rust-cache@v2.2.0
|
||||||
with:
|
|
||||||
toolchain: "1.74"
|
|
||||||
components: clippy,rustfmt
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
uses: dprint/check@v2.2
|
uses: dprint/check@v2.1
|
||||||
with:
|
|
||||||
dprint-version: 0.39.1
|
|
||||||
|
|
||||||
- name: Run clippy with default features
|
- name: Run clippy with default features
|
||||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||||
@ -35,9 +30,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
- uses: Swatinem/rust-cache@v2.0.2
|
||||||
|
|
||||||
- name: Build swap
|
- name: Build swap
|
||||||
run: cargo build --bin swap
|
run: cargo build --bin swap
|
||||||
@ -49,12 +44,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
- uses: Swatinem/rust-cache@v2.0.2
|
||||||
|
|
||||||
- name: Install sqlx-cli
|
- name: Install sqlx-cli
|
||||||
run: cargo install --locked --version 0.6.3 sqlx-cli
|
run: cargo install sqlx-cli
|
||||||
|
|
||||||
- name: Run sqlite_dev_setup.sh script
|
- name: Run sqlite_dev_setup.sh script
|
||||||
run: |
|
run: |
|
||||||
@ -70,21 +65,18 @@ jobs:
|
|||||||
- target: armv7-unknown-linux-gnueabihf
|
- target: armv7-unknown-linux-gnueabihf
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
- target: x86_64-apple-darwin
|
- target: x86_64-apple-darwin
|
||||||
os: macos-12
|
|
||||||
- target: aarch64-apple-darwin
|
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
- target: x86_64-pc-windows-msvc
|
- target: x86_64-pc-windows-msvc
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
- uses: Swatinem/rust-cache@v2.2.0
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: "1.74"
|
|
||||||
targets: armv7-unknown-linux-gnueabihf
|
targets: armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
- name: Build binary
|
- name: Build binary
|
||||||
@ -100,13 +92,13 @@ jobs:
|
|||||||
run: cross build -p swap --target ${{ matrix.target }}
|
run: cross build -p swap --target ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Upload swap binary
|
- name: Upload swap binary
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: swap-${{ matrix.target }}
|
name: swap-${{ matrix.target }}
|
||||||
path: target/${{ matrix.target }}/debug/swap
|
path: target/${{ matrix.target }}/debug/swap
|
||||||
|
|
||||||
- name: Upload asb binary
|
- name: Upload asb binary
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: asb-${{ matrix.target }}
|
name: asb-${{ matrix.target }}
|
||||||
path: target/${{ matrix.target }}/debug/asb
|
path: target/${{ matrix.target }}/debug/asb
|
||||||
@ -117,23 +109,10 @@ jobs:
|
|||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: (Free disk space on Ubuntu)
|
|
||||||
if: matrix.os == 'ubuntu-latest'
|
|
||||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
|
|
||||||
with:
|
|
||||||
# removing all of these takes ~10 mins, so just do as needed
|
|
||||||
android: true
|
|
||||||
dotnet: true
|
|
||||||
haskell: true
|
|
||||||
docker-images: false
|
|
||||||
large-packages: false
|
|
||||||
swap-storage: false
|
|
||||||
tool-cache: false
|
|
||||||
|
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
- uses: Swatinem/rust-cache@v2.2.0
|
||||||
|
|
||||||
- name: Build tests
|
- name: Build tests
|
||||||
run: cargo build --tests --workspace --all-features
|
run: cargo build --tests --workspace --all-features
|
||||||
@ -157,47 +136,20 @@ jobs:
|
|||||||
alice_and_bob_refund_using_cancel_and_refund_command,
|
alice_and_bob_refund_using_cancel_and_refund_command,
|
||||||
alice_and_bob_refund_using_cancel_then_refund_command,
|
alice_and_bob_refund_using_cancel_then_refund_command,
|
||||||
alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired,
|
alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired,
|
||||||
alice_manually_punishes_after_bob_dead_and_bob_cancels,
|
|
||||||
punish,
|
punish,
|
||||||
alice_punishes_after_restart_bob_dead,
|
alice_punishes_after_restart_bob_dead,
|
||||||
alice_manually_punishes_after_bob_dead,
|
alice_manually_punishes_after_bob_dead,
|
||||||
alice_refunds_after_restart_bob_refunded,
|
alice_refunds_after_restart_bob_refunded,
|
||||||
ensure_same_swap_id,
|
ensure_same_swap_id,
|
||||||
concurrent_bobs_before_xmr_lock_proof_sent,
|
concurrent_bobs_before_xmr_lock_proof_sent,
|
||||||
concurrent_bobs_after_xmr_lock_proof_sent,
|
|
||||||
alice_manually_redeems_after_enc_sig_learned,
|
alice_manually_redeems_after_enc_sig_learned,
|
||||||
happy_path_bob_offline_while_alice_redeems_btc,
|
|
||||||
]
|
]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
- uses: Swatinem/rust-cache@v2.2.0
|
||||||
|
|
||||||
- name: Run test ${{ matrix.test_name }}
|
- name: Run test ${{ matrix.test_name }}
|
||||||
run: cargo test --package swap --all-features --test ${{ matrix.test_name }} -- --nocapture
|
run: cargo test --package swap --all-features --test ${{ matrix.test_name }} -- --nocapture
|
||||||
|
|
||||||
rpc_tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v4.2.1
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
|
||||||
|
|
||||||
- name: Run RPC server tests
|
|
||||||
run: cargo test --package swap --all-features --test rpc -- --nocapture
|
|
||||||
|
|
||||||
check_stable:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v4.2.1
|
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2.7.5
|
|
||||||
|
|
||||||
- name: Run cargo check on stable rust
|
|
||||||
run: cargo check --all-targets
|
|
||||||
|
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/')
|
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.2.1
|
- uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Extract version from branch name
|
- name: Extract version from branch name
|
||||||
id: extract-version
|
id: extract-version
|
||||||
|
12
.github/workflows/draft-new-release.yml
vendored
12
.github/workflows/draft-new-release.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
name: "Draft a new release"
|
name: "Draft a new release"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.2.1
|
- uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOTTY_GITHUB_TOKEN }}
|
token: ${{ secrets.BOTTY_GITHUB_TOKEN }}
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ jobs:
|
|||||||
run: git checkout -b release/${{ github.event.inputs.version }}
|
run: git checkout -b release/${{ github.event.inputs.version }}
|
||||||
|
|
||||||
- name: Update changelog
|
- name: Update changelog
|
||||||
uses: thomaseizinger/keep-a-changelog-new-release@3.1.0
|
uses: thomaseizinger/keep-a-changelog-new-release@1.3.0
|
||||||
with:
|
with:
|
||||||
version: ${{ github.event.inputs.version }}
|
version: ${{ github.event.inputs.version }}
|
||||||
changelogPath: CHANGELOG.md
|
changelogPath: CHANGELOG.md
|
||||||
@ -41,12 +41,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit changelog and manifest files
|
- name: Commit changelog and manifest files
|
||||||
id: make-commit
|
id: make-commit
|
||||||
env:
|
|
||||||
DPRINT_VERSION: "0.39.1"
|
|
||||||
RUST_TOOLCHAIN: "1.74"
|
|
||||||
run: |
|
run: |
|
||||||
rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu"
|
curl -fsSL https://dprint.dev/install.sh | sh
|
||||||
curl -fsSL https://dprint.dev/install.sh | sh -s $DPRINT_VERSION
|
|
||||||
/home/runner/.dprint/bin/dprint fmt
|
/home/runner/.dprint/bin/dprint fmt
|
||||||
|
|
||||||
git add CHANGELOG.md Cargo.lock swap/Cargo.toml
|
git add CHANGELOG.md Cargo.lock swap/Cargo.toml
|
||||||
@ -58,7 +54,7 @@ jobs:
|
|||||||
run: git push origin release/${{ github.event.inputs.version }} --force
|
run: git push origin release/${{ github.event.inputs.version }} --force
|
||||||
|
|
||||||
- name: Create pull request
|
- name: Create pull request
|
||||||
uses: thomaseizinger/create-pull-request@1.4.0
|
uses: thomaseizinger/create-pull-request@1.3.0
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.BOTTY_GITHUB_TOKEN }}
|
||||||
head: release/${{ github.event.inputs.version }}
|
head: release/${{ github.event.inputs.version }}
|
||||||
|
2
.github/workflows/preview-release.yml
vendored
2
.github/workflows/preview-release.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
|||||||
name: Create preview release
|
name: Create preview release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.2.1
|
- uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Delete 'preview' release
|
- name: Delete 'preview' release
|
||||||
uses: larryjoelane/delete-release-action@v1.0.24
|
uses: larryjoelane/delete-release-action@v1.0.24
|
||||||
|
49
CHANGELOG.md
49
CHANGELOG.md
@ -7,46 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
- ASB + CLI: You can now use the `logs` command to retrieve logs stored in the past, redacting addresses and id's using `logs --redact`.
|
|
||||||
- ASB: The `--disable-timestamp` flag has been removed
|
|
||||||
|
|
||||||
## [0.13.4] - 2024-07-25
|
|
||||||
|
|
||||||
- ASB: The `history` command can now be used while the asb is running.
|
|
||||||
- ASB: Retry locking of Monero if it fails on first attempt
|
|
||||||
|
|
||||||
## [0.13.3] - 2024-07-15
|
|
||||||
|
|
||||||
- Introduced a cooperative Monero redeem feature for Bob to request from Alice if Bob is punished for not refunding in time. Alice can choose to cooperate but is not obligated to do so. This change is backwards compatible. To attempt recovery, resume a swap in the "Bitcoin punished" state. Success depends on Alice being active and still having a record of the swap. Note that Alice's cooperation is voluntary and recovery is not guaranteed
|
|
||||||
- CLI: `--change-address` can now be omitted. In that case, any change is refunded to the internal bitcoin wallet.
|
|
||||||
|
|
||||||
## [0.13.2] - 2024-07-02
|
|
||||||
|
|
||||||
- CLI: Buffer received transfer proofs for later processing if we're currently running a different swap
|
|
||||||
- CLI: We now display the reason for a failed cancel-refund operation to the user (#683)
|
|
||||||
|
|
||||||
## [0.13.1] - 2024-06-10
|
|
||||||
|
|
||||||
- Add retry logic to monero-wallet-rpc wallet refresh
|
|
||||||
|
|
||||||
## [0.13.0] - 2024-05-29
|
|
||||||
|
|
||||||
- Minimum Supported Rust Version (MSRV) bumped to 1.74
|
|
||||||
- Lowered default Bitcoin confirmation target for Bob to 1 to make sure Bitcoin transactions get confirmed in time
|
|
||||||
- Added support for starting the CLI (using the `start-daemon` subcommand) as a Daemon that accepts JSON-RPC requests
|
|
||||||
- Update monero-wallet-rpc version to v0.18.3.1
|
|
||||||
|
|
||||||
## [0.12.3] - 2023-09-20
|
|
||||||
|
|
||||||
- Swap: If no Monero daemon is manually specified, we will automatically choose one from a list of public daemons by connecting to each and checking their availability.
|
|
||||||
|
|
||||||
## [0.12.2] - 2023-08-08
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Minimum Supported Rust Version (MSRV) bumped to 1.67
|
|
||||||
- ASB can now register with multiple rendezvous nodes. The `rendezvous_point` option in `config.toml` can be a string with comma separated addresses, or a toml array of address strings.
|
|
||||||
|
|
||||||
## [0.12.1] - 2023-01-09
|
## [0.12.1] - 2023-01-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -378,14 +338,7 @@ It is possible to migrate critical data from the old db to the sqlite but there
|
|||||||
- Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them.
|
- Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them.
|
||||||
Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version.
|
Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version.
|
||||||
|
|
||||||
[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.4...HEAD
|
[Unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.1...HEAD
|
||||||
[0.13.4]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.3...0.13.4
|
|
||||||
[0.13.3]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.2...0.13.3
|
|
||||||
[0.13.2]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.1...0.13.2
|
|
||||||
[0.13.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.13.0...0.13.1
|
|
||||||
[0.13.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.3...0.13.0
|
|
||||||
[0.12.3]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.2...0.12.3
|
|
||||||
[0.12.2]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.1...0.12.2
|
|
||||||
[0.12.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.0...0.12.1
|
[0.12.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.12.0...0.12.1
|
||||||
[0.12.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.11.0...0.12.0
|
[0.12.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.11.0...0.12.0
|
||||||
[0.11.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.2...0.11.0
|
[0.11.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.2...0.11.0
|
||||||
|
2276
Cargo.lock
generated
2276
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
|
||||||
members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet" ]
|
members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet" ]
|
||||||
|
|
||||||
[patch.crates-io]
|
[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" }
|
|
||||||
monero = { git = "https://github.com/comit-network/monero-rs", rev = "818f38b" }
|
monero = { git = "https://github.com/comit-network/monero-rs", rev = "818f38b" }
|
||||||
|
22
Dockerfile
22
Dockerfile
@ -1,22 +0,0 @@
|
|||||||
# This Dockerfile builds the asb binary
|
|
||||||
|
|
||||||
FROM rust:1.79-slim AS builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install -y git clang cmake libsnappy-dev
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
WORKDIR /build/swap
|
|
||||||
|
|
||||||
RUN cargo build --release --bin=asb
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
|
|
||||||
WORKDIR /data
|
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/asb /bin/asb
|
|
||||||
|
|
||||||
ENTRYPOINT ["asb"]
|
|
@ -9,10 +9,6 @@ Currently, swaps are only offered in one direction with the `swap` CLI on the bu
|
|||||||
We are working on implementing a protocol where XMR moves first, but are currently blocked by advances on Monero itself.
|
We are working on implementing a protocol where XMR moves first, but are currently blocked by advances on Monero itself.
|
||||||
You can read [this blogpost](https://comit.network/blog/2021/07/02/transaction-presigning) for more information.
|
You can read [this blogpost](https://comit.network/blog/2021/07/02/transaction-presigning) for more information.
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
**This repository is unmaintained**. The original developers (@comit-network) have moved on to other projects. Community volunteers are continuing development at [UnstoppableSwap/core](https://github.com/UnstoppableSwap/core), which includes a graphical user interface. Please note that the fork has introduced network-level breaking changes, making it incompatible with peers running this repository - you will not be able to initiate swaps with them.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
1. Download the [latest `swap` binary release](https://github.com/comit-network/xmr-btc-swap/releases/latest) for your operating system.
|
1. Download the [latest `swap` binary release](https://github.com/comit-network/xmr-btc-swap/releases/latest) for your operating system.
|
||||||
@ -48,13 +44,13 @@ It is not recommended to bump fees when swapping because it can have unpredictab
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We encourage community contributions whether it be a bug fix or an improvement to the documentation.
|
We are encourage community contributions whether it be a bug fix or an improvement to the documentation.
|
||||||
Please have a look at the [contribution guidelines](./CONTRIBUTING.md).
|
Please have a look at the [contribution guidelines](./CONTRIBUTING.md).
|
||||||
|
|
||||||
## Rust Version Support
|
## Rust Version Support
|
||||||
|
|
||||||
Please note that only the latest stable Rust toolchain is supported.
|
Please note that only the latest stable Rust toolchain is supported.
|
||||||
All stable toolchains since 1.74 _should_ work.
|
All stable toolchains since 1.62 _should_ work.
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
|
25
bors.toml
Normal file
25
bors.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
status = [
|
||||||
|
"static_analysis",
|
||||||
|
"bdk_test",
|
||||||
|
"sqlx_test",
|
||||||
|
"build (x86_64-unknown-linux-gnu, ubuntu-latest)",
|
||||||
|
"build (armv7-unknown-linux-gnueabihf, ubuntu-latest)",
|
||||||
|
"build (x86_64-apple-darwin, macos-latest)",
|
||||||
|
"build (x86_64-pc-windows-msvc, windows-latest)",
|
||||||
|
"test (ubuntu-latest)",
|
||||||
|
"test (macos-latest)",
|
||||||
|
"docker_tests (happy_path)",
|
||||||
|
"docker_tests (happy_path_restart_bob_after_xmr_locked)",
|
||||||
|
"docker_tests (happy_path_restart_alice_after_xmr_locked)",
|
||||||
|
"docker_tests (happy_path_restart_bob_before_xmr_locked)",
|
||||||
|
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command)",
|
||||||
|
"docker_tests (alice_and_bob_refund_using_cancel_then_refund_command)",
|
||||||
|
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired)",
|
||||||
|
"docker_tests (punish)",
|
||||||
|
"docker_tests (alice_punishes_after_restart_bob_dead)",
|
||||||
|
"docker_tests (alice_manually_punishes_after_bob_dead)",
|
||||||
|
"docker_tests (alice_refunds_after_restart_bob_refunded)",
|
||||||
|
"docker_tests (ensure_same_swap_id)",
|
||||||
|
"docker_tests (concurrent_bobs_before_xmr_lock_proof_sent)",
|
||||||
|
"docker_tests (alice_manually_redeems_after_enc_sig_learned)"
|
||||||
|
]
|
@ -42,16 +42,13 @@ Since the ASB is a long running task we specify the person running an ASB as ser
|
|||||||
The ASB daemon supports the libp2p [rendezvous-protocol](https://github.com/libp2p/specs/tree/master/rendezvous).
|
The ASB daemon supports the libp2p [rendezvous-protocol](https://github.com/libp2p/specs/tree/master/rendezvous).
|
||||||
Usage of the rendezvous functionality is entirely optional.
|
Usage of the rendezvous functionality is entirely optional.
|
||||||
|
|
||||||
You can configure one or more rendezvous points in the `[network]` section of your config file.
|
You can configure a rendezvous point in the `[network]` section of your config file.
|
||||||
For the registration to be successful, you also need to configure the externally reachable addresses within the `[network]` section.
|
For the registration to be successful, you also need to configure the externally reachable addresses within the `[network]` section.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[network]
|
[network]
|
||||||
rendezvous_point = [
|
rendezvous_point = "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE"
|
||||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
|
||||||
"/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs",
|
|
||||||
]
|
|
||||||
external_addresses = ["/dns4/example.com/tcp/9939"]
|
external_addresses = ["/dns4/example.com/tcp/9939"]
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -110,7 +107,7 @@ The minimum and maximum amount as well as a spread, that is added on top of the
|
|||||||
In order to be able to trade, the ASB must define a price to be able to agree on the amounts to be swapped with a CLI.
|
In order to be able to trade, the ASB must define a price to be able to agree on the amounts to be swapped with a CLI.
|
||||||
The `XMR<>BTC` price is currently determined by the price from the central exchange Kraken.
|
The `XMR<>BTC` price is currently determined by the price from the central exchange Kraken.
|
||||||
Upon startup the ASB connects to the Kraken price websocket and listens on the stream for price updates.
|
Upon startup the ASB connects to the Kraken price websocket and listens on the stream for price updates.
|
||||||
You can plug in a different price ticker websocket using the `price_ticker_ws_url` configuration option.
|
You can plug in a different price ticker websocket using the the `price_ticker_ws_url` configuration option.
|
||||||
You will have to make sure that the format returned is the same as the format used by Kraken.
|
You will have to make sure that the format returned is the same as the format used by Kraken.
|
||||||
|
|
||||||
Currently, we use a spot-price model, i.e. the ASB dictates the price to the CLI.
|
Currently, we use a spot-price model, i.e. the ASB dictates the price to the CLI.
|
||||||
@ -164,38 +161,3 @@ May 01 01:32:05.018 INFO Tor found. Setting up hidden service.
|
|||||||
May 01 01:32:07.475 INFO /onion3/z4findrdwtfbpoq64ayjtmxvr52vvxnsynerlenlfkmm52dqxsl4deyd:9939
|
May 01 01:32:07.475 INFO /onion3/z4findrdwtfbpoq64ayjtmxvr52vvxnsynerlenlfkmm52dqxsl4deyd:9939
|
||||||
May 01 01:32:07.476 INFO /onion3/z4findrdwtfbpoq64ayjtmxvr52vvxnsynerlenlfkmm52dqxsl4deyd:9940
|
May 01 01:32:07.476 INFO /onion3/z4findrdwtfbpoq64ayjtmxvr52vvxnsynerlenlfkmm52dqxsl4deyd:9940
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exporting the Bitcoin wallet descriptor
|
|
||||||
|
|
||||||
First use `swap` or `asb` with the `export-bitcoin-wallet` subcommand.
|
|
||||||
|
|
||||||
Output example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"descriptor":"wpkh(tprv8Zgredacted.../84'/1'/0'/0/*)","blockheight":2415616,"label":"asb-testnet"}
|
|
||||||
```
|
|
||||||
|
|
||||||
The wallet can theoretically be directly imported into
|
|
||||||
[bdk-cli](https://bitcoindevkit.org/bdk-cli/installation/) but it is easier to
|
|
||||||
use Sparrow Wallet.
|
|
||||||
|
|
||||||
Sparrow wallet import works as follows:
|
|
||||||
|
|
||||||
- File -> New wallet -> Give it a name
|
|
||||||
- Select "New or Imported Software Wallet"
|
|
||||||
- Click "Enter Private Key" for "Master Private Key (BIP32)"
|
|
||||||
- Enter the `xprv...` or `tprv...` part of the descriptor (example above is `tprv8Zgredacted...`:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- Click "Import"
|
|
||||||
- Leave the derivation path as `m/84'/0'/0'` and click "Import Keystore" button
|
|
||||||
- Click "Apply" and then supply password
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- Click Transactions tab
|
|
||||||
- ???
|
|
||||||
- Profit!
|
|
||||||
|
|
||||||

|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
Before Width: | Height: | Size: 182 KiB |
Binary file not shown.
Before Width: | Height: | Size: 109 KiB |
@ -75,7 +75,7 @@ OPTIONS:
|
|||||||
|
|
||||||
This command has three core options:
|
This command has three core options:
|
||||||
|
|
||||||
- `--change-address`: A Bitcoin address you control. Will be used for refunds of any kind. You can also omit this flag which will refund any change to the internal wallet.
|
- `--change-address`: A Bitcoin address you control. Will be used for refunds of any kind.
|
||||||
- `--receive-address`: A Monero address you control. This is where you will receive the Monero after the swap.
|
- `--receive-address`: A Monero address you control. This is where you will receive the Monero after the swap.
|
||||||
- `--seller`: The multiaddress of the seller you want to swap with.
|
- `--seller`: The multiaddress of the seller you want to swap with.
|
||||||
|
|
||||||
|
16
dprint.json
16
dprint.json
@ -3,16 +3,22 @@
|
|||||||
"projectType": "openSource",
|
"projectType": "openSource",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"markdown": {},
|
"markdown": {},
|
||||||
"exec": {
|
"rustfmt": {
|
||||||
"associations": "**/*.{rs}",
|
"edition": 2021,
|
||||||
"rustfmt": "rustfmt --edition 2021",
|
"condense_wildcard_suffixes": true,
|
||||||
"rustfmt.associations": "**/*.rs"
|
"format_macro_matchers": true,
|
||||||
|
"imports_granularity": "Module",
|
||||||
|
"use_field_init_shorthand": true,
|
||||||
|
"format_code_in_doc_comments": true,
|
||||||
|
"normalize_comments": true,
|
||||||
|
"wrap_comments": true,
|
||||||
|
"overflow_delimited_expr": true
|
||||||
},
|
},
|
||||||
"includes": ["**/*.{md}", "**/*.{toml}", "**/*.{rs}"],
|
"includes": ["**/*.{md}", "**/*.{toml}", "**/*.{rs}"],
|
||||||
"excludes": ["target/"],
|
"excludes": ["target/"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"https://plugins.dprint.dev/markdown-0.13.1.wasm",
|
"https://plugins.dprint.dev/markdown-0.13.1.wasm",
|
||||||
"https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm",
|
"https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm",
|
||||||
"https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab"
|
"https://plugins.dprint.dev/rustfmt-0.6.1.exe-plugin@99b89a0599fd3a63e597e03436862157901f3facae2f0c2fbd0b9f656cdbc2a5"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ anyhow = "1"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
monero-rpc = { path = "../monero-rpc" }
|
monero-rpc = { path = "../monero-rpc" }
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
testcontainers = "0.15"
|
spectral = "0.6"
|
||||||
|
testcontainers = "0.12"
|
||||||
tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "macros" ] }
|
tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "macros" ] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "tracing-log" ] }
|
tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "tracing-log" ] }
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use testcontainers::{core::WaitFor, Image, ImageArgs};
|
use std::collections::HashMap;
|
||||||
|
use testcontainers::core::{Container, Docker, WaitForMessage};
|
||||||
|
use testcontainers::Image;
|
||||||
|
|
||||||
pub const MONEROD_DAEMON_CONTAINER_NAME: &str = "monerod";
|
pub const MONEROD_DAEMON_CONTAINER_NAME: &str = "monerod";
|
||||||
pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network";
|
pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network";
|
||||||
@ -11,22 +13,43 @@ pub const MONEROD_DEFAULT_NETWORK: &str = "monero_network";
|
|||||||
/// this doesn't matter.
|
/// this doesn't matter.
|
||||||
pub const RPC_PORT: u16 = 18081;
|
pub const RPC_PORT: u16 = 18081;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Monerod;
|
pub struct Monerod {
|
||||||
|
args: MonerodArgs,
|
||||||
|
}
|
||||||
|
|
||||||
impl Image for Monerod {
|
impl Image for Monerod {
|
||||||
type Args = MonerodArgs;
|
type Args = MonerodArgs;
|
||||||
|
type EnvVars = HashMap<String, String>;
|
||||||
|
type Volumes = HashMap<String, String>;
|
||||||
|
type EntryPoint = str;
|
||||||
|
|
||||||
fn name(&self) -> String {
|
fn descriptor(&self) -> String {
|
||||||
"rinocommunity/monero".into()
|
"rinocommunity/monero:v0.18.1.2".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tag(&self) -> String {
|
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
|
||||||
"v0.18.1.2".into()
|
container
|
||||||
|
.logs()
|
||||||
|
.stdout
|
||||||
|
.wait_for_message("RPC server started ok")
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ready_conditions(&self) -> Vec<WaitFor> {
|
fn args(&self) -> <Self as Image>::Args {
|
||||||
vec![WaitFor::message_on_stdout("RPC server started ok")]
|
self.args.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn volumes(&self) -> Self::Volumes {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_vars(&self) -> Self::EnvVars {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_args(self, args: <Self as Image>::Args) -> Self {
|
||||||
|
Self { args }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entrypoint(&self) -> Option<String> {
|
fn entrypoint(&self) -> Option<String> {
|
||||||
@ -35,22 +58,43 @@ impl Image for Monerod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct MoneroWalletRpc;
|
pub struct MoneroWalletRpc {
|
||||||
|
args: MoneroWalletRpcArgs,
|
||||||
|
}
|
||||||
|
|
||||||
impl Image for MoneroWalletRpc {
|
impl Image for MoneroWalletRpc {
|
||||||
type Args = MoneroWalletRpcArgs;
|
type Args = MoneroWalletRpcArgs;
|
||||||
|
type EnvVars = HashMap<String, String>;
|
||||||
|
type Volumes = HashMap<String, String>;
|
||||||
|
type EntryPoint = str;
|
||||||
|
|
||||||
fn name(&self) -> String {
|
fn descriptor(&self) -> String {
|
||||||
"rinocommunity/monero".into()
|
"rinocommunity/monero:v0.18.1.2".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tag(&self) -> String {
|
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
|
||||||
"v0.18.1.2".into()
|
container
|
||||||
|
.logs()
|
||||||
|
.stdout
|
||||||
|
.wait_for_message("Run server thread name: RPC")
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ready_conditions(&self) -> Vec<WaitFor> {
|
fn args(&self) -> <Self as Image>::Args {
|
||||||
vec![WaitFor::message_on_stdout("Run server thread name: RPC")]
|
self.args.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn volumes(&self) -> Self::Volumes {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_vars(&self) -> Self::EnvVars {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_args(self, args: <Self as Image>::Args) -> Self {
|
||||||
|
Self { args }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entrypoint(&self) -> Option<String> {
|
fn entrypoint(&self) -> Option<String> {
|
||||||
@ -60,9 +104,10 @@ impl Image for MoneroWalletRpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MoneroWalletRpc {
|
impl MoneroWalletRpc {
|
||||||
pub fn new(name: &str, daemon_address: String) -> (Self, MoneroWalletRpcArgs) {
|
pub fn new(name: &str, daemon_address: String) -> Self {
|
||||||
let args = MoneroWalletRpcArgs::new(name, daemon_address);
|
Self {
|
||||||
(Self, args)
|
args: MoneroWalletRpcArgs::new(name, daemon_address),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +191,6 @@ impl IntoIterator for MonerodArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageArgs for MonerodArgs {
|
|
||||||
fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
|
|
||||||
Box::new(self.into_iter())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MoneroWalletRpcArgs {
|
pub struct MoneroWalletRpcArgs {
|
||||||
pub disable_rpc_login: bool,
|
pub disable_rpc_login: bool,
|
||||||
@ -161,6 +200,12 @@ pub struct MoneroWalletRpcArgs {
|
|||||||
pub daemon_address: String,
|
pub daemon_address: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for MoneroWalletRpcArgs {
|
||||||
|
fn default() -> Self {
|
||||||
|
unimplemented!("A default instance for `MoneroWalletRpc` doesn't make sense because we always need to connect to a node.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MoneroWalletRpcArgs {
|
impl MoneroWalletRpcArgs {
|
||||||
pub fn new(wallet_name: &str, daemon_address: String) -> Self {
|
pub fn new(wallet_name: &str, daemon_address: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -202,9 +247,3 @@ impl IntoIterator for MoneroWalletRpcArgs {
|
|||||||
args.into_iter()
|
args.into_iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageArgs for MoneroWalletRpcArgs {
|
|
||||||
fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
|
|
||||||
Box::new(self.into_iter())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -20,20 +20,17 @@
|
|||||||
//! every BLOCK_TIME_SECS seconds.
|
//! every BLOCK_TIME_SECS seconds.
|
||||||
//!
|
//!
|
||||||
//! Also provides standalone JSON RPC clients for monerod and monero-wallet-rpc.
|
//! Also provides standalone JSON RPC clients for monerod and monero-wallet-rpc.
|
||||||
use std::time::Duration;
|
pub mod image;
|
||||||
|
|
||||||
|
use crate::image::{MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, RPC_PORT};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use testcontainers::clients::Cli;
|
|
||||||
use testcontainers::{Container, RunnableImage};
|
|
||||||
use tokio::time;
|
|
||||||
|
|
||||||
use monero_rpc::monerod;
|
use monero_rpc::monerod;
|
||||||
use monero_rpc::monerod::MonerodRpc as _;
|
use monero_rpc::monerod::MonerodRpc as _;
|
||||||
use monero_rpc::wallet::{self, GetAddress, MoneroWalletRpc as _, Refreshed, Transfer};
|
use monero_rpc::wallet::{self, GetAddress, MoneroWalletRpc as _, Refreshed, Transfer};
|
||||||
|
use std::time::Duration;
|
||||||
use crate::image::{MONEROD_DAEMON_CONTAINER_NAME, MONEROD_DEFAULT_NETWORK, RPC_PORT};
|
use testcontainers::clients::Cli;
|
||||||
|
use testcontainers::{Container, Docker, RunArgs};
|
||||||
pub mod image;
|
use tokio::time;
|
||||||
|
|
||||||
/// How often we mine a block.
|
/// How often we mine a block.
|
||||||
const BLOCK_TIME_SECS: u64 = 1;
|
const BLOCK_TIME_SECS: u64 = 1;
|
||||||
@ -59,8 +56,8 @@ impl<'c> Monero {
|
|||||||
additional_wallets: Vec<&'static str>,
|
additional_wallets: Vec<&'static str>,
|
||||||
) -> Result<(
|
) -> Result<(
|
||||||
Self,
|
Self,
|
||||||
Container<'c, image::Monerod>,
|
Container<'c, Cli, image::Monerod>,
|
||||||
Vec<Container<'c, image::MoneroWalletRpc>>,
|
Vec<Container<'c, Cli, image::MoneroWalletRpc>>,
|
||||||
)> {
|
)> {
|
||||||
let prefix = format!("{}_", random_prefix());
|
let prefix = format!("{}_", random_prefix());
|
||||||
let monerod_name = format!("{}{}", prefix, MONEROD_DAEMON_CONTAINER_NAME);
|
let monerod_name = format!("{}{}", prefix, MONEROD_DAEMON_CONTAINER_NAME);
|
||||||
@ -224,14 +221,15 @@ impl<'c> Monerod {
|
|||||||
cli: &'c Cli,
|
cli: &'c Cli,
|
||||||
name: String,
|
name: String,
|
||||||
network: String,
|
network: String,
|
||||||
) -> Result<(Self, Container<'c, image::Monerod>)> {
|
) -> Result<(Self, Container<'c, Cli, image::Monerod>)> {
|
||||||
let image = image::Monerod;
|
let image = image::Monerod::default();
|
||||||
let image: RunnableImage<image::Monerod> = RunnableImage::from(image)
|
let run_args = RunArgs::default()
|
||||||
.with_container_name(name.clone())
|
.with_name(name.clone())
|
||||||
.with_network(network.clone());
|
.with_network(network.clone());
|
||||||
|
let container = cli.run_with_args(image, run_args);
|
||||||
let container = cli.run(image);
|
let monerod_rpc_port = container
|
||||||
let monerod_rpc_port = container.get_host_port_ipv4(RPC_PORT);
|
.get_host_port(RPC_PORT)
|
||||||
|
.context("port not exposed")?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
Self {
|
Self {
|
||||||
@ -251,7 +249,7 @@ impl<'c> Monerod {
|
|||||||
/// address
|
/// address
|
||||||
pub async fn start_miner(&self, miner_wallet_address: &str) -> Result<()> {
|
pub async fn start_miner(&self, miner_wallet_address: &str) -> Result<()> {
|
||||||
let monerod = self.client().clone();
|
let monerod = self.client().clone();
|
||||||
tokio::spawn(mine(monerod, miner_wallet_address.to_string()));
|
let _ = tokio::spawn(mine(monerod, miner_wallet_address.to_string()));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,15 +262,19 @@ impl<'c> MoneroWalletRpc {
|
|||||||
name: &str,
|
name: &str,
|
||||||
monerod: &Monerod,
|
monerod: &Monerod,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
) -> Result<(Self, Container<'c, image::MoneroWalletRpc>)> {
|
) -> Result<(Self, Container<'c, Cli, image::MoneroWalletRpc>)> {
|
||||||
let daemon_address = format!("{}:{}", monerod.name, RPC_PORT);
|
let daemon_address = format!("{}:{}", monerod.name, RPC_PORT);
|
||||||
let (image, args) = image::MoneroWalletRpc::new(name, daemon_address);
|
let image = image::MoneroWalletRpc::new(name, daemon_address);
|
||||||
let image = RunnableImage::from((image, args))
|
|
||||||
.with_container_name(format!("{}{}", prefix, name))
|
|
||||||
.with_network(monerod.network.clone());
|
|
||||||
|
|
||||||
let container = cli.run(image);
|
let network = monerod.network.clone();
|
||||||
let wallet_rpc_port = container.get_host_port_ipv4(RPC_PORT);
|
let run_args = RunArgs::default()
|
||||||
|
// prefix the container name so we can run multiple tests
|
||||||
|
.with_name(format!("{}{}", prefix, name))
|
||||||
|
.with_network(network.clone());
|
||||||
|
let container = cli.run_with_args(image, run_args);
|
||||||
|
let wallet_rpc_port = container
|
||||||
|
.get_host_port(RPC_PORT)
|
||||||
|
.context("port not exposed")?;
|
||||||
|
|
||||||
let client = wallet::Client::localhost(wallet_rpc_port)?;
|
let client = wallet::Client::localhost(wallet_rpc_port)?;
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use monero_harness::Monero;
|
use monero_harness::Monero;
|
||||||
use monero_rpc::monerod::MonerodRpc as _;
|
use monero_rpc::monerod::MonerodRpc as _;
|
||||||
|
use spectral::prelude::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
@ -20,12 +21,12 @@ async fn init_miner_and_mine_to_miner_address() {
|
|||||||
let miner_wallet = monero.wallet("miner").unwrap();
|
let miner_wallet = monero.wallet("miner").unwrap();
|
||||||
|
|
||||||
let got_miner_balance = miner_wallet.balance().await.unwrap();
|
let got_miner_balance = miner_wallet.balance().await.unwrap();
|
||||||
assert!(got_miner_balance > 0);
|
assert_that!(got_miner_balance).is_greater_than(0);
|
||||||
|
|
||||||
time::sleep(Duration::from_millis(1010)).await;
|
time::sleep(Duration::from_millis(1010)).await;
|
||||||
|
|
||||||
// after a bit more than 1 sec another block should have been mined
|
// after a bit more than 1 sec another block should have been mined
|
||||||
let block_height = monerod.client().get_block_count().await.unwrap().count;
|
let block_height = monerod.client().get_block_count().await.unwrap().count;
|
||||||
|
|
||||||
assert!(block_height > 70);
|
assert_that(&block_height).is_greater_than(70);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use monero_harness::{Monero, MoneroWalletRpc};
|
use monero_harness::{Monero, MoneroWalletRpc};
|
||||||
use monero_rpc::wallet::MoneroWalletRpc as _;
|
use monero_rpc::wallet::MoneroWalletRpc as _;
|
||||||
|
use spectral::prelude::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
@ -28,7 +29,7 @@ async fn fund_transfer_and_check_tx_key() {
|
|||||||
|
|
||||||
// check alice balance
|
// check alice balance
|
||||||
let got_alice_balance = alice_wallet.balance().await.unwrap();
|
let got_alice_balance = alice_wallet.balance().await.unwrap();
|
||||||
assert_eq!(got_alice_balance, fund_alice);
|
assert_that(&got_alice_balance).is_equal_to(fund_alice);
|
||||||
|
|
||||||
// transfer from alice to bob
|
// transfer from alice to bob
|
||||||
let bob_address = bob_wallet.address().await.unwrap().address;
|
let bob_address = bob_wallet.address().await.unwrap().address;
|
||||||
@ -40,7 +41,7 @@ async fn fund_transfer_and_check_tx_key() {
|
|||||||
wait_for_wallet_to_catch_up(bob_wallet, send_to_bob).await;
|
wait_for_wallet_to_catch_up(bob_wallet, send_to_bob).await;
|
||||||
|
|
||||||
let got_bob_balance = bob_wallet.balance().await.unwrap();
|
let got_bob_balance = bob_wallet.balance().await.unwrap();
|
||||||
assert_eq!(got_bob_balance, send_to_bob);
|
assert_that(&got_bob_balance).is_equal_to(send_to_bob);
|
||||||
|
|
||||||
// check if tx was actually seen
|
// check if tx was actually seen
|
||||||
let tx_id = transfer.tx_hash;
|
let tx_id = transfer.tx_hash;
|
||||||
@ -51,7 +52,7 @@ async fn fund_transfer_and_check_tx_key() {
|
|||||||
.await
|
.await
|
||||||
.expect("failed to check tx by key");
|
.expect("failed to check tx by key");
|
||||||
|
|
||||||
assert_eq!(res.received, send_to_bob);
|
assert_that!(res.received).is_equal_to(send_to_bob);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_wallet_to_catch_up(wallet: &MoneroWalletRpc, expected_balance: u64) {
|
async fn wait_for_wallet_to_catch_up(wallet: &MoneroWalletRpc, expected_balance: u64) {
|
||||||
|
@ -12,12 +12,12 @@ jsonrpc_client = { version = "0.7", features = [ "reqwest" ] }
|
|||||||
monero = "0.12"
|
monero = "0.12"
|
||||||
monero-epee-bin-serde = "1"
|
monero-epee-bin-serde = "1"
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
reqwest = { version = "0.12", default-features = false, features = [ "json" ] }
|
reqwest = { version = "0.11", default-features = false, features = [ "json" ] }
|
||||||
rust_decimal = { version = "1", features = [ "serde-float" ] }
|
rust_decimal = { version = "1", features = [ "serde-float" ] }
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = "0.4"
|
hex-literal = "0.3"
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
|
@ -47,10 +47,9 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_o_indexes(&self, txid: Hash) -> Result<GetOIndexesResponse> {
|
pub async fn get_o_indexes(&self, txid: Hash) -> Result<GetOIndexesResponse> {
|
||||||
self.binary_request(
|
self.binary_request(self.get_o_indexes_bin_url.clone(), GetOIndexesPayload {
|
||||||
self.get_o_indexes_bin_url.clone(),
|
txid,
|
||||||
GetOIndexesPayload { txid },
|
})
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +157,7 @@ pub struct OutKey {
|
|||||||
pub unlocked: bool,
|
pub unlocked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
pub struct BaseResponse {
|
pub struct BaseResponse {
|
||||||
pub credits: u64,
|
pub credits: u64,
|
||||||
pub status: Status,
|
pub status: Status,
|
||||||
@ -166,7 +165,7 @@ pub struct BaseResponse {
|
|||||||
pub untrusted: bool,
|
pub untrusted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
pub struct GetOIndexesResponse {
|
pub struct GetOIndexesResponse {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub base: BaseResponse,
|
pub base: BaseResponse,
|
||||||
@ -174,7 +173,7 @@ pub struct GetOIndexesResponse {
|
|||||||
pub o_indexes: Vec<u64>,
|
pub o_indexes: Vec<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
#[serde(rename = "OK")]
|
#[serde(rename = "OK")]
|
||||||
Ok,
|
Ok,
|
||||||
@ -195,7 +194,7 @@ mod monero_serde_hex_block {
|
|||||||
{
|
{
|
||||||
let hex = String::deserialize(deserializer)?;
|
let hex = String::deserialize(deserializer)?;
|
||||||
|
|
||||||
let bytes = hex::decode(hex).map_err(D::Error::custom)?;
|
let bytes = hex::decode(&hex).map_err(D::Error::custom)?;
|
||||||
let mut cursor = Cursor::new(bytes);
|
let mut cursor = Cursor::new(bytes);
|
||||||
|
|
||||||
let block = monero::Block::consensus_decode(&mut cursor).map_err(D::Error::custom)?;
|
let block = monero::Block::consensus_decode(&mut cursor).map_err(D::Error::custom)?;
|
||||||
|
@ -157,17 +157,11 @@ pub struct Transfer {
|
|||||||
pub unsigned_txset: String,
|
pub unsigned_txset: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct BlockHeight {
|
pub struct BlockHeight {
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for BlockHeight {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||||
#[serde(from = "CheckTxKeyResponse")]
|
#[serde(from = "CheckTxKeyResponse")]
|
||||||
pub struct CheckTxKey {
|
pub struct CheckTxKey {
|
||||||
|
@ -14,6 +14,6 @@ rand = "0.7"
|
|||||||
curve25519-dalek = "3"
|
curve25519-dalek = "3"
|
||||||
monero-harness = { path = "../monero-harness" }
|
monero-harness = { path = "../monero-harness" }
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
testcontainers = "0.15"
|
testcontainers = "0.12"
|
||||||
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs" ] }
|
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs" ] }
|
||||||
tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "chrono", "tracing-log" ] }
|
tracing-subscriber = { version = "0.2", default-features = false, features = [ "fmt", "ansi", "env-filter", "chrono", "tracing-log" ] }
|
||||||
|
@ -61,12 +61,13 @@ mod tests {
|
|||||||
use monero_harness::image::Monerod;
|
use monero_harness::image::Monerod;
|
||||||
use monero_rpc::monerod::{Client, GetOutputsOut};
|
use monero_rpc::monerod::{Client, GetOutputsOut};
|
||||||
use testcontainers::clients::Cli;
|
use testcontainers::clients::Cli;
|
||||||
|
use testcontainers::Docker;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_outs_for_key_offsets() {
|
async fn get_outs_for_key_offsets() {
|
||||||
let cli = Cli::default();
|
let cli = Cli::default();
|
||||||
let container = cli.run(Monerod);
|
let container = cli.run(Monerod::default());
|
||||||
let rpc_client = Client::localhost(container.get_host_port_ipv4(18081)).unwrap();
|
let rpc_client = Client::localhost(container.get_host_port(18081).unwrap()).unwrap();
|
||||||
rpc_client.generateblocks(150, "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".to_owned()).await.unwrap();
|
rpc_client.generateblocks(150, "498AVruCDWgP9Az9LjMm89VWjrBrSZ2W2K3HFBiyzzrRjUJWUcCVxvY1iitfuKoek2FdX6MKGAD9Qb1G1P8QgR5jPmmt3Vj".to_owned()).await.unwrap();
|
||||||
let wallet = Wallet {
|
let wallet = Wallet {
|
||||||
client: rpc_client.clone(),
|
client: rpc_client.clone(),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.74" # also update this in the readme, changelog, and github actions
|
channel = "1.62"
|
||||||
components = ["clippy"]
|
components = ["clippy"]
|
||||||
targets = ["armv7-unknown-linux-gnueabihf"]
|
targets = ["armv7-unknown-linux-gnueabihf"]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "swap"
|
name = "swap"
|
||||||
version = "0.13.4"
|
version = "0.12.1"
|
||||||
authors = [ "The COMIT guys <hello@comit.network>" ]
|
authors = [ "The COMIT guys <hello@comit.network>" ]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "XMR/BTC trustless atomic swaps."
|
description = "XMR/BTC trustless atomic swaps."
|
||||||
@ -14,37 +14,32 @@ async-compression = { version = "0.3", features = [ "bzip2", "tokio" ] }
|
|||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
atty = "0.2"
|
atty = "0.2"
|
||||||
backoff = { version = "0.4", features = [ "tokio" ] }
|
backoff = { version = "0.4", features = [ "tokio" ] }
|
||||||
base64 = "0.22"
|
base64 = "0.20"
|
||||||
bdk = "0.28"
|
bdk = "0.25"
|
||||||
big-bytes = "1"
|
big-bytes = "1"
|
||||||
bitcoin = { version = "0.29", features = [ "rand", "serde" ] }
|
bitcoin = { version = "0.29", features = [ "rand", "serde" ] }
|
||||||
bmrng = "0.5"
|
bmrng = "0.5"
|
||||||
comfy-table = "7.1"
|
comfy-table = "6.1"
|
||||||
config = { version = "0.14", default-features = false, features = [ "toml" ] }
|
config = { version = "0.13", default-features = false, features = [ "toml" ] }
|
||||||
conquer-once = "0.4"
|
conquer-once = "0.3"
|
||||||
curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" }
|
curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" }
|
||||||
data-encoding = "2.6"
|
data-encoding = "2.3"
|
||||||
dialoguer = "0.11"
|
dialoguer = "0.10"
|
||||||
digest = "0.10.7"
|
|
||||||
directories-next = "2"
|
directories-next = "2"
|
||||||
ecdsa_fun = { version = "0.10", default-features = false, features = [ "libsecp_compat", "serde", "adaptor" ] }
|
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "libsecp_compat", "serde", "adaptor" ] }
|
||||||
ed25519-dalek = "1"
|
ed25519-dalek = "1"
|
||||||
futures = { version = "0.3", default-features = false }
|
futures = { version = "0.3", default-features = false }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
itertools = "0.13"
|
itertools = "0.10"
|
||||||
jsonrpsee = { version = "0.16.2", features = [ "server" ] }
|
|
||||||
jsonrpsee-core = "0.16.2"
|
|
||||||
libp2p = { version = "0.42.2", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous", "identify" ] }
|
libp2p = { version = "0.42.2", default-features = false, features = [ "tcp-tokio", "yamux", "mplex", "dns-tokio", "noise", "request-response", "websocket", "ping", "rendezvous", "identify" ] }
|
||||||
monero = { version = "0.12", features = [ "serde_support" ] }
|
monero = { version = "0.12", features = [ "serde_support" ] }
|
||||||
monero-rpc = { path = "../monero-rpc" }
|
monero-rpc = { path = "../monero-rpc" }
|
||||||
once_cell = "1.20"
|
pem = "1.1"
|
||||||
pem = "3.0"
|
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
qrcode = "0.14"
|
qrcode = "0.12"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.3"
|
||||||
regex = "1.11"
|
reqwest = { version = "0.11", features = [ "rustls-tls", "stream", "socks" ], default-features = false }
|
||||||
reqwest = { version = "0.12", features = [ "http2", "rustls-tls", "stream", "socks" ], default-features = false }
|
|
||||||
rust_decimal = { version = "1", features = [ "serde-float" ] }
|
rust_decimal = { version = "1", features = [ "serde-float" ] }
|
||||||
rust_decimal_macros = "1"
|
rust_decimal_macros = "1"
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
@ -52,24 +47,24 @@ serde_cbor = "0.11"
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_with = { version = "1", features = [ "macros" ] }
|
serde_with = { version = "1", features = [ "macros" ] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
sigma_fun = { version = "0.7", default-features = false, features = [ "ed25519", "serde", "secp256k1", "alloc" ] }
|
sigma_fun = { git = "https://github.com/LLFourn/secp256kfun", default-features = false, features = [ "ed25519", "serde", "secp256k1", "alloc" ] }
|
||||||
sqlx = { version = "0.6.3", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] }
|
sqlx = { version = "0.6", features = [ "sqlite", "runtime-tokio-rustls", "offline" ] }
|
||||||
structopt = "0.3"
|
structopt = "0.3"
|
||||||
strum = { version = "0.26", features = [ "derive" ] }
|
strum = { version = "0.24", features = [ "derive" ] }
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "parking_lot" ] }
|
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs", "net" ] }
|
||||||
tokio-socks = "0.5"
|
tokio-socks = "0.5"
|
||||||
tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] }
|
tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] }
|
||||||
tokio-util = { version = "0.7", features = [ "io", "codec" ] }
|
tokio-util = { version = "0.7", features = [ "io", "codec" ] }
|
||||||
toml = "0.8"
|
toml = "0.5"
|
||||||
torut = { version = "0.2", default-features = false, features = [ "v3", "control" ] }
|
torut = { version = "0.2", default-features = false, features = [ "v3", "control" ] }
|
||||||
tracing = { version = "0.1", features = [ "attributes" ] }
|
tracing = { version = "0.1", features = [ "attributes" ] }
|
||||||
tracing-appender = "0.2"
|
tracing-appender = "0.2"
|
||||||
tracing-futures = { version = "0.2", features = [ "std-future", "futures-03" ] }
|
tracing-futures = { version = "0.2", features = [ "std-future", "futures-03" ] }
|
||||||
tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "time", "tracing-log", "json" ] }
|
tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "time", "tracing-log", "json" ] }
|
||||||
url = { version = "2", features = [ "serde" ] }
|
url = { version = "2", features = [ "serde" ] }
|
||||||
uuid = { version = "1.11", features = [ "serde", "v4" ] }
|
uuid = { version = "1.2", features = [ "serde", "v4" ] }
|
||||||
void = "1"
|
void = "1"
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
@ -79,20 +74,18 @@ tokio-tar = "0.3"
|
|||||||
zip = "0.5"
|
zip = "0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bitcoin-harness = { git = "https://github.com/delta1/bitcoin-harness-rs.git", rev = "80cc8d05db2610d8531011be505b7bee2b5cdf9f" }
|
bitcoin-harness = "0.2.2"
|
||||||
get-port = "3"
|
get-port = "3"
|
||||||
hyper = "1.5"
|
hyper = "0.14"
|
||||||
jsonrpsee = { version = "0.16.2", features = [ "ws-client" ] }
|
|
||||||
mockito = "1.5"
|
|
||||||
monero-harness = { path = "../monero-harness" }
|
monero-harness = { path = "../monero-harness" }
|
||||||
port_check = "0.2"
|
port_check = "0.1"
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
sequential-test = "0.2.4"
|
|
||||||
serde_cbor = "0.11"
|
serde_cbor = "0.11"
|
||||||
serial_test = "3.1"
|
serial_test = "0.10"
|
||||||
|
spectral = "0.6"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
testcontainers = "0.15"
|
testcontainers = "0.12"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
vergen = { version = "8.3", default-features = false, features = [ "build", "git", "git2" ] }
|
vergen = { version = "7", default-features = false, features = [ "git", "build" ] }
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use vergen::EmitBuilder;
|
use vergen::{vergen, Config, SemverKind};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
EmitBuilder::builder()
|
let mut config = Config::default();
|
||||||
.git_describe(true, true, None)
|
*config.git_mut().semver_kind_mut() = SemverKind::Lightweight;
|
||||||
.emit()?;
|
|
||||||
Ok(())
|
vergen(config)
|
||||||
}
|
}
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
-- This migration script modifies swap states to be compatible with the new state structure introduced in PR #1676.
|
|
||||||
-- The following changes are made:
|
|
||||||
-- 1. Bob: BtcPunished state now has a new attribute 'state' (type: State6), 'tx_lock_id' (type: string) remains the same
|
|
||||||
-- 2. Bob: State6 has two new attributes: 'v' (monero viewkey) and 'monero_wallet_restore_blockheight' (type: BlockHeight)
|
|
||||||
-- State6 is used in BtcPunished, CancelTimelockExpired, BtcCancelled, BtcRefunded states
|
|
||||||
-- 3. Alice: BtcPunished state now has a new attribute 'state3' (type: State3)
|
|
||||||
|
|
||||||
-- Alice: Add new attribute 'state3' (type: State3) to the BtcPunished state by copying it from the BtcLocked state
|
|
||||||
UPDATE swap_states SET
|
|
||||||
state = json_replace( -- Replaces "{"Alice":{"Done":"BtcPunished"}}" with "{"Alice": {"Done": "BtcPunished": {"state": <state3 object from BtcLocked>} }}"
|
|
||||||
state,
|
|
||||||
'$.Alice.Done',
|
|
||||||
json_object(
|
|
||||||
'BtcPunished',
|
|
||||||
(
|
|
||||||
SELECT json_extract(states.state, '$.Alice.BtcLocked') -- Read state3 object from BtcLocked
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id -- swap_states.swap_id is id of the BtcPunished row
|
|
||||||
AND json_extract(states.state, '$.Alice.BtcLocked') IS NOT NULL -- Filters out only the BtcLocked state
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE json_extract(state, '$.Alice.Done') = 'BtcPunished'; -- Apply update only to BtcPunished state rows
|
|
||||||
|
|
||||||
-- Bob: Add new attribute 'state6' (type: State6) to the BtcPunished state by copying it from the BtcCancelled state
|
|
||||||
-- and add new State6 attributes 'v' and 'monero_wallet_restore_blockheight' from the BtcLocked state
|
|
||||||
UPDATE swap_states SET
|
|
||||||
state = json_replace(
|
|
||||||
state,
|
|
||||||
'$.Bob', -- Replace '{"Bob":{"Done": {"BtcPunished": {"tx_lock_id":"..."} }}}' with {"Bob":{"BtcPunished":{"state":{<state6 object>}, "tx_lock_id": "..."}}}
|
|
||||||
json_object(
|
|
||||||
'BtcPunished', -- {"Bob":{"BtcPunished":{}}
|
|
||||||
json_object(
|
|
||||||
'state', -- {"Bob":{"BtcPunished":{"state": {}}}
|
|
||||||
json_insert(
|
|
||||||
( -- object that we insert properties into (original state6 from BtcCancelled state)
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcCancelled') -- Get state6 from BtcCancelled state
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcCancelled') IS NOT NULL -- Filters out only the BtcCancelled state
|
|
||||||
),
|
|
||||||
'$.v', -- {"Bob":{"BtcPunished":{"state": {..., "v": "..."}, "tx_lock_id": "..."}}}
|
|
||||||
( -- Get v property from BtcLocked state
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcLocked.state3.v')
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id -- swap_states.swap_id is id of the BtcPunished row
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL -- Filters out only the BtcLocked state
|
|
||||||
),
|
|
||||||
'$.monero_wallet_restore_blockheight', -- { "Bob": { "BtcPunished":{"state": {..., "monero_wallet_restore_blockheight": {"height":...}} }, "tx_lock_id": "..."} } }
|
|
||||||
( -- Get monero_wallet_restore_blockheight property from BtcLocked state
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcLocked.monero_wallet_restore_blockheight')
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id -- swap_states.swap_id is id of the BtcPunished row, states.swap_id is id of the row that we are looking for
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL -- Filters out only the BtcLocked state
|
|
||||||
)
|
|
||||||
),
|
|
||||||
'tx_lock_id', -- Insert tx_lock_id BtcPunished -> {"Bob": {"Done": {"BtcPunished": {"state":{<state object>}, "tx_lock_id": "..."} } }
|
|
||||||
json_extract(state, '$.Bob.Done.BtcPunished.tx_lock_id') -- Gets tx_lock_id from original state row
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE json_extract(state, '$.Bob.Done.BtcPunished') IS NOT NULL; -- Apply update only to BtcPunished state rows
|
|
||||||
|
|
||||||
-- Bob: Add new State6 attributes 'v' and 'monero_wallet_restore_blockheight' to the BtcRefunded state
|
|
||||||
UPDATE swap_states SET
|
|
||||||
state = json_insert(
|
|
||||||
state, -- Object that we insert properties into (original state from the row)
|
|
||||||
'$.Bob.Done.BtcRefunded.v', -- {"Bob":{"BtcRefunded":{..., "v": "..."}}}
|
|
||||||
(
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcLocked.state3.v')
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id -- swap_states.swap_id is id of the BtcRefunded row, states.swap_id is id of the row that we are looking for
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL
|
|
||||||
),
|
|
||||||
'$.Bob.Done.BtcRefunded.monero_wallet_restore_blockheight', -- {"Bob":{"BtcRefunded":{..., "monero_wallet_restore_blockheight": {"height":...}} }}
|
|
||||||
(
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcLocked.monero_wallet_restore_blockheight')
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE json_extract(state, '$.Bob.Done.BtcRefunded') IS NOT NULL; -- Apply update only to BtcRefunded state rows
|
|
||||||
|
|
||||||
-- Bob: Add new State6 attributes 'v' and 'monero_wallet_restore_blockheight' to the BtcCancelled state
|
|
||||||
UPDATE swap_states SET
|
|
||||||
state = json_insert(
|
|
||||||
state,
|
|
||||||
'$.Bob.BtcCancelled.v',
|
|
||||||
(
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcLocked.state3.v')
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL
|
|
||||||
),
|
|
||||||
'$.Bob.BtcCancelled.monero_wallet_restore_blockheight',
|
|
||||||
(
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcLocked.monero_wallet_restore_blockheight')
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE json_extract(state, '$.Bob.BtcCancelled') IS NOT NULL; -- Apply update only to BtcCancelled state rows
|
|
||||||
|
|
||||||
-- Bob: Add new State6 attributes 'v' and 'monero_wallet_restore_blockheight' to the CancelTimelockExpired state
|
|
||||||
UPDATE swap_states SET
|
|
||||||
state = json_insert(
|
|
||||||
state,
|
|
||||||
'$.Bob.CancelTimelockExpired.v',
|
|
||||||
(
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcLocked.state3.v')
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL
|
|
||||||
),
|
|
||||||
'$.Bob.CancelTimelockExpired.monero_wallet_restore_blockheight',
|
|
||||||
(
|
|
||||||
SELECT json_extract(states.state, '$.Bob.BtcLocked.monero_wallet_restore_blockheight')
|
|
||||||
FROM swap_states AS states
|
|
||||||
WHERE
|
|
||||||
states.swap_id = swap_states.swap_id
|
|
||||||
AND json_extract(states.state, '$.Bob.BtcLocked') IS NOT NULL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE json_extract(state, '$.Bob.CancelTimelockExpired') IS NOT NULL; -- Apply update only to CancelTimelockExpired state rows
|
|
@ -1,5 +0,0 @@
|
|||||||
CREATE TABLE if NOT EXISTS buffered_transfer_proofs
|
|
||||||
(
|
|
||||||
swap_id TEXT PRIMARY KEY NOT NULL,
|
|
||||||
proof TEXT NOT NULL
|
|
||||||
);
|
|
@ -1,8 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# run this script from the swap dir
|
# run this script from the swap dir
|
||||||
# make sure you have sqlx-cli installed: cargo install --version 0.6.3 sqlx-cli
|
# make sure you have sqlx-cli installed: cargo install sqlx-cli
|
||||||
# it's advised for the sqlx-cli to be the same version as specified in cargo.toml
|
|
||||||
|
|
||||||
# this script creates a temporary sqlite database
|
# this script creates a temporary sqlite database
|
||||||
# then runs the migration scripts to create the tables (migrations folder)
|
# then runs the migration scripts to create the tables (migrations folder)
|
||||||
|
@ -28,24 +28,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n insert into peer_addresses (\n peer_id,\n address\n ) values (?, ?);\n "
|
"query": "\n insert into peer_addresses (\n peer_id,\n address\n ) values (?, ?);\n "
|
||||||
},
|
},
|
||||||
"0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "start_date",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
true
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT min(entered_at) as start_date\n FROM swap_states\n WHERE swap_id = ?\n "
|
|
||||||
},
|
|
||||||
"1ec38c85e7679b2eb42b3df75d9098772ce44fdb8db3012d3c2410d828b74157": {
|
"1ec38c85e7679b2eb42b3df75d9098772ce44fdb8db3012d3c2410d828b74157": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
@ -80,30 +62,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n insert into peers (\n swap_id,\n peer_id\n ) values (?, ?);\n "
|
"query": "\n insert into peers (\n swap_id,\n peer_id\n ) values (?, ?);\n "
|
||||||
},
|
},
|
||||||
"3f2bfdd2d134586ccad22171cd85a465800fc5c4fdaf191d206974e530240c87": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "swap_id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "state",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT swap_id, state\n FROM swap_states\n "
|
|
||||||
},
|
|
||||||
"50a5764546f69c118fa0b64120da50f51073d36257d49768de99ff863e3511e0": {
|
"50a5764546f69c118fa0b64120da50f51073d36257d49768de99ff863e3511e0": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -132,6 +90,24 @@
|
|||||||
},
|
},
|
||||||
"query": "\n SELECT state\n FROM swap_states\n WHERE swap_id = ?\n ORDER BY id desc\n LIMIT 1;\n\n "
|
"query": "\n SELECT state\n FROM swap_states\n WHERE swap_id = ?\n ORDER BY id desc\n LIMIT 1;\n\n "
|
||||||
},
|
},
|
||||||
|
"a0eb85d04ee3842c52291dad4d225941d1141af735922fcbc665868997fce304": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "address",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n SELECT address\n FROM peer_addresses\n WHERE peer_id = ?\n "
|
||||||
|
},
|
||||||
"b703032b4ddc627a1124817477e7a8e5014bdc694c36a14053ef3bb2fc0c69b0": {
|
"b703032b4ddc627a1124817477e7a8e5014bdc694c36a14053ef3bb2fc0c69b0": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -159,69 +135,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"query": "\n SELECT address\n FROM monero_addresses\n WHERE swap_id = ?\n "
|
"query": "\n SELECT address\n FROM monero_addresses\n WHERE swap_id = ?\n "
|
||||||
},
|
|
||||||
"d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "address",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT DISTINCT address\n FROM peer_addresses\n WHERE peer_id = ?\n "
|
|
||||||
},
|
|
||||||
"e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "state",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT state\n FROM swap_states\n WHERE swap_id = ?\n "
|
|
||||||
},
|
|
||||||
"e36c287aa98ae80ad4b6bb6f7e4b59cced041406a9db71da827b09f0d3bacfd6": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"nullable": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n INSERT INTO buffered_transfer_proofs (\n swap_id,\n proof\n ) VALUES (?, ?);\n "
|
|
||||||
},
|
|
||||||
"e9d422daf774d099fcbde6c4cda35821da948bd86cc57798b4d8375baf0b51ae": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "proof",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT proof\n FROM buffered_transfer_proofs\n WHERE swap_id = ?\n "
|
|
||||||
}
|
}
|
||||||
}
|
}
|
478
swap/src/api.rs
478
swap/src/api.rs
@ -1,478 +0,0 @@
|
|||||||
pub mod request;
|
|
||||||
use crate::cli::command::{Bitcoin, Monero, Tor};
|
|
||||||
use crate::common::tracing_util::Format;
|
|
||||||
use crate::database::{open_db, AccessMode};
|
|
||||||
use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet};
|
|
||||||
use crate::fs::system_data_dir;
|
|
||||||
use crate::network::rendezvous::XmrBtcNamespace;
|
|
||||||
use crate::protocol::Database;
|
|
||||||
use crate::seed::Seed;
|
|
||||||
use crate::{bitcoin, common, monero};
|
|
||||||
use anyhow::{bail, Context as AnyContext, Error, Result};
|
|
||||||
use futures::future::try_join_all;
|
|
||||||
use std::fmt;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::{Arc, Once};
|
|
||||||
use tokio::sync::{broadcast, broadcast::Sender, Mutex, RwLock};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tracing::level_filters::LevelFilter;
|
|
||||||
use tracing::Level;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
static START: Once = Once::new();
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
tor_socks5_port: u16,
|
|
||||||
namespace: XmrBtcNamespace,
|
|
||||||
server_address: Option<SocketAddr>,
|
|
||||||
pub env_config: EnvConfig,
|
|
||||||
seed: Option<Seed>,
|
|
||||||
debug: bool,
|
|
||||||
json: bool,
|
|
||||||
data_dir: PathBuf,
|
|
||||||
is_testnet: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct PendingTaskList(Mutex<Vec<JoinHandle<()>>>);
|
|
||||||
|
|
||||||
impl PendingTaskList {
|
|
||||||
pub async fn spawn<F, T>(&self, future: F)
|
|
||||||
where
|
|
||||||
F: Future<Output = T> + Send + 'static,
|
|
||||||
T: Send + 'static,
|
|
||||||
{
|
|
||||||
let handle = tokio::spawn(async move {
|
|
||||||
let _ = future.await;
|
|
||||||
});
|
|
||||||
|
|
||||||
self.0.lock().await.push(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wait_for_tasks(&self) -> Result<()> {
|
|
||||||
let tasks = {
|
|
||||||
// Scope for the lock, to avoid holding it for the entire duration of the async block
|
|
||||||
let mut guard = self.0.lock().await;
|
|
||||||
guard.drain(..).collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
|
|
||||||
try_join_all(tasks).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SwapLock {
|
|
||||||
current_swap: RwLock<Option<Uuid>>,
|
|
||||||
suspension_trigger: Sender<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SwapLock {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (suspension_trigger, _) = broadcast::channel(10);
|
|
||||||
SwapLock {
|
|
||||||
current_swap: RwLock::new(None),
|
|
||||||
suspension_trigger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn listen_for_swap_force_suspension(&self) -> Result<(), Error> {
|
|
||||||
let mut listener = self.suspension_trigger.subscribe();
|
|
||||||
let event = listener.recv().await;
|
|
||||||
match event {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Error receiving swap suspension signal: {}", e);
|
|
||||||
bail!(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn acquire_swap_lock(&self, swap_id: Uuid) -> Result<(), Error> {
|
|
||||||
let mut current_swap = self.current_swap.write().await;
|
|
||||||
if current_swap.is_some() {
|
|
||||||
bail!("There already exists an active swap lock");
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!(swap_id = %swap_id, "Acquiring swap lock");
|
|
||||||
*current_swap = Some(swap_id);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_current_swap_id(&self) -> Option<Uuid> {
|
|
||||||
*self.current_swap.read().await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a signal to suspend all ongoing swap processes.
|
|
||||||
///
|
|
||||||
/// This function performs the following steps:
|
|
||||||
/// 1. Triggers the suspension by sending a unit `()` signal to all listeners via `self.suspension_trigger`.
|
|
||||||
/// 2. Polls the `current_swap` state every 50 milliseconds to check if it has been set to `None`, indicating that the swap processes have been suspended and the lock released.
|
|
||||||
/// 3. If the lock is not released within 10 seconds, the function returns an error.
|
|
||||||
///
|
|
||||||
/// If we send a suspend signal while no swap is in progress, the function will not fail, but will return immediately.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// - `Ok(())` if the swap lock is successfully released.
|
|
||||||
/// - `Err(Error)` if the function times out waiting for the swap lock to be released.
|
|
||||||
///
|
|
||||||
/// # Notes
|
|
||||||
/// The 50ms polling interval is considered negligible overhead compared to the typical time required to suspend ongoing swap processes.
|
|
||||||
pub async fn send_suspend_signal(&self) -> Result<(), Error> {
|
|
||||||
const TIMEOUT: u64 = 10_000;
|
|
||||||
const INTERVAL: u64 = 50;
|
|
||||||
|
|
||||||
let _ = self.suspension_trigger.send(())?;
|
|
||||||
|
|
||||||
for _ in 0..(TIMEOUT / INTERVAL) {
|
|
||||||
if self.get_current_swap_id().await.is_none() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(INTERVAL)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("Timed out waiting for swap lock to be released");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn release_swap_lock(&self) -> Result<Uuid, Error> {
|
|
||||||
let mut current_swap = self.current_swap.write().await;
|
|
||||||
if let Some(swap_id) = current_swap.as_ref() {
|
|
||||||
tracing::debug!(swap_id = %swap_id, "Releasing swap lock");
|
|
||||||
|
|
||||||
let prev_swap_id = *swap_id;
|
|
||||||
*current_swap = None;
|
|
||||||
drop(current_swap);
|
|
||||||
Ok(prev_swap_id)
|
|
||||||
} else {
|
|
||||||
bail!("There is no current swap lock to release");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SwapLock {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// workaround for warning over monero_rpc_process which we must own but not read
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct Context {
|
|
||||||
pub db: Arc<dyn Database + Send + Sync>,
|
|
||||||
bitcoin_wallet: Option<Arc<bitcoin::Wallet>>,
|
|
||||||
monero_wallet: Option<Arc<monero::Wallet>>,
|
|
||||||
monero_rpc_process: Option<monero::WalletRpcProcess>,
|
|
||||||
pub swap_lock: Arc<SwapLock>,
|
|
||||||
pub config: Config,
|
|
||||||
pub tasks: Arc<PendingTaskList>,
|
|
||||||
pub is_daemon: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
impl Context {
|
|
||||||
pub async fn build(
|
|
||||||
bitcoin: Option<Bitcoin>,
|
|
||||||
monero: Option<Monero>,
|
|
||||||
tor: Option<Tor>,
|
|
||||||
data: Option<PathBuf>,
|
|
||||||
is_testnet: bool,
|
|
||||||
debug: bool,
|
|
||||||
json: bool,
|
|
||||||
server_address: Option<SocketAddr>,
|
|
||||||
is_daemon: bool,
|
|
||||||
) -> Result<Context> {
|
|
||||||
let data_dir = data::data_dir_from(data, is_testnet)?;
|
|
||||||
let env_config = env_config_from(is_testnet);
|
|
||||||
|
|
||||||
let format = if json { Format::Json } else { Format::Raw };
|
|
||||||
let level_filter = if debug {
|
|
||||||
LevelFilter::from_level(Level::DEBUG)
|
|
||||||
} else {
|
|
||||||
LevelFilter::from_level(Level::INFO)
|
|
||||||
};
|
|
||||||
|
|
||||||
START.call_once(|| {
|
|
||||||
let _ = common::tracing_util::init(level_filter, format, data_dir.join("logs"));
|
|
||||||
});
|
|
||||||
|
|
||||||
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
|
||||||
.context("Failed to read seed in file")?;
|
|
||||||
|
|
||||||
let bitcoin_wallet = {
|
|
||||||
if let Some(bitcoin) = bitcoin {
|
|
||||||
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
|
|
||||||
bitcoin.apply_defaults(is_testnet)?;
|
|
||||||
Some(Arc::new(
|
|
||||||
init_bitcoin_wallet(
|
|
||||||
bitcoin_electrum_rpc_url,
|
|
||||||
&seed,
|
|
||||||
data_dir.clone(),
|
|
||||||
env_config,
|
|
||||||
bitcoin_target_block,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (monero_wallet, monero_rpc_process) = {
|
|
||||||
if let Some(monero) = monero {
|
|
||||||
let monero_daemon_address = monero.apply_defaults(is_testnet);
|
|
||||||
let (wlt, prc) =
|
|
||||||
init_monero_wallet(data_dir.clone(), monero_daemon_address, env_config).await?;
|
|
||||||
(Some(Arc::new(wlt)), Some(prc))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tor_socks5_port = tor.map_or(9050, |tor| tor.tor_socks5_port);
|
|
||||||
|
|
||||||
let context = Context {
|
|
||||||
db: open_db(data_dir.join("sqlite"), AccessMode::ReadWrite).await?,
|
|
||||||
bitcoin_wallet,
|
|
||||||
monero_wallet,
|
|
||||||
monero_rpc_process,
|
|
||||||
config: Config {
|
|
||||||
tor_socks5_port,
|
|
||||||
namespace: XmrBtcNamespace::from_is_testnet(is_testnet),
|
|
||||||
env_config,
|
|
||||||
seed: Some(seed),
|
|
||||||
server_address,
|
|
||||||
debug,
|
|
||||||
json,
|
|
||||||
is_testnet,
|
|
||||||
data_dir,
|
|
||||||
},
|
|
||||||
swap_lock: Arc::new(SwapLock::new()),
|
|
||||||
tasks: Arc::new(PendingTaskList::default()),
|
|
||||||
is_daemon,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn for_harness(
|
|
||||||
seed: Seed,
|
|
||||||
env_config: EnvConfig,
|
|
||||||
db_path: PathBuf,
|
|
||||||
bob_bitcoin_wallet: Arc<bitcoin::Wallet>,
|
|
||||||
bob_monero_wallet: Arc<monero::Wallet>,
|
|
||||||
) -> Self {
|
|
||||||
let config = Config::for_harness(seed, env_config);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
bitcoin_wallet: Some(bob_bitcoin_wallet),
|
|
||||||
monero_wallet: Some(bob_monero_wallet),
|
|
||||||
config,
|
|
||||||
db: open_db(db_path, AccessMode::ReadWrite)
|
|
||||||
.await
|
|
||||||
.expect("Could not open sqlite database"),
|
|
||||||
monero_rpc_process: None,
|
|
||||||
swap_lock: Arc::new(SwapLock::new()),
|
|
||||||
tasks: Arc::new(PendingTaskList::default()),
|
|
||||||
is_daemon: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bitcoin_wallet(&self) -> Option<Arc<bitcoin::Wallet>> {
|
|
||||||
self.bitcoin_wallet.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Context {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn init_bitcoin_wallet(
|
|
||||||
electrum_rpc_url: Url,
|
|
||||||
seed: &Seed,
|
|
||||||
data_dir: PathBuf,
|
|
||||||
env_config: EnvConfig,
|
|
||||||
bitcoin_target_block: usize,
|
|
||||||
) -> Result<bitcoin::Wallet> {
|
|
||||||
let wallet_dir = data_dir.join("wallet");
|
|
||||||
|
|
||||||
let wallet = bitcoin::Wallet::new(
|
|
||||||
electrum_rpc_url.clone(),
|
|
||||||
&wallet_dir,
|
|
||||||
seed.derive_extended_private_key(env_config.bitcoin_network)?,
|
|
||||||
env_config,
|
|
||||||
bitcoin_target_block,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.context("Failed to initialize Bitcoin wallet")?;
|
|
||||||
|
|
||||||
wallet.sync().await?;
|
|
||||||
|
|
||||||
Ok(wallet)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn init_monero_wallet(
|
|
||||||
data_dir: PathBuf,
|
|
||||||
monero_daemon_address: String,
|
|
||||||
env_config: EnvConfig,
|
|
||||||
) -> Result<(monero::Wallet, monero::WalletRpcProcess)> {
|
|
||||||
let network = env_config.monero_network;
|
|
||||||
|
|
||||||
const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet";
|
|
||||||
|
|
||||||
let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?;
|
|
||||||
|
|
||||||
let monero_wallet_rpc_process = monero_wallet_rpc
|
|
||||||
.run(network, Some(monero_daemon_address))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let monero_wallet = monero::Wallet::open_or_create(
|
|
||||||
monero_wallet_rpc_process.endpoint(),
|
|
||||||
MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(),
|
|
||||||
env_config,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((monero_wallet, monero_wallet_rpc_process))
|
|
||||||
}
|
|
||||||
|
|
||||||
mod data {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub fn data_dir_from(arg_dir: Option<PathBuf>, testnet: bool) -> Result<PathBuf> {
|
|
||||||
let base_dir = match arg_dir {
|
|
||||||
Some(custom_base_dir) => custom_base_dir,
|
|
||||||
None => os_default()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let sub_directory = if testnet { "testnet" } else { "mainnet" };
|
|
||||||
|
|
||||||
Ok(base_dir.join(sub_directory))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn os_default() -> Result<PathBuf> {
|
|
||||||
Ok(system_data_dir()?.join("cli"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn env_config_from(testnet: bool) -> EnvConfig {
|
|
||||||
if testnet {
|
|
||||||
Testnet::get_config()
|
|
||||||
} else {
|
|
||||||
Mainnet::get_config()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn for_harness(seed: Seed, env_config: EnvConfig) -> Self {
|
|
||||||
let data_dir = data::data_dir_from(None, false).expect("Could not find data directory");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
tor_socks5_port: 9050,
|
|
||||||
namespace: XmrBtcNamespace::from_is_testnet(false),
|
|
||||||
server_address: None,
|
|
||||||
env_config,
|
|
||||||
seed: Some(seed),
|
|
||||||
debug: false,
|
|
||||||
json: false,
|
|
||||||
is_testnet: false,
|
|
||||||
data_dir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub mod api_test {
|
|
||||||
use super::*;
|
|
||||||
use crate::api::request::{Method, Request};
|
|
||||||
|
|
||||||
use libp2p::Multiaddr;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub const MULTI_ADDRESS: &str =
|
|
||||||
"/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi";
|
|
||||||
pub const MONERO_STAGENET_ADDRESS: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a";
|
|
||||||
pub const BITCOIN_TESTNET_ADDRESS: &str = "tb1qr3em6k3gfnyl8r7q0v7t4tlnyxzgxma3lressv";
|
|
||||||
pub const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa";
|
|
||||||
pub const BITCOIN_MAINNET_ADDRESS: &str = "bc1qe4epnfklcaa0mun26yz5g8k24em5u9f92hy325";
|
|
||||||
pub const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b";
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn default(
|
|
||||||
is_testnet: bool,
|
|
||||||
data_dir: Option<PathBuf>,
|
|
||||||
debug: bool,
|
|
||||||
json: bool,
|
|
||||||
) -> Self {
|
|
||||||
let data_dir = data::data_dir_from(data_dir, is_testnet).unwrap();
|
|
||||||
|
|
||||||
let seed = Seed::from_file_or_generate(data_dir.as_path()).unwrap();
|
|
||||||
|
|
||||||
let env_config = env_config_from(is_testnet);
|
|
||||||
Self {
|
|
||||||
tor_socks5_port: 9050,
|
|
||||||
namespace: XmrBtcNamespace::from_is_testnet(is_testnet),
|
|
||||||
server_address: None,
|
|
||||||
env_config,
|
|
||||||
seed: Some(seed),
|
|
||||||
debug,
|
|
||||||
json,
|
|
||||||
is_testnet,
|
|
||||||
data_dir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
pub fn buy_xmr(is_testnet: bool) -> Request {
|
|
||||||
let seller = Multiaddr::from_str(MULTI_ADDRESS).unwrap();
|
|
||||||
let bitcoin_change_address = {
|
|
||||||
if is_testnet {
|
|
||||||
bitcoin::Address::from_str(BITCOIN_TESTNET_ADDRESS).unwrap()
|
|
||||||
} else {
|
|
||||||
bitcoin::Address::from_str(BITCOIN_MAINNET_ADDRESS).unwrap()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let monero_receive_address = {
|
|
||||||
if is_testnet {
|
|
||||||
monero::Address::from_str(MONERO_STAGENET_ADDRESS).unwrap()
|
|
||||||
} else {
|
|
||||||
monero::Address::from_str(MONERO_MAINNET_ADDRESS).unwrap()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Request::new(Method::BuyXmr {
|
|
||||||
seller,
|
|
||||||
bitcoin_change_address: Some(bitcoin_change_address),
|
|
||||||
monero_receive_address,
|
|
||||||
swap_id: Uuid::new_v4(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume() -> Request {
|
|
||||||
Request::new(Method::Resume {
|
|
||||||
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cancel() -> Request {
|
|
||||||
Request::new(Method::CancelAndRefund {
|
|
||||||
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refund() -> Request {
|
|
||||||
Request::new(Method::CancelAndRefund {
|
|
||||||
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -4,10 +4,10 @@ mod event_loop;
|
|||||||
mod network;
|
mod network;
|
||||||
mod rate;
|
mod rate;
|
||||||
mod recovery;
|
mod recovery;
|
||||||
|
pub mod tracing;
|
||||||
|
|
||||||
pub use event_loop::{EventLoop, EventLoopHandle, FixedRate, KrakenRate, LatestRate};
|
pub use event_loop::{EventLoop, EventLoopHandle, FixedRate, KrakenRate, LatestRate};
|
||||||
pub use network::behaviour::{Behaviour, OutEvent};
|
pub use network::behaviour::{Behaviour, OutEvent};
|
||||||
pub use network::rendezvous::RendezvousNode;
|
|
||||||
pub use network::transport;
|
pub use network::transport;
|
||||||
pub use rate::Rate;
|
pub use rate::Rate;
|
||||||
pub use recovery::cancel::cancel;
|
pub use recovery::cancel::cancel;
|
||||||
@ -18,4 +18,4 @@ pub use recovery::safely_abort::safely_abort;
|
|||||||
pub use recovery::{cancel, refund};
|
pub use recovery::{cancel, refund};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use network::rendezvous;
|
pub use network::rendezous;
|
||||||
|
@ -19,6 +19,7 @@ where
|
|||||||
let args = RawArguments::from_clap(&matches);
|
let args = RawArguments::from_clap(&matches);
|
||||||
|
|
||||||
let json = args.json;
|
let json = args.json;
|
||||||
|
let disable_timestamp = args.disable_timestamp;
|
||||||
let testnet = args.testnet;
|
let testnet = args.testnet;
|
||||||
let config = args.config;
|
let config = args.config;
|
||||||
let command: RawCommand = args.cmd;
|
let command: RawCommand = args.cmd;
|
||||||
@ -27,35 +28,23 @@ where
|
|||||||
RawCommand::Start { resume_only } => Arguments {
|
RawCommand::Start { resume_only } => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::Start { resume_only },
|
cmd: Command::Start { resume_only },
|
||||||
},
|
},
|
||||||
RawCommand::History { only_unfinished } => Arguments {
|
RawCommand::History => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::History { only_unfinished },
|
cmd: Command::History,
|
||||||
},
|
|
||||||
RawCommand::Logs {
|
|
||||||
logs_dir: dir_path,
|
|
||||||
swap_id,
|
|
||||||
redact,
|
|
||||||
} => Arguments {
|
|
||||||
testnet,
|
|
||||||
json,
|
|
||||||
config_path: config_path(config, testnet)?,
|
|
||||||
env_config: env_config(testnet),
|
|
||||||
cmd: Command::Logs {
|
|
||||||
logs_dir: dir_path,
|
|
||||||
swap_id,
|
|
||||||
redact,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
RawCommand::WithdrawBtc { amount, address } => Arguments {
|
RawCommand::WithdrawBtc { amount, address } => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::WithdrawBtc {
|
cmd: Command::WithdrawBtc {
|
||||||
@ -66,6 +55,7 @@ where
|
|||||||
RawCommand::Balance => Arguments {
|
RawCommand::Balance => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::Balance,
|
cmd: Command::Balance,
|
||||||
@ -73,6 +63,7 @@ where
|
|||||||
RawCommand::Config => Arguments {
|
RawCommand::Config => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::Config,
|
cmd: Command::Config,
|
||||||
@ -80,6 +71,7 @@ where
|
|||||||
RawCommand::ExportBitcoinWallet => Arguments {
|
RawCommand::ExportBitcoinWallet => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::ExportBitcoinWallet,
|
cmd: Command::ExportBitcoinWallet,
|
||||||
@ -90,6 +82,7 @@ where
|
|||||||
}) => Arguments {
|
}) => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::Redeem {
|
cmd: Command::Redeem {
|
||||||
@ -103,6 +96,7 @@ where
|
|||||||
}) => Arguments {
|
}) => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::Cancel { swap_id },
|
cmd: Command::Cancel { swap_id },
|
||||||
@ -112,6 +106,7 @@ where
|
|||||||
}) => Arguments {
|
}) => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::Refund { swap_id },
|
cmd: Command::Refund { swap_id },
|
||||||
@ -121,6 +116,7 @@ where
|
|||||||
}) => Arguments {
|
}) => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::Punish { swap_id },
|
cmd: Command::Punish { swap_id },
|
||||||
@ -128,6 +124,7 @@ where
|
|||||||
RawCommand::ManualRecovery(ManualRecovery::SafelyAbort { swap_id }) => Arguments {
|
RawCommand::ManualRecovery(ManualRecovery::SafelyAbort { swap_id }) => Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path: config_path(config, testnet)?,
|
config_path: config_path(config, testnet)?,
|
||||||
env_config: env_config(testnet),
|
env_config: env_config(testnet),
|
||||||
cmd: Command::SafelyAbort { swap_id },
|
cmd: Command::SafelyAbort { swap_id },
|
||||||
@ -174,7 +171,7 @@ fn env_config(is_testnet: bool) -> env::Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)]
|
||||||
#[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
|
#[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
|
||||||
pub struct BitcoinAddressNetworkMismatch {
|
pub struct BitcoinAddressNetworkMismatch {
|
||||||
#[serde(with = "crate::bitcoin::network")]
|
#[serde(with = "crate::bitcoin::network")]
|
||||||
@ -183,29 +180,23 @@ pub struct BitcoinAddressNetworkMismatch {
|
|||||||
actual: bitcoin::Network,
|
actual: bitcoin::Network,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct Arguments {
|
pub struct Arguments {
|
||||||
pub testnet: bool,
|
pub testnet: bool,
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
|
pub disable_timestamp: bool,
|
||||||
pub config_path: PathBuf,
|
pub config_path: PathBuf,
|
||||||
pub env_config: env::Config,
|
pub env_config: env::Config,
|
||||||
pub cmd: Command,
|
pub cmd: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Start {
|
Start {
|
||||||
resume_only: bool,
|
resume_only: bool,
|
||||||
},
|
},
|
||||||
History {
|
History,
|
||||||
only_unfinished: bool,
|
|
||||||
},
|
|
||||||
Config,
|
Config,
|
||||||
Logs {
|
|
||||||
logs_dir: Option<PathBuf>,
|
|
||||||
swap_id: Option<Uuid>,
|
|
||||||
redact: bool,
|
|
||||||
},
|
|
||||||
WithdrawBtc {
|
WithdrawBtc {
|
||||||
amount: Option<Amount>,
|
amount: Option<Amount>,
|
||||||
address: Address,
|
address: Address,
|
||||||
@ -235,7 +226,7 @@ pub enum Command {
|
|||||||
name = "asb",
|
name = "asb",
|
||||||
about = "Automated Swap Backend for swapping XMR for BTC",
|
about = "Automated Swap Backend for swapping XMR for BTC",
|
||||||
author,
|
author,
|
||||||
version = env!("VERGEN_GIT_DESCRIBE")
|
version = env!("VERGEN_GIT_SEMVER_LIGHTWEIGHT")
|
||||||
)]
|
)]
|
||||||
pub struct RawArguments {
|
pub struct RawArguments {
|
||||||
#[structopt(long, help = "Swap on testnet")]
|
#[structopt(long, help = "Swap on testnet")]
|
||||||
@ -277,33 +268,8 @@ pub enum RawCommand {
|
|||||||
)]
|
)]
|
||||||
resume_only: bool,
|
resume_only: bool,
|
||||||
},
|
},
|
||||||
#[structopt(about = "Prints all logging messages issued in the past.")]
|
|
||||||
Logs {
|
|
||||||
#[structopt(
|
|
||||||
short = "d",
|
|
||||||
help = "Print the logs from this directory instead of the default one."
|
|
||||||
)]
|
|
||||||
logs_dir: Option<PathBuf>,
|
|
||||||
#[structopt(
|
|
||||||
help = "Redact swap-ids, Bitcoin and Monero addresses.",
|
|
||||||
long = "redact"
|
|
||||||
)]
|
|
||||||
redact: bool,
|
|
||||||
#[structopt(
|
|
||||||
long = "swap-id",
|
|
||||||
help = "Filter for logs concerning this swap.",
|
|
||||||
long_help = "This checks whether each logging message contains the swap id. Some messages might be skipped when they don't contain the swap id even though they're relevant."
|
|
||||||
)]
|
|
||||||
swap_id: Option<Uuid>,
|
|
||||||
},
|
|
||||||
#[structopt(about = "Prints swap-id and the state of each swap ever made.")]
|
#[structopt(about = "Prints swap-id and the state of each swap ever made.")]
|
||||||
History {
|
History,
|
||||||
#[structopt(
|
|
||||||
long = "only-unfinished",
|
|
||||||
help = "If set, only unfinished swaps will be printed."
|
|
||||||
)]
|
|
||||||
only_unfinished: bool,
|
|
||||||
},
|
|
||||||
#[structopt(about = "Prints the current config")]
|
#[structopt(about = "Prints the current config")]
|
||||||
Config,
|
Config,
|
||||||
#[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")]
|
#[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")]
|
||||||
@ -400,6 +366,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::Start { resume_only: false },
|
cmd: Command::Start { resume_only: false },
|
||||||
@ -417,11 +384,10 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::History {
|
cmd: Command::History,
|
||||||
only_unfinished: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
let args = parse_args(raw_ars).unwrap();
|
let args = parse_args(raw_ars).unwrap();
|
||||||
assert_eq!(expected_args, args);
|
assert_eq!(expected_args, args);
|
||||||
@ -436,6 +402,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::Balance,
|
cmd: Command::Balance,
|
||||||
@ -457,6 +424,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::WithdrawBtc {
|
cmd: Command::WithdrawBtc {
|
||||||
@ -483,6 +451,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::Cancel {
|
cmd: Command::Cancel {
|
||||||
@ -508,6 +477,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::Refund {
|
cmd: Command::Refund {
|
||||||
@ -533,6 +503,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::Punish {
|
cmd: Command::Punish {
|
||||||
@ -558,6 +529,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::SafelyAbort {
|
cmd: Command::SafelyAbort {
|
||||||
@ -577,6 +549,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: true,
|
testnet: true,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_testnet_conf_path,
|
config_path: default_testnet_conf_path,
|
||||||
env_config: testnet_env_config,
|
env_config: testnet_env_config,
|
||||||
cmd: Command::Start { resume_only: false },
|
cmd: Command::Start { resume_only: false },
|
||||||
@ -594,11 +567,10 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: true,
|
testnet: true,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_testnet_conf_path,
|
config_path: default_testnet_conf_path,
|
||||||
env_config: testnet_env_config,
|
env_config: testnet_env_config,
|
||||||
cmd: Command::History {
|
cmd: Command::History,
|
||||||
only_unfinished: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
let args = parse_args(raw_ars).unwrap();
|
let args = parse_args(raw_ars).unwrap();
|
||||||
assert_eq!(expected_args, args);
|
assert_eq!(expected_args, args);
|
||||||
@ -613,6 +585,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: true,
|
testnet: true,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_testnet_conf_path,
|
config_path: default_testnet_conf_path,
|
||||||
env_config: testnet_env_config,
|
env_config: testnet_env_config,
|
||||||
cmd: Command::Balance,
|
cmd: Command::Balance,
|
||||||
@ -636,6 +609,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: true,
|
testnet: true,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_testnet_conf_path,
|
config_path: default_testnet_conf_path,
|
||||||
env_config: testnet_env_config,
|
env_config: testnet_env_config,
|
||||||
cmd: Command::WithdrawBtc {
|
cmd: Command::WithdrawBtc {
|
||||||
@ -662,6 +636,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: true,
|
testnet: true,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_testnet_conf_path,
|
config_path: default_testnet_conf_path,
|
||||||
env_config: testnet_env_config,
|
env_config: testnet_env_config,
|
||||||
cmd: Command::Cancel {
|
cmd: Command::Cancel {
|
||||||
@ -688,6 +663,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: true,
|
testnet: true,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_testnet_conf_path,
|
config_path: default_testnet_conf_path,
|
||||||
env_config: testnet_env_config,
|
env_config: testnet_env_config,
|
||||||
cmd: Command::Refund {
|
cmd: Command::Refund {
|
||||||
@ -714,6 +690,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: true,
|
testnet: true,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_testnet_conf_path,
|
config_path: default_testnet_conf_path,
|
||||||
env_config: testnet_env_config,
|
env_config: testnet_env_config,
|
||||||
cmd: Command::Punish {
|
cmd: Command::Punish {
|
||||||
@ -740,6 +717,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: true,
|
testnet: true,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: false,
|
||||||
config_path: default_testnet_conf_path,
|
config_path: default_testnet_conf_path,
|
||||||
env_config: testnet_env_config,
|
env_config: testnet_env_config,
|
||||||
cmd: Command::SafelyAbort {
|
cmd: Command::SafelyAbort {
|
||||||
@ -759,6 +737,7 @@ mod tests {
|
|||||||
let expected_args = Arguments {
|
let expected_args = Arguments {
|
||||||
testnet: false,
|
testnet: false,
|
||||||
json: false,
|
json: false,
|
||||||
|
disable_timestamp: true,
|
||||||
config_path: default_mainnet_conf_path,
|
config_path: default_mainnet_conf_path,
|
||||||
env_config: mainnet_env_config,
|
env_config: mainnet_env_config,
|
||||||
cmd: Command::Start { resume_only: false },
|
cmd: Command::Start { resume_only: false },
|
||||||
|
@ -84,7 +84,7 @@ const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64;
|
|||||||
const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
|
const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
|
||||||
const DEFAULT_SPREAD: f64 = 0.02f64;
|
const DEFAULT_SPREAD: f64 = 0.02f64;
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub data: Data,
|
pub data: Data,
|
||||||
@ -123,19 +123,19 @@ impl TryFrom<config::Config> for Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
pub dir: PathBuf,
|
pub dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
#[serde(deserialize_with = "addr_list::deserialize")]
|
#[serde(deserialize_with = "addr_list::deserialize")]
|
||||||
pub listen: Vec<Multiaddr>,
|
pub listen: Vec<Multiaddr>,
|
||||||
#[serde(default, deserialize_with = "addr_list::deserialize")]
|
#[serde(default)]
|
||||||
pub rendezvous_point: Vec<Multiaddr>,
|
pub rendezvous_point: Option<Multiaddr>,
|
||||||
#[serde(default, deserialize_with = "addr_list::deserialize")]
|
#[serde(default, deserialize_with = "addr_list::deserialize")]
|
||||||
pub external_addresses: Vec<Multiaddr>,
|
pub external_addresses: Vec<Multiaddr>,
|
||||||
}
|
}
|
||||||
@ -156,7 +156,7 @@ mod addr_list {
|
|||||||
let list: Result<Vec<_>, _> = s
|
let list: Result<Vec<_>, _> = s
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.map(|s| s.trim().parse().map_err(de::Error::custom))
|
.map(|s| s.parse().map_err(de::Error::custom))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(list?)
|
Ok(list?)
|
||||||
}
|
}
|
||||||
@ -165,7 +165,7 @@ mod addr_list {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
if let Value::String(s) = v {
|
if let Value::String(s) = v {
|
||||||
s.trim().parse().map_err(de::Error::custom)
|
s.parse().map_err(de::Error::custom)
|
||||||
} else {
|
} else {
|
||||||
Err(de::Error::custom("expected a string"))
|
Err(de::Error::custom("expected a string"))
|
||||||
}
|
}
|
||||||
@ -181,7 +181,7 @@ mod addr_list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Bitcoin {
|
pub struct Bitcoin {
|
||||||
pub electrum_rpc_url: Url,
|
pub electrum_rpc_url: Url,
|
||||||
@ -191,7 +191,7 @@ pub struct Bitcoin {
|
|||||||
pub network: bitcoin::Network,
|
pub network: bitcoin::Network,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Monero {
|
pub struct Monero {
|
||||||
pub wallet_rpc_url: Url,
|
pub wallet_rpc_url: Url,
|
||||||
@ -200,14 +200,14 @@ pub struct Monero {
|
|||||||
pub network: monero::Network,
|
pub network: monero::Network,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct TorConf {
|
pub struct TorConf {
|
||||||
pub control_port: u16,
|
pub control_port: u16,
|
||||||
pub socks5_port: u16,
|
pub socks5_port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Maker {
|
pub struct Maker {
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_btc")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_btc")]
|
||||||
@ -347,27 +347,10 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
|||||||
}
|
}
|
||||||
let ask_spread = Decimal::from_f64(ask_spread).context("Unable to parse spread")?;
|
let ask_spread = Decimal::from_f64(ask_spread).context("Unable to parse spread")?;
|
||||||
|
|
||||||
let mut number = 1;
|
let rendezvous_point = Input::<Multiaddr>::with_theme(&ColorfulTheme::default())
|
||||||
let mut done = false;
|
.with_prompt("Do you want to advertise your ASB instance with a rendezvous node? Enter an empty string if not.")
|
||||||
let mut rendezvous_points = Vec::new();
|
.allow_empty(true)
|
||||||
println!("ASB can register with multiple rendezvous nodes for discoverability. This can also be edited in the config file later.");
|
.interact_text()?;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
@ -375,7 +358,11 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
|||||||
data: Data { dir: data_dir },
|
data: Data { dir: data_dir },
|
||||||
network: Network {
|
network: Network {
|
||||||
listen: listen_addresses,
|
listen: listen_addresses,
|
||||||
rendezvous_point: rendezvous_points, // keeping the singular key name for backcompat
|
rendezvous_point: if rendezvous_point.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(rendezvous_point)
|
||||||
|
},
|
||||||
external_addresses: vec![],
|
external_addresses: vec![],
|
||||||
},
|
},
|
||||||
bitcoin: Bitcoin {
|
bitcoin: Bitcoin {
|
||||||
@ -430,7 +417,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
network: Network {
|
network: Network {
|
||||||
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
|
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
|
||||||
rendezvous_point: vec![],
|
rendezvous_point: None,
|
||||||
external_addresses: vec![],
|
external_addresses: vec![],
|
||||||
},
|
},
|
||||||
monero: Monero {
|
monero: Monero {
|
||||||
@ -474,7 +461,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
network: Network {
|
network: Network {
|
||||||
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
|
listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws],
|
||||||
rendezvous_point: vec![],
|
rendezvous_point: None,
|
||||||
external_addresses: vec![],
|
external_addresses: vec![],
|
||||||
},
|
},
|
||||||
monero: Monero {
|
monero: Monero {
|
||||||
@ -528,7 +515,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
network: Network {
|
network: Network {
|
||||||
listen,
|
listen,
|
||||||
rendezvous_point: vec![],
|
rendezvous_point: None,
|
||||||
external_addresses,
|
external_addresses,
|
||||||
},
|
},
|
||||||
monero: Monero {
|
monero: Monero {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
use crate::asb::{Behaviour, OutEvent, Rate};
|
use crate::asb::{Behaviour, OutEvent, Rate};
|
||||||
use crate::monero::Amount;
|
use crate::monero::Amount;
|
||||||
use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason;
|
|
||||||
use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected};
|
|
||||||
use crate::network::quote::BidQuote;
|
use crate::network::quote::BidQuote;
|
||||||
use crate::network::swap_setup::alice::WalletSnapshot;
|
use crate::network::swap_setup::alice::WalletSnapshot;
|
||||||
use crate::network::transfer_proof;
|
use crate::network::transfer_proof;
|
||||||
@ -255,61 +253,8 @@ where
|
|||||||
channel
|
channel
|
||||||
}.boxed());
|
}.boxed());
|
||||||
}
|
}
|
||||||
SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemRequested { swap_id, channel, peer }) => {
|
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::Registered { .. })) => {
|
||||||
let swap_peer = self.db.get_peer_id(swap_id).await;
|
tracing::info!("Successfully registered with rendezvous node");
|
||||||
let swap_state = self.db.get_state(swap_id).await;
|
|
||||||
|
|
||||||
let (swap_peer, swap_state) = match (swap_peer, swap_state) {
|
|
||||||
(Ok(peer), Ok(state)) => (peer, state),
|
|
||||||
_ => {
|
|
||||||
tracing::warn!(
|
|
||||||
swap_id = %swap_id,
|
|
||||||
received_from = %peer,
|
|
||||||
reason = "swap not found",
|
|
||||||
"Rejecting cooperative XMR redeem request"
|
|
||||||
);
|
|
||||||
if self.swarm.behaviour_mut().cooperative_xmr_redeem.send_response(channel, Rejected { swap_id, reason: CooperativeXmrRedeemRejectReason::UnknownSwap }).is_err() {
|
|
||||||
tracing::error!(swap_id = %swap_id, "Failed to reject cooperative XMR redeem request");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if swap_peer != peer {
|
|
||||||
tracing::warn!(
|
|
||||||
swap_id = %swap_id,
|
|
||||||
received_from = %peer,
|
|
||||||
expected_from = %swap_peer,
|
|
||||||
reason = "unexpected peer",
|
|
||||||
"Rejecting cooperative XMR redeem request"
|
|
||||||
);
|
|
||||||
if self.swarm.behaviour_mut().cooperative_xmr_redeem.send_response(channel, Rejected { swap_id, reason: CooperativeXmrRedeemRejectReason::MaliciousRequest }).is_err() {
|
|
||||||
tracing::error!(swap_id = %swap_id, "Failed to reject cooperative XMR redeem request");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let State::Alice (AliceState::BtcPunished { state3 }) = swap_state else {
|
|
||||||
tracing::warn!(
|
|
||||||
swap_id = %swap_id,
|
|
||||||
reason = "swap is in invalid state",
|
|
||||||
"Rejecting cooperative XMR redeem request"
|
|
||||||
);
|
|
||||||
if self.swarm.behaviour_mut().cooperative_xmr_redeem.send_response(channel, Rejected { swap_id, reason: CooperativeXmrRedeemRejectReason::SwapInvalidState }).is_err() {
|
|
||||||
tracing::error!(swap_id = %swap_id, "Failed to reject cooperative XMR redeem request");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.swarm.behaviour_mut().cooperative_xmr_redeem.send_response(channel, Fullfilled { swap_id, s_a: state3.s_a }).is_err() {
|
|
||||||
tracing::error!(peer = %peer, "Failed to respond to cooperative XMR redeem request");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(swap_id = %swap_id, peer = %peer, "Fullfilled cooperative XMR redeem request");
|
|
||||||
}
|
|
||||||
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::Registered { rendezvous_node, ttl, namespace })) => {
|
|
||||||
tracing::info!("Successfully registered with rendezvous node: {} with namespace: {} and TTL: {:?}", rendezvous_node, namespace, ttl);
|
|
||||||
}
|
}
|
||||||
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::RegisterFailed(error))) => {
|
SwarmEvent::Behaviour(OutEvent::Rendezvous(libp2p::rendezvous::client::Event::RegisterFailed(error))) => {
|
||||||
tracing::error!("Registration with rendezvous node failed: {:?}", error);
|
tracing::error!("Registration with rendezvous node failed: {:?}", error);
|
||||||
@ -334,10 +279,10 @@ where
|
|||||||
SwarmEvent::IncomingConnectionError { send_back_addr: address, error, .. } => {
|
SwarmEvent::IncomingConnectionError { send_back_addr: address, error, .. } => {
|
||||||
tracing::warn!(%address, "Failed to set up connection with peer: {:#}", error);
|
tracing::warn!(%address, "Failed to set up connection with peer: {:#}", error);
|
||||||
}
|
}
|
||||||
SwarmEvent::ConnectionClosed { peer_id: peer, num_established: 0, endpoint, cause: Some(error) } => {
|
SwarmEvent::ConnectionClosed { peer_id: peer, num_established, endpoint, cause: Some(error) } if num_established == 0 => {
|
||||||
tracing::debug!(%peer, address = %endpoint.get_remote_address(), "Lost connection to peer: {:#}", error);
|
tracing::debug!(%peer, address = %endpoint.get_remote_address(), "Lost connection to peer: {:#}", error);
|
||||||
}
|
}
|
||||||
SwarmEvent::ConnectionClosed { peer_id: peer, num_established: 0, endpoint, cause: None } => {
|
SwarmEvent::ConnectionClosed { peer_id: peer, num_established, endpoint, cause: None } if num_established == 0 => {
|
||||||
tracing::info!(%peer, address = %endpoint.get_remote_address(), "Successfully closed connection");
|
tracing::info!(%peer, address = %endpoint.get_remote_address(), "Successfully closed connection");
|
||||||
}
|
}
|
||||||
SwarmEvent::NewListenAddr{address, ..} => {
|
SwarmEvent::NewListenAddr{address, ..} => {
|
||||||
@ -351,7 +296,7 @@ where
|
|||||||
Some(Ok((peer, transfer_proof, responder))) => {
|
Some(Ok((peer, transfer_proof, responder))) => {
|
||||||
if !self.swarm.behaviour_mut().transfer_proof.is_connected(&peer) {
|
if !self.swarm.behaviour_mut().transfer_proof.is_connected(&peer) {
|
||||||
tracing::warn!(%peer, "No active connection to peer, buffering transfer proof");
|
tracing::warn!(%peer, "No active connection to peer, buffering transfer proof");
|
||||||
self.buffered_transfer_proofs.entry(peer).or_default().push((transfer_proof, responder));
|
self.buffered_transfer_proofs.entry(peer).or_insert_with(Vec::new).push((transfer_proof, responder));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,7 @@ use crate::network::rendezvous::XmrBtcNamespace;
|
|||||||
use crate::network::swap_setup::alice;
|
use crate::network::swap_setup::alice;
|
||||||
use crate::network::swap_setup::alice::WalletSnapshot;
|
use crate::network::swap_setup::alice::WalletSnapshot;
|
||||||
use crate::network::transport::authenticate_and_multiplex;
|
use crate::network::transport::authenticate_and_multiplex;
|
||||||
use crate::network::{
|
use crate::network::{encrypted_signature, quote, transfer_proof};
|
||||||
cooperative_xmr_redeem_after_punish, encrypted_signature, quote, transfer_proof,
|
|
||||||
};
|
|
||||||
use crate::protocol::alice::State3;
|
use crate::protocol::alice::State3;
|
||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{anyhow, Error, Result};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
@ -46,9 +44,7 @@ pub mod transport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod behaviour {
|
pub mod behaviour {
|
||||||
use libp2p::swarm::behaviour::toggle::Toggle;
|
use super::*;
|
||||||
|
|
||||||
use super::{rendezvous::RendezvousNode, *};
|
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -78,11 +74,6 @@ pub mod behaviour {
|
|||||||
channel: ResponseChannel<()>,
|
channel: ResponseChannel<()>,
|
||||||
peer: PeerId,
|
peer: PeerId,
|
||||||
},
|
},
|
||||||
CooperativeXmrRedeemRequested {
|
|
||||||
channel: ResponseChannel<cooperative_xmr_redeem_after_punish::Response>,
|
|
||||||
swap_id: Uuid,
|
|
||||||
peer: PeerId,
|
|
||||||
},
|
|
||||||
Rendezvous(libp2p::rendezvous::client::Event),
|
Rendezvous(libp2p::rendezvous::client::Event),
|
||||||
Failure {
|
Failure {
|
||||||
peer: PeerId,
|
peer: PeerId,
|
||||||
@ -117,11 +108,10 @@ pub mod behaviour {
|
|||||||
where
|
where
|
||||||
LR: LatestRate + Send + 'static,
|
LR: LatestRate + Send + 'static,
|
||||||
{
|
{
|
||||||
pub rendezvous: Toggle<rendezvous::Behaviour>,
|
pub rendezvous: libp2p::swarm::behaviour::toggle::Toggle<rendezous::Behaviour>,
|
||||||
pub quote: quote::Behaviour,
|
pub quote: quote::Behaviour,
|
||||||
pub swap_setup: alice::Behaviour<LR>,
|
pub swap_setup: alice::Behaviour<LR>,
|
||||||
pub transfer_proof: transfer_proof::Behaviour,
|
pub transfer_proof: transfer_proof::Behaviour,
|
||||||
pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour,
|
|
||||||
pub encrypted_signature: encrypted_signature::Behaviour,
|
pub encrypted_signature: encrypted_signature::Behaviour,
|
||||||
pub identify: Identify,
|
pub identify: Identify,
|
||||||
|
|
||||||
@ -142,22 +132,25 @@ pub mod behaviour {
|
|||||||
resume_only: bool,
|
resume_only: bool,
|
||||||
env_config: env::Config,
|
env_config: env::Config,
|
||||||
identify_params: (identity::Keypair, XmrBtcNamespace),
|
identify_params: (identity::Keypair, XmrBtcNamespace),
|
||||||
rendezvous_nodes: Vec<RendezvousNode>,
|
rendezvous_params: Option<(identity::Keypair, PeerId, Multiaddr, XmrBtcNamespace)>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (identity, namespace) = identify_params;
|
let agentVersion = format!("asb/{} ({})", env!("CARGO_PKG_VERSION"), identify_params.1);
|
||||||
let agent_version = format!("asb/{} ({})", env!("CARGO_PKG_VERSION"), namespace);
|
let protocolVersion = "/comit/xmr/btc/1.0.0".to_string();
|
||||||
let protocol_version = "/comit/xmr/btc/1.0.0".to_string();
|
let identifyConfig = IdentifyConfig::new(protocolVersion, identify_params.0.public())
|
||||||
let identifyConfig = IdentifyConfig::new(protocol_version, identity.public())
|
.with_agent_version(agentVersion);
|
||||||
.with_agent_version(agent_version);
|
|
||||||
|
|
||||||
let behaviour = if rendezvous_nodes.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(rendezvous::Behaviour::new(identity, rendezvous_nodes))
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
rendezvous: Toggle::from(behaviour),
|
rendezvous: libp2p::swarm::behaviour::toggle::Toggle::from(rendezvous_params.map(
|
||||||
|
|(identity, rendezvous_peer_id, rendezvous_address, namespace)| {
|
||||||
|
rendezous::Behaviour::new(
|
||||||
|
identity,
|
||||||
|
rendezvous_peer_id,
|
||||||
|
rendezvous_address,
|
||||||
|
namespace,
|
||||||
|
None, // use default ttl on rendezvous point
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)),
|
||||||
quote: quote::asb(),
|
quote: quote::asb(),
|
||||||
swap_setup: alice::Behaviour::new(
|
swap_setup: alice::Behaviour::new(
|
||||||
min_buy,
|
min_buy,
|
||||||
@ -168,7 +161,6 @@ pub mod behaviour {
|
|||||||
),
|
),
|
||||||
transfer_proof: transfer_proof::alice(),
|
transfer_proof: transfer_proof::alice(),
|
||||||
encrypted_signature: encrypted_signature::alice(),
|
encrypted_signature: encrypted_signature::alice(),
|
||||||
cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::alice(),
|
|
||||||
ping: Ping::new(PingConfig::new().with_keep_alive(true)),
|
ping: Ping::new(PingConfig::new().with_keep_alive(true)),
|
||||||
identify: Identify::new(identifyConfig),
|
identify: Identify::new(identifyConfig),
|
||||||
}
|
}
|
||||||
@ -194,14 +186,13 @@ pub mod behaviour {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod rendezvous {
|
pub mod rendezous {
|
||||||
use super::*;
|
use super::*;
|
||||||
use libp2p::swarm::dial_opts::DialOpts;
|
use libp2p::swarm::dial_opts::DialOpts;
|
||||||
use libp2p::swarm::DialError;
|
use libp2p::swarm::DialError;
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(PartialEq)]
|
||||||
enum ConnectionStatus {
|
enum ConnectionStatus {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
Dialling,
|
Dialling,
|
||||||
@ -218,59 +209,39 @@ pub mod rendezvous {
|
|||||||
|
|
||||||
pub struct Behaviour {
|
pub struct Behaviour {
|
||||||
inner: libp2p::rendezvous::client::Behaviour,
|
inner: libp2p::rendezvous::client::Behaviour,
|
||||||
rendezvous_nodes: Vec<RendezvousNode>,
|
rendezvous_point: Multiaddr,
|
||||||
to_dial: VecDeque<PeerId>,
|
rendezvous_peer_id: PeerId,
|
||||||
}
|
namespace: XmrBtcNamespace,
|
||||||
|
|
||||||
pub struct RendezvousNode {
|
|
||||||
pub address: Multiaddr,
|
|
||||||
connection_status: ConnectionStatus,
|
|
||||||
pub peer_id: PeerId,
|
|
||||||
registration_status: RegistrationStatus,
|
registration_status: RegistrationStatus,
|
||||||
pub registration_ttl: Option<u64>,
|
connection_status: ConnectionStatus,
|
||||||
pub namespace: XmrBtcNamespace,
|
registration_ttl: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RendezvousNode {
|
impl Behaviour {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
address: &Multiaddr,
|
identity: identity::Keypair,
|
||||||
peer_id: PeerId,
|
rendezvous_peer_id: PeerId,
|
||||||
|
rendezvous_address: Multiaddr,
|
||||||
namespace: XmrBtcNamespace,
|
namespace: XmrBtcNamespace,
|
||||||
registration_ttl: Option<u64>,
|
registration_ttl: Option<u64>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
address: address.to_owned(),
|
inner: libp2p::rendezvous::client::Behaviour::new(identity),
|
||||||
connection_status: ConnectionStatus::Disconnected,
|
rendezvous_point: rendezvous_address,
|
||||||
|
rendezvous_peer_id,
|
||||||
namespace,
|
namespace,
|
||||||
peer_id,
|
|
||||||
registration_status: RegistrationStatus::RegisterOnNextConnection,
|
registration_status: RegistrationStatus::RegisterOnNextConnection,
|
||||||
|
connection_status: ConnectionStatus::Disconnected,
|
||||||
registration_ttl,
|
registration_ttl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_connection(&mut self, status: ConnectionStatus) {
|
fn register(&mut self) {
|
||||||
self.connection_status = status;
|
self.inner.register(
|
||||||
}
|
self.namespace.into(),
|
||||||
|
self.rendezvous_peer_id,
|
||||||
fn set_registration(&mut self, status: RegistrationStatus) {
|
self.registration_ttl,
|
||||||
self.registration_status = status;
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Behaviour {
|
|
||||||
pub fn new(identity: identity::Keypair, rendezvous_nodes: Vec<RendezvousNode>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: libp2p::rendezvous::client::Behaviour::new(identity),
|
|
||||||
rendezvous_nodes,
|
|
||||||
to_dial: VecDeque::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calls the rendezvous register method of the node at node_index in the Vec of rendezvous nodes
|
|
||||||
fn register(&mut self, node_index: usize) {
|
|
||||||
let node = &self.rendezvous_nodes[node_index];
|
|
||||||
self.inner
|
|
||||||
.register(node.namespace.into(), node.peer_id, node.registration_ttl);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,37 +255,31 @@ pub mod rendezvous {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec<Multiaddr> {
|
fn addresses_of_peer(&mut self, peer_id: &PeerId) -> Vec<Multiaddr> {
|
||||||
for node in self.rendezvous_nodes.iter() {
|
if peer_id == &self.rendezvous_peer_id {
|
||||||
if peer_id == &node.peer_id {
|
return vec![self.rendezvous_point.clone()];
|
||||||
return vec![node.address.clone()];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_connected(&mut self, peer_id: &PeerId) {
|
fn inject_connected(&mut self, peer_id: &PeerId) {
|
||||||
for i in 0..self.rendezvous_nodes.len() {
|
if peer_id == &self.rendezvous_peer_id {
|
||||||
if peer_id == &self.rendezvous_nodes[i].peer_id {
|
self.connection_status = ConnectionStatus::Connected;
|
||||||
self.rendezvous_nodes[i].set_connection(ConnectionStatus::Connected);
|
|
||||||
match &self.rendezvous_nodes[i].registration_status {
|
match &self.registration_status {
|
||||||
RegistrationStatus::RegisterOnNextConnection => {
|
RegistrationStatus::RegisterOnNextConnection => {
|
||||||
self.register(i);
|
self.register();
|
||||||
self.rendezvous_nodes[i].set_registration(RegistrationStatus::Pending);
|
self.registration_status = RegistrationStatus::Pending;
|
||||||
}
|
|
||||||
RegistrationStatus::Registered { .. } => {}
|
|
||||||
RegistrationStatus::Pending => {}
|
|
||||||
}
|
}
|
||||||
|
RegistrationStatus::Registered { .. } => {}
|
||||||
|
RegistrationStatus::Pending => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_disconnected(&mut self, peer_id: &PeerId) {
|
fn inject_disconnected(&mut self, peer_id: &PeerId) {
|
||||||
for i in 0..self.rendezvous_nodes.len() {
|
if peer_id == &self.rendezvous_peer_id {
|
||||||
let node = &mut self.rendezvous_nodes[i];
|
self.connection_status = ConnectionStatus::Disconnected;
|
||||||
if peer_id == &node.peer_id {
|
|
||||||
node.connection_status = ConnectionStatus::Disconnected;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,12 +298,9 @@ pub mod rendezvous {
|
|||||||
_handler: Self::ProtocolsHandler,
|
_handler: Self::ProtocolsHandler,
|
||||||
_error: &DialError,
|
_error: &DialError,
|
||||||
) {
|
) {
|
||||||
for i in 0..self.rendezvous_nodes.len() {
|
if let Some(id) = peer_id {
|
||||||
let node = &mut self.rendezvous_nodes[i];
|
if id == self.rendezvous_peer_id {
|
||||||
if let Some(id) = peer_id {
|
self.connection_status = ConnectionStatus::Disconnected;
|
||||||
if id == node.peer_id {
|
|
||||||
node.connection_status = ConnectionStatus::Disconnected;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -349,73 +311,62 @@ pub mod rendezvous {
|
|||||||
cx: &mut std::task::Context<'_>,
|
cx: &mut std::task::Context<'_>,
|
||||||
params: &mut impl PollParameters,
|
params: &mut impl PollParameters,
|
||||||
) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ProtocolsHandler>> {
|
) -> Poll<NetworkBehaviourAction<Self::OutEvent, Self::ProtocolsHandler>> {
|
||||||
if let Some(peer_id) = self.to_dial.pop_front() {
|
match &mut self.registration_status {
|
||||||
return Poll::Ready(NetworkBehaviourAction::Dial {
|
RegistrationStatus::RegisterOnNextConnection => match self.connection_status {
|
||||||
opts: DialOpts::peer_id(peer_id)
|
ConnectionStatus::Disconnected => {
|
||||||
.condition(PeerCondition::Disconnected)
|
self.connection_status = ConnectionStatus::Dialling;
|
||||||
.build(),
|
|
||||||
|
|
||||||
handler: Self::ProtocolsHandler::new(Duration::from_secs(30)),
|
return Poll::Ready(NetworkBehaviourAction::Dial {
|
||||||
});
|
opts: DialOpts::peer_id(self.rendezvous_peer_id)
|
||||||
}
|
.condition(PeerCondition::Disconnected)
|
||||||
// check the status of each rendezvous node
|
.build(),
|
||||||
for i in 0..self.rendezvous_nodes.len() {
|
|
||||||
let connection_status = self.rendezvous_nodes[i].connection_status.clone();
|
handler: Self::ProtocolsHandler::new(Duration::from_secs(30)),
|
||||||
match &mut self.rendezvous_nodes[i].registration_status {
|
});
|
||||||
RegistrationStatus::RegisterOnNextConnection => match connection_status {
|
}
|
||||||
ConnectionStatus::Disconnected => {
|
ConnectionStatus::Dialling => {}
|
||||||
self.rendezvous_nodes[i].set_connection(ConnectionStatus::Dialling);
|
ConnectionStatus::Connected => {
|
||||||
self.to_dial.push_back(self.rendezvous_nodes[i].peer_id);
|
self.registration_status = RegistrationStatus::Pending;
|
||||||
}
|
self.register();
|
||||||
ConnectionStatus::Dialling => {}
|
}
|
||||||
ConnectionStatus::Connected => {
|
},
|
||||||
self.rendezvous_nodes[i].set_registration(RegistrationStatus::Pending);
|
RegistrationStatus::Registered { re_register_in } => {
|
||||||
self.register(i);
|
if let Poll::Ready(()) = re_register_in.poll_unpin(cx) {
|
||||||
}
|
match self.connection_status {
|
||||||
},
|
ConnectionStatus::Connected => {
|
||||||
RegistrationStatus::Registered { re_register_in } => {
|
self.registration_status = RegistrationStatus::Pending;
|
||||||
if let Poll::Ready(()) = re_register_in.poll_unpin(cx) {
|
self.register();
|
||||||
match connection_status {
|
|
||||||
ConnectionStatus::Connected => {
|
|
||||||
self.rendezvous_nodes[i]
|
|
||||||
.set_registration(RegistrationStatus::Pending);
|
|
||||||
self.register(i);
|
|
||||||
}
|
|
||||||
ConnectionStatus::Disconnected => {
|
|
||||||
self.rendezvous_nodes[i].set_registration(
|
|
||||||
RegistrationStatus::RegisterOnNextConnection,
|
|
||||||
);
|
|
||||||
self.to_dial.push_back(self.rendezvous_nodes[i].peer_id);
|
|
||||||
}
|
|
||||||
ConnectionStatus::Dialling => {}
|
|
||||||
}
|
}
|
||||||
|
ConnectionStatus::Disconnected => {
|
||||||
|
self.registration_status =
|
||||||
|
RegistrationStatus::RegisterOnNextConnection;
|
||||||
|
|
||||||
|
return Poll::Ready(NetworkBehaviourAction::Dial {
|
||||||
|
opts: DialOpts::peer_id(self.rendezvous_peer_id)
|
||||||
|
.condition(PeerCondition::Disconnected)
|
||||||
|
.build(),
|
||||||
|
handler: Self::ProtocolsHandler::new(Duration::from_secs(30)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ConnectionStatus::Dialling => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RegistrationStatus::Pending => {}
|
|
||||||
}
|
}
|
||||||
|
RegistrationStatus::Pending => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let inner_poll = self.inner.poll(cx, params);
|
let inner_poll = self.inner.poll(cx, params);
|
||||||
|
|
||||||
// reset the timer for the specific rendezvous node if we successfully registered
|
// reset the timer if we successfully registered
|
||||||
if let Poll::Ready(NetworkBehaviourAction::GenerateEvent(
|
if let Poll::Ready(NetworkBehaviourAction::GenerateEvent(
|
||||||
libp2p::rendezvous::client::Event::Registered {
|
libp2p::rendezvous::client::Event::Registered { ttl, .. },
|
||||||
ttl,
|
|
||||||
rendezvous_node,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
)) = &inner_poll
|
)) = &inner_poll
|
||||||
{
|
{
|
||||||
if let Some(i) = self
|
let half_of_ttl = Duration::from_secs(*ttl) / 2;
|
||||||
.rendezvous_nodes
|
|
||||||
.iter()
|
self.registration_status = RegistrationStatus::Registered {
|
||||||
.position(|n| &n.peer_id == rendezvous_node)
|
re_register_in: Box::pin(tokio::time::sleep(half_of_ttl)),
|
||||||
{
|
};
|
||||||
let half_of_ttl = Duration::from_secs(*ttl) / 2;
|
|
||||||
let re_register_in = Box::pin(tokio::time::sleep(half_of_ttl));
|
|
||||||
let status = RegistrationStatus::Registered { re_register_in };
|
|
||||||
self.rendezvous_nodes[i].set_registration(status);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner_poll
|
inner_poll
|
||||||
@ -429,7 +380,6 @@ pub mod rendezvous {
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use libp2p::rendezvous;
|
use libp2p::rendezvous;
|
||||||
use libp2p::swarm::SwarmEvent;
|
use libp2p::swarm::SwarmEvent;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node(
|
async fn given_no_initial_connection_when_constructed_asb_connects_and_registers_with_rendezvous_node(
|
||||||
@ -437,16 +387,16 @@ pub mod rendezvous {
|
|||||||
let mut rendezvous_node = new_swarm(|_, _| {
|
let mut rendezvous_node = new_swarm(|_, _| {
|
||||||
rendezvous::server::Behaviour::new(rendezvous::server::Config::default())
|
rendezvous::server::Behaviour::new(rendezvous::server::Config::default())
|
||||||
});
|
});
|
||||||
let address = rendezvous_node.listen_on_random_memory_address().await;
|
let rendezvous_address = rendezvous_node.listen_on_random_memory_address().await;
|
||||||
let rendezvous_point = RendezvousNode::new(
|
|
||||||
&address,
|
|
||||||
rendezvous_node.local_peer_id().to_owned(),
|
|
||||||
XmrBtcNamespace::Testnet,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut asb = new_swarm(|_, identity| {
|
let mut asb = new_swarm(|_, identity| {
|
||||||
super::rendezvous::Behaviour::new(identity, vec![rendezvous_point])
|
rendezous::Behaviour::new(
|
||||||
|
identity,
|
||||||
|
*rendezvous_node.local_peer_id(),
|
||||||
|
rendezvous_address,
|
||||||
|
XmrBtcNamespace::Testnet,
|
||||||
|
None,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
asb.listen_on_random_memory_address().await; // this adds an external address
|
asb.listen_on_random_memory_address().await; // this adds an external address
|
||||||
|
|
||||||
@ -478,16 +428,16 @@ pub mod rendezvous {
|
|||||||
rendezvous::server::Config::default().with_min_ttl(2),
|
rendezvous::server::Config::default().with_min_ttl(2),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let address = rendezvous_node.listen_on_random_memory_address().await;
|
let rendezvous_address = rendezvous_node.listen_on_random_memory_address().await;
|
||||||
let rendezvous_point = RendezvousNode::new(
|
|
||||||
&address,
|
|
||||||
rendezvous_node.local_peer_id().to_owned(),
|
|
||||||
XmrBtcNamespace::Testnet,
|
|
||||||
Some(5),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut asb = new_swarm(|_, identity| {
|
let mut asb = new_swarm(|_, identity| {
|
||||||
super::rendezvous::Behaviour::new(identity, vec![rendezvous_point])
|
rendezous::Behaviour::new(
|
||||||
|
identity,
|
||||||
|
*rendezvous_node.local_peer_id(),
|
||||||
|
rendezvous_address,
|
||||||
|
XmrBtcNamespace::Testnet,
|
||||||
|
Some(5),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
asb.listen_on_random_memory_address().await; // this adds an external address
|
asb.listen_on_random_memory_address().await; // this adds an external address
|
||||||
|
|
||||||
@ -517,62 +467,5 @@ pub mod rendezvous {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn asb_registers_multiple() {
|
|
||||||
let registration_ttl = Some(10);
|
|
||||||
let mut rendezvous_nodes = Vec::new();
|
|
||||||
let mut registrations = HashMap::new();
|
|
||||||
// register with 5 rendezvous nodes
|
|
||||||
for _ in 0..5 {
|
|
||||||
let mut rendezvous = new_swarm(|_, _| {
|
|
||||||
rendezvous::server::Behaviour::new(
|
|
||||||
rendezvous::server::Config::default().with_min_ttl(2),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let address = rendezvous.listen_on_random_memory_address().await;
|
|
||||||
let id = *rendezvous.local_peer_id();
|
|
||||||
registrations.insert(id, 0);
|
|
||||||
rendezvous_nodes.push(RendezvousNode::new(
|
|
||||||
&address,
|
|
||||||
*rendezvous.local_peer_id(),
|
|
||||||
XmrBtcNamespace::Testnet,
|
|
||||||
registration_ttl,
|
|
||||||
));
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
rendezvous.next().await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut asb = new_swarm(|_, identity| {
|
|
||||||
super::rendezvous::Behaviour::new(identity, rendezvous_nodes)
|
|
||||||
});
|
|
||||||
asb.listen_on_random_memory_address().await; // this adds an external address
|
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
if let SwarmEvent::Behaviour(rendezvous::client::Event::Registered {
|
|
||||||
rendezvous_node,
|
|
||||||
..
|
|
||||||
}) = asb.select_next_some().await
|
|
||||||
{
|
|
||||||
registrations
|
|
||||||
.entry(rendezvous_node)
|
|
||||||
.and_modify(|counter| *counter += 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if registrations.iter().all(|(_, &count)| count >= 4) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::time::timeout(Duration::from_secs(30), handle)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ use rust_decimal::Decimal;
|
|||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
|
||||||
/// Represents the rate at which we are willing to trade 1 XMR.
|
/// Represents the rate at which we are willing to trade 1 XMR.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Rate {
|
pub struct Rate {
|
||||||
/// Represents the asking price from the market.
|
/// Represents the asking price from the market.
|
||||||
ask: bitcoin::Amount,
|
ask: bitcoin::Amount,
|
||||||
|
@ -38,8 +38,8 @@ pub async fn cancel(
|
|||||||
// Alice already in final state
|
// Alice already in final state
|
||||||
| AliceState::BtcRedeemed
|
| AliceState::BtcRedeemed
|
||||||
| AliceState::XmrRefunded
|
| AliceState::XmrRefunded
|
||||||
| AliceState::BtcPunished { .. }
|
| AliceState::BtcPunished
|
||||||
| AliceState::SafelyAborted => bail!("Swap is in state {} which is not cancelable", state),
|
| AliceState::SafelyAborted => bail!("Swap is is in state {} which is not cancelable", state),
|
||||||
};
|
};
|
||||||
|
|
||||||
let txid = match state3.submit_tx_cancel(bitcoin_wallet.as_ref()).await {
|
let txid = match state3.submit_tx_cancel(bitcoin_wallet.as_ref()).await {
|
||||||
|
@ -38,7 +38,7 @@ pub async fn punish(
|
|||||||
// Alice already in final state
|
// Alice already in final state
|
||||||
| AliceState::BtcRedeemed
|
| AliceState::BtcRedeemed
|
||||||
| AliceState::XmrRefunded
|
| AliceState::XmrRefunded
|
||||||
| AliceState::BtcPunished { .. }
|
| AliceState::BtcPunished
|
||||||
| AliceState::SafelyAborted => bail!(Error::SwapNotPunishable(state)),
|
| AliceState::SafelyAborted => bail!(Error::SwapNotPunishable(state)),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,9 +46,7 @@ pub async fn punish(
|
|||||||
|
|
||||||
let txid = state3.punish_btc(&bitcoin_wallet).await?;
|
let txid = state3.punish_btc(&bitcoin_wallet).await?;
|
||||||
|
|
||||||
let state = AliceState::BtcPunished {
|
let state = AliceState::BtcPunished;
|
||||||
state3: state3.clone(),
|
|
||||||
};
|
|
||||||
db.insert_latest_state(swap_id, state.clone().into())
|
db.insert_latest_state(swap_id, state.clone().into())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ pub async fn redeem(
|
|||||||
| AliceState::BtcPunishable { .. }
|
| AliceState::BtcPunishable { .. }
|
||||||
| AliceState::BtcRedeemed
|
| AliceState::BtcRedeemed
|
||||||
| AliceState::XmrRefunded
|
| AliceState::XmrRefunded
|
||||||
| AliceState::BtcPunished { .. }
|
| AliceState::BtcPunished
|
||||||
| AliceState::SafelyAborted => bail!(
|
| AliceState::SafelyAborted => bail!(
|
||||||
"Cannot redeem swap {} because it is in state {} which cannot be manually redeemed",
|
"Cannot redeem swap {} because it is in state {} which cannot be manually redeemed",
|
||||||
swap_id,
|
swap_id,
|
||||||
|
@ -55,7 +55,7 @@ pub async fn refund(
|
|||||||
AliceState::BtcRedeemTransactionPublished { .. }
|
AliceState::BtcRedeemTransactionPublished { .. }
|
||||||
| AliceState::BtcRedeemed
|
| AliceState::BtcRedeemed
|
||||||
| AliceState::XmrRefunded
|
| AliceState::XmrRefunded
|
||||||
| AliceState::BtcPunished { .. }
|
| AliceState::BtcPunished
|
||||||
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
|
| AliceState::SafelyAborted => bail!(Error::SwapNotRefundable(state)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ pub async fn safely_abort(swap_id: Uuid, db: Arc<dyn Database>) -> Result<AliceS
|
|||||||
| AliceState::BtcPunishable { .. }
|
| AliceState::BtcPunishable { .. }
|
||||||
| AliceState::BtcRedeemed
|
| AliceState::BtcRedeemed
|
||||||
| AliceState::XmrRefunded
|
| AliceState::XmrRefunded
|
||||||
| AliceState::BtcPunished { .. }
|
| AliceState::BtcPunished
|
||||||
| AliceState::SafelyAborted => bail!(
|
| AliceState::SafelyAborted => bail!(
|
||||||
"Cannot safely abort swap {} because it is in state {} which cannot be safely aborted",
|
"Cannot safely abort swap {} because it is in state {} which cannot be safely aborted",
|
||||||
swap_id,
|
swap_id,
|
||||||
|
30
swap/src/asb/tracing.rs
Normal file
30
swap/src/asb/tracing.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use tracing_subscriber::filter::LevelFilter;
|
||||||
|
use tracing_subscriber::fmt::time::UtcTime;
|
||||||
|
use tracing_subscriber::FmtSubscriber;
|
||||||
|
|
||||||
|
pub fn init(level: LevelFilter, json_format: bool, timestamp: bool) -> Result<()> {
|
||||||
|
if level == LevelFilter::OFF {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_terminal = atty::is(atty::Stream::Stderr);
|
||||||
|
|
||||||
|
let builder = FmtSubscriber::builder()
|
||||||
|
.with_env_filter(format!("asb={},swap={}", level, level))
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.with_ansi(is_terminal)
|
||||||
|
.with_timer(UtcTime::rfc_3339())
|
||||||
|
.with_target(false);
|
||||||
|
|
||||||
|
match (json_format, timestamp) {
|
||||||
|
(true, true) => builder.json().init(),
|
||||||
|
(true, false) => builder.json().without_time().init(),
|
||||||
|
(false, true) => builder.init(),
|
||||||
|
(false, false) => builder.without_time().init(),
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(%level, "Initialized tracing");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -18,8 +18,6 @@ use libp2p::core::multiaddr::Protocol;
|
|||||||
use libp2p::core::Multiaddr;
|
use libp2p::core::Multiaddr;
|
||||||
use libp2p::swarm::AddressScore;
|
use libp2p::swarm::AddressScore;
|
||||||
use libp2p::Swarm;
|
use libp2p::Swarm;
|
||||||
use rust_decimal::prelude::FromPrimitive;
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
@ -31,50 +29,51 @@ use swap::asb::config::{
|
|||||||
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
|
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
|
||||||
};
|
};
|
||||||
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
|
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
|
||||||
use swap::common::tracing_util::Format;
|
use swap::common::check_latest_version;
|
||||||
use swap::common::{self, check_latest_version, get_logs};
|
use swap::database::open_db;
|
||||||
use swap::database::{open_db, AccessMode};
|
|
||||||
use swap::network::rendezvous::XmrBtcNamespace;
|
use swap::network::rendezvous::XmrBtcNamespace;
|
||||||
use swap::network::swarm;
|
use swap::network::swarm;
|
||||||
use swap::protocol::alice::swap::is_complete;
|
|
||||||
use swap::protocol::alice::{run, AliceState};
|
use swap::protocol::alice::{run, AliceState};
|
||||||
use swap::protocol::State;
|
|
||||||
use swap::seed::Seed;
|
use swap::seed::Seed;
|
||||||
use swap::tor::AuthenticatedClient;
|
use swap::tor::AuthenticatedClient;
|
||||||
use swap::{bitcoin, kraken, monero, tor};
|
use swap::{asb, bitcoin, kraken, monero, tor};
|
||||||
use tracing_subscriber::filter::LevelFilter;
|
use tracing_subscriber::filter::LevelFilter;
|
||||||
|
|
||||||
const DEFAULT_WALLET_NAME: &str = "asb-wallet";
|
const DEFAULT_WALLET_NAME: &str = "asb-wallet";
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// parse cli arguments
|
|
||||||
let Arguments {
|
let Arguments {
|
||||||
testnet,
|
testnet,
|
||||||
json,
|
json,
|
||||||
|
disable_timestamp,
|
||||||
config_path,
|
config_path,
|
||||||
env_config,
|
env_config,
|
||||||
cmd,
|
cmd,
|
||||||
} = match parse_args(env::args_os()) {
|
} = match parse_args(env::args_os()) {
|
||||||
Ok(args) => args,
|
Ok(args) => args,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// make sure to display the clap error message it exists
|
|
||||||
if let Some(clap_err) = e.downcast_ref::<clap::Error>() {
|
if let Some(clap_err) = e.downcast_ref::<clap::Error>() {
|
||||||
if let ErrorKind::HelpDisplayed | ErrorKind::VersionDisplayed = clap_err.kind {
|
match clap_err.kind {
|
||||||
println!("{}", clap_err.message);
|
ErrorKind::HelpDisplayed | ErrorKind::VersionDisplayed => {
|
||||||
std::process::exit(0);
|
println!("{}", clap_err.message);
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
bail!(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bail!(e);
|
bail!(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// warn if we're not on the latest version
|
|
||||||
if let Err(e) = check_latest_version(env!("CARGO_PKG_VERSION")).await {
|
if let Err(e) = check_latest_version(env!("CARGO_PKG_VERSION")).await {
|
||||||
eprintln!("{}", e);
|
eprintln!("{}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// read config from the specified path
|
asb::tracing::init(LevelFilter::DEBUG, json, !disable_timestamp).expect("initialize tracing");
|
||||||
|
|
||||||
let config = match read_config(config_path.clone())? {
|
let config = match read_config(config_path.clone())? {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(ConfigNotInitialized {}) => {
|
Err(ConfigNotInitialized {}) => {
|
||||||
@ -83,12 +82,6 @@ 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).expect("initialize tracing");
|
|
||||||
|
|
||||||
// check for conflicting env / config values
|
|
||||||
if config.monero.network != env_config.monero_network {
|
if config.monero.network != env_config.monero_network {
|
||||||
bail!(format!(
|
bail!(format!(
|
||||||
"Expected monero network in config file to be {:?} but was {:?}",
|
"Expected monero network in config file to be {:?} but was {:?}",
|
||||||
@ -102,33 +95,16 @@ async fn main() -> Result<()> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let db = open_db(config.data.dir.join("sqlite")).await?;
|
||||||
|
|
||||||
let seed =
|
let seed =
|
||||||
Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed");
|
Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed");
|
||||||
|
|
||||||
match cmd {
|
match cmd {
|
||||||
Command::Start { resume_only } => {
|
Command::Start { resume_only } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
|
||||||
|
|
||||||
// check and warn for duplicate rendezvous points
|
|
||||||
let mut rendezvous_addrs = config.network.rendezvous_point.clone();
|
|
||||||
let prev_len = rendezvous_addrs.len();
|
|
||||||
rendezvous_addrs.sort();
|
|
||||||
rendezvous_addrs.dedup();
|
|
||||||
let new_len = rendezvous_addrs.len();
|
|
||||||
|
|
||||||
if new_len < prev_len {
|
|
||||||
tracing::warn!(
|
|
||||||
"`rendezvous_point` config has {} duplicate entries, they are being ignored.",
|
|
||||||
prev_len - new_len
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize monero wallet
|
|
||||||
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
||||||
let monero_address = monero_wallet.get_main_address();
|
let monero_address = monero_wallet.get_main_address();
|
||||||
tracing::info!(%monero_address, "Monero wallet address");
|
tracing::info!(%monero_address, "Monero wallet address");
|
||||||
|
|
||||||
// check monero balance
|
|
||||||
let monero = monero_wallet.get_balance().await?;
|
let monero = monero_wallet.get_balance().await?;
|
||||||
match (monero.balance, monero.unlocked_balance) {
|
match (monero.balance, monero.unlocked_balance) {
|
||||||
(0, _) => {
|
(0, _) => {
|
||||||
@ -151,7 +127,6 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// init bitcoin wallet
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
let bitcoin_balance = bitcoin_wallet.balance().await?;
|
let bitcoin_balance = bitcoin_wallet.balance().await?;
|
||||||
tracing::info!(%bitcoin_balance, "Bitcoin wallet balance");
|
tracing::info!(%bitcoin_balance, "Bitcoin wallet balance");
|
||||||
@ -186,7 +161,7 @@ async fn main() -> Result<()> {
|
|||||||
resume_only,
|
resume_only,
|
||||||
env_config,
|
env_config,
|
||||||
namespace,
|
namespace,
|
||||||
&rendezvous_addrs,
|
config.network.rendezvous_point,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for listen in config.network.listen.clone() {
|
for listen in config.network.listen.clone() {
|
||||||
@ -236,105 +211,22 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
event_loop.run().await;
|
event_loop.run().await;
|
||||||
}
|
}
|
||||||
Command::History { only_unfinished } => {
|
Command::History => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadOnly).await?;
|
let mut table = Table::new();
|
||||||
let mut table: Table = Table::new();
|
|
||||||
|
|
||||||
table.set_header(vec![
|
table.set_header(vec!["SWAP ID", "STATE"]);
|
||||||
"Swap ID",
|
|
||||||
"Start Date",
|
|
||||||
"State",
|
|
||||||
"BTC Amount",
|
|
||||||
"XMR Amount",
|
|
||||||
"Exchange Rate",
|
|
||||||
"Trading Partner Peer ID",
|
|
||||||
"Completed",
|
|
||||||
]);
|
|
||||||
|
|
||||||
let all_swaps = db.all().await?;
|
for (swap_id, state) in db.all().await? {
|
||||||
for (swap_id, state) in all_swaps {
|
let state: AliceState = state.try_into()?;
|
||||||
if let Err(e) = async {
|
table.add_row(vec![swap_id.to_string(), state.to_string()]);
|
||||||
let latest_state: AliceState = state.try_into()?;
|
|
||||||
let is_completed = is_complete(&latest_state);
|
|
||||||
|
|
||||||
if only_unfinished && is_completed {
|
|
||||||
return Ok::<_, anyhow::Error>(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let all_states = db.get_states(swap_id).await?;
|
|
||||||
let state3 = all_states
|
|
||||||
.iter()
|
|
||||||
.find_map(|s| match s {
|
|
||||||
State::Alice(AliceState::BtcLockTransactionSeen { state3 }) => {
|
|
||||||
Some(state3)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.context("Failed to get \"BtcLockTransactionSeen\" state")?;
|
|
||||||
|
|
||||||
let swap_start_date = db.get_swap_start_date(swap_id).await?;
|
|
||||||
let peer_id = db.get_peer_id(swap_id).await?;
|
|
||||||
|
|
||||||
let exchange_rate = Decimal::from_f64(state3.btc.to_btc())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Failed to convert BTC amount to Decimal"))?
|
|
||||||
.checked_div(state3.xmr.as_xmr())
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Division by zero or overflow"))?;
|
|
||||||
let exchange_rate = format!("{} XMR/BTC", exchange_rate.round_dp(8));
|
|
||||||
|
|
||||||
if json {
|
|
||||||
tracing::info!(
|
|
||||||
swap_id = %swap_id,
|
|
||||||
swap_start_date = %swap_start_date,
|
|
||||||
latest_state = %latest_state,
|
|
||||||
btc_amount = %state3.btc,
|
|
||||||
xmr_amount = %state3.xmr,
|
|
||||||
exchange_rate = %exchange_rate,
|
|
||||||
trading_partner_peer_id = %peer_id,
|
|
||||||
completed = is_completed,
|
|
||||||
"Found swap in database"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
table.add_row(vec![
|
|
||||||
swap_id.to_string(),
|
|
||||||
swap_start_date.to_string(),
|
|
||||||
latest_state.to_string(),
|
|
||||||
state3.btc.to_string(),
|
|
||||||
state3.xmr.to_string(),
|
|
||||||
exchange_rate,
|
|
||||||
peer_id.to_string(),
|
|
||||||
is_completed.to_string(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(())
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!(swap_id = %swap_id, error = %e, "Failed to get swap details");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !json {
|
println!("{}", table);
|
||||||
println!("{}", table);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Command::Config => {
|
Command::Config => {
|
||||||
let config_json = serde_json::to_string_pretty(&config)?;
|
let config_json = serde_json::to_string_pretty(&config)?;
|
||||||
println!("{}", config_json);
|
println!("{}", config_json);
|
||||||
}
|
}
|
||||||
Command::Logs {
|
|
||||||
logs_dir,
|
|
||||||
swap_id,
|
|
||||||
redact,
|
|
||||||
} => {
|
|
||||||
let dir = logs_dir.unwrap_or(config.data.dir.join("logs"));
|
|
||||||
|
|
||||||
let log_messages = get_logs(dir, swap_id, redact).await?;
|
|
||||||
|
|
||||||
for msg in log_messages {
|
|
||||||
println!("{msg}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::WithdrawBtc { amount, address } => {
|
Command::WithdrawBtc { amount, address } => {
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
|
||||||
@ -365,8 +257,6 @@ async fn main() -> Result<()> {
|
|||||||
tracing::info!(%bitcoin_balance, %monero_balance, "Current balance");
|
tracing::info!(%bitcoin_balance, %monero_balance, "Current balance");
|
||||||
}
|
}
|
||||||
Command::Cancel { swap_id } => {
|
Command::Cancel { swap_id } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
|
||||||
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
|
||||||
let (txid, _) = cancel(swap_id, Arc::new(bitcoin_wallet), db).await?;
|
let (txid, _) = cancel(swap_id, Arc::new(bitcoin_wallet), db).await?;
|
||||||
@ -374,8 +264,6 @@ async fn main() -> Result<()> {
|
|||||||
tracing::info!("Cancel transaction successfully published with id {}", txid);
|
tracing::info!("Cancel transaction successfully published with id {}", txid);
|
||||||
}
|
}
|
||||||
Command::Refund { swap_id } => {
|
Command::Refund { swap_id } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
|
||||||
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
||||||
|
|
||||||
@ -390,8 +278,6 @@ async fn main() -> Result<()> {
|
|||||||
tracing::info!("Monero successfully refunded");
|
tracing::info!("Monero successfully refunded");
|
||||||
}
|
}
|
||||||
Command::Punish { swap_id } => {
|
Command::Punish { swap_id } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
|
||||||
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
|
||||||
let (txid, _) = punish(swap_id, Arc::new(bitcoin_wallet), db).await?;
|
let (txid, _) = punish(swap_id, Arc::new(bitcoin_wallet), db).await?;
|
||||||
@ -399,8 +285,6 @@ async fn main() -> Result<()> {
|
|||||||
tracing::info!("Punish transaction successfully published with id {}", txid);
|
tracing::info!("Punish transaction successfully published with id {}", txid);
|
||||||
}
|
}
|
||||||
Command::SafelyAbort { swap_id } => {
|
Command::SafelyAbort { swap_id } => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
|
||||||
|
|
||||||
safely_abort(swap_id, db).await?;
|
safely_abort(swap_id, db).await?;
|
||||||
|
|
||||||
tracing::info!("Swap safely aborted");
|
tracing::info!("Swap safely aborted");
|
||||||
@ -409,8 +293,6 @@ async fn main() -> Result<()> {
|
|||||||
swap_id,
|
swap_id,
|
||||||
do_not_await_finality,
|
do_not_await_finality,
|
||||||
} => {
|
} => {
|
||||||
let db = open_db(config.data.dir.join("sqlite"), AccessMode::ReadWrite).await?;
|
|
||||||
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
|
|
||||||
let (txid, _) = redeem(
|
let (txid, _) = redeem(
|
||||||
|
@ -12,15 +12,43 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Context, Result};
|
||||||
|
use comfy_table::Table;
|
||||||
|
use qrcode::render::unicode;
|
||||||
|
use qrcode::QrCode;
|
||||||
|
use std::cmp::min;
|
||||||
|
use std::convert::TryInto;
|
||||||
use std::env;
|
use std::env;
|
||||||
use swap::cli::command::{parse_args_and_apply_defaults, ParseResult};
|
use std::future::Future;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use swap::bitcoin::TxLock;
|
||||||
|
use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command, ParseResult};
|
||||||
|
use swap::cli::{list_sellers, EventLoop, SellerStatus};
|
||||||
use swap::common::check_latest_version;
|
use swap::common::check_latest_version;
|
||||||
|
use swap::database::open_db;
|
||||||
|
use swap::env::Config;
|
||||||
|
use swap::libp2p_ext::MultiAddrExt;
|
||||||
|
use swap::network::quote::{BidQuote, ZeroQuoteReceived};
|
||||||
|
use swap::network::swarm;
|
||||||
|
use swap::protocol::bob;
|
||||||
|
use swap::protocol::bob::{BobState, Swap};
|
||||||
|
use swap::seed::Seed;
|
||||||
|
use swap::{bitcoin, cli, monero};
|
||||||
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let (context, request) = match parse_args_and_apply_defaults(env::args_os()).await? {
|
let Arguments {
|
||||||
ParseResult::Context(context, request) => (context, request),
|
env_config,
|
||||||
|
data_dir,
|
||||||
|
debug,
|
||||||
|
json,
|
||||||
|
cmd,
|
||||||
|
} = match parse_args_and_apply_defaults(env::args_os())? {
|
||||||
|
ParseResult::Arguments(args) => *args,
|
||||||
ParseResult::PrintAndExitZero { message } => {
|
ParseResult::PrintAndExitZero { message } => {
|
||||||
println!("{}", message);
|
println!("{}", message);
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
@ -30,19 +58,601 @@ async fn main() -> Result<()> {
|
|||||||
if let Err(e) = check_latest_version(env!("CARGO_PKG_VERSION")).await {
|
if let Err(e) = check_latest_version(env!("CARGO_PKG_VERSION")).await {
|
||||||
eprintln!("{}", e);
|
eprintln!("{}", e);
|
||||||
}
|
}
|
||||||
request.call(context.clone()).await?;
|
|
||||||
context.tasks.wait_for_tasks().await?;
|
match cmd {
|
||||||
|
Command::BuyXmr {
|
||||||
|
seller,
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
bitcoin_target_block,
|
||||||
|
bitcoin_change_address,
|
||||||
|
monero_receive_address,
|
||||||
|
monero_daemon_address,
|
||||||
|
tor_socks5_port,
|
||||||
|
namespace,
|
||||||
|
} => {
|
||||||
|
let swap_id = Uuid::new_v4();
|
||||||
|
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
|
||||||
|
|
||||||
|
let db = open_db(data_dir.join("sqlite")).await?;
|
||||||
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
|
.context("Failed to read in seed file")?;
|
||||||
|
|
||||||
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
&seed,
|
||||||
|
data_dir.clone(),
|
||||||
|
env_config,
|
||||||
|
bitcoin_target_block,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let (monero_wallet, _process) =
|
||||||
|
init_monero_wallet(data_dir, monero_daemon_address, env_config).await?;
|
||||||
|
let bitcoin_wallet = Arc::new(bitcoin_wallet);
|
||||||
|
let seller_peer_id = seller
|
||||||
|
.extract_peer_id()
|
||||||
|
.context("Seller address must contain peer ID")?;
|
||||||
|
db.insert_address(seller_peer_id, seller.clone()).await?;
|
||||||
|
|
||||||
|
let behaviour = cli::Behaviour::new(
|
||||||
|
seller_peer_id,
|
||||||
|
env_config,
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
(seed.derive_libp2p_identity(), namespace),
|
||||||
|
);
|
||||||
|
let mut swarm =
|
||||||
|
swarm::cli(seed.derive_libp2p_identity(), tor_socks5_port, behaviour).await?;
|
||||||
|
swarm.behaviour_mut().add_address(seller_peer_id, seller);
|
||||||
|
|
||||||
|
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
||||||
|
|
||||||
|
let (event_loop, mut event_loop_handle) =
|
||||||
|
EventLoop::new(swap_id, swarm, seller_peer_id)?;
|
||||||
|
let event_loop = tokio::spawn(event_loop.run());
|
||||||
|
|
||||||
|
let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size());
|
||||||
|
let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount);
|
||||||
|
|
||||||
|
let (amount, fees) = match determine_btc_to_swap(
|
||||||
|
json,
|
||||||
|
event_loop_handle.request_quote(),
|
||||||
|
bitcoin_wallet.new_address(),
|
||||||
|
|| bitcoin_wallet.balance(),
|
||||||
|
max_givable,
|
||||||
|
|| bitcoin_wallet.sync(),
|
||||||
|
estimate_fee,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(error) => match error.downcast::<ZeroQuoteReceived>() {
|
||||||
|
Ok(_) => {
|
||||||
|
bail!("Seller's XMR balance is currently too low to initiate a swap, please try again later")
|
||||||
|
}
|
||||||
|
Err(other) => bail!(other),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!(%amount, %fees, "Determined swap amount");
|
||||||
|
|
||||||
|
db.insert_peer_id(swap_id, seller_peer_id).await?;
|
||||||
|
db.insert_monero_address(swap_id, monero_receive_address)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let swap = Swap::new(
|
||||||
|
db,
|
||||||
|
swap_id,
|
||||||
|
bitcoin_wallet,
|
||||||
|
Arc::new(monero_wallet),
|
||||||
|
env_config,
|
||||||
|
event_loop_handle,
|
||||||
|
monero_receive_address,
|
||||||
|
bitcoin_change_address,
|
||||||
|
amount,
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
result = event_loop => {
|
||||||
|
result
|
||||||
|
.context("EventLoop panicked")?;
|
||||||
|
},
|
||||||
|
result = bob::run(swap) => {
|
||||||
|
result.context("Failed to complete swap")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::History => {
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), None)?;
|
||||||
|
|
||||||
|
let db = open_db(data_dir.join("sqlite")).await?;
|
||||||
|
let swaps = db.all().await?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
for (swap_id, state) in swaps {
|
||||||
|
let state: BobState = state.try_into()?;
|
||||||
|
tracing::info!(swap_id=%swap_id.to_string(), state=%state.to_string(), "Read swap state from database");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
table.set_header(vec!["SWAP ID", "STATE"]);
|
||||||
|
|
||||||
|
for (swap_id, state) in swaps {
|
||||||
|
let state: BobState = state.try_into()?;
|
||||||
|
table.add_row(vec![swap_id.to_string(), state.to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Config => {
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), None)?;
|
||||||
|
|
||||||
|
tracing::info!(path=%data_dir.display(), "Data directory");
|
||||||
|
tracing::info!(path=%format!("{}/logs", data_dir.display()), "Log files directory");
|
||||||
|
tracing::info!(path=%format!("{}/sqlite", data_dir.display()), "Sqlite file location");
|
||||||
|
tracing::info!(path=%format!("{}/seed.pem", data_dir.display()), "Seed file location");
|
||||||
|
tracing::info!(path=%format!("{}/monero", data_dir.display()), "Monero-wallet-rpc directory");
|
||||||
|
tracing::info!(path=%format!("{}/wallet", data_dir.display()), "Internal bitcoin wallet directory");
|
||||||
|
}
|
||||||
|
Command::WithdrawBtc {
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
bitcoin_target_block,
|
||||||
|
amount,
|
||||||
|
address,
|
||||||
|
} => {
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), None)?;
|
||||||
|
|
||||||
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
|
.context("Failed to read in seed file")?;
|
||||||
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
&seed,
|
||||||
|
data_dir.clone(),
|
||||||
|
env_config,
|
||||||
|
bitcoin_target_block,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let amount = match amount {
|
||||||
|
Some(amount) => amount,
|
||||||
|
None => {
|
||||||
|
bitcoin_wallet
|
||||||
|
.max_giveable(address.script_pubkey().len())
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let psbt = bitcoin_wallet
|
||||||
|
.send_to_address(address, amount, None)
|
||||||
|
.await?;
|
||||||
|
let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?;
|
||||||
|
|
||||||
|
bitcoin_wallet.broadcast(signed_tx, "withdraw").await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::Balance {
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
bitcoin_target_block,
|
||||||
|
} => {
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), None)?;
|
||||||
|
|
||||||
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
|
.context("Failed to read in seed file")?;
|
||||||
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
&seed,
|
||||||
|
data_dir.clone(),
|
||||||
|
env_config,
|
||||||
|
bitcoin_target_block,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let bitcoin_balance = bitcoin_wallet.balance().await?;
|
||||||
|
tracing::info!(
|
||||||
|
balance = %bitcoin_balance,
|
||||||
|
"Checked Bitcoin balance",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Command::Resume {
|
||||||
|
swap_id,
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
bitcoin_target_block,
|
||||||
|
monero_daemon_address,
|
||||||
|
tor_socks5_port,
|
||||||
|
namespace,
|
||||||
|
} => {
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
|
||||||
|
|
||||||
|
let db = open_db(data_dir.join("sqlite")).await?;
|
||||||
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
|
.context("Failed to read in seed file")?;
|
||||||
|
|
||||||
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
&seed,
|
||||||
|
data_dir.clone(),
|
||||||
|
env_config,
|
||||||
|
bitcoin_target_block,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let (monero_wallet, _process) =
|
||||||
|
init_monero_wallet(data_dir, monero_daemon_address, env_config).await?;
|
||||||
|
let bitcoin_wallet = Arc::new(bitcoin_wallet);
|
||||||
|
|
||||||
|
let seller_peer_id = db.get_peer_id(swap_id).await?;
|
||||||
|
let seller_addresses = db.get_addresses(seller_peer_id).await?;
|
||||||
|
|
||||||
|
let behaviour = cli::Behaviour::new(
|
||||||
|
seller_peer_id,
|
||||||
|
env_config,
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
(seed.derive_libp2p_identity(), namespace),
|
||||||
|
);
|
||||||
|
let mut swarm =
|
||||||
|
swarm::cli(seed.derive_libp2p_identity(), tor_socks5_port, behaviour).await?;
|
||||||
|
let our_peer_id = swarm.local_peer_id();
|
||||||
|
tracing::debug!(peer_id = %our_peer_id, "Network layer initialized");
|
||||||
|
|
||||||
|
for seller_address in seller_addresses {
|
||||||
|
swarm
|
||||||
|
.behaviour_mut()
|
||||||
|
.add_address(seller_peer_id, seller_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (event_loop, event_loop_handle) = EventLoop::new(swap_id, swarm, seller_peer_id)?;
|
||||||
|
let handle = tokio::spawn(event_loop.run());
|
||||||
|
|
||||||
|
let monero_receive_address = db.get_monero_address(swap_id).await?;
|
||||||
|
let swap = Swap::from_db(
|
||||||
|
db,
|
||||||
|
swap_id,
|
||||||
|
bitcoin_wallet,
|
||||||
|
Arc::new(monero_wallet),
|
||||||
|
env_config,
|
||||||
|
event_loop_handle,
|
||||||
|
monero_receive_address,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
event_loop_result = handle => {
|
||||||
|
event_loop_result?;
|
||||||
|
},
|
||||||
|
swap_result = bob::run(swap) => {
|
||||||
|
swap_result?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::CancelAndRefund {
|
||||||
|
swap_id,
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
bitcoin_target_block,
|
||||||
|
} => {
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
|
||||||
|
|
||||||
|
let db = open_db(data_dir.join("sqlite")).await?;
|
||||||
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
|
.context("Failed to read in seed file")?;
|
||||||
|
|
||||||
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
&seed,
|
||||||
|
data_dir,
|
||||||
|
env_config,
|
||||||
|
bitcoin_target_block,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
cli::cancel_and_refund(swap_id, Arc::new(bitcoin_wallet), db).await?;
|
||||||
|
}
|
||||||
|
Command::ListSellers {
|
||||||
|
rendezvous_point,
|
||||||
|
namespace,
|
||||||
|
tor_socks5_port,
|
||||||
|
} => {
|
||||||
|
let rendezvous_node_peer_id = rendezvous_point
|
||||||
|
.extract_peer_id()
|
||||||
|
.context("Rendezvous node address must contain peer ID")?;
|
||||||
|
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), None)?;
|
||||||
|
|
||||||
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
|
.context("Failed to read in seed file")?;
|
||||||
|
let identity = seed.derive_libp2p_identity();
|
||||||
|
|
||||||
|
let sellers = list_sellers(
|
||||||
|
rendezvous_node_peer_id,
|
||||||
|
rendezvous_point,
|
||||||
|
namespace,
|
||||||
|
tor_socks5_port,
|
||||||
|
identity,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
for seller in sellers {
|
||||||
|
match seller.status {
|
||||||
|
SellerStatus::Online(quote) => {
|
||||||
|
tracing::info!(
|
||||||
|
price = %quote.price.to_string(),
|
||||||
|
min_quantity = %quote.min_quantity.to_string(),
|
||||||
|
max_quantity = %quote.max_quantity.to_string(),
|
||||||
|
status = "Online",
|
||||||
|
address = %seller.multiaddr.to_string(),
|
||||||
|
"Fetched peer status"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SellerStatus::Unreachable => {
|
||||||
|
tracing::info!(
|
||||||
|
status = "Unreachable",
|
||||||
|
address = %seller.multiaddr.to_string(),
|
||||||
|
"Fetched peer status"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
table.set_header(vec![
|
||||||
|
"PRICE",
|
||||||
|
"MIN_QUANTITY",
|
||||||
|
"MAX_QUANTITY",
|
||||||
|
"STATUS",
|
||||||
|
"ADDRESS",
|
||||||
|
]);
|
||||||
|
|
||||||
|
for seller in sellers {
|
||||||
|
let row = match seller.status {
|
||||||
|
SellerStatus::Online(quote) => {
|
||||||
|
vec![
|
||||||
|
quote.price.to_string(),
|
||||||
|
quote.min_quantity.to_string(),
|
||||||
|
quote.max_quantity.to_string(),
|
||||||
|
"Online".to_owned(),
|
||||||
|
seller.multiaddr.to_string(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
SellerStatus::Unreachable => {
|
||||||
|
vec![
|
||||||
|
"???".to_owned(),
|
||||||
|
"???".to_owned(),
|
||||||
|
"???".to_owned(),
|
||||||
|
"Unreachable".to_owned(),
|
||||||
|
seller.multiaddr.to_string(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
table.add_row(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::ExportBitcoinWallet {
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
bitcoin_target_block,
|
||||||
|
} => {
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), None)?;
|
||||||
|
|
||||||
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
|
.context("Failed to read in seed file")?;
|
||||||
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
|
bitcoin_electrum_rpc_url,
|
||||||
|
&seed,
|
||||||
|
data_dir.clone(),
|
||||||
|
env_config,
|
||||||
|
bitcoin_target_block,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let wallet_export = bitcoin_wallet.wallet_export("cli").await?;
|
||||||
|
tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet");
|
||||||
|
}
|
||||||
|
Command::MoneroRecovery { swap_id } => {
|
||||||
|
cli::tracing::init(debug, json, data_dir.join("logs"), Some(swap_id))?;
|
||||||
|
|
||||||
|
let db = open_db(data_dir.join("sqlite")).await?;
|
||||||
|
|
||||||
|
let swap_state: BobState = db.get_state(swap_id).await?.try_into()?;
|
||||||
|
|
||||||
|
match swap_state {
|
||||||
|
BobState::Started { .. }
|
||||||
|
| BobState::SwapSetupCompleted(_)
|
||||||
|
| BobState::BtcLocked { .. }
|
||||||
|
| BobState::XmrLockProofReceived { .. }
|
||||||
|
| BobState::XmrLocked(_)
|
||||||
|
| BobState::EncSigSent(_)
|
||||||
|
| BobState::CancelTimelockExpired(_)
|
||||||
|
| BobState::BtcCancelled(_)
|
||||||
|
| BobState::BtcRefunded(_)
|
||||||
|
| BobState::BtcPunished { .. }
|
||||||
|
| BobState::SafelyAborted
|
||||||
|
| BobState::XmrRedeemed { .. } => {
|
||||||
|
bail!("Cannot print monero recovery information in state {}, only possible for BtcRedeemed", swap_state)
|
||||||
|
}
|
||||||
|
BobState::BtcRedeemed(state5) => {
|
||||||
|
let (spend_key, view_key) = state5.xmr_keys();
|
||||||
|
|
||||||
|
let address = monero::Address::standard(
|
||||||
|
env_config.monero_network,
|
||||||
|
monero::PublicKey::from_private_key(&spend_key),
|
||||||
|
monero::PublicKey::from(view_key.public()),
|
||||||
|
);
|
||||||
|
tracing::info!("Wallet address: {}", address.to_string());
|
||||||
|
|
||||||
|
let view_key = serde_json::to_string(&view_key)?;
|
||||||
|
println!("View key: {}", view_key);
|
||||||
|
|
||||||
|
println!("Spend key: {}", spend_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn init_bitcoin_wallet(
|
||||||
|
electrum_rpc_url: Url,
|
||||||
|
seed: &Seed,
|
||||||
|
data_dir: PathBuf,
|
||||||
|
env_config: Config,
|
||||||
|
bitcoin_target_block: usize,
|
||||||
|
) -> Result<bitcoin::Wallet> {
|
||||||
|
tracing::debug!("Initializing bitcoin wallet");
|
||||||
|
let xprivkey = seed.derive_extended_private_key(env_config.bitcoin_network)?;
|
||||||
|
|
||||||
|
let wallet = bitcoin::Wallet::new(
|
||||||
|
electrum_rpc_url.clone(),
|
||||||
|
data_dir,
|
||||||
|
xprivkey,
|
||||||
|
env_config,
|
||||||
|
bitcoin_target_block,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to initialize Bitcoin wallet")?;
|
||||||
|
|
||||||
|
tracing::debug!("Syncing bitcoin wallet");
|
||||||
|
wallet.sync().await?;
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init_monero_wallet(
|
||||||
|
data_dir: PathBuf,
|
||||||
|
monero_daemon_address: String,
|
||||||
|
env_config: Config,
|
||||||
|
) -> Result<(monero::Wallet, monero::WalletRpcProcess)> {
|
||||||
|
let network = env_config.monero_network;
|
||||||
|
|
||||||
|
const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet";
|
||||||
|
|
||||||
|
let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?;
|
||||||
|
|
||||||
|
let monero_wallet_rpc_process = monero_wallet_rpc
|
||||||
|
.run(network, monero_daemon_address.as_str())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let monero_wallet = monero::Wallet::open_or_create(
|
||||||
|
monero_wallet_rpc_process.endpoint(),
|
||||||
|
MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(),
|
||||||
|
env_config,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((monero_wallet, monero_wallet_rpc_process))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn qr_code(value: &impl ToString) -> Result<String> {
|
||||||
|
let code = QrCode::new(value.to_string())?;
|
||||||
|
let qr_code = code
|
||||||
|
.render::<unicode::Dense1x2>()
|
||||||
|
.dark_color(unicode::Dense1x2::Light)
|
||||||
|
.light_color(unicode::Dense1x2::Dark)
|
||||||
|
.build();
|
||||||
|
Ok(qr_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS, FFE, TFE>(
|
||||||
|
json: bool,
|
||||||
|
bid_quote: impl Future<Output = Result<BidQuote>>,
|
||||||
|
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
|
||||||
|
balance: FB,
|
||||||
|
max_giveable_fn: FMG,
|
||||||
|
sync: FS,
|
||||||
|
estimate_fee: FFE,
|
||||||
|
) -> Result<(bitcoin::Amount, bitcoin::Amount)>
|
||||||
|
where
|
||||||
|
TB: Future<Output = Result<bitcoin::Amount>>,
|
||||||
|
FB: Fn() -> TB,
|
||||||
|
TMG: Future<Output = Result<bitcoin::Amount>>,
|
||||||
|
FMG: Fn() -> TMG,
|
||||||
|
TS: Future<Output = Result<()>>,
|
||||||
|
FS: Fn() -> TS,
|
||||||
|
FFE: Fn(bitcoin::Amount) -> TFE,
|
||||||
|
TFE: Future<Output = Result<bitcoin::Amount>>,
|
||||||
|
{
|
||||||
|
tracing::debug!("Requesting quote");
|
||||||
|
let bid_quote = bid_quote.await?;
|
||||||
|
|
||||||
|
if bid_quote.max_quantity == bitcoin::Amount::ZERO {
|
||||||
|
bail!(ZeroQuoteReceived)
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
price = %bid_quote.price,
|
||||||
|
minimum_amount = %bid_quote.min_quantity,
|
||||||
|
maximum_amount = %bid_quote.max_quantity,
|
||||||
|
"Received quote",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut max_giveable = max_giveable_fn().await?;
|
||||||
|
|
||||||
|
if max_giveable == bitcoin::Amount::ZERO || max_giveable < bid_quote.min_quantity {
|
||||||
|
let deposit_address = get_new_address.await?;
|
||||||
|
let minimum_amount = bid_quote.min_quantity;
|
||||||
|
let maximum_amount = bid_quote.max_quantity;
|
||||||
|
|
||||||
|
if !json {
|
||||||
|
eprintln!("{}", qr_code(&deposit_address)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let min_outstanding = bid_quote.min_quantity - max_giveable;
|
||||||
|
let min_fee = estimate_fee(min_outstanding).await?;
|
||||||
|
let min_deposit = min_outstanding + min_fee;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Deposit at least {} to cover the min quantity with fee!",
|
||||||
|
min_deposit
|
||||||
|
);
|
||||||
|
tracing::info!(
|
||||||
|
%deposit_address,
|
||||||
|
%min_deposit,
|
||||||
|
%max_giveable,
|
||||||
|
%minimum_amount,
|
||||||
|
%maximum_amount,
|
||||||
|
"Waiting for Bitcoin deposit",
|
||||||
|
);
|
||||||
|
|
||||||
|
max_giveable = loop {
|
||||||
|
sync().await?;
|
||||||
|
let new_max_givable = max_giveable_fn().await?;
|
||||||
|
|
||||||
|
if new_max_givable > max_giveable {
|
||||||
|
break new_max_givable;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_balance = balance().await?;
|
||||||
|
tracing::info!(%new_balance, %max_giveable, "Received Bitcoin");
|
||||||
|
|
||||||
|
if max_giveable < bid_quote.min_quantity {
|
||||||
|
tracing::info!("Deposited amount is less than `min_quantity`");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let balance = balance().await?;
|
||||||
|
let fees = balance - max_giveable;
|
||||||
|
let max_accepted = bid_quote.max_quantity;
|
||||||
|
let btc_swap_amount = min(max_giveable, max_accepted);
|
||||||
|
|
||||||
|
Ok((btc_swap_amount, fees))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::determine_btc_to_swap;
|
||||||
use ::bitcoin::Amount;
|
use ::bitcoin::Amount;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Mutex;
|
||||||
use std::time::Duration;
|
|
||||||
use swap::api::request::determine_btc_to_swap;
|
|
||||||
use swap::network::quote::BidQuote;
|
|
||||||
use swap::tracing_ext::capture_logs;
|
use swap::tracing_ext::capture_logs;
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
|
|
||||||
@ -56,7 +666,7 @@ mod tests {
|
|||||||
|
|
||||||
let (amount, fees) = determine_btc_to_swap(
|
let (amount, fees) = determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_max(0.01),
|
async { Ok(quote_with_max(0.01)) },
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
|| async { Ok(Amount::from_btc(0.001)?) },
|
|| async { Ok(Amount::from_btc(0.001)?) },
|
||||||
|| async {
|
|| async {
|
||||||
@ -75,10 +685,10 @@ mod tests {
|
|||||||
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
writer.captured(),
|
writer.captured(),
|
||||||
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC
|
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
|
||||||
INFO swap::api::request: Deposit at least 0.00001 BTC to cover the min quantity with fee!
|
INFO swap: Deposit at least 0.00001000 BTC to cover the min quantity with fee!
|
||||||
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001 BTC max_giveable=0 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC
|
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001000 BTC max_giveable=0.00000000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
|
||||||
INFO swap::api::request: Received Bitcoin new_balance=0.001 BTC max_giveable=0.0009 BTC
|
INFO swap: Received Bitcoin new_balance=0.00100000 BTC max_giveable=0.00090000 BTC
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -93,7 +703,7 @@ mod tests {
|
|||||||
|
|
||||||
let (amount, fees) = determine_btc_to_swap(
|
let (amount, fees) = determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_max(0.01),
|
async { Ok(quote_with_max(0.01)) },
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
|| async { Ok(Amount::from_btc(0.1001)?) },
|
|| async { Ok(Amount::from_btc(0.1001)?) },
|
||||||
|| async {
|
|| async {
|
||||||
@ -112,10 +722,10 @@ mod tests {
|
|||||||
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
writer.captured(),
|
writer.captured(),
|
||||||
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC
|
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
|
||||||
INFO swap::api::request: Deposit at least 0.00001 BTC to cover the min quantity with fee!
|
INFO swap: Deposit at least 0.00001000 BTC to cover the min quantity with fee!
|
||||||
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001 BTC max_giveable=0 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC
|
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00001000 BTC max_giveable=0.00000000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC
|
||||||
INFO swap::api::request: Received Bitcoin new_balance=0.1001 BTC max_giveable=0.1 BTC
|
INFO swap: Received Bitcoin new_balance=0.10010000 BTC max_giveable=0.10000000 BTC
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -130,7 +740,7 @@ mod tests {
|
|||||||
|
|
||||||
let (amount, fees) = determine_btc_to_swap(
|
let (amount, fees) = determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_max(0.01),
|
async { Ok(quote_with_max(0.01)) },
|
||||||
async { panic!("should not request new address when initial balance is > 0") },
|
async { panic!("should not request new address when initial balance is > 0") },
|
||||||
|| async { Ok(Amount::from_btc(0.005)?) },
|
|| async { Ok(Amount::from_btc(0.005)?) },
|
||||||
|| async {
|
|| async {
|
||||||
@ -149,7 +759,7 @@ mod tests {
|
|||||||
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
writer.captured(),
|
writer.captured(),
|
||||||
" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC\n"
|
" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +773,7 @@ mod tests {
|
|||||||
|
|
||||||
let (amount, fees) = determine_btc_to_swap(
|
let (amount, fees) = determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_max(0.01),
|
async { Ok(quote_with_max(0.01)) },
|
||||||
async { panic!("should not request new address when initial balance is > 0") },
|
async { panic!("should not request new address when initial balance is > 0") },
|
||||||
|| async { Ok(Amount::from_btc(0.1001)?) },
|
|| async { Ok(Amount::from_btc(0.1001)?) },
|
||||||
|| async {
|
|| async {
|
||||||
@ -182,7 +792,7 @@ mod tests {
|
|||||||
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
writer.captured(),
|
writer.captured(),
|
||||||
" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0 BTC maximum_amount=0.01 BTC\n"
|
" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.00000000 BTC maximum_amount=0.01000000 BTC\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +806,7 @@ mod tests {
|
|||||||
|
|
||||||
let (amount, fees) = determine_btc_to_swap(
|
let (amount, fees) = determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_min(0.01),
|
async { Ok(quote_with_min(0.01)) },
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
|| async { Ok(Amount::from_btc(0.0101)?) },
|
|| async { Ok(Amount::from_btc(0.0101)?) },
|
||||||
|| async {
|
|| async {
|
||||||
@ -215,10 +825,10 @@ mod tests {
|
|||||||
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
writer.captured(),
|
writer.captured(),
|
||||||
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC
|
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.01000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
INFO swap::api::request: Deposit at least 0.01001 BTC to cover the min quantity with fee!
|
INFO swap: Deposit at least 0.01001000 BTC to cover the min quantity with fee!
|
||||||
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.01001 BTC max_giveable=0 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC
|
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.01001000 BTC max_giveable=0.00000000 BTC minimum_amount=0.01000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC
|
INFO swap: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -233,7 +843,7 @@ mod tests {
|
|||||||
|
|
||||||
let (amount, fees) = determine_btc_to_swap(
|
let (amount, fees) = determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_min(0.01),
|
async { Ok(quote_with_min(0.01)) },
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
|| async { Ok(Amount::from_btc(0.0101)?) },
|
|| async { Ok(Amount::from_btc(0.0101)?) },
|
||||||
|| async {
|
|| async {
|
||||||
@ -252,10 +862,10 @@ mod tests {
|
|||||||
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
assert_eq!((amount, fees), (expected_amount, expected_fees));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
writer.captured(),
|
writer.captured(),
|
||||||
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC
|
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.01000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
INFO swap::api::request: Deposit at least 0.00991 BTC to cover the min quantity with fee!
|
INFO swap: Deposit at least 0.00991000 BTC to cover the min quantity with fee!
|
||||||
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00991 BTC max_giveable=0.0001 BTC minimum_amount=0.01 BTC maximum_amount=184467440737.09551615 BTC
|
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.00991000 BTC max_giveable=0.00010000 BTC minimum_amount=0.01000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC
|
INFO swap: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -275,7 +885,7 @@ mod tests {
|
|||||||
Duration::from_secs(1),
|
Duration::from_secs(1),
|
||||||
determine_btc_to_swap(
|
determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_min(0.1),
|
async { Ok(quote_with_min(0.1)) },
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
|| async { Ok(Amount::from_btc(0.0101)?) },
|
|| async { Ok(Amount::from_btc(0.0101)?) },
|
||||||
|| async {
|
|| async {
|
||||||
@ -292,13 +902,13 @@ mod tests {
|
|||||||
assert!(matches!(error, tokio::time::error::Elapsed { .. }));
|
assert!(matches!(error, tokio::time::error::Elapsed { .. }));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
writer.captured(),
|
writer.captured(),
|
||||||
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
|
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
INFO swap::api::request: Deposit at least 0.10001 BTC to cover the min quantity with fee!
|
INFO swap: Deposit at least 0.10001000 BTC to cover the min quantity with fee!
|
||||||
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001 BTC max_giveable=0 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
|
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001000 BTC max_giveable=0.00000000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
INFO swap::api::request: Received Bitcoin new_balance=0.0101 BTC max_giveable=0.01 BTC
|
INFO swap: Received Bitcoin new_balance=0.01010000 BTC max_giveable=0.01000000 BTC
|
||||||
INFO swap::api::request: Deposited amount is less than `min_quantity`
|
INFO swap: Deposited amount is less than `min_quantity`
|
||||||
INFO swap::api::request: Deposit at least 0.09001 BTC to cover the min quantity with fee!
|
INFO swap: Deposit at least 0.09001000 BTC to cover the min quantity with fee!
|
||||||
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.09001 BTC max_giveable=0.01 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
|
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.09001000 BTC max_giveable=0.01000000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -323,7 +933,7 @@ mod tests {
|
|||||||
Duration::from_secs(10),
|
Duration::from_secs(10),
|
||||||
determine_btc_to_swap(
|
determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_min(0.1),
|
async { Ok(quote_with_min(0.1)) },
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
|| async { Ok(Amount::from_btc(0.21)?) },
|
|| async { Ok(Amount::from_btc(0.21)?) },
|
||||||
|| async {
|
|| async {
|
||||||
@ -341,10 +951,10 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
writer.captured(),
|
writer.captured(),
|
||||||
r" INFO swap::api::request: Received quote price=0.001 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
|
r" INFO swap: Received quote price=0.00100000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
INFO swap::api::request: Deposit at least 0.10001 BTC to cover the min quantity with fee!
|
INFO swap: Deposit at least 0.10001000 BTC to cover the min quantity with fee!
|
||||||
INFO swap::api::request: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001 BTC max_giveable=0 BTC minimum_amount=0.1 BTC maximum_amount=184467440737.09551615 BTC
|
INFO swap: Waiting for Bitcoin deposit deposit_address=1PdfytjS7C8wwd9Lq5o4x9aXA2YRqaCpH6 min_deposit=0.10001000 BTC max_giveable=0.00000000 BTC minimum_amount=0.10000000 BTC maximum_amount=184467440737.09551615 BTC
|
||||||
INFO swap::api::request: Received Bitcoin new_balance=0.21 BTC max_giveable=0.2 BTC
|
INFO swap: Received Bitcoin new_balance=0.21000000 BTC max_giveable=0.20000000 BTC
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -358,7 +968,7 @@ mod tests {
|
|||||||
|
|
||||||
let determination_error = determine_btc_to_swap(
|
let determination_error = determine_btc_to_swap(
|
||||||
true,
|
true,
|
||||||
quote_with_max(0.00),
|
async { Ok(quote_with_max(0.00)) },
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
|| async { Ok(Amount::from_btc(0.0101)?) },
|
|| async { Ok(Amount::from_btc(0.0101)?) },
|
||||||
|| async {
|
|| async {
|
||||||
|
@ -15,8 +15,7 @@ pub use crate::bitcoin::refund::TxRefund;
|
|||||||
pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks};
|
pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks};
|
||||||
pub use ::bitcoin::util::amount::Amount;
|
pub use ::bitcoin::util::amount::Amount;
|
||||||
pub use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
pub use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
||||||
pub use ::bitcoin::{Address, AddressType, Network, Transaction, Txid};
|
pub use ::bitcoin::{Address, Network, Transaction, Txid};
|
||||||
use bitcoin::secp256k1::ecdsa;
|
|
||||||
pub use ecdsa_fun::adaptor::EncryptedSignature;
|
pub use ecdsa_fun::adaptor::EncryptedSignature;
|
||||||
pub use ecdsa_fun::fun::Scalar;
|
pub use ecdsa_fun::fun::Scalar;
|
||||||
pub use ecdsa_fun::Signature;
|
pub use ecdsa_fun::Signature;
|
||||||
@ -26,8 +25,9 @@ pub use wallet::Wallet;
|
|||||||
pub use wallet::WalletBuilder;
|
pub use wallet::WalletBuilder;
|
||||||
|
|
||||||
use crate::bitcoin::wallet::ScriptStatus;
|
use crate::bitcoin::wallet::ScriptStatus;
|
||||||
|
use ::bitcoin::hashes::hex::ToHex;
|
||||||
use ::bitcoin::hashes::Hash;
|
use ::bitcoin::hashes::Hash;
|
||||||
use ::bitcoin::Sighash;
|
use ::bitcoin::{secp256k1, Sighash};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use bdk::miniscript::descriptor::Wsh;
|
use bdk::miniscript::descriptor::Wsh;
|
||||||
use bdk::miniscript::{Descriptor, Segwitv0};
|
use bdk::miniscript::{Descriptor, Segwitv0};
|
||||||
@ -108,7 +108,7 @@ impl SecretKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct PublicKey(Point);
|
pub struct PublicKey(Point);
|
||||||
|
|
||||||
impl PublicKey {
|
impl PublicKey {
|
||||||
@ -206,21 +206,20 @@ pub fn verify_encsig(
|
|||||||
#[error("encrypted signature is invalid")]
|
#[error("encrypted signature is invalid")]
|
||||||
pub struct InvalidEncryptedSignature;
|
pub struct InvalidEncryptedSignature;
|
||||||
|
|
||||||
pub fn build_shared_output_descriptor(
|
pub fn build_shared_output_descriptor(A: Point, B: Point) -> Descriptor<bitcoin::PublicKey> {
|
||||||
A: Point,
|
|
||||||
B: Point,
|
|
||||||
) -> Result<Descriptor<bitcoin::PublicKey>> {
|
|
||||||
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))";
|
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))";
|
||||||
|
|
||||||
let miniscript = MINISCRIPT_TEMPLATE
|
// NOTE: This shouldn't be a source of error, but maybe it is
|
||||||
.replace('A', &A.to_string())
|
let A = ToHex::to_hex(&secp256k1::PublicKey::from(A));
|
||||||
.replace('B', &B.to_string());
|
let B = ToHex::to_hex(&secp256k1::PublicKey::from(B));
|
||||||
|
|
||||||
|
let miniscript = MINISCRIPT_TEMPLATE.replace('A', &A).replace('B', &B);
|
||||||
|
|
||||||
let miniscript =
|
let miniscript =
|
||||||
bdk::miniscript::Miniscript::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
|
bdk::miniscript::Miniscript::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
|
||||||
.expect("a valid miniscript");
|
.expect("a valid miniscript");
|
||||||
|
|
||||||
Ok(Descriptor::Wsh(Wsh::new(miniscript)?))
|
Descriptor::Wsh(Wsh::new(miniscript).expect("a valid descriptor"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
||||||
@ -245,72 +244,10 @@ pub fn current_epoch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tx_lock_status.is_confirmed_with(cancel_timelock) {
|
if tx_lock_status.is_confirmed_with(cancel_timelock) {
|
||||||
return ExpiredTimelocks::Cancel {
|
return ExpiredTimelocks::Cancel;
|
||||||
blocks_left: tx_cancel_status.blocks_left_until(punish_timelock),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ExpiredTimelocks::None {
|
ExpiredTimelocks::None
|
||||||
blocks_left: tx_lock_status.blocks_left_until(cancel_timelock),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod bitcoin_address {
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)]
|
|
||||||
#[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
|
|
||||||
pub struct BitcoinAddressNetworkMismatch {
|
|
||||||
#[serde(with = "crate::bitcoin::network")]
|
|
||||||
expected: bitcoin::Network,
|
|
||||||
#[serde(with = "crate::bitcoin::network")]
|
|
||||||
actual: bitcoin::Network,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(addr_str: &str) -> Result<bitcoin::Address> {
|
|
||||||
let address = bitcoin::Address::from_str(addr_str)?;
|
|
||||||
|
|
||||||
if address.address_type() != Some(bitcoin::AddressType::P2wpkh) {
|
|
||||||
anyhow::bail!("Invalid Bitcoin address provided, only bech32 format is supported!")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate(
|
|
||||||
address: bitcoin::Address,
|
|
||||||
expected_network: bitcoin::Network,
|
|
||||||
) -> Result<bitcoin::Address> {
|
|
||||||
if address.network != expected_network {
|
|
||||||
bail!(BitcoinAddressNetworkMismatch {
|
|
||||||
expected: expected_network,
|
|
||||||
actual: address.network
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_is_testnet(
|
|
||||||
address: bitcoin::Address,
|
|
||||||
is_testnet: bool,
|
|
||||||
) -> Result<bitcoin::Address> {
|
|
||||||
let expected_network = if is_testnet {
|
|
||||||
bitcoin::Network::Testnet
|
|
||||||
} else {
|
|
||||||
bitcoin::Network::Bitcoin
|
|
||||||
};
|
|
||||||
validate(address, expected_network)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform the ecdsa der signature bytes into a secp256kfun ecdsa signature type.
|
|
||||||
pub fn extract_ecdsa_sig(sig: &[u8]) -> Result<Signature> {
|
|
||||||
let data = &sig[..sig.len() - 1];
|
|
||||||
let sig = ecdsa::Signature::from_der(data)?.serialize_compact();
|
|
||||||
Signature::from_bytes(sig).ok_or(anyhow::anyhow!("invalid signature"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23
|
/// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23
|
||||||
@ -386,10 +323,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::env::{GetConfig, Regtest};
|
use crate::env::{GetConfig, Regtest};
|
||||||
use crate::protocol::{alice, bob};
|
use crate::protocol::{alice, bob};
|
||||||
use bitcoin::secp256k1;
|
|
||||||
use ecdsa_fun::fun::marker::{NonZero, Public};
|
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use std::matches;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -404,7 +338,7 @@ mod tests {
|
|||||||
tx_cancel_status,
|
tx_cancel_status,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(matches!(expired_timelock, ExpiredTimelocks::None { .. }));
|
assert_eq!(expired_timelock, ExpiredTimelocks::None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -419,7 +353,7 @@ mod tests {
|
|||||||
tx_cancel_status,
|
tx_cancel_status,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(matches!(expired_timelock, ExpiredTimelocks::Cancel { .. }));
|
assert_eq!(expired_timelock, ExpiredTimelocks::Cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -534,16 +468,4 @@ mod tests {
|
|||||||
transaction
|
transaction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn compare_point_hex() {
|
|
||||||
// secp256kfun Point and secp256k1 PublicKey should have the same bytes and hex representation
|
|
||||||
let secp = secp256k1::Secp256k1::default();
|
|
||||||
let keypair = secp256k1::KeyPair::new(&secp, &mut OsRng);
|
|
||||||
|
|
||||||
let pubkey = keypair.public_key();
|
|
||||||
let point: Point<_, Public, NonZero> = Point::from_bytes(pubkey.serialize()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(pubkey.to_string(), point.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,7 @@ use crate::bitcoin::{
|
|||||||
};
|
};
|
||||||
use ::bitcoin::util::sighash::SighashCache;
|
use ::bitcoin::util::sighash::SighashCache;
|
||||||
use ::bitcoin::{
|
use ::bitcoin::{
|
||||||
secp256k1, EcdsaSighashType, OutPoint, PackedLockTime, Script, Sequence, Sighash, TxIn, TxOut,
|
EcdsaSighashType, OutPoint, PackedLockTime, Script, Sequence, Sighash, TxIn, TxOut, Txid,
|
||||||
Txid,
|
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bdk::miniscript::Descriptor;
|
use bdk::miniscript::Descriptor;
|
||||||
@ -25,12 +24,6 @@ use std::ops::Add;
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct CancelTimelock(u32);
|
pub struct CancelTimelock(u32);
|
||||||
|
|
||||||
impl From<CancelTimelock> for u32 {
|
|
||||||
fn from(cancel_timelock: CancelTimelock) -> Self {
|
|
||||||
cancel_timelock.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CancelTimelock {
|
impl CancelTimelock {
|
||||||
pub const fn new(number_of_blocks: u32) -> Self {
|
pub const fn new(number_of_blocks: u32) -> Self {
|
||||||
Self(number_of_blocks)
|
Self(number_of_blocks)
|
||||||
@ -71,12 +64,6 @@ impl fmt::Display for CancelTimelock {
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct PunishTimelock(u32);
|
pub struct PunishTimelock(u32);
|
||||||
|
|
||||||
impl From<PunishTimelock> for u32 {
|
|
||||||
fn from(punish_timelock: PunishTimelock) -> Self {
|
|
||||||
punish_timelock.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PunishTimelock {
|
impl PunishTimelock {
|
||||||
pub const fn new(number_of_blocks: u32) -> Self {
|
pub const fn new(number_of_blocks: u32) -> Self {
|
||||||
Self(number_of_blocks)
|
Self(number_of_blocks)
|
||||||
@ -118,8 +105,8 @@ impl TxCancel {
|
|||||||
A: PublicKey,
|
A: PublicKey,
|
||||||
B: PublicKey,
|
B: PublicKey,
|
||||||
spending_fee: Amount,
|
spending_fee: Amount,
|
||||||
) -> Result<Self> {
|
) -> Self {
|
||||||
let cancel_output_descriptor = build_shared_output_descriptor(A.0, B.0)?;
|
let cancel_output_descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||||
|
|
||||||
let tx_in = TxIn {
|
let tx_in = TxIn {
|
||||||
previous_output: tx_lock.as_outpoint(),
|
previous_output: tx_lock.as_outpoint(),
|
||||||
@ -149,12 +136,12 @@ impl TxCancel {
|
|||||||
)
|
)
|
||||||
.expect("sighash");
|
.expect("sighash");
|
||||||
|
|
||||||
Ok(Self {
|
Self {
|
||||||
inner: transaction,
|
inner: transaction,
|
||||||
digest,
|
digest,
|
||||||
output_descriptor: cancel_output_descriptor,
|
output_descriptor: cancel_output_descriptor,
|
||||||
lock_output_descriptor: tx_lock.output_descriptor.clone(),
|
lock_output_descriptor: tx_lock.output_descriptor.clone(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn txid(&self) -> Txid {
|
pub fn txid(&self) -> Txid {
|
||||||
@ -215,30 +202,22 @@ impl TxCancel {
|
|||||||
|
|
||||||
let A = ::bitcoin::PublicKey {
|
let A = ::bitcoin::PublicKey {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
inner: secp256k1::PublicKey::from_slice(&A.0.to_bytes())?,
|
inner: A.0.into(),
|
||||||
};
|
};
|
||||||
let B = ::bitcoin::PublicKey {
|
let B = ::bitcoin::PublicKey {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
inner: secp256k1::PublicKey::from_slice(&B.0.to_bytes())?,
|
inner: B.0.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// The order in which these are inserted doesn't matter
|
// The order in which these are inserted doesn't matter
|
||||||
let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?;
|
satisfier.insert(A, ::bitcoin::EcdsaSig {
|
||||||
let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?;
|
sig: sig_a.into(),
|
||||||
satisfier.insert(
|
hash_ty: EcdsaSighashType::All,
|
||||||
A,
|
});
|
||||||
::bitcoin::EcdsaSig {
|
satisfier.insert(B, ::bitcoin::EcdsaSig {
|
||||||
sig: sig_a,
|
sig: sig_b.into(),
|
||||||
hash_ty: EcdsaSighashType::All,
|
hash_ty: EcdsaSighashType::All,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
satisfier.insert(
|
|
||||||
B,
|
|
||||||
::bitcoin::EcdsaSig {
|
|
||||||
sig: sig_b,
|
|
||||||
hash_ty: EcdsaSighashType::All,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
satisfier
|
satisfier
|
||||||
};
|
};
|
||||||
|
@ -4,17 +4,16 @@ use crate::bitcoin::{
|
|||||||
};
|
};
|
||||||
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
||||||
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
|
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Result};
|
||||||
use bdk::database::BatchDatabase;
|
use bdk::database::BatchDatabase;
|
||||||
use bdk::miniscript::Descriptor;
|
use bdk::miniscript::Descriptor;
|
||||||
use bdk::psbt::PsbtUtils;
|
|
||||||
use bitcoin::{PackedLockTime, Script, Sequence};
|
use bitcoin::{PackedLockTime, Script, Sequence};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const SCRIPT_SIZE: usize = 34;
|
const SCRIPT_SIZE: usize = 34;
|
||||||
const TX_LOCK_WEIGHT: usize = 485;
|
const TX_LOCK_WEIGHT: usize = 485;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct TxLock {
|
pub struct TxLock {
|
||||||
inner: PartiallySignedTransaction,
|
inner: PartiallySignedTransaction,
|
||||||
pub(in crate::bitcoin) output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
pub(in crate::bitcoin) output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||||
@ -32,7 +31,7 @@ impl TxLock {
|
|||||||
C: EstimateFeeRate,
|
C: EstimateFeeRate,
|
||||||
D: BatchDatabase,
|
D: BatchDatabase,
|
||||||
{
|
{
|
||||||
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0)?;
|
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||||
let address = lock_output_descriptor
|
let address = lock_output_descriptor
|
||||||
.address(wallet.get_network())
|
.address(wallet.get_network())
|
||||||
.expect("can derive address from descriptor");
|
.expect("can derive address from descriptor");
|
||||||
@ -84,7 +83,7 @@ impl TxLock {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let descriptor = build_shared_output_descriptor(A.0, B.0)?;
|
let descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||||
let legit_shared_output_script = descriptor.script_pubkey();
|
let legit_shared_output_script = descriptor.script_pubkey();
|
||||||
|
|
||||||
if shared_output_candidate.script_pubkey != legit_shared_output_script {
|
if shared_output_candidate.script_pubkey != legit_shared_output_script {
|
||||||
@ -101,15 +100,6 @@ impl TxLock {
|
|||||||
Amount::from_sat(self.inner.clone().extract_tx().output[self.lock_output_vout()].value)
|
Amount::from_sat(self.inner.clone().extract_tx().output[self.lock_output_vout()].value)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fee(&self) -> Result<Amount> {
|
|
||||||
Ok(Amount::from_sat(
|
|
||||||
self.inner
|
|
||||||
.clone()
|
|
||||||
.fee_amount()
|
|
||||||
.context("The PSBT is missing a TxOut for an input")?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn txid(&self) -> Txid {
|
pub fn txid(&self) -> Txid {
|
||||||
self.inner.clone().extract_tx().txid()
|
self.inner.clone().extract_tx().txid()
|
||||||
}
|
}
|
||||||
@ -263,7 +253,7 @@ mod tests {
|
|||||||
fn estimated_tx_lock_script_size_never_changes(a in crate::proptest::ecdsa_fun::point(), b in crate::proptest::ecdsa_fun::point()) {
|
fn estimated_tx_lock_script_size_never_changes(a in crate::proptest::ecdsa_fun::point(), b in crate::proptest::ecdsa_fun::point()) {
|
||||||
proptest::prop_assume!(a != b);
|
proptest::prop_assume!(a != b);
|
||||||
|
|
||||||
let computed_size = build_shared_output_descriptor(a, b).unwrap().script_pubkey().len();
|
let computed_size = build_shared_output_descriptor(a, b).script_pubkey().len();
|
||||||
|
|
||||||
assert_eq!(computed_size, SCRIPT_SIZE);
|
assert_eq!(computed_size, SCRIPT_SIZE);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::bitcoin::wallet::Watchable;
|
use crate::bitcoin::wallet::Watchable;
|
||||||
use crate::bitcoin::{self, Address, Amount, PunishTimelock, Transaction, TxCancel, Txid};
|
use crate::bitcoin::{self, Address, Amount, PunishTimelock, Transaction, TxCancel, Txid};
|
||||||
use ::bitcoin::util::sighash::SighashCache;
|
use ::bitcoin::util::sighash::SighashCache;
|
||||||
use ::bitcoin::{secp256k1, EcdsaSighashType, Sighash};
|
use ::bitcoin::{EcdsaSighashType, Sighash};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bdk::bitcoin::Script;
|
use bdk::bitcoin::Script;
|
||||||
use bdk::miniscript::Descriptor;
|
use bdk::miniscript::Descriptor;
|
||||||
@ -64,23 +64,15 @@ impl TxPunish {
|
|||||||
let A = a.public().try_into()?;
|
let A = a.public().try_into()?;
|
||||||
let B = B.try_into()?;
|
let B = B.try_into()?;
|
||||||
|
|
||||||
let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?;
|
|
||||||
let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?;
|
|
||||||
// The order in which these are inserted doesn't matter
|
// The order in which these are inserted doesn't matter
|
||||||
satisfier.insert(
|
satisfier.insert(A, ::bitcoin::EcdsaSig {
|
||||||
A,
|
sig: sig_a.into(),
|
||||||
::bitcoin::EcdsaSig {
|
hash_ty: EcdsaSighashType::All,
|
||||||
sig: sig_a,
|
});
|
||||||
hash_ty: EcdsaSighashType::All,
|
satisfier.insert(B, ::bitcoin::EcdsaSig {
|
||||||
},
|
sig: sig_b.into(),
|
||||||
);
|
hash_ty: EcdsaSighashType::All,
|
||||||
satisfier.insert(
|
});
|
||||||
B,
|
|
||||||
::bitcoin::EcdsaSig {
|
|
||||||
sig: sig_b,
|
|
||||||
hash_ty: EcdsaSighashType::All,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
satisfier
|
satisfier
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ use crate::bitcoin::{
|
|||||||
use ::bitcoin::{Sighash, Txid};
|
use ::bitcoin::{Sighash, Txid};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use bdk::miniscript::Descriptor;
|
use bdk::miniscript::Descriptor;
|
||||||
use bitcoin::secp256k1;
|
use bitcoin::secp256k1::ecdsa;
|
||||||
use bitcoin::util::sighash::SighashCache;
|
use bitcoin::util::sighash::SighashCache;
|
||||||
use bitcoin::{EcdsaSighashType, Script};
|
use bitcoin::{EcdsaSighashType, Script};
|
||||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||||
@ -16,8 +16,6 @@ use ecdsa_fun::Signature;
|
|||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::extract_ecdsa_sig;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TxRedeem {
|
pub struct TxRedeem {
|
||||||
inner: Transaction,
|
inner: Transaction,
|
||||||
@ -66,7 +64,7 @@ impl TxRedeem {
|
|||||||
) -> Result<Transaction> {
|
) -> Result<Transaction> {
|
||||||
verify_encsig(
|
verify_encsig(
|
||||||
B,
|
B,
|
||||||
PublicKey::from(s_a),
|
PublicKey::from(s_a.clone()),
|
||||||
&self.digest(),
|
&self.digest(),
|
||||||
&encrypted_signature,
|
&encrypted_signature,
|
||||||
)
|
)
|
||||||
@ -81,30 +79,22 @@ impl TxRedeem {
|
|||||||
|
|
||||||
let A = ::bitcoin::PublicKey {
|
let A = ::bitcoin::PublicKey {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
inner: secp256k1::PublicKey::from_slice(&a.public.to_bytes())?,
|
inner: a.public.into(),
|
||||||
};
|
};
|
||||||
let B = ::bitcoin::PublicKey {
|
let B = ::bitcoin::PublicKey {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
inner: secp256k1::PublicKey::from_slice(&B.0.to_bytes())?,
|
inner: B.0.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?;
|
|
||||||
let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?;
|
|
||||||
// The order in which these are inserted doesn't matter
|
// The order in which these are inserted doesn't matter
|
||||||
satisfier.insert(
|
satisfier.insert(A, ::bitcoin::EcdsaSig {
|
||||||
A,
|
sig: sig_a.into(),
|
||||||
::bitcoin::EcdsaSig {
|
hash_ty: EcdsaSighashType::All,
|
||||||
sig: sig_a,
|
});
|
||||||
hash_ty: EcdsaSighashType::All,
|
satisfier.insert(B, ::bitcoin::EcdsaSig {
|
||||||
},
|
sig: sig_b.into(),
|
||||||
);
|
hash_ty: EcdsaSighashType::All,
|
||||||
satisfier.insert(
|
});
|
||||||
B,
|
|
||||||
::bitcoin::EcdsaSig {
|
|
||||||
sig: sig_b,
|
|
||||||
hash_ty: EcdsaSighashType::All,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
satisfier
|
satisfier
|
||||||
};
|
};
|
||||||
@ -124,16 +114,16 @@ impl TxRedeem {
|
|||||||
let input = match candidate_transaction.input.as_slice() {
|
let input = match candidate_transaction.input.as_slice() {
|
||||||
[input] => input,
|
[input] => input,
|
||||||
[] => bail!(NoInputs),
|
[] => bail!(NoInputs),
|
||||||
inputs => bail!(TooManyInputs(inputs.len())),
|
[inputs @ ..] => bail!(TooManyInputs(inputs.len())),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sigs = match input.witness.to_vec().as_slice() {
|
let sigs = match input.witness.iter().collect::<Vec<_>>().as_slice() {
|
||||||
[sig_1, sig_2, _script] => [sig_1, sig_2]
|
[sig_1, sig_2, _script] => [sig_1, sig_2]
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|sig| extract_ecdsa_sig(sig))
|
.map(|sig| ecdsa::Signature::from_der(&sig[..sig.len() - 1]).map(Signature::from))
|
||||||
.collect::<Result<Vec<_>, _>>(),
|
.collect::<std::result::Result<Vec<_>, _>>(),
|
||||||
[] => bail!(EmptyWitnessStack),
|
[] => bail!(EmptyWitnessStack),
|
||||||
witnesses => bail!(NotThreeWitnesses(witnesses.len())),
|
[witnesses @ ..] => bail!(NotThreeWitnesses(witnesses.len())),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let sig = sigs
|
let sig = sigs
|
||||||
|
@ -4,7 +4,7 @@ use crate::bitcoin::{
|
|||||||
TooManyInputs, Transaction, TxCancel,
|
TooManyInputs, Transaction, TxCancel,
|
||||||
};
|
};
|
||||||
use crate::{bitcoin, monero};
|
use crate::{bitcoin, monero};
|
||||||
use ::bitcoin::secp256k1;
|
use ::bitcoin::secp256k1::ecdsa;
|
||||||
use ::bitcoin::util::sighash::SighashCache;
|
use ::bitcoin::util::sighash::SighashCache;
|
||||||
use ::bitcoin::{EcdsaSighashType, Script, Sighash, Txid};
|
use ::bitcoin::{EcdsaSighashType, Script, Sighash, Txid};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
@ -12,8 +12,6 @@ use bdk::miniscript::Descriptor;
|
|||||||
use ecdsa_fun::Signature;
|
use ecdsa_fun::Signature;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::extract_ecdsa_sig;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TxRefund {
|
pub struct TxRefund {
|
||||||
inner: Transaction,
|
inner: Transaction,
|
||||||
@ -64,30 +62,22 @@ impl TxRefund {
|
|||||||
|
|
||||||
let A = ::bitcoin::PublicKey {
|
let A = ::bitcoin::PublicKey {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
inner: secp256k1::PublicKey::from_slice(&A.0.to_bytes())?,
|
inner: A.0.into(),
|
||||||
};
|
};
|
||||||
let B = ::bitcoin::PublicKey {
|
let B = ::bitcoin::PublicKey {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
inner: secp256k1::PublicKey::from_slice(&B.0.to_bytes())?,
|
inner: B.0.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sig_a = secp256k1::ecdsa::Signature::from_compact(&sig_a.to_bytes())?;
|
|
||||||
let sig_b = secp256k1::ecdsa::Signature::from_compact(&sig_b.to_bytes())?;
|
|
||||||
// The order in which these are inserted doesn't matter
|
// The order in which these are inserted doesn't matter
|
||||||
satisfier.insert(
|
satisfier.insert(A, ::bitcoin::EcdsaSig {
|
||||||
A,
|
sig: sig_a.into(),
|
||||||
::bitcoin::EcdsaSig {
|
hash_ty: EcdsaSighashType::All,
|
||||||
sig: sig_a,
|
});
|
||||||
hash_ty: EcdsaSighashType::All,
|
satisfier.insert(B, ::bitcoin::EcdsaSig {
|
||||||
},
|
sig: sig_b.into(),
|
||||||
);
|
hash_ty: EcdsaSighashType::All,
|
||||||
satisfier.insert(
|
});
|
||||||
B,
|
|
||||||
::bitcoin::EcdsaSig {
|
|
||||||
sig: sig_b,
|
|
||||||
hash_ty: EcdsaSighashType::All,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
satisfier
|
satisfier
|
||||||
};
|
};
|
||||||
@ -131,16 +121,16 @@ impl TxRefund {
|
|||||||
let input = match candidate_transaction.input.as_slice() {
|
let input = match candidate_transaction.input.as_slice() {
|
||||||
[input] => input,
|
[input] => input,
|
||||||
[] => bail!(NoInputs),
|
[] => bail!(NoInputs),
|
||||||
inputs => bail!(TooManyInputs(inputs.len())),
|
[inputs @ ..] => bail!(TooManyInputs(inputs.len())),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sigs = match input.witness.to_vec().as_slice() {
|
let sigs = match input.witness.iter().collect::<Vec<_>>().as_slice() {
|
||||||
[sig_1, sig_2, _script] => [sig_1, sig_2]
|
[sig_1, sig_2, _script] => [sig_1, sig_2]
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|sig| extract_ecdsa_sig(sig))
|
.map(|sig| ecdsa::Signature::from_der(&sig[..sig.len() - 1]).map(Signature::from))
|
||||||
.collect::<Result<Vec<_>, _>>(),
|
.collect::<std::result::Result<Vec<_>, _>>(),
|
||||||
[] => bail!(EmptyWitnessStack),
|
[] => bail!(EmptyWitnessStack),
|
||||||
witnesses => bail!(NotThreeWitnesses(witnesses.len())),
|
[witnesses @ ..] => bail!(NotThreeWitnesses(witnesses.len())),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let sig = sigs
|
let sig = sigs
|
||||||
|
@ -37,9 +37,9 @@ impl Add<u32> for BlockHeight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum ExpiredTimelocks {
|
pub enum ExpiredTimelocks {
|
||||||
None { blocks_left: u32 },
|
None,
|
||||||
Cancel { blocks_left: u32 },
|
Cancel,
|
||||||
Punish,
|
Punish,
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@ use std::path::Path;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::{watch, Mutex};
|
use tokio::sync::{watch, Mutex};
|
||||||
use tracing::{debug_span, Instrument};
|
|
||||||
|
|
||||||
const SLED_TREE_NAME: &str = "default_tree";
|
const SLED_TREE_NAME: &str = "default_tree";
|
||||||
|
|
||||||
@ -55,7 +54,7 @@ impl Wallet {
|
|||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let data_dir = data_dir.as_ref();
|
let data_dir = data_dir.as_ref();
|
||||||
let wallet_dir = data_dir.join(WALLET);
|
let wallet_dir = data_dir.join(WALLET);
|
||||||
let database = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
|
let database = bdk::sled::open(&wallet_dir)?.open_tree(SLED_TREE_NAME)?;
|
||||||
let network = env_config.bitcoin_network;
|
let network = env_config.bitcoin_network;
|
||||||
|
|
||||||
let wallet = match bdk::Wallet::new(
|
let wallet = match bdk::Wallet::new(
|
||||||
@ -65,7 +64,9 @@ impl Wallet {
|
|||||||
database,
|
database,
|
||||||
) {
|
) {
|
||||||
Ok(w) => w,
|
Ok(w) => w,
|
||||||
Err(bdk::Error::ChecksumMismatch) => Self::migrate(data_dir, xprivkey, network)?,
|
Err(e) if matches!(e, bdk::Error::ChecksumMismatch) => {
|
||||||
|
Self::migrate(data_dir, xprivkey, network)?
|
||||||
|
}
|
||||||
err => err?,
|
err => err?,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ impl Wallet {
|
|||||||
std::fs::rename(from, to)?;
|
std::fs::rename(from, to)?;
|
||||||
|
|
||||||
let wallet_dir = data_dir.join(WALLET);
|
let wallet_dir = data_dir.join(WALLET);
|
||||||
let database = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
|
let database = bdk::sled::open(&wallet_dir)?.open_tree(SLED_TREE_NAME)?;
|
||||||
|
|
||||||
let wallet = bdk::Wallet::new(
|
let wallet = bdk::Wallet::new(
|
||||||
bdk::template::Bip84(xprivkey, KeychainKind::External),
|
bdk::template::Bip84(xprivkey, KeychainKind::External),
|
||||||
@ -191,7 +192,7 @@ impl Wallet {
|
|||||||
|
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
}
|
}
|
||||||
}.instrument(debug_span!("BitcoinWalletSubscription")));
|
});
|
||||||
|
|
||||||
Subscription {
|
Subscription {
|
||||||
receiver,
|
receiver,
|
||||||
@ -273,7 +274,7 @@ impl Subscription {
|
|||||||
|
|
||||||
pub async fn wait_until_confirmed_with<T>(&self, target: T) -> Result<()>
|
pub async fn wait_until_confirmed_with<T>(&self, target: T) -> Result<()>
|
||||||
where
|
where
|
||||||
T: Into<u32>,
|
u32: PartialOrd<T>,
|
||||||
T: Copy,
|
T: Copy,
|
||||||
{
|
{
|
||||||
self.wait_until(|status| status.is_confirmed_with(target))
|
self.wait_until(|status| status.is_confirmed_with(target))
|
||||||
@ -468,7 +469,6 @@ where
|
|||||||
) -> Result<bitcoin::Amount> {
|
) -> Result<bitcoin::Amount> {
|
||||||
let client = self.client.lock().await;
|
let client = self.client.lock().await;
|
||||||
let fee_rate = client.estimate_feerate(self.target_block)?;
|
let fee_rate = client.estimate_feerate(self.target_block)?;
|
||||||
|
|
||||||
let min_relay_fee = client.min_relay_fee()?;
|
let min_relay_fee = client.min_relay_fee()?;
|
||||||
|
|
||||||
estimate_fee(weight, transfer_amount, fee_rate, min_relay_fee)
|
estimate_fee(weight, transfer_amount, fee_rate, min_relay_fee)
|
||||||
@ -738,15 +738,12 @@ impl Client {
|
|||||||
let client = bdk::electrum_client::Client::new(electrum_rpc_url.as_str())
|
let client = bdk::electrum_client::Client::new(electrum_rpc_url.as_str())
|
||||||
.context("Failed to initialize Electrum RPC client")?;
|
.context("Failed to initialize Electrum RPC client")?;
|
||||||
let blockchain = ElectrumBlockchain::from(client);
|
let blockchain = ElectrumBlockchain::from(client);
|
||||||
let last_sync = Instant::now()
|
|
||||||
.checked_sub(interval)
|
|
||||||
.expect("no underflow since block time is only 600 secs");
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
electrum,
|
electrum,
|
||||||
blockchain,
|
blockchain,
|
||||||
latest_block_height: BlockHeight::try_from(latest_block)?,
|
latest_block_height: BlockHeight::try_from(latest_block)?,
|
||||||
last_sync,
|
last_sync: Instant::now() - interval,
|
||||||
sync_interval: interval,
|
sync_interval: interval,
|
||||||
script_history: Default::default(),
|
script_history: Default::default(),
|
||||||
subscriptions: Default::default(),
|
subscriptions: Default::default(),
|
||||||
@ -761,10 +758,9 @@ impl Client {
|
|||||||
self.blockchain.get_tx(txid)
|
self.blockchain.get_tx(txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_state(&mut self, force_sync: bool) -> Result<()> {
|
fn update_state(&mut self) -> Result<()> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
if now < self.last_sync + self.sync_interval {
|
||||||
if !force_sync && now < self.last_sync + self.sync_interval {
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -784,15 +780,10 @@ impl Client {
|
|||||||
|
|
||||||
if !self.script_history.contains_key(&script) {
|
if !self.script_history.contains_key(&script) {
|
||||||
self.script_history.insert(script.clone(), vec![]);
|
self.script_history.insert(script.clone(), vec![]);
|
||||||
|
|
||||||
// When we first subscribe to a script we want to immediately fetch its status
|
|
||||||
// Otherwise we would have to wait for the next sync interval, which can take a minute
|
|
||||||
// This would result in potentially inaccurate status updates until that next sync interval is hit
|
|
||||||
self.update_state(true)?;
|
|
||||||
} else {
|
|
||||||
self.update_state(false)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.update_state()?;
|
||||||
|
|
||||||
let history = self.script_history.entry(script).or_default();
|
let history = self.script_history.entry(script).or_default();
|
||||||
|
|
||||||
let history_of_tx = history
|
let history_of_tx = history
|
||||||
@ -872,11 +863,6 @@ impl EstimateFeeRate for Client {
|
|||||||
// https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L213
|
// https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L213
|
||||||
// Returned estimated fees are per BTC/kb.
|
// Returned estimated fees are per BTC/kb.
|
||||||
let fee_per_byte = self.electrum.estimate_fee(target_block)?;
|
let fee_per_byte = self.electrum.estimate_fee(target_block)?;
|
||||||
|
|
||||||
if fee_per_byte < 0.0 {
|
|
||||||
bail!("Fee per byte returned by electrum server is negative: {}. This may indicate that fee estimation is not supported by this server", fee_per_byte);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we do not expect fees being that high.
|
// we do not expect fees being that high.
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
Ok(FeeRate::from_btc_per_kvb(fee_per_byte as f32))
|
Ok(FeeRate::from_btc_per_kvb(fee_per_byte as f32))
|
||||||
@ -890,7 +876,7 @@ impl EstimateFeeRate for Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum ScriptStatus {
|
pub enum ScriptStatus {
|
||||||
Unseen,
|
Unseen,
|
||||||
InMempool,
|
InMempool,
|
||||||
@ -907,7 +893,7 @@ impl ScriptStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub struct Confirmed {
|
pub struct Confirmed {
|
||||||
/// The depth of this transaction within the blockchain.
|
/// The depth of this transaction within the blockchain.
|
||||||
///
|
///
|
||||||
@ -938,20 +924,9 @@ impl Confirmed {
|
|||||||
|
|
||||||
pub fn meets_target<T>(&self, target: T) -> bool
|
pub fn meets_target<T>(&self, target: T) -> bool
|
||||||
where
|
where
|
||||||
T: Into<u32>,
|
u32: PartialOrd<T>,
|
||||||
{
|
{
|
||||||
self.confirmations() >= target.into()
|
self.confirmations() >= target
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blocks_left_until<T>(&self, target: T) -> u32
|
|
||||||
where
|
|
||||||
T: Into<u32> + Copy,
|
|
||||||
{
|
|
||||||
if self.meets_target(target) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
target.into() - self.confirmations()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -964,7 +939,7 @@ impl ScriptStatus {
|
|||||||
/// Check if the script has met the given confirmation target.
|
/// Check if the script has met the given confirmation target.
|
||||||
pub fn is_confirmed_with<T>(&self, target: T) -> bool
|
pub fn is_confirmed_with<T>(&self, target: T) -> bool
|
||||||
where
|
where
|
||||||
T: Into<u32>,
|
u32: PartialOrd<T>,
|
||||||
{
|
{
|
||||||
match self {
|
match self {
|
||||||
ScriptStatus::Confirmed(inner) => inner.meets_target(target),
|
ScriptStatus::Confirmed(inner) => inner.meets_target(target),
|
||||||
@ -972,17 +947,6 @@ impl ScriptStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the number of blocks left until the target is met.
|
|
||||||
pub fn blocks_left_until<T>(&self, target: T) -> u32
|
|
||||||
where
|
|
||||||
T: Into<u32> + Copy,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
ScriptStatus::Confirmed(inner) => inner.blocks_left_until(target),
|
|
||||||
_ => target.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_been_seen(&self) -> bool {
|
pub fn has_been_seen(&self) -> bool {
|
||||||
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
|
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
|
||||||
}
|
}
|
||||||
@ -1014,7 +978,7 @@ mod tests {
|
|||||||
fn given_depth_0_should_meet_confirmation_target_one() {
|
fn given_depth_0_should_meet_confirmation_target_one() {
|
||||||
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
|
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
|
||||||
|
|
||||||
let confirmed = script.is_confirmed_with(1_u32);
|
let confirmed = script.is_confirmed_with(1);
|
||||||
|
|
||||||
assert!(confirmed)
|
assert!(confirmed)
|
||||||
}
|
}
|
||||||
@ -1023,7 +987,7 @@ mod tests {
|
|||||||
fn given_confirmations_1_should_meet_confirmation_target_one() {
|
fn given_confirmations_1_should_meet_confirmation_target_one() {
|
||||||
let script = ScriptStatus::from_confirmations(1);
|
let script = ScriptStatus::from_confirmations(1);
|
||||||
|
|
||||||
let confirmed = script.is_confirmed_with(1_u32);
|
let confirmed = script.is_confirmed_with(1);
|
||||||
|
|
||||||
assert!(confirmed)
|
assert!(confirmed)
|
||||||
}
|
}
|
||||||
@ -1038,33 +1002,6 @@ mod tests {
|
|||||||
assert_eq!(confirmed.depth, 0)
|
assert_eq!(confirmed.depth, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn given_depth_0_should_return_0_blocks_left_until_1() {
|
|
||||||
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
|
|
||||||
|
|
||||||
let blocks_left = script.blocks_left_until(1_u32);
|
|
||||||
|
|
||||||
assert_eq!(blocks_left, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn given_depth_1_should_return_0_blocks_left_until_1() {
|
|
||||||
let script = ScriptStatus::Confirmed(Confirmed { depth: 1 });
|
|
||||||
|
|
||||||
let blocks_left = script.blocks_left_until(1_u32);
|
|
||||||
|
|
||||||
assert_eq!(blocks_left, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn given_depth_0_should_return_1_blocks_left_until_2() {
|
|
||||||
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
|
|
||||||
|
|
||||||
let blocks_left = script.blocks_left_until(2_u32);
|
|
||||||
|
|
||||||
assert_eq!(blocks_left, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() {
|
fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() {
|
||||||
// 400 weight = 100 vbyte
|
// 400 weight = 100 vbyte
|
||||||
|
@ -3,6 +3,7 @@ pub mod cancel_and_refund;
|
|||||||
pub mod command;
|
pub mod command;
|
||||||
mod event_loop;
|
mod event_loop;
|
||||||
mod list_sellers;
|
mod list_sellers;
|
||||||
|
pub mod tracing;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
||||||
pub use behaviour::{Behaviour, OutEvent};
|
pub use behaviour::{Behaviour, OutEvent};
|
||||||
@ -14,7 +15,6 @@ pub use list_sellers::{list_sellers, Seller, Status as SellerStatus};
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::asb;
|
use crate::asb;
|
||||||
use crate::asb::rendezvous::RendezvousNode;
|
|
||||||
use crate::cli::list_sellers::{Seller, Status};
|
use crate::cli::list_sellers::{Seller, Status};
|
||||||
use crate::network::quote;
|
use crate::network::quote;
|
||||||
use crate::network::quote::BidQuote;
|
use crate::network::quote::BidQuote;
|
||||||
@ -33,8 +33,10 @@ mod tests {
|
|||||||
async fn list_sellers_should_report_all_registered_asbs_with_a_quote() {
|
async fn list_sellers_should_report_all_registered_asbs_with_a_quote() {
|
||||||
let namespace = XmrBtcNamespace::Mainnet;
|
let namespace = XmrBtcNamespace::Mainnet;
|
||||||
let (rendezvous_address, rendezvous_peer_id) = setup_rendezvous_point().await;
|
let (rendezvous_address, rendezvous_peer_id) = setup_rendezvous_point().await;
|
||||||
let expected_seller_1 = setup_asb(rendezvous_peer_id, &rendezvous_address, namespace).await;
|
let expected_seller_1 =
|
||||||
let expected_seller_2 = setup_asb(rendezvous_peer_id, &rendezvous_address, namespace).await;
|
setup_asb(rendezvous_peer_id, rendezvous_address.clone(), namespace).await;
|
||||||
|
let expected_seller_2 =
|
||||||
|
setup_asb(rendezvous_peer_id, rendezvous_address.clone(), namespace).await;
|
||||||
|
|
||||||
let list_sellers = list_sellers(
|
let list_sellers = list_sellers(
|
||||||
rendezvous_peer_id,
|
rendezvous_peer_id,
|
||||||
@ -70,7 +72,7 @@ mod tests {
|
|||||||
|
|
||||||
async fn setup_asb(
|
async fn setup_asb(
|
||||||
rendezvous_peer_id: PeerId,
|
rendezvous_peer_id: PeerId,
|
||||||
rendezvous_address: &Multiaddr,
|
rendezvous_address: Multiaddr,
|
||||||
namespace: XmrBtcNamespace,
|
namespace: XmrBtcNamespace,
|
||||||
) -> Seller {
|
) -> Seller {
|
||||||
let static_quote = BidQuote {
|
let static_quote = BidQuote {
|
||||||
@ -79,18 +81,18 @@ mod tests {
|
|||||||
max_quantity: bitcoin::Amount::from_sat(9001),
|
max_quantity: bitcoin::Amount::from_sat(9001),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut asb = new_swarm(|_, identity| {
|
let mut asb = new_swarm(|_, identity| StaticQuoteAsbBehaviour {
|
||||||
let rendezvous_node =
|
rendezvous: asb::rendezous::Behaviour::new(
|
||||||
RendezvousNode::new(rendezvous_address, rendezvous_peer_id, namespace, None);
|
identity,
|
||||||
let rendezvous = asb::rendezvous::Behaviour::new(identity, vec![rendezvous_node]);
|
rendezvous_peer_id,
|
||||||
|
rendezvous_address,
|
||||||
StaticQuoteAsbBehaviour {
|
namespace,
|
||||||
rendezvous,
|
None,
|
||||||
ping: Default::default(),
|
),
|
||||||
quote: quote::asb(),
|
ping: Default::default(),
|
||||||
static_quote,
|
quote: quote::asb(),
|
||||||
registered: false,
|
static_quote,
|
||||||
}
|
registered: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let asb_address = asb.listen_on_tcp_localhost().await;
|
let asb_address = asb.listen_on_tcp_localhost().await;
|
||||||
@ -119,7 +121,7 @@ mod tests {
|
|||||||
#[derive(libp2p::NetworkBehaviour)]
|
#[derive(libp2p::NetworkBehaviour)]
|
||||||
#[behaviour(event_process = true)]
|
#[behaviour(event_process = true)]
|
||||||
struct StaticQuoteAsbBehaviour {
|
struct StaticQuoteAsbBehaviour {
|
||||||
rendezvous: asb::rendezvous::Behaviour,
|
rendezvous: asb::rendezous::Behaviour,
|
||||||
// Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed.
|
// Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed.
|
||||||
ping: libp2p::ping::Ping,
|
ping: libp2p::ping::Ping,
|
||||||
quote: quote::Behaviour,
|
quote: quote::Behaviour,
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
use crate::monero::Scalar;
|
|
||||||
use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason;
|
|
||||||
use crate::network::quote::BidQuote;
|
use crate::network::quote::BidQuote;
|
||||||
use crate::network::rendezvous::XmrBtcNamespace;
|
use crate::network::rendezvous::XmrBtcNamespace;
|
||||||
use crate::network::swap_setup::bob;
|
use crate::network::swap_setup::bob;
|
||||||
use crate::network::{
|
use crate::network::{encrypted_signature, quote, redial, transfer_proof};
|
||||||
cooperative_xmr_redeem_after_punish, encrypted_signature, quote, redial, transfer_proof,
|
|
||||||
};
|
|
||||||
use crate::protocol::bob::State2;
|
use crate::protocol::bob::State2;
|
||||||
use crate::{bitcoin, env};
|
use crate::{bitcoin, env};
|
||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{anyhow, Error, Result};
|
||||||
@ -32,16 +28,6 @@ pub enum OutEvent {
|
|||||||
EncryptedSignatureAcknowledged {
|
EncryptedSignatureAcknowledged {
|
||||||
id: RequestId,
|
id: RequestId,
|
||||||
},
|
},
|
||||||
CooperativeXmrRedeemFulfilled {
|
|
||||||
id: RequestId,
|
|
||||||
s_a: Scalar,
|
|
||||||
swap_id: uuid::Uuid,
|
|
||||||
},
|
|
||||||
CooperativeXmrRedeemRejected {
|
|
||||||
id: RequestId,
|
|
||||||
reason: CooperativeXmrRedeemRejectReason,
|
|
||||||
swap_id: uuid::Uuid,
|
|
||||||
},
|
|
||||||
AllRedialAttemptsExhausted {
|
AllRedialAttemptsExhausted {
|
||||||
peer: PeerId,
|
peer: PeerId,
|
||||||
},
|
},
|
||||||
@ -78,7 +64,6 @@ pub struct Behaviour {
|
|||||||
pub quote: quote::Behaviour,
|
pub quote: quote::Behaviour,
|
||||||
pub swap_setup: bob::Behaviour,
|
pub swap_setup: bob::Behaviour,
|
||||||
pub transfer_proof: transfer_proof::Behaviour,
|
pub transfer_proof: transfer_proof::Behaviour,
|
||||||
pub cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::Behaviour,
|
|
||||||
pub encrypted_signature: encrypted_signature::Behaviour,
|
pub encrypted_signature: encrypted_signature::Behaviour,
|
||||||
pub redial: redial::Behaviour,
|
pub redial: redial::Behaviour,
|
||||||
pub identify: Identify,
|
pub identify: Identify,
|
||||||
@ -106,7 +91,6 @@ impl Behaviour {
|
|||||||
swap_setup: bob::Behaviour::new(env_config, bitcoin_wallet),
|
swap_setup: bob::Behaviour::new(env_config, bitcoin_wallet),
|
||||||
transfer_proof: transfer_proof::bob(),
|
transfer_proof: transfer_proof::bob(),
|
||||||
encrypted_signature: encrypted_signature::bob(),
|
encrypted_signature: encrypted_signature::bob(),
|
||||||
cooperative_xmr_redeem: cooperative_xmr_redeem_after_punish::bob(),
|
|
||||||
redial: redial::Behaviour::new(alice, Duration::from_secs(2)),
|
redial: redial::Behaviour::new(alice, Duration::from_secs(2)),
|
||||||
ping: Ping::new(PingConfig::new().with_keep_alive(true)),
|
ping: Ping::new(PingConfig::new().with_keep_alive(true)),
|
||||||
identify: Identify::new(identifyConfig),
|
identify: Identify::new(identifyConfig),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::bitcoin::{ExpiredTimelocks, Wallet};
|
use crate::bitcoin::wallet::Subscription;
|
||||||
|
use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Wallet};
|
||||||
use crate::protocol::bob::BobState;
|
use crate::protocol::bob::BobState;
|
||||||
use crate::protocol::Database;
|
use crate::protocol::Database;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
@ -9,10 +10,10 @@ use uuid::Uuid;
|
|||||||
pub async fn cancel_and_refund(
|
pub async fn cancel_and_refund(
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
bitcoin_wallet: Arc<Wallet>,
|
bitcoin_wallet: Arc<Wallet>,
|
||||||
db: Arc<dyn Database + Send + Sync>,
|
db: Arc<dyn Database>,
|
||||||
) -> Result<BobState> {
|
) -> Result<BobState> {
|
||||||
if let Err(err) = cancel(swap_id, bitcoin_wallet.clone(), db.clone()).await {
|
if let Err(err) = cancel(swap_id, bitcoin_wallet.clone(), db.clone()).await {
|
||||||
tracing::warn!(%err, "Could not cancel swap. Attempting to refund anyway");
|
tracing::info!(%err, "Could not submit cancel transaction");
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = match refund(swap_id, bitcoin_wallet, db).await {
|
let state = match refund(swap_id, bitcoin_wallet, db).await {
|
||||||
@ -20,27 +21,20 @@ pub async fn cancel_and_refund(
|
|||||||
Err(e) => bail!(e),
|
Err(e) => bail!(e),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tracing::info!("Refund transaction submitted");
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cancel(
|
pub async fn cancel(
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
bitcoin_wallet: Arc<Wallet>,
|
bitcoin_wallet: Arc<Wallet>,
|
||||||
db: Arc<dyn Database + Send + Sync>,
|
db: Arc<dyn Database>,
|
||||||
) -> Result<(Txid, BobState)> {
|
) -> Result<(Txid, Subscription, BobState)> {
|
||||||
let state = db.get_state(swap_id).await?.try_into()?;
|
let state = db.get_state(swap_id).await?.try_into()?;
|
||||||
|
|
||||||
let state6 = match state {
|
let state6 = match state {
|
||||||
BobState::BtcLocked {
|
BobState::BtcLocked { state3, .. } => state3.cancel(),
|
||||||
state3,
|
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
..
|
|
||||||
} => state3.cancel(monero_wallet_restore_blockheight),
|
|
||||||
BobState::XmrLockProofReceived {
|
|
||||||
state,
|
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
..
|
|
||||||
} => state.cancel(monero_wallet_restore_blockheight),
|
|
||||||
BobState::XmrLocked(state4) => state4.cancel(),
|
BobState::XmrLocked(state4) => state4.cancel(),
|
||||||
BobState::EncSigSent(state4) => state4.cancel(),
|
BobState::EncSigSent(state4) => state4.cancel(),
|
||||||
BobState::CancelTimelockExpired(state6) => state6,
|
BobState::CancelTimelockExpired(state6) => state6,
|
||||||
@ -53,89 +47,46 @@ pub async fn cancel(
|
|||||||
| BobState::XmrRedeemed { .. }
|
| BobState::XmrRedeemed { .. }
|
||||||
| BobState::BtcPunished { .. }
|
| BobState::BtcPunished { .. }
|
||||||
| BobState::SafelyAborted => bail!(
|
| BobState::SafelyAborted => bail!(
|
||||||
"Cannot cancel swap {} because it is in state {} which is not cancellable.",
|
"Cannot cancel swap {} because it is in state {} which is not refundable.",
|
||||||
swap_id,
|
swap_id,
|
||||||
state
|
state
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(%swap_id, "Attempting to manually cancel swap");
|
tracing::info!(%swap_id, "Manually cancelling swap");
|
||||||
|
|
||||||
// Attempt to just publish the cancel transaction
|
let (txid, subscription) = match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await {
|
||||||
match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await {
|
Ok(txid) => txid,
|
||||||
Ok((txid, _)) => {
|
|
||||||
let state = BobState::BtcCancelled(state6);
|
|
||||||
db.insert_latest_state(swap_id, state.clone().into())
|
|
||||||
.await?;
|
|
||||||
Ok((txid, state))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we fail to submit the cancel transaction it can have one of two reasons:
|
|
||||||
// 1. The cancel timelock hasn't expired yet
|
|
||||||
// 2. The cancel transaction has already been published by Alice
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Check if Alice has already published the cancel transaction while we were absent
|
if let Ok(error_code) = parse_rpc_error_code(&err) {
|
||||||
if let Ok(tx) = state6.check_for_tx_cancel(bitcoin_wallet.as_ref()).await {
|
tracing::debug!(%error_code, "parse rpc error");
|
||||||
let state = BobState::BtcCancelled(state6);
|
if error_code == i64::from(RpcErrorCode::RpcVerifyAlreadyInChain) {
|
||||||
db.insert_latest_state(swap_id, state.clone().into())
|
tracing::info!("Cancel transaction has already been confirmed on chain");
|
||||||
.await?;
|
} else if error_code == i64::from(RpcErrorCode::RpcVerifyError) {
|
||||||
tracing::info!("Alice has already cancelled the swap");
|
tracing::info!("General error trying to submit cancel transaction");
|
||||||
return Ok((tx.txid(), state));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The cancel transaction has not been published yet and we failed to publish it ourselves
|
|
||||||
// Here we try to figure out why
|
|
||||||
match state6.expired_timelock(bitcoin_wallet.as_ref()).await {
|
|
||||||
// We cannot cancel because Alice has already cancelled and punished afterwards
|
|
||||||
Ok(ExpiredTimelocks::Punish { .. }) => {
|
|
||||||
let state = BobState::BtcPunished {
|
|
||||||
state: state6.clone(),
|
|
||||||
tx_lock_id: state6.tx_lock_id(),
|
|
||||||
};
|
|
||||||
db.insert_latest_state(swap_id, state.clone().into())
|
|
||||||
.await?;
|
|
||||||
tracing::info!("You have been punished for not refunding in time");
|
|
||||||
bail!(err.context("Cannot cancel swap because we have already been punished"));
|
|
||||||
}
|
|
||||||
// We cannot cancel because the cancel timelock has not expired yet
|
|
||||||
Ok(ExpiredTimelocks::None { blocks_left }) => {
|
|
||||||
bail!(err.context(
|
|
||||||
format!(
|
|
||||||
"Cannot cancel swap because the cancel timelock has not expired yet. Blocks left: {}",
|
|
||||||
blocks_left
|
|
||||||
)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(ExpiredTimelocks::Cancel { .. }) => {
|
|
||||||
bail!(err.context("Failed to cancel swap even though cancel timelock has expired. This is unexpected."));
|
|
||||||
}
|
|
||||||
Err(timelock_err) => {
|
|
||||||
bail!(err
|
|
||||||
.context(timelock_err)
|
|
||||||
.context("Failed to cancel swap and could not check timelock status"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bail!(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
let state = BobState::BtcCancelled(state6);
|
||||||
|
db.insert_latest_state(swap_id, state.clone().into())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((txid, subscription, state))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refund(
|
pub async fn refund(
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
bitcoin_wallet: Arc<Wallet>,
|
bitcoin_wallet: Arc<Wallet>,
|
||||||
db: Arc<dyn Database + Send + Sync>,
|
db: Arc<dyn Database>,
|
||||||
) -> Result<BobState> {
|
) -> Result<BobState> {
|
||||||
let state = db.get_state(swap_id).await?.try_into()?;
|
let state = db.get_state(swap_id).await?.try_into()?;
|
||||||
|
|
||||||
let state6 = match state {
|
let state6 = match state {
|
||||||
BobState::BtcLocked {
|
BobState::BtcLocked { state3, .. } => state3.cancel(),
|
||||||
state3,
|
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
} => state3.cancel(monero_wallet_restore_blockheight),
|
|
||||||
BobState::XmrLockProofReceived {
|
|
||||||
state,
|
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
..
|
|
||||||
} => state.cancel(monero_wallet_restore_blockheight),
|
|
||||||
BobState::XmrLocked(state4) => state4.cancel(),
|
BobState::XmrLocked(state4) => state4.cancel(),
|
||||||
BobState::EncSigSent(state4) => state4.cancel(),
|
BobState::EncSigSent(state4) => state4.cancel(),
|
||||||
BobState::CancelTimelockExpired(state6) => state6,
|
BobState::CancelTimelockExpired(state6) => state6,
|
||||||
@ -153,52 +104,12 @@ pub async fn refund(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(%swap_id, "Attempting to manually refund swap");
|
tracing::info!(%swap_id, "Manually refunding swap");
|
||||||
|
state6.publish_refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||||
|
|
||||||
// Attempt to just publish the refund transaction
|
let state = BobState::BtcRefunded(state6);
|
||||||
match state6.publish_refund_btc(bitcoin_wallet.as_ref()).await {
|
db.insert_latest_state(swap_id, state.clone().into())
|
||||||
Ok(_) => {
|
.await?;
|
||||||
let state = BobState::BtcRefunded(state6);
|
|
||||||
db.insert_latest_state(swap_id, state.clone().into())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
|
||||||
|
|
||||||
// If we fail to submit the refund transaction it can have one of two reasons:
|
|
||||||
// 1. The cancel transaction has not been published yet
|
|
||||||
// 2. The refund timelock has already expired and we have been punished
|
|
||||||
Err(bitcoin_publication_err) => {
|
|
||||||
match state6.expired_timelock(bitcoin_wallet.as_ref()).await {
|
|
||||||
// We have been punished
|
|
||||||
Ok(ExpiredTimelocks::Punish { .. }) => {
|
|
||||||
let state = BobState::BtcPunished {
|
|
||||||
state: state6.clone(),
|
|
||||||
tx_lock_id: state6.tx_lock_id(),
|
|
||||||
};
|
|
||||||
db.insert_latest_state(swap_id, state.clone().into())
|
|
||||||
.await?;
|
|
||||||
tracing::info!("You have been punished for not refunding in time");
|
|
||||||
bail!(bitcoin_publication_err
|
|
||||||
.context("Cannot refund swap because we have already been punished"));
|
|
||||||
}
|
|
||||||
Ok(ExpiredTimelocks::None { blocks_left }) => {
|
|
||||||
bail!(
|
|
||||||
bitcoin_publication_err.context(format!(
|
|
||||||
"Cannot refund swap because the cancel timelock has not expired yet. Blocks left: {}",
|
|
||||||
blocks_left
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(ExpiredTimelocks::Cancel { .. }) => {
|
|
||||||
bail!(bitcoin_publication_err.context("Failed to refund swap even though cancel timelock has expired. This is unexpected."));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
bail!(bitcoin_publication_err
|
|
||||||
.context(e)
|
|
||||||
.context("Failed to refund swap and could not check timelock status"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,10 @@
|
|||||||
use crate::bitcoin::EncryptedSignature;
|
use crate::bitcoin::EncryptedSignature;
|
||||||
use crate::cli::behaviour::{Behaviour, OutEvent};
|
use crate::cli::behaviour::{Behaviour, OutEvent};
|
||||||
use crate::monero;
|
use crate::monero;
|
||||||
use crate::network::cooperative_xmr_redeem_after_punish::{Request, Response};
|
|
||||||
use crate::network::encrypted_signature;
|
use crate::network::encrypted_signature;
|
||||||
use crate::network::quote::BidQuote;
|
use crate::network::quote::BidQuote;
|
||||||
use crate::network::swap_setup::bob::NewSwap;
|
use crate::network::swap_setup::bob::NewSwap;
|
||||||
use crate::protocol::bob::State2;
|
use crate::protocol::bob::State2;
|
||||||
use crate::protocol::Database;
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use futures::future::{BoxFuture, OptionFuture};
|
use futures::future::{BoxFuture, OptionFuture};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
@ -15,7 +13,6 @@ use libp2p::swarm::dial_opts::DialOpts;
|
|||||||
use libp2p::swarm::SwarmEvent;
|
use libp2p::swarm::SwarmEvent;
|
||||||
use libp2p::{PeerId, Swarm};
|
use libp2p::{PeerId, Swarm};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -24,11 +21,9 @@ pub struct EventLoop {
|
|||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
swarm: libp2p::Swarm<Behaviour>,
|
swarm: libp2p::Swarm<Behaviour>,
|
||||||
alice_peer_id: PeerId,
|
alice_peer_id: PeerId,
|
||||||
db: Arc<dyn Database + Send + Sync>,
|
|
||||||
|
|
||||||
// these streams represents outgoing requests that we have to make
|
// these streams represents outgoing requests that we have to make
|
||||||
quote_requests: bmrng::RequestReceiverStream<(), BidQuote>,
|
quote_requests: bmrng::RequestReceiverStream<(), BidQuote>,
|
||||||
cooperative_xmr_redeem_requests: bmrng::RequestReceiverStream<Uuid, Response>,
|
|
||||||
encrypted_signatures: bmrng::RequestReceiverStream<EncryptedSignature, ()>,
|
encrypted_signatures: bmrng::RequestReceiverStream<EncryptedSignature, ()>,
|
||||||
swap_setup_requests: bmrng::RequestReceiverStream<NewSwap, Result<State2>>,
|
swap_setup_requests: bmrng::RequestReceiverStream<NewSwap, Result<State2>>,
|
||||||
|
|
||||||
@ -38,7 +33,7 @@ pub struct EventLoop {
|
|||||||
inflight_quote_requests: HashMap<RequestId, bmrng::Responder<BidQuote>>,
|
inflight_quote_requests: HashMap<RequestId, bmrng::Responder<BidQuote>>,
|
||||||
inflight_encrypted_signature_requests: HashMap<RequestId, bmrng::Responder<()>>,
|
inflight_encrypted_signature_requests: HashMap<RequestId, bmrng::Responder<()>>,
|
||||||
inflight_swap_setup: Option<bmrng::Responder<Result<State2>>>,
|
inflight_swap_setup: Option<bmrng::Responder<Result<State2>>>,
|
||||||
inflight_cooperative_xmr_redeem_requests: HashMap<RequestId, bmrng::Responder<Response>>,
|
|
||||||
/// The sender we will use to relay incoming transfer proofs.
|
/// The sender we will use to relay incoming transfer proofs.
|
||||||
transfer_proof: bmrng::RequestSender<monero::TransferProof, ()>,
|
transfer_proof: bmrng::RequestSender<monero::TransferProof, ()>,
|
||||||
/// The future representing the successful handling of an incoming transfer
|
/// The future representing the successful handling of an incoming transfer
|
||||||
@ -56,13 +51,12 @@ impl EventLoop {
|
|||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
swarm: Swarm<Behaviour>,
|
swarm: Swarm<Behaviour>,
|
||||||
alice_peer_id: PeerId,
|
alice_peer_id: PeerId,
|
||||||
db: Arc<dyn Database + Send + Sync>,
|
|
||||||
) -> Result<(Self, EventLoopHandle)> {
|
) -> Result<(Self, EventLoopHandle)> {
|
||||||
let execution_setup = bmrng::channel_with_timeout(1, Duration::from_secs(60));
|
let execution_setup = bmrng::channel_with_timeout(1, Duration::from_secs(60));
|
||||||
let transfer_proof = bmrng::channel_with_timeout(1, Duration::from_secs(60));
|
let transfer_proof = bmrng::channel_with_timeout(1, Duration::from_secs(60));
|
||||||
let encrypted_signature = bmrng::channel(1);
|
let encrypted_signature = bmrng::channel(1);
|
||||||
let quote = bmrng::channel_with_timeout(1, Duration::from_secs(60));
|
let quote = bmrng::channel_with_timeout(1, Duration::from_secs(60));
|
||||||
let cooperative_xmr_redeem = bmrng::channel_with_timeout(1, Duration::from_secs(60));
|
|
||||||
let event_loop = EventLoop {
|
let event_loop = EventLoop {
|
||||||
swap_id,
|
swap_id,
|
||||||
swarm,
|
swarm,
|
||||||
@ -70,21 +64,17 @@ impl EventLoop {
|
|||||||
swap_setup_requests: execution_setup.1.into(),
|
swap_setup_requests: execution_setup.1.into(),
|
||||||
transfer_proof: transfer_proof.0,
|
transfer_proof: transfer_proof.0,
|
||||||
encrypted_signatures: encrypted_signature.1.into(),
|
encrypted_signatures: encrypted_signature.1.into(),
|
||||||
cooperative_xmr_redeem_requests: cooperative_xmr_redeem.1.into(),
|
|
||||||
quote_requests: quote.1.into(),
|
quote_requests: quote.1.into(),
|
||||||
inflight_quote_requests: HashMap::default(),
|
inflight_quote_requests: HashMap::default(),
|
||||||
inflight_swap_setup: None,
|
inflight_swap_setup: None,
|
||||||
inflight_encrypted_signature_requests: HashMap::default(),
|
inflight_encrypted_signature_requests: HashMap::default(),
|
||||||
inflight_cooperative_xmr_redeem_requests: HashMap::default(),
|
|
||||||
pending_transfer_proof: OptionFuture::from(None),
|
pending_transfer_proof: OptionFuture::from(None),
|
||||||
db,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle = EventLoopHandle {
|
let handle = EventLoopHandle {
|
||||||
swap_setup: execution_setup.0,
|
swap_setup: execution_setup.0,
|
||||||
transfer_proof: transfer_proof.1,
|
transfer_proof: transfer_proof.1,
|
||||||
encrypted_signature: encrypted_signature.0,
|
encrypted_signature: encrypted_signature.0,
|
||||||
cooperative_xmr_redeem: cooperative_xmr_redeem.0,
|
|
||||||
quote: quote.0,
|
quote: quote.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -118,106 +108,71 @@ impl EventLoop {
|
|||||||
SwarmEvent::Behaviour(OutEvent::TransferProofReceived { msg, channel, peer }) => {
|
SwarmEvent::Behaviour(OutEvent::TransferProofReceived { msg, channel, peer }) => {
|
||||||
let swap_id = msg.swap_id;
|
let swap_id = msg.swap_id;
|
||||||
|
|
||||||
if swap_id == self.swap_id {
|
if peer != self.alice_peer_id {
|
||||||
if peer != self.alice_peer_id {
|
tracing::warn!(
|
||||||
tracing::warn!(
|
%swap_id,
|
||||||
%swap_id,
|
"Ignoring malicious transfer proof from {}, expected to receive it from {}",
|
||||||
"Ignoring malicious transfer proof from {}, expected to receive it from {}",
|
peer,
|
||||||
peer,
|
self.alice_peer_id);
|
||||||
self.alice_peer_id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut responder = match self.transfer_proof.send(msg.tx_lock_proof).await {
|
|
||||||
Ok(responder) => responder,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Failed to pass on transfer proof: {:#}", e);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.pending_transfer_proof = OptionFuture::from(Some(async move {
|
|
||||||
let _ = responder.recv().await;
|
|
||||||
|
|
||||||
channel
|
|
||||||
}.boxed()));
|
|
||||||
}else {
|
|
||||||
// Check if the transfer proof is sent from the correct peer and if we have a record of the swap
|
|
||||||
match self.db.get_peer_id(swap_id).await {
|
|
||||||
// We have a record of the swap
|
|
||||||
Ok(buffer_swap_alice_peer_id) => {
|
|
||||||
if buffer_swap_alice_peer_id == self.alice_peer_id {
|
|
||||||
// Save transfer proof in the database such that we can process it later when we resume the swap
|
|
||||||
match self.db.insert_buffered_transfer_proof(swap_id, msg.tx_lock_proof).await {
|
|
||||||
Ok(_) => {
|
|
||||||
tracing::info!("Received transfer proof for swap {} while running swap {}. Buffering this transfer proof in the database for later retrieval", swap_id, self.swap_id);
|
|
||||||
let _ = self.swarm.behaviour_mut().transfer_proof.send_response(channel, ());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to buffer transfer proof for swap {}: {:#}", swap_id, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}else {
|
|
||||||
tracing::warn!(
|
|
||||||
%swap_id,
|
|
||||||
"Ignoring malicious transfer proof from {}, expected to receive it from {}",
|
|
||||||
self.swap_id,
|
|
||||||
buffer_swap_alice_peer_id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// We do not have a record of the swap or an error occurred while retrieving the peer id of Alice
|
|
||||||
Err(e) => {
|
|
||||||
if let Some(sqlx::Error::RowNotFound) = e.downcast_ref::<sqlx::Error>() {
|
|
||||||
tracing::warn!("Ignoring transfer proof for swap {} while running swap {}. We do not have a record of this swap", swap_id, self.swap_id);
|
|
||||||
} else {
|
|
||||||
tracing::error!("Ignoring transfer proof for swap {} while running swap {}. Failed to retrieve the peer id of Alice for the corresponding swap: {:#}", swap_id, self.swap_id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if swap_id != self.swap_id {
|
||||||
|
|
||||||
|
// TODO: Save unexpected transfer proofs in the database and check for messages in the database when handling swaps
|
||||||
|
tracing::warn!("Received unexpected transfer proof for swap {} while running swap {}. This transfer proof will be ignored", swap_id, self.swap_id);
|
||||||
|
|
||||||
|
// When receiving a transfer proof that is unexpected we still have to acknowledge that it was received
|
||||||
|
let _ = self.swarm.behaviour_mut().transfer_proof.send_response(channel, ());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut responder = match self.transfer_proof.send(msg.tx_lock_proof).await {
|
||||||
|
Ok(responder) => responder,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to pass on transfer proof: {:#}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.pending_transfer_proof = OptionFuture::from(Some(async move {
|
||||||
|
let _ = responder.recv().await;
|
||||||
|
|
||||||
|
channel
|
||||||
|
}.boxed()));
|
||||||
}
|
}
|
||||||
SwarmEvent::Behaviour(OutEvent::EncryptedSignatureAcknowledged { id }) => {
|
SwarmEvent::Behaviour(OutEvent::EncryptedSignatureAcknowledged { id }) => {
|
||||||
if let Some(responder) = self.inflight_encrypted_signature_requests.remove(&id) {
|
if let Some(responder) = self.inflight_encrypted_signature_requests.remove(&id) {
|
||||||
let _ = responder.respond(());
|
let _ = responder.respond(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemFulfilled { id, swap_id, s_a }) => {
|
|
||||||
if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) {
|
|
||||||
let _ = responder.respond(Response::Fullfilled { s_a, swap_id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SwarmEvent::Behaviour(OutEvent::CooperativeXmrRedeemRejected { id, swap_id, reason }) => {
|
|
||||||
if let Some(responder) = self.inflight_cooperative_xmr_redeem_requests.remove(&id) {
|
|
||||||
let _ = responder.respond(Response::Rejected { reason, swap_id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SwarmEvent::Behaviour(OutEvent::AllRedialAttemptsExhausted { peer }) if peer == self.alice_peer_id => {
|
SwarmEvent::Behaviour(OutEvent::AllRedialAttemptsExhausted { peer }) if peer == self.alice_peer_id => {
|
||||||
tracing::error!("Exhausted all re-dial attempts to Alice");
|
tracing::error!("Exhausted all re-dial attempts to Alice");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SwarmEvent::Behaviour(OutEvent::Failure { peer, error }) => {
|
SwarmEvent::Behaviour(OutEvent::Failure { peer, error }) => {
|
||||||
tracing::warn!(%peer, err = %error, "Communication error");
|
tracing::warn!(%peer, "Communication error: {:#}", error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } if peer_id == self.alice_peer_id => {
|
SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } if peer_id == self.alice_peer_id => {
|
||||||
tracing::info!(peer_id = %endpoint.get_remote_address(), "Connected to Alice");
|
tracing::info!("Connected to Alice at {}", endpoint.get_remote_address());
|
||||||
}
|
}
|
||||||
SwarmEvent::Dialing(peer_id) if peer_id == self.alice_peer_id => {
|
SwarmEvent::Dialing(peer_id) if peer_id == self.alice_peer_id => {
|
||||||
tracing::debug!(%peer_id, "Dialling Alice");
|
tracing::debug!("Dialling Alice at {}", peer_id);
|
||||||
}
|
}
|
||||||
SwarmEvent::ConnectionClosed { peer_id, endpoint, num_established, cause: Some(error) } if peer_id == self.alice_peer_id && num_established == 0 => {
|
SwarmEvent::ConnectionClosed { peer_id, endpoint, num_established, cause: Some(error) } if peer_id == self.alice_peer_id && num_established == 0 => {
|
||||||
tracing::warn!(peer_id = %endpoint.get_remote_address(), cause = %error, "Lost connection to Alice");
|
tracing::warn!("Lost connection to Alice at {}, cause: {}", endpoint.get_remote_address(), error);
|
||||||
}
|
}
|
||||||
SwarmEvent::ConnectionClosed { peer_id, num_established, cause: None, .. } if peer_id == self.alice_peer_id && num_established == 0 => {
|
SwarmEvent::ConnectionClosed { peer_id, num_established, cause: None, .. } if peer_id == self.alice_peer_id && num_established == 0 => {
|
||||||
// no error means the disconnection was requested
|
// no error means the disconnection was requested
|
||||||
tracing::info!("Successfully closed connection to Alice");
|
tracing::info!("Successfully closed connection to Alice");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SwarmEvent::OutgoingConnectionError { peer_id: Some(alice_peer_id), error } if alice_peer_id == self.alice_peer_id => {
|
SwarmEvent::OutgoingConnectionError { peer_id, error } if matches!(peer_id, Some(alice_peer_id) if alice_peer_id == self.alice_peer_id) => {
|
||||||
tracing::warn!(%error, "Failed to dial Alice");
|
tracing::warn!( "Failed to dial Alice: {}", error);
|
||||||
|
|
||||||
if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() {
|
if let Some(duration) = self.swarm.behaviour_mut().redial.until_next_redial() {
|
||||||
tracing::info!(seconds_until_next_redial = %duration.as_secs(), "Waiting for next redial attempt");
|
tracing::info!("Next redial attempt in {}s", duration.as_secs());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -249,14 +204,7 @@ impl EventLoop {
|
|||||||
let _ = self.swarm.behaviour_mut().transfer_proof.send_response(response_channel, ());
|
let _ = self.swarm.behaviour_mut().transfer_proof.send_response(response_channel, ());
|
||||||
|
|
||||||
self.pending_transfer_proof = OptionFuture::from(None);
|
self.pending_transfer_proof = OptionFuture::from(None);
|
||||||
},
|
}
|
||||||
|
|
||||||
Some((swap_id, responder)) = self.cooperative_xmr_redeem_requests.next().fuse(), if self.is_connected_to_alice() => {
|
|
||||||
let id = self.swarm.behaviour_mut().cooperative_xmr_redeem.send_request(&self.alice_peer_id, Request {
|
|
||||||
swap_id
|
|
||||||
});
|
|
||||||
self.inflight_cooperative_xmr_redeem_requests.insert(id, responder);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -272,7 +220,6 @@ pub struct EventLoopHandle {
|
|||||||
transfer_proof: bmrng::RequestReceiver<monero::TransferProof, ()>,
|
transfer_proof: bmrng::RequestReceiver<monero::TransferProof, ()>,
|
||||||
encrypted_signature: bmrng::RequestSender<EncryptedSignature, ()>,
|
encrypted_signature: bmrng::RequestSender<EncryptedSignature, ()>,
|
||||||
quote: bmrng::RequestSender<(), BidQuote>,
|
quote: bmrng::RequestSender<(), BidQuote>,
|
||||||
cooperative_xmr_redeem: bmrng::RequestSender<Uuid, Response>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventLoopHandle {
|
impl EventLoopHandle {
|
||||||
@ -294,12 +241,8 @@ impl EventLoopHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request_quote(&mut self) -> Result<BidQuote> {
|
pub async fn request_quote(&mut self) -> Result<BidQuote> {
|
||||||
tracing::debug!("Requesting quote");
|
|
||||||
Ok(self.quote.send_receive(()).await?)
|
Ok(self.quote.send_receive(()).await?)
|
||||||
}
|
}
|
||||||
pub async fn request_cooperative_xmr_redeem(&mut self, swap_id: Uuid) -> Result<Response> {
|
|
||||||
Ok(self.cooperative_xmr_redeem.send_receive(swap_id).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_encrypted_signature(
|
pub async fn send_encrypted_signature(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -350,26 +350,23 @@ mod tests {
|
|||||||
|
|
||||||
list.sort();
|
list.sort();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(list, vec![
|
||||||
list,
|
Seller {
|
||||||
vec![
|
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
|
||||||
Seller {
|
status: Status::Online(BidQuote {
|
||||||
multiaddr: "/ip4/127.0.0.1/tcp/5678".parse().unwrap(),
|
price: Default::default(),
|
||||||
status: Status::Online(BidQuote {
|
min_quantity: Default::default(),
|
||||||
price: Default::default(),
|
max_quantity: Default::default(),
|
||||||
min_quantity: Default::default(),
|
})
|
||||||
max_quantity: Default::default(),
|
},
|
||||||
})
|
Seller {
|
||||||
},
|
multiaddr: Multiaddr::empty(),
|
||||||
Seller {
|
status: Status::Unreachable
|
||||||
multiaddr: Multiaddr::empty(),
|
},
|
||||||
status: Status::Unreachable
|
Seller {
|
||||||
},
|
multiaddr: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(),
|
||||||
Seller {
|
status: Status::Unreachable
|
||||||
multiaddr: "/ip4/127.0.0.1/tcp/1234".parse().unwrap(),
|
},
|
||||||
status: Status::Unreachable
|
])
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
145
swap/src/cli/tracing.rs
Normal file
145
swap/src/cli/tracing.rs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::option::Option::Some;
|
||||||
|
use std::path::Path;
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
use tracing::subscriber::set_global_default;
|
||||||
|
use tracing::{Event, Level, Subscriber};
|
||||||
|
use tracing_subscriber::fmt::format::{DefaultFields, Format, JsonFields};
|
||||||
|
use tracing_subscriber::fmt::time::UtcTime;
|
||||||
|
use tracing_subscriber::layer::{Context, SubscriberExt};
|
||||||
|
use tracing_subscriber::{fmt, EnvFilter, FmtSubscriber, Layer, Registry};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub fn init(debug: bool, json: bool, dir: impl AsRef<Path>, swap_id: Option<Uuid>) -> Result<()> {
|
||||||
|
if let Some(swap_id) = swap_id {
|
||||||
|
let level_filter = EnvFilter::try_new("swap=debug")?;
|
||||||
|
|
||||||
|
let registry = Registry::default().with(level_filter);
|
||||||
|
|
||||||
|
let appender =
|
||||||
|
tracing_appender::rolling::never(dir.as_ref(), format!("swap-{}.log", swap_id));
|
||||||
|
let (appender, guard) = tracing_appender::non_blocking(appender);
|
||||||
|
|
||||||
|
std::mem::forget(guard);
|
||||||
|
|
||||||
|
let file_logger = registry.with(
|
||||||
|
fmt::layer()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_target(false)
|
||||||
|
.json()
|
||||||
|
.with_writer(appender),
|
||||||
|
);
|
||||||
|
|
||||||
|
if json && debug {
|
||||||
|
set_global_default(file_logger.with(debug_json_terminal_printer()))?;
|
||||||
|
} else if json && !debug {
|
||||||
|
set_global_default(file_logger.with(info_json_terminal_printer()))?;
|
||||||
|
} else if !json && debug {
|
||||||
|
set_global_default(file_logger.with(debug_terminal_printer()))?;
|
||||||
|
} else {
|
||||||
|
set_global_default(file_logger.with(info_terminal_printer()))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let level = if debug { Level::DEBUG } else { Level::INFO };
|
||||||
|
let is_terminal = atty::is(atty::Stream::Stderr);
|
||||||
|
|
||||||
|
let builder = FmtSubscriber::builder()
|
||||||
|
.with_env_filter(format!("swap={}", level))
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.with_ansi(is_terminal)
|
||||||
|
.with_timer(UtcTime::rfc_3339())
|
||||||
|
.with_target(false);
|
||||||
|
|
||||||
|
if json {
|
||||||
|
builder.json().init();
|
||||||
|
} else {
|
||||||
|
builder.init();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("Logging initialized to {}", dir.as_ref().display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StdErrPrinter<L> {
|
||||||
|
inner: L,
|
||||||
|
level: Level,
|
||||||
|
}
|
||||||
|
|
||||||
|
type StdErrLayer<S, T> = tracing_subscriber::fmt::Layer<
|
||||||
|
S,
|
||||||
|
DefaultFields,
|
||||||
|
Format<tracing_subscriber::fmt::format::Full, T>,
|
||||||
|
fn() -> std::io::Stderr,
|
||||||
|
>;
|
||||||
|
|
||||||
|
type StdErrJsonLayer<S, T> = tracing_subscriber::fmt::Layer<
|
||||||
|
S,
|
||||||
|
JsonFields,
|
||||||
|
Format<tracing_subscriber::fmt::format::Json, T>,
|
||||||
|
fn() -> std::io::Stderr,
|
||||||
|
>;
|
||||||
|
|
||||||
|
fn debug_terminal_printer<S>() -> StdErrPrinter<StdErrLayer<S, UtcTime<Rfc3339>>> {
|
||||||
|
let is_terminal = atty::is(atty::Stream::Stderr);
|
||||||
|
StdErrPrinter {
|
||||||
|
inner: fmt::layer()
|
||||||
|
.with_ansi(is_terminal)
|
||||||
|
.with_target(false)
|
||||||
|
.with_timer(UtcTime::rfc_3339())
|
||||||
|
.with_writer(std::io::stderr),
|
||||||
|
level: Level::DEBUG,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_json_terminal_printer<S>() -> StdErrPrinter<StdErrJsonLayer<S, UtcTime<Rfc3339>>> {
|
||||||
|
let is_terminal = atty::is(atty::Stream::Stderr);
|
||||||
|
StdErrPrinter {
|
||||||
|
inner: fmt::layer()
|
||||||
|
.with_ansi(is_terminal)
|
||||||
|
.with_target(false)
|
||||||
|
.with_timer(UtcTime::rfc_3339())
|
||||||
|
.json()
|
||||||
|
.with_writer(std::io::stderr),
|
||||||
|
level: Level::DEBUG,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info_terminal_printer<S>() -> StdErrPrinter<StdErrLayer<S, ()>> {
|
||||||
|
let is_terminal = atty::is(atty::Stream::Stderr);
|
||||||
|
StdErrPrinter {
|
||||||
|
inner: fmt::layer()
|
||||||
|
.with_ansi(is_terminal)
|
||||||
|
.with_target(false)
|
||||||
|
.with_level(false)
|
||||||
|
.without_time()
|
||||||
|
.with_writer(std::io::stderr),
|
||||||
|
level: Level::INFO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info_json_terminal_printer<S>() -> StdErrPrinter<StdErrJsonLayer<S, ()>> {
|
||||||
|
let is_terminal = atty::is(atty::Stream::Stderr);
|
||||||
|
StdErrPrinter {
|
||||||
|
inner: fmt::layer()
|
||||||
|
.with_ansi(is_terminal)
|
||||||
|
.with_target(false)
|
||||||
|
.with_level(false)
|
||||||
|
.without_time()
|
||||||
|
.json()
|
||||||
|
.with_writer(std::io::stderr),
|
||||||
|
level: Level::INFO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, S> Layer<S> for StdErrPrinter<L>
|
||||||
|
where
|
||||||
|
L: 'static + Layer<S>,
|
||||||
|
S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
|
||||||
|
{
|
||||||
|
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
|
||||||
|
if self.level.ge(event.metadata().level()) {
|
||||||
|
self.inner.on_event(event, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
swap/src/common.rs
Normal file
56
swap/src/common.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
const LATEST_RELEASE_URL: &str = "https://github.com/comit-network/xmr-btc-swap/releases/latest";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Version {
|
||||||
|
Current,
|
||||||
|
Available,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the latest release from GitHub API.
|
||||||
|
pub async fn check_latest_version(current_version: &str) -> anyhow::Result<Version> {
|
||||||
|
let response = reqwest::get(LATEST_RELEASE_URL).await?;
|
||||||
|
let e = "Failed to get latest release.";
|
||||||
|
let download_url = response.url();
|
||||||
|
let segments = download_url.path_segments().ok_or_else(|| anyhow!(e))?;
|
||||||
|
let latest_version = segments.last().ok_or_else(|| anyhow!(e))?;
|
||||||
|
|
||||||
|
let result = if is_latest_version(current_version, latest_version) {
|
||||||
|
Version::Current
|
||||||
|
} else {
|
||||||
|
tracing::warn!(%current_version, %latest_version, %download_url,
|
||||||
|
"You are not on the latest version",
|
||||||
|
);
|
||||||
|
Version::Available
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: naive implementation can be improved using semver
|
||||||
|
fn is_latest_version(current: &str, latest: &str) -> bool {
|
||||||
|
current == latest
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_compares_versions() {
|
||||||
|
assert!(is_latest_version("0.10.2", "0.10.2"));
|
||||||
|
assert!(!is_latest_version("0.10.2", "0.10.3"));
|
||||||
|
assert!(!is_latest_version("0.10.2", "0.11.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "For local testing, makes http requests to github."]
|
||||||
|
async fn it_compares_with_github() {
|
||||||
|
let result = check_latest_version("0.11.0").await.unwrap();
|
||||||
|
assert_eq!(result, Version::Available);
|
||||||
|
|
||||||
|
let result = check_latest_version("0.11.1").await.unwrap();
|
||||||
|
assert_eq!(result, Version::Current);
|
||||||
|
}
|
||||||
|
}
|
@ -1,221 +0,0 @@
|
|||||||
pub mod tracing_util;
|
|
||||||
|
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use tokio::{
|
|
||||||
fs::{read_dir, File},
|
|
||||||
io::{AsyncBufReadExt, BufReader},
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
const LATEST_RELEASE_URL: &str = "https://github.com/comit-network/xmr-btc-swap/releases/latest";
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Version {
|
|
||||||
Current,
|
|
||||||
Available,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check the latest release from GitHub API.
|
|
||||||
pub async fn check_latest_version(current_version: &str) -> anyhow::Result<Version> {
|
|
||||||
let response = reqwest::get(LATEST_RELEASE_URL).await?;
|
|
||||||
let e = "Failed to get latest release.";
|
|
||||||
let download_url = response.url();
|
|
||||||
let segments = download_url.path_segments().ok_or_else(|| anyhow!(e))?;
|
|
||||||
let latest_version = segments.last().ok_or_else(|| anyhow!(e))?;
|
|
||||||
|
|
||||||
let result = if is_latest_version(current_version, latest_version) {
|
|
||||||
Version::Current
|
|
||||||
} else {
|
|
||||||
tracing::warn!(%current_version, %latest_version, %download_url,
|
|
||||||
"You are not on the latest version",
|
|
||||||
);
|
|
||||||
Version::Available
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: naive implementation can be improved using semver
|
|
||||||
fn is_latest_version(current: &str, latest: &str) -> bool {
|
|
||||||
current == latest
|
|
||||||
}
|
|
||||||
|
|
||||||
/// helper macro for [`redact`]... eldrich sorcery
|
|
||||||
/// the macro does in essence the following:
|
|
||||||
/// 1. create a static regex automaton for the pattern
|
|
||||||
/// 2. find all matching patterns using regex
|
|
||||||
/// 3. create a placeholder for each distinct matching pattern
|
|
||||||
/// 4. add the placeholder to the hashmap
|
|
||||||
macro_rules! regex_find_placeholders {
|
|
||||||
($pattern:expr, $create_placeholder:expr, $replacements:expr, $input:expr) => {{
|
|
||||||
// compile the regex pattern
|
|
||||||
static REGEX: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy::new(|| {
|
|
||||||
tracing::debug!("initializing regex");
|
|
||||||
regex::Regex::new($pattern).expect("invalid regex pattern")
|
|
||||||
});
|
|
||||||
|
|
||||||
// keep count of count patterns to generate distinct placeholders
|
|
||||||
let mut counter: usize = 0;
|
|
||||||
|
|
||||||
// for every matched address check whether we already found it
|
|
||||||
// and if we didn't, generate a placeholder for it
|
|
||||||
for address in REGEX.find_iter($input) {
|
|
||||||
if !$replacements.contains_key(address.as_str()) {
|
|
||||||
#[allow(clippy::redundant_closure_call)]
|
|
||||||
$replacements.insert(address.as_str().to_owned(), $create_placeholder(counter));
|
|
||||||
counter += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print the logs from the specified logs or from the default location
|
|
||||||
/// to the specified path or the terminal.
|
|
||||||
///
|
|
||||||
/// If specified, filter by swap id or redact addresses.
|
|
||||||
pub async fn get_logs(
|
|
||||||
logs_dir: PathBuf,
|
|
||||||
swap_id: Option<Uuid>,
|
|
||||||
redact_addresses: bool,
|
|
||||||
) -> anyhow::Result<Vec<String>> {
|
|
||||||
tracing::debug!("reading logfiles from {}", logs_dir.display());
|
|
||||||
|
|
||||||
// get all files in the directory
|
|
||||||
let mut log_files = read_dir(&logs_dir).await?;
|
|
||||||
|
|
||||||
let mut log_messages = Vec::new();
|
|
||||||
// when we redact we need to store the placeholder
|
|
||||||
let mut placeholders = HashMap::new();
|
|
||||||
|
|
||||||
// print all lines from every log file. TODO: sort files by date?
|
|
||||||
while let Some(entry) = log_files.next_entry().await? {
|
|
||||||
// get the file path
|
|
||||||
let file_path = entry.path();
|
|
||||||
|
|
||||||
// filter for .log files
|
|
||||||
let file_name = file_path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
if !file_name.ends_with(".log") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// use BufReader to stay easy on memory and then read line by line
|
|
||||||
let buf_reader = BufReader::new(File::open(&file_path).await?);
|
|
||||||
let mut lines = buf_reader.lines();
|
|
||||||
|
|
||||||
// print each line, redacted if the flag is set
|
|
||||||
while let Some(line) = lines.next_line().await? {
|
|
||||||
// if we should filter by swap id, check if the line contains it
|
|
||||||
if let Some(swap_id) = swap_id {
|
|
||||||
// we only want lines which contain the swap id
|
|
||||||
if !line.contains(&swap_id.to_string()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// redact if necessary
|
|
||||||
let line = if redact_addresses {
|
|
||||||
redact_with(&line, &mut placeholders)
|
|
||||||
} else {
|
|
||||||
line
|
|
||||||
};
|
|
||||||
// save redacted message
|
|
||||||
log_messages.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(log_messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Redact logs, etc. by replacing Bitcoin and Monero addresses
|
|
||||||
/// with generic placeholders.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use swap::common::redact;
|
|
||||||
///
|
|
||||||
/// let redacted = redact("a9165a1e-d26d-4b56-bf6d-ca9658825c44");
|
|
||||||
/// assert_eq!(redacted, "<swap_id_0>");
|
|
||||||
/// ```
|
|
||||||
pub fn redact(input: &str) -> String {
|
|
||||||
let mut replacements = HashMap::new();
|
|
||||||
redact_with(input, &mut replacements)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as [`redact`] but retrieves palceholders from and stores them
|
|
||||||
/// in a specified hashmap.
|
|
||||||
pub fn redact_with(input: &str, replacements: &mut HashMap<String, String>) -> String {
|
|
||||||
// TODO: verify regex patterns
|
|
||||||
const MONERO_ADDR_REGEX: &str = r#"[48][1-9A-HJ-NP-Za-km-z]{94}"#;
|
|
||||||
const BITCOIN_ADDR_REGEX: &str = r#"\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b"#;
|
|
||||||
// Both XMR and BTC transactions have
|
|
||||||
// a 64 bit hex id so they aren't distinguishible
|
|
||||||
const TX_ID_REGEX: &str = r#"\b[a-fA-F0-9]{64}\b"#;
|
|
||||||
const SWAP_ID_REGEX: &str =
|
|
||||||
r#"\b[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}\b"#;
|
|
||||||
|
|
||||||
// use the macro to find all addresses and generate placeholders
|
|
||||||
// has to be a macro in order to create the regex automata only once.
|
|
||||||
regex_find_placeholders!(
|
|
||||||
MONERO_ADDR_REGEX,
|
|
||||||
|count| format!("<monero_address_{count}>"),
|
|
||||||
replacements,
|
|
||||||
input
|
|
||||||
);
|
|
||||||
regex_find_placeholders!(
|
|
||||||
BITCOIN_ADDR_REGEX,
|
|
||||||
|count| format!("<bitcoin_address_{count}>"),
|
|
||||||
replacements,
|
|
||||||
input
|
|
||||||
);
|
|
||||||
regex_find_placeholders!(
|
|
||||||
TX_ID_REGEX,
|
|
||||||
|count| format!("<tx_id_{count}>"),
|
|
||||||
replacements,
|
|
||||||
input
|
|
||||||
);
|
|
||||||
regex_find_placeholders!(
|
|
||||||
SWAP_ID_REGEX,
|
|
||||||
|count| format!("<swap_id_{count}>"),
|
|
||||||
replacements,
|
|
||||||
input
|
|
||||||
);
|
|
||||||
|
|
||||||
// allocate string variable to operate on
|
|
||||||
let mut redacted = input.to_owned();
|
|
||||||
|
|
||||||
// Finally we go through the input string and replace each occurance of an
|
|
||||||
// address we want to redact with the corresponding placeholder
|
|
||||||
for (address, placeholder) in replacements.iter() {
|
|
||||||
redacted = redacted.replace(address, placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
redacted
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_compares_versions() {
|
|
||||||
assert!(is_latest_version("0.10.2", "0.10.2"));
|
|
||||||
assert!(!is_latest_version("0.10.2", "0.10.3"));
|
|
||||||
assert!(!is_latest_version("0.10.2", "0.11.0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[ignore = "For local testing, makes http requests to github."]
|
|
||||||
async fn it_compares_with_github() {
|
|
||||||
let result = check_latest_version("0.11.0").await.unwrap();
|
|
||||||
assert_eq!(result, Version::Available);
|
|
||||||
|
|
||||||
let result = check_latest_version("0.11.1").await.unwrap();
|
|
||||||
assert_eq!(result, Version::Current);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
use std::path::Path;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use tracing_subscriber::filter::{Directive, LevelFilter};
|
|
||||||
use tracing_subscriber::fmt::time::UtcTime;
|
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
|
||||||
use tracing_subscriber::{fmt, EnvFilter, Layer};
|
|
||||||
|
|
||||||
/// Output formats for logging messages.
|
|
||||||
pub enum Format {
|
|
||||||
/// Standard, human readable format.
|
|
||||||
Raw,
|
|
||||||
/// JSON, machine readable format.
|
|
||||||
Json,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize tracing and enable logging messages according to these options.
|
|
||||||
/// Besides printing to `stdout`, this will append to a log file.
|
|
||||||
/// Said file will contain JSON-formatted logs of all levels,
|
|
||||||
/// disregarding the arguments to this function.
|
|
||||||
pub fn init(level_filter: LevelFilter, format: Format, dir: impl AsRef<Path>) -> Result<()> {
|
|
||||||
let env_filter = EnvFilter::from_default_env()
|
|
||||||
.add_directive(Directive::from_str(&format!("asb={}", &level_filter))?)
|
|
||||||
.add_directive(Directive::from_str(&format!("swap={}", &level_filter))?);
|
|
||||||
|
|
||||||
// file logger will always write in JSON format and with timestamps
|
|
||||||
let file_appender = tracing_appender::rolling::never(&dir, "swap-all.log");
|
|
||||||
|
|
||||||
let file_layer = fmt::layer()
|
|
||||||
.with_writer(file_appender)
|
|
||||||
.with_ansi(false)
|
|
||||||
.with_timer(UtcTime::rfc_3339())
|
|
||||||
.with_target(false)
|
|
||||||
.json()
|
|
||||||
.with_filter(env_filter);
|
|
||||||
|
|
||||||
// terminal logger
|
|
||||||
let is_terminal = atty::is(atty::Stream::Stderr);
|
|
||||||
let terminal_layer = fmt::layer()
|
|
||||||
.with_writer(std::io::stdout)
|
|
||||||
.with_ansi(is_terminal)
|
|
||||||
.with_timer(UtcTime::rfc_3339())
|
|
||||||
.with_target(false);
|
|
||||||
|
|
||||||
// combine the layers and start logging, format with json if specified
|
|
||||||
if let Format::Json = format {
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(file_layer)
|
|
||||||
.with(terminal_layer.json().with_filter(level_filter))
|
|
||||||
.init();
|
|
||||||
} else {
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(file_layer)
|
|
||||||
.with(terminal_layer.with_filter(level_filter))
|
|
||||||
.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we can use the tracing macros to log messages
|
|
||||||
tracing::info!(%level_filter, logs_dir=%dir.as_ref().display(), "Initialized tracing");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -83,25 +83,16 @@ impl Swap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq)]
|
pub async fn open_db(sqlite_path: impl AsRef<Path>) -> Result<Arc<dyn Database + Send + Sync>> {
|
||||||
pub enum AccessMode {
|
|
||||||
ReadWrite,
|
|
||||||
ReadOnly,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn open_db(
|
|
||||||
sqlite_path: impl AsRef<Path>,
|
|
||||||
access_mode: AccessMode,
|
|
||||||
) -> Result<Arc<dyn Database + Send + Sync>> {
|
|
||||||
if sqlite_path.as_ref().exists() {
|
if sqlite_path.as_ref().exists() {
|
||||||
tracing::debug!("Using existing sqlite database.");
|
tracing::debug!("Using existing sqlite database.");
|
||||||
let sqlite = SqliteDatabase::open(sqlite_path, access_mode).await?;
|
let sqlite = SqliteDatabase::open(sqlite_path).await?;
|
||||||
Ok(Arc::new(sqlite))
|
Ok(Arc::new(sqlite))
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!("Creating and using new sqlite database.");
|
tracing::debug!("Creating and using new sqlite database.");
|
||||||
ensure_directory_exists(sqlite_path.as_ref())?;
|
ensure_directory_exists(sqlite_path.as_ref())?;
|
||||||
tokio::fs::File::create(&sqlite_path).await?;
|
tokio::fs::File::create(&sqlite_path).await?;
|
||||||
let sqlite = SqliteDatabase::open(sqlite_path, access_mode).await?;
|
let sqlite = SqliteDatabase::open(sqlite_path).await?;
|
||||||
Ok(Arc::new(sqlite))
|
Ok(Arc::new(sqlite))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,12 +70,12 @@ pub enum Alice {
|
|||||||
Done(AliceEndState),
|
Done(AliceEndState),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, strum::Display, Debug, Deserialize, Serialize, PartialEq)]
|
#[derive(Copy, Clone, strum::Display, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
pub enum AliceEndState {
|
pub enum AliceEndState {
|
||||||
SafelyAborted,
|
SafelyAborted,
|
||||||
BtcRedeemed,
|
BtcRedeemed,
|
||||||
XmrRefunded,
|
XmrRefunded,
|
||||||
BtcPunished { state3: alice::State3 },
|
BtcPunished,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AliceState> for Alice {
|
impl From<AliceState> for Alice {
|
||||||
@ -173,9 +173,7 @@ impl From<AliceState> for Alice {
|
|||||||
transfer_proof,
|
transfer_proof,
|
||||||
state3: state3.as_ref().clone(),
|
state3: state3.as_ref().clone(),
|
||||||
},
|
},
|
||||||
AliceState::BtcPunished { state3 } => Alice::Done(AliceEndState::BtcPunished {
|
AliceState::BtcPunished => Alice::Done(AliceEndState::BtcPunished),
|
||||||
state3: state3.as_ref().clone(),
|
|
||||||
}),
|
|
||||||
AliceState::SafelyAborted => Alice::Done(AliceEndState::SafelyAborted),
|
AliceState::SafelyAborted => Alice::Done(AliceEndState::SafelyAborted),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,9 +277,7 @@ impl From<Alice> for AliceState {
|
|||||||
AliceEndState::SafelyAborted => AliceState::SafelyAborted,
|
AliceEndState::SafelyAborted => AliceState::SafelyAborted,
|
||||||
AliceEndState::BtcRedeemed => AliceState::BtcRedeemed,
|
AliceEndState::BtcRedeemed => AliceState::BtcRedeemed,
|
||||||
AliceEndState::XmrRefunded => AliceState::XmrRefunded,
|
AliceEndState::XmrRefunded => AliceState::XmrRefunded,
|
||||||
AliceEndState::BtcPunished { state3 } => AliceState::BtcPunished {
|
AliceEndState::BtcPunished => AliceState::BtcPunished,
|
||||||
state3: Box::new(state3),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,10 +33,6 @@ pub enum Bob {
|
|||||||
EncSigSent {
|
EncSigSent {
|
||||||
state4: bob::State4,
|
state4: bob::State4,
|
||||||
},
|
},
|
||||||
BtcPunished {
|
|
||||||
state: bob::State6,
|
|
||||||
tx_lock_id: bitcoin::Txid,
|
|
||||||
},
|
|
||||||
BtcRedeemed(bob::State5),
|
BtcRedeemed(bob::State5),
|
||||||
CancelTimelockExpired(bob::State6),
|
CancelTimelockExpired(bob::State6),
|
||||||
BtcCancelled(bob::State6),
|
BtcCancelled(bob::State6),
|
||||||
@ -48,6 +44,7 @@ pub enum BobEndState {
|
|||||||
SafelyAborted,
|
SafelyAborted,
|
||||||
XmrRedeemed { tx_lock_id: bitcoin::Txid },
|
XmrRedeemed { tx_lock_id: bitcoin::Txid },
|
||||||
BtcRefunded(Box<bob::State6>),
|
BtcRefunded(Box<bob::State6>),
|
||||||
|
BtcPunished { tx_lock_id: bitcoin::Txid },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BobState> for Bob {
|
impl From<BobState> for Bob {
|
||||||
@ -82,11 +79,13 @@ impl From<BobState> for Bob {
|
|||||||
BobState::BtcRedeemed(state5) => Bob::BtcRedeemed(state5),
|
BobState::BtcRedeemed(state5) => Bob::BtcRedeemed(state5),
|
||||||
BobState::CancelTimelockExpired(state6) => Bob::CancelTimelockExpired(state6),
|
BobState::CancelTimelockExpired(state6) => Bob::CancelTimelockExpired(state6),
|
||||||
BobState::BtcCancelled(state6) => Bob::BtcCancelled(state6),
|
BobState::BtcCancelled(state6) => Bob::BtcCancelled(state6),
|
||||||
BobState::BtcPunished { state, tx_lock_id } => Bob::BtcPunished { state, tx_lock_id },
|
|
||||||
BobState::BtcRefunded(state6) => Bob::Done(BobEndState::BtcRefunded(Box::new(state6))),
|
BobState::BtcRefunded(state6) => Bob::Done(BobEndState::BtcRefunded(Box::new(state6))),
|
||||||
BobState::XmrRedeemed { tx_lock_id } => {
|
BobState::XmrRedeemed { tx_lock_id } => {
|
||||||
Bob::Done(BobEndState::XmrRedeemed { tx_lock_id })
|
Bob::Done(BobEndState::XmrRedeemed { tx_lock_id })
|
||||||
}
|
}
|
||||||
|
BobState::BtcPunished { tx_lock_id } => {
|
||||||
|
Bob::Done(BobEndState::BtcPunished { tx_lock_id })
|
||||||
|
}
|
||||||
BobState::SafelyAborted => Bob::Done(BobEndState::SafelyAborted),
|
BobState::SafelyAborted => Bob::Done(BobEndState::SafelyAborted),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,11 +123,11 @@ impl From<Bob> for BobState {
|
|||||||
Bob::BtcRedeemed(state5) => BobState::BtcRedeemed(state5),
|
Bob::BtcRedeemed(state5) => BobState::BtcRedeemed(state5),
|
||||||
Bob::CancelTimelockExpired(state6) => BobState::CancelTimelockExpired(state6),
|
Bob::CancelTimelockExpired(state6) => BobState::CancelTimelockExpired(state6),
|
||||||
Bob::BtcCancelled(state6) => BobState::BtcCancelled(state6),
|
Bob::BtcCancelled(state6) => BobState::BtcCancelled(state6),
|
||||||
Bob::BtcPunished { state, tx_lock_id } => BobState::BtcPunished { state, tx_lock_id },
|
|
||||||
Bob::Done(end_state) => match end_state {
|
Bob::Done(end_state) => match end_state {
|
||||||
BobEndState::SafelyAborted => BobState::SafelyAborted,
|
BobEndState::SafelyAborted => BobState::SafelyAborted,
|
||||||
BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
|
BobEndState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
|
||||||
BobEndState::BtcRefunded(state6) => BobState::BtcRefunded(*state6),
|
BobEndState::BtcRefunded(state6) => BobState::BtcRefunded(*state6),
|
||||||
|
BobEndState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,7 +148,6 @@ impl fmt::Display for Bob {
|
|||||||
Bob::BtcRedeemed(_) => f.write_str("Monero redeemable"),
|
Bob::BtcRedeemed(_) => f.write_str("Monero redeemable"),
|
||||||
Bob::Done(end_state) => write!(f, "Done: {}", end_state),
|
Bob::Done(end_state) => write!(f, "Done: {}", end_state),
|
||||||
Bob::EncSigSent { .. } => f.write_str("Encrypted signature sent"),
|
Bob::EncSigSent { .. } => f.write_str("Encrypted signature sent"),
|
||||||
Bob::BtcPunished { .. } => f.write_str("Bitcoin punished"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,29 @@
|
|||||||
use crate::database::Swap;
|
use crate::database::Swap;
|
||||||
use crate::monero::{Address, TransferProof};
|
use crate::monero::Address;
|
||||||
use crate::protocol::{Database, State};
|
use crate::protocol::{Database, State};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use libp2p::{Multiaddr, PeerId};
|
use libp2p::{Multiaddr, PeerId};
|
||||||
use sqlx::sqlite::{Sqlite, SqliteConnectOptions};
|
use sqlx::sqlite::Sqlite;
|
||||||
use sqlx::{ConnectOptions, Pool, SqlitePool};
|
use sqlx::{Pool, SqlitePool};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::AccessMode;
|
|
||||||
|
|
||||||
pub struct SqliteDatabase {
|
pub struct SqliteDatabase {
|
||||||
pool: Pool<Sqlite>,
|
pool: Pool<Sqlite>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteDatabase {
|
impl SqliteDatabase {
|
||||||
pub async fn open(path: impl AsRef<Path>, access_mode: AccessMode) -> Result<Self>
|
pub async fn open(path: impl AsRef<Path>) -> Result<Self>
|
||||||
where
|
where
|
||||||
Self: std::marker::Sized,
|
Self: std::marker::Sized,
|
||||||
{
|
{
|
||||||
let read_only = matches!(access_mode, AccessMode::ReadOnly);
|
|
||||||
|
|
||||||
let path_str = format!("sqlite:{}", path.as_ref().display());
|
let path_str = format!("sqlite:{}", path.as_ref().display());
|
||||||
let mut options = SqliteConnectOptions::from_str(&path_str)?.read_only(read_only);
|
let pool = SqlitePool::connect(&path_str).await?;
|
||||||
options.disable_statement_logging();
|
|
||||||
|
|
||||||
let pool = SqlitePool::connect_with(options).await?;
|
|
||||||
let mut sqlite = Self { pool };
|
let mut sqlite = Self { pool };
|
||||||
|
sqlite.run_migrations().await?;
|
||||||
if !read_only {
|
|
||||||
sqlite.run_migrations().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(sqlite)
|
Ok(sqlite)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +149,7 @@ impl Database for SqliteDatabase {
|
|||||||
|
|
||||||
let rows = sqlx::query!(
|
let rows = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT DISTINCT address
|
SELECT address
|
||||||
FROM peer_addresses
|
FROM peer_addresses
|
||||||
WHERE peer_id = ?
|
WHERE peer_id = ?
|
||||||
"#,
|
"#,
|
||||||
@ -181,25 +169,6 @@ impl Database for SqliteDatabase {
|
|||||||
addresses
|
addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_swap_start_date(&self, swap_id: Uuid) -> Result<String> {
|
|
||||||
let mut conn = self.pool.acquire().await?;
|
|
||||||
let swap_id = swap_id.to_string();
|
|
||||||
|
|
||||||
let row = sqlx::query!(
|
|
||||||
r#"
|
|
||||||
SELECT min(entered_at) as start_date
|
|
||||||
FROM swap_states
|
|
||||||
WHERE swap_id = ?
|
|
||||||
"#,
|
|
||||||
swap_id
|
|
||||||
)
|
|
||||||
.fetch_one(&mut conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
row.start_date
|
|
||||||
.ok_or_else(|| anyhow!("Could not get swap start date"))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()> {
|
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()> {
|
||||||
let mut conn = self.pool.acquire().await?;
|
let mut conn = self.pool.acquire().await?;
|
||||||
let entered_at = OffsetDateTime::now_utc();
|
let entered_at = OffsetDateTime::now_utc();
|
||||||
@ -280,119 +249,6 @@ impl Database for SqliteDatabase {
|
|||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_states(&self, swap_id: Uuid) -> Result<Vec<State>> {
|
|
||||||
let mut conn = self.pool.acquire().await?;
|
|
||||||
let swap_id = swap_id.to_string();
|
|
||||||
|
|
||||||
// TODO: We should use query! instead of query here to allow for at-compile-time validation
|
|
||||||
// I didn't manage to generate the mappings for the query! macro because of problems with sqlx-cli
|
|
||||||
let rows = sqlx::query!(
|
|
||||||
r#"
|
|
||||||
SELECT state
|
|
||||||
FROM swap_states
|
|
||||||
WHERE swap_id = ?
|
|
||||||
"#,
|
|
||||||
swap_id
|
|
||||||
)
|
|
||||||
.fetch_all(&mut conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let result = rows
|
|
||||||
.iter()
|
|
||||||
.map(|row| {
|
|
||||||
let state_str: &str = &row.state;
|
|
||||||
|
|
||||||
let state = match serde_json::from_str::<Swap>(state_str) {
|
|
||||||
Ok(a) => Ok(State::from(a)),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}?;
|
|
||||||
Ok(state)
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<State>>>();
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_buffered_transfer_proof(
|
|
||||||
&self,
|
|
||||||
swap_id: Uuid,
|
|
||||||
proof: TransferProof,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut conn = self.pool.acquire().await?;
|
|
||||||
let swap_id = swap_id.to_string();
|
|
||||||
let proof = serde_json::to_string(&proof)?;
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO buffered_transfer_proofs (
|
|
||||||
swap_id,
|
|
||||||
proof
|
|
||||||
) VALUES (?, ?);
|
|
||||||
"#,
|
|
||||||
swap_id,
|
|
||||||
proof
|
|
||||||
)
|
|
||||||
.execute(&mut conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_buffered_transfer_proof(&self, swap_id: Uuid) -> Result<Option<TransferProof>> {
|
|
||||||
let mut conn = self.pool.acquire().await?;
|
|
||||||
let swap_id = swap_id.to_string();
|
|
||||||
|
|
||||||
let row = sqlx::query!(
|
|
||||||
r#"
|
|
||||||
SELECT proof
|
|
||||||
FROM buffered_transfer_proofs
|
|
||||||
WHERE swap_id = ?
|
|
||||||
"#,
|
|
||||||
swap_id
|
|
||||||
)
|
|
||||||
.fetch_all(&mut conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if row.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let proof_str = &row[0].proof;
|
|
||||||
let proof = serde_json::from_str(proof_str)?;
|
|
||||||
|
|
||||||
Ok(Some(proof))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn raw_all(&self) -> Result<HashMap<Uuid, Vec<serde_json::Value>>> {
|
|
||||||
let mut conn = self.pool.acquire().await?;
|
|
||||||
let rows = sqlx::query!(
|
|
||||||
r#"
|
|
||||||
SELECT swap_id, state
|
|
||||||
FROM swap_states
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.fetch_all(&mut conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut swaps: HashMap<Uuid, Vec<serde_json::Value>> = HashMap::new();
|
|
||||||
|
|
||||||
for row in &rows {
|
|
||||||
let swap_id = Uuid::from_str(&row.swap_id)?;
|
|
||||||
let state = serde_json::from_str(&row.state)?;
|
|
||||||
|
|
||||||
if let std::collections::hash_map::Entry::Vacant(e) = swaps.entry(swap_id) {
|
|
||||||
e.insert(vec![state]);
|
|
||||||
} else {
|
|
||||||
swaps
|
|
||||||
.get_mut(&swap_id)
|
|
||||||
.ok_or_else(|| anyhow!("Error while retrieving the swap"))?
|
|
||||||
.push(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(swaps)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -428,8 +284,9 @@ mod tests {
|
|||||||
let db = setup_test_db().await.unwrap();
|
let db = setup_test_db().await.unwrap();
|
||||||
|
|
||||||
let state_1 = State::Alice(AliceState::BtcRedeemed);
|
let state_1 = State::Alice(AliceState::BtcRedeemed);
|
||||||
let state_2 = State::Alice(AliceState::SafelyAborted);
|
let state_2 = State::Alice(AliceState::BtcPunished);
|
||||||
let state_3 = State::Bob(BobState::SafelyAborted);
|
let state_3 = State::Alice(AliceState::SafelyAborted);
|
||||||
|
let state_4 = State::Bob(BobState::SafelyAborted);
|
||||||
let swap_id_1 = Uuid::new_v4();
|
let swap_id_1 = Uuid::new_v4();
|
||||||
let swap_id_2 = Uuid::new_v4();
|
let swap_id_2 = Uuid::new_v4();
|
||||||
|
|
||||||
@ -439,7 +296,10 @@ mod tests {
|
|||||||
db.insert_latest_state(swap_id_1, state_2.clone())
|
db.insert_latest_state(swap_id_1, state_2.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.insert_latest_state(swap_id_2, state_3.clone())
|
db.insert_latest_state(swap_id_1, state_3.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.insert_latest_state(swap_id_2, state_4.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -447,10 +307,11 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(latest_loaded.len(), 2);
|
assert_eq!(latest_loaded.len(), 2);
|
||||||
|
|
||||||
assert!(latest_loaded.contains(&(swap_id_1, state_2)));
|
assert!(latest_loaded.contains(&(swap_id_1, state_3)));
|
||||||
assert!(latest_loaded.contains(&(swap_id_2, state_3)));
|
assert!(latest_loaded.contains(&(swap_id_2, state_4)));
|
||||||
|
|
||||||
assert!(!latest_loaded.contains(&(swap_id_1, state_1)));
|
assert!(!latest_loaded.contains(&(swap_id_1, state_1)));
|
||||||
|
assert!(!latest_loaded.contains(&(swap_id_1, state_2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@ -515,7 +376,7 @@ mod tests {
|
|||||||
// file has to exist in order to connect with sqlite
|
// file has to exist in order to connect with sqlite
|
||||||
File::create(temp_db.clone()).unwrap();
|
File::create(temp_db.clone()).unwrap();
|
||||||
|
|
||||||
let db = SqliteDatabase::open(temp_db, AccessMode::ReadWrite).await?;
|
let db = SqliteDatabase::open(temp_db).await?;
|
||||||
|
|
||||||
Ok(db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ use std::cmp::max;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::ext::NumericalStdDuration;
|
use time::ext::NumericalStdDuration;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Copy, Clone, PartialEq, Serialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub bitcoin_lock_mempool_timeout: Duration,
|
pub bitcoin_lock_mempool_timeout: Duration,
|
||||||
pub bitcoin_lock_confirmed_timeout: Duration,
|
pub bitcoin_lock_confirmed_timeout: Duration,
|
||||||
@ -46,7 +46,7 @@ pub struct Regtest;
|
|||||||
impl GetConfig for Mainnet {
|
impl GetConfig for Mainnet {
|
||||||
fn get_config() -> Config {
|
fn get_config() -> Config {
|
||||||
Config {
|
Config {
|
||||||
bitcoin_lock_mempool_timeout: 10.std_minutes(),
|
bitcoin_lock_mempool_timeout: 3.std_minutes(),
|
||||||
bitcoin_lock_confirmed_timeout: 2.std_hours(),
|
bitcoin_lock_confirmed_timeout: 2.std_hours(),
|
||||||
bitcoin_finality_confirmations: 1,
|
bitcoin_finality_confirmations: 1,
|
||||||
bitcoin_avg_block_time: 10.std_minutes(),
|
bitcoin_avg_block_time: 10.std_minutes(),
|
||||||
@ -63,7 +63,7 @@ impl GetConfig for Mainnet {
|
|||||||
impl GetConfig for Testnet {
|
impl GetConfig for Testnet {
|
||||||
fn get_config() -> Config {
|
fn get_config() -> Config {
|
||||||
Config {
|
Config {
|
||||||
bitcoin_lock_mempool_timeout: 10.std_minutes(),
|
bitcoin_lock_mempool_timeout: 3.std_minutes(),
|
||||||
bitcoin_lock_confirmed_timeout: 1.std_hours(),
|
bitcoin_lock_confirmed_timeout: 1.std_hours(),
|
||||||
bitcoin_finality_confirmations: 1,
|
bitcoin_finality_confirmations: 1,
|
||||||
bitcoin_avg_block_time: 10.std_minutes(),
|
bitcoin_avg_block_time: 10.std_minutes(),
|
||||||
|
@ -230,7 +230,7 @@ mod wire {
|
|||||||
use bitcoin::util::amount::ParseAmountError;
|
use bitcoin::util::amount::ParseAmountError;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Deserialize, PartialEq)]
|
||||||
#[serde(tag = "event")]
|
#[serde(tag = "event")]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
#[serde(rename = "systemStatus")]
|
#[serde(rename = "systemStatus")]
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
missing_copy_implementations
|
missing_copy_implementations
|
||||||
)]
|
)]
|
||||||
|
|
||||||
pub mod api;
|
|
||||||
pub mod asb;
|
pub mod asb;
|
||||||
pub mod bitcoin;
|
pub mod bitcoin;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
@ -29,7 +28,6 @@ pub mod libp2p_ext;
|
|||||||
pub mod monero;
|
pub mod monero;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod rpc;
|
|
||||||
pub mod seed;
|
pub mod seed;
|
||||||
pub mod tor;
|
pub mod tor;
|
||||||
pub mod tracing_ext;
|
pub mod tracing_ext;
|
||||||
|
@ -39,16 +39,9 @@ pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey
|
|||||||
PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes))
|
PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
|
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
|
||||||
|
|
||||||
impl fmt::Display for PrivateViewKey {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
// Delegate to the Display implementation of PrivateKey
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrivateViewKey {
|
impl PrivateViewKey {
|
||||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||||
let scalar = Scalar::random(rng);
|
let scalar = Scalar::random(rng);
|
||||||
@ -85,7 +78,7 @@ impl From<PublicViewKey> for PublicKey {
|
|||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct PublicViewKey(PublicKey);
|
pub struct PublicViewKey(PublicKey);
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)]
|
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
|
||||||
pub struct Amount(u64);
|
pub struct Amount(u64);
|
||||||
|
|
||||||
// Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, XMR 0.000_008 * 2 (to be on the safe side)
|
// Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, XMR 0.000_008 * 2 (to be on the safe side)
|
||||||
@ -142,14 +135,6 @@ impl Amount {
|
|||||||
Decimal::from(self.as_piconero())
|
Decimal::from(self.as_piconero())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_xmr(&self) -> Decimal {
|
|
||||||
let mut decimal = Decimal::from(self.0);
|
|
||||||
decimal
|
|
||||||
.set_scale(12)
|
|
||||||
.expect("12 is smaller than max precision of 28");
|
|
||||||
decimal
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_decimal(amount: Decimal) -> Result<Self> {
|
fn from_decimal(amount: Decimal) -> Result<Self> {
|
||||||
let piconeros_dec =
|
let piconeros_dec =
|
||||||
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
||||||
@ -192,12 +177,15 @@ impl From<Amount> for u64 {
|
|||||||
|
|
||||||
impl fmt::Display for Amount {
|
impl fmt::Display for Amount {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let xmr_value = self.as_xmr();
|
let mut decimal = Decimal::from(self.0);
|
||||||
write!(f, "{} XMR", xmr_value)
|
decimal
|
||||||
|
.set_scale(12)
|
||||||
|
.expect("12 is smaller than max precision of 28");
|
||||||
|
write!(f, "{} XMR", decimal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct TransferProof {
|
pub struct TransferProof {
|
||||||
tx_hash: TxHash,
|
tx_hash: TxHash,
|
||||||
#[serde(with = "monero_private_key")]
|
#[serde(with = "monero_private_key")]
|
||||||
@ -217,7 +205,7 @@ impl TransferProof {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add constructor/ change String to fixed length byte array
|
// TODO: add constructor/ change String to fixed length byte array
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct TxHash(pub String);
|
pub struct TxHash(pub String);
|
||||||
|
|
||||||
impl From<TxHash> for String {
|
impl From<TxHash> for String {
|
||||||
@ -239,7 +227,7 @@ pub struct InsufficientFunds {
|
|||||||
pub actual: Amount,
|
pub actual: Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
|
||||||
#[error("Overflow, cannot convert {0} to u64")]
|
#[error("Overflow, cannot convert {0} to u64")]
|
||||||
pub struct OverflowError(pub String);
|
pub struct OverflowError(pub String);
|
||||||
|
|
||||||
@ -332,52 +320,6 @@ pub mod monero_amount {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod monero_address {
|
|
||||||
use anyhow::{bail, Context, Result};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
|
|
||||||
#[error("Invalid monero address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
|
|
||||||
pub struct MoneroAddressNetworkMismatch {
|
|
||||||
pub expected: monero::Network,
|
|
||||||
pub actual: monero::Network,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(s: &str) -> Result<monero::Address> {
|
|
||||||
monero::Address::from_str(s).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"Failed to parse {} as a monero address, please make sure it is a valid address",
|
|
||||||
s
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate(
|
|
||||||
address: monero::Address,
|
|
||||||
expected_network: monero::Network,
|
|
||||||
) -> Result<monero::Address> {
|
|
||||||
if address.network != expected_network {
|
|
||||||
bail!(MoneroAddressNetworkMismatch {
|
|
||||||
expected: expected_network,
|
|
||||||
actual: address.network,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_is_testnet(
|
|
||||||
address: monero::Address,
|
|
||||||
is_testnet: bool,
|
|
||||||
) -> Result<monero::Address> {
|
|
||||||
let expected_network = if is_testnet {
|
|
||||||
monero::Network::Stagenet
|
|
||||||
} else {
|
|
||||||
monero::Network::Mainnet
|
|
||||||
};
|
|
||||||
validate(address, expected_network)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -528,10 +470,10 @@ mod tests {
|
|||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -6,7 +6,6 @@ use ::monero::{Address, Network, PrivateKey, PublicKey};
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use monero_rpc::wallet::{BlockHeight, MoneroWalletRpc as _, Refreshed};
|
use monero_rpc::wallet::{BlockHeight, MoneroWalletRpc as _, Refreshed};
|
||||||
use monero_rpc::{jsonrpc, wallet};
|
use monero_rpc::{jsonrpc, wallet};
|
||||||
use std::ops::Div;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@ -27,16 +26,15 @@ impl Wallet {
|
|||||||
pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result<Self> {
|
pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result<Self> {
|
||||||
let client = wallet::Client::new(url)?;
|
let client = wallet::Client::new(url)?;
|
||||||
|
|
||||||
match client.open_wallet(name.clone()).await {
|
let open_wallet_response = client.open_wallet(name.clone()).await;
|
||||||
Err(error) => {
|
if open_wallet_response.is_err() {
|
||||||
tracing::debug!(%error, "Open wallet response error");
|
client.create_wallet(name.clone(), "English".to_owned()).await.context(
|
||||||
client.create_wallet(name.clone(), "English".to_owned()).await.context(
|
"Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available",
|
||||||
"Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available",
|
)?;
|
||||||
)?;
|
|
||||||
|
|
||||||
tracing::debug!(monero_wallet_name = %name, "Created Monero wallet");
|
tracing::debug!(monero_wallet_name = %name, "Created Monero wallet");
|
||||||
}
|
} else {
|
||||||
Ok(_) => tracing::debug!(monero_wallet_name = %name, "Opened Monero wallet"),
|
tracing::debug!(monero_wallet_name = %name, "Opened Monero wallet");
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::connect(client, name, env_config).await
|
Self::connect(client, name, env_config).await
|
||||||
@ -46,7 +44,6 @@ impl Wallet {
|
|||||||
pub async fn connect(client: wallet::Client, name: String, env_config: Config) -> Result<Self> {
|
pub async fn connect(client: wallet::Client, name: String, env_config: Config) -> Result<Self> {
|
||||||
let main_address =
|
let main_address =
|
||||||
monero::Address::from_str(client.get_address(0).await?.address.as_str())?;
|
monero::Address::from_str(client.get_address(0).await?.address.as_str())?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner: Mutex::new(client),
|
inner: Mutex::new(client),
|
||||||
network: env_config.monero_network,
|
network: env_config.monero_network,
|
||||||
@ -127,14 +124,13 @@ impl Wallet {
|
|||||||
let temp_wallet_address =
|
let temp_wallet_address =
|
||||||
Address::standard(self.network, public_spend_key, public_view_key);
|
Address::standard(self.network, public_spend_key, public_view_key);
|
||||||
|
|
||||||
|
let wallet = self.inner.lock().await;
|
||||||
|
|
||||||
// Close the default wallet before generating the other wallet to ensure that
|
// Close the default wallet before generating the other wallet to ensure that
|
||||||
// it saves its state correctly
|
// it saves its state correctly
|
||||||
let _ = self.inner.lock().await.close_wallet().await?;
|
let _ = wallet.close_wallet().await?;
|
||||||
|
|
||||||
let _ = self
|
let _ = wallet
|
||||||
.inner
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.generate_from_keys(
|
.generate_from_keys(
|
||||||
file_name,
|
file_name,
|
||||||
temp_wallet_address.to_string(),
|
temp_wallet_address.to_string(),
|
||||||
@ -147,14 +143,8 @@ impl Wallet {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Try to send all the funds from the generated wallet to the default wallet
|
// Try to send all the funds from the generated wallet to the default wallet
|
||||||
match self.refresh(3).await {
|
match wallet.refresh().await {
|
||||||
Ok(_) => match self
|
Ok(_) => match wallet.sweep_all(self.main_address.to_string()).await {
|
||||||
.inner
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.sweep_all(self.main_address.to_string())
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(sweep_all) => {
|
Ok(sweep_all) => {
|
||||||
for tx in sweep_all.tx_hash_list {
|
for tx in sweep_all.tx_hash_list {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@ -175,12 +165,7 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = self
|
let _ = wallet.open_wallet(self.name.clone()).await?;
|
||||||
.inner
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.open_wallet(self.name.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -188,6 +173,11 @@ impl Wallet {
|
|||||||
pub async fn transfer(&self, request: TransferRequest) -> Result<TransferProof> {
|
pub async fn transfer(&self, request: TransferRequest) -> Result<TransferProof> {
|
||||||
let inner = self.inner.lock().await;
|
let inner = self.inner.lock().await;
|
||||||
|
|
||||||
|
inner
|
||||||
|
.open_wallet(self.name.clone())
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to open wallet {}", self.name))?;
|
||||||
|
|
||||||
let TransferRequest {
|
let TransferRequest {
|
||||||
public_spend_key,
|
public_spend_key,
|
||||||
public_view_key,
|
public_view_key,
|
||||||
@ -234,7 +224,7 @@ impl Wallet {
|
|||||||
|
|
||||||
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||||
|
|
||||||
let check_interval = tokio::time::interval(self.sync_interval.div(10));
|
let check_interval = tokio::time::interval(self.sync_interval);
|
||||||
|
|
||||||
wait_for_confirmations(
|
wait_for_confirmations(
|
||||||
&self.inner,
|
&self.inner,
|
||||||
@ -275,44 +265,8 @@ impl Wallet {
|
|||||||
self.main_address
|
self.main_address
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(&self, max_attempts: usize) -> Result<Refreshed> {
|
pub async fn refresh(&self) -> Result<Refreshed> {
|
||||||
const RETRY_INTERVAL: Duration = Duration::from_secs(1);
|
Ok(self.inner.lock().await.refresh().await?)
|
||||||
|
|
||||||
for i in 1..=max_attempts {
|
|
||||||
tracing::info!(name = %self.name, attempt=i, "Syncing Monero wallet");
|
|
||||||
|
|
||||||
let result = self.inner.lock().await.refresh().await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(refreshed) => {
|
|
||||||
tracing::info!(name = %self.name, "Monero wallet synced");
|
|
||||||
return Ok(refreshed);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
let attempts_left = max_attempts - i;
|
|
||||||
|
|
||||||
// We would not want to fail here if the height is not available
|
|
||||||
// as it is not critical for the operation of the wallet.
|
|
||||||
// We can just log a warning and continue.
|
|
||||||
let height = match self.inner.lock().await.get_height().await {
|
|
||||||
Ok(height) => height.to_string(),
|
|
||||||
Err(_) => {
|
|
||||||
tracing::warn!(name = %self.name, "Failed to fetch Monero wallet height during sync");
|
|
||||||
"unknown".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::warn!(attempt=i, %height, %attempts_left, name = %self.name, %error, "Failed to sync Monero wallet");
|
|
||||||
|
|
||||||
if attempts_left == 0 {
|
|
||||||
return Err(error.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(RETRY_INTERVAL).await;
|
|
||||||
}
|
|
||||||
unreachable!("Loop should have returned by now");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,75 +1,37 @@
|
|||||||
use ::monero::Network;
|
use ::monero::Network;
|
||||||
use anyhow::{bail, Context, Error, Result};
|
use anyhow::{Context, Result};
|
||||||
use big_bytes::BigByte;
|
use big_bytes::BigByte;
|
||||||
use data_encoding::HEXLOWER;
|
|
||||||
use futures::{StreamExt, TryStreamExt};
|
use futures::{StreamExt, TryStreamExt};
|
||||||
use monero_rpc::wallet::{Client, MoneroWalletRpc as _};
|
use monero_rpc::wallet::{Client, MoneroWalletRpc as _};
|
||||||
use reqwest::header::CONTENT_LENGTH;
|
use reqwest::header::CONTENT_LENGTH;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use serde::Deserialize;
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::fmt;
|
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::fs::{remove_file, OpenOptions};
|
use tokio::fs::{remove_file, OpenOptions};
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::process::{Child, Command};
|
use tokio::process::{Child, Command};
|
||||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||||
use tokio_util::io::StreamReader;
|
use tokio_util::io::StreamReader;
|
||||||
|
|
||||||
// See: https://www.moneroworld.com/#nodes, https://monero.fail
|
|
||||||
// We don't need any testnet nodes because we don't support testnet at all
|
|
||||||
const MONERO_DAEMONS: [MoneroDaemon; 17] = [
|
|
||||||
MoneroDaemon::new("xmr-node.cakewallet.com", 18081, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("nodex.monerujo.io", 18081, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("node.moneroworld.com", 18089, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("nodes.hashvault.pro", 18081, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("p2pmd.xmrvsbeast.com", 18081, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("node.monerodevs.org", 18089, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("xmr-node-usa-east.cakewallet.com", 18081, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("xmr-node-uk.cakewallet.com", 18081, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("node.community.rino.io", 18081, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("testingjohnross.com", 20031, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("xmr.litepay.ch", 18081, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("node.trocador.app", 18089, Network::Mainnet),
|
|
||||||
MoneroDaemon::new("stagenet.xmr-tw.org", 38081, Network::Stagenet),
|
|
||||||
MoneroDaemon::new("node.monerodevs.org", 38089, Network::Stagenet),
|
|
||||||
MoneroDaemon::new("singapore.node.xmr.pm", 38081, Network::Stagenet),
|
|
||||||
MoneroDaemon::new("xmr-lux.boldsuck.org", 38081, Network::Stagenet),
|
|
||||||
MoneroDaemon::new("stagenet.community.rino.io", 38081, Network::Stagenet),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||||
compile_error!("unsupported operating system");
|
compile_error!("unsupported operating system");
|
||||||
|
|
||||||
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
||||||
const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-mac-x64-v0.18.3.1.tar.bz2";
|
const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-mac-x64-v0.18.1.2.tar.bz2";
|
||||||
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
|
||||||
const DOWNLOAD_HASH: &str = "7f8bd9364ef16482b418aa802a65be0e4cc660c794bb5d77b2d17bc84427883a";
|
|
||||||
|
|
||||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||||
const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-mac-armv8-v0.18.3.1.tar.bz2";
|
const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-mac-armv8-v0.18.0.0.tar.bz2";
|
||||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
|
||||||
const DOWNLOAD_HASH: &str = "915288b023cb5811e626e10052adc6ac5323dd283c5a25b91059b0fb86a21fb6";
|
|
||||||
|
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.1.tar.bz2";
|
const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.1.2.tar.bz2";
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
|
||||||
const DOWNLOAD_HASH: &str = "23af572fdfe3459b9ab97e2e9aa7e3c11021c955d6064b801a27d7e8c21ae09d";
|
|
||||||
|
|
||||||
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
||||||
const DOWNLOAD_URL: &str =
|
const DOWNLOAD_URL: &str =
|
||||||
"https://downloads.getmonero.org/cli/monero-linux-armv7-v0.18.3.1.tar.bz2";
|
"https://downloads.getmonero.org/cli/monero-linux-armv7-v0.18.1.2.tar.bz2";
|
||||||
#[cfg(all(target_os = "linux", target_arch = "arm"))]
|
|
||||||
const DOWNLOAD_HASH: &str = "2ea2c8898cbab88f49423f4f6c15f2a94046cb4bbe827493dd061edc0fd5f1ca";
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-win-x64-v0.18.3.1.zip";
|
const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-win-x64-v0.18.1.2.zip";
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
const DOWNLOAD_HASH: &str = "35dcc4bee4caad3442659d37837e0119e4649a77f2e3b5e80dd6d9b8fc4fb6ad";
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||||
const PACKED_FILE: &str = "monero-wallet-rpc";
|
const PACKED_FILE: &str = "monero-wallet-rpc";
|
||||||
@ -77,7 +39,7 @@ const PACKED_FILE: &str = "monero-wallet-rpc";
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const PACKED_FILE: &str = "monero-wallet-rpc.exe";
|
const PACKED_FILE: &str = "monero-wallet-rpc.exe";
|
||||||
|
|
||||||
const WALLET_RPC_VERSION: &str = "v0.18.3.1";
|
const WALLET_RPC_VERSION: &str = "v0.18.1.2";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||||
#[error("monero wallet rpc executable not found in downloaded archive")]
|
#[error("monero wallet rpc executable not found in downloaded archive")]
|
||||||
@ -88,91 +50,6 @@ pub struct WalletRpcProcess {
|
|||||||
port: u16,
|
port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MoneroDaemon {
|
|
||||||
address: &'static str,
|
|
||||||
port: u16,
|
|
||||||
network: Network,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MoneroDaemon {
|
|
||||||
const fn new(address: &'static str, port: u16, network: Network) -> Self {
|
|
||||||
Self {
|
|
||||||
address,
|
|
||||||
port,
|
|
||||||
network,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the Monero daemon is available by sending a request to its `get_info` endpoint.
|
|
||||||
async fn is_available(&self, client: &reqwest::Client) -> Result<bool, Error> {
|
|
||||||
let url = format!("http://{}:{}/get_info", self.address, self.port);
|
|
||||||
let res = client
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.context("Failed to send request to get_info endpoint")?;
|
|
||||||
|
|
||||||
let json: MoneroDaemonGetInfoResponse = res
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.context("Failed to deserialize daemon get_info response")?;
|
|
||||||
|
|
||||||
let is_status_ok = json.status == "OK";
|
|
||||||
let is_synchronized = json.synchronized;
|
|
||||||
let is_correct_network = match self.network {
|
|
||||||
Network::Mainnet => json.mainnet,
|
|
||||||
Network::Stagenet => json.stagenet,
|
|
||||||
Network::Testnet => json.testnet,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(is_status_ok && is_synchronized && is_correct_network)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for MoneroDaemon {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}:{}", self.address, self.port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct MoneroDaemonGetInfoResponse {
|
|
||||||
status: String,
|
|
||||||
synchronized: bool,
|
|
||||||
mainnet: bool,
|
|
||||||
stagenet: bool,
|
|
||||||
testnet: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Chooses an available Monero daemon based on the specified network.
|
|
||||||
async fn choose_monero_daemon(network: Network) -> Result<&'static MoneroDaemon, Error> {
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(30))
|
|
||||||
.https_only(false)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
// We only want to check for daemons that match the specified network
|
|
||||||
let network_matching_daemons = MONERO_DAEMONS
|
|
||||||
.iter()
|
|
||||||
.filter(|daemon| daemon.network == network);
|
|
||||||
|
|
||||||
for daemon in network_matching_daemons {
|
|
||||||
match daemon.is_available(&client).await {
|
|
||||||
Ok(true) => {
|
|
||||||
tracing::debug!(%daemon, "Found available Monero daemon");
|
|
||||||
return Ok(daemon);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::debug!(%err, %daemon, "Failed to connect to Monero daemon");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Ok(false) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!("No Monero daemon could be found. Please specify one manually or try again later.")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletRpcProcess {
|
impl WalletRpcProcess {
|
||||||
pub fn endpoint(&self) -> Url {
|
pub fn endpoint(&self) -> Url {
|
||||||
Url::parse(&format!("http://127.0.0.1:{}/json_rpc", self.port))
|
Url::parse(&format!("http://127.0.0.1:{}/json_rpc", self.port))
|
||||||
@ -233,20 +110,13 @@ impl WalletRpc {
|
|||||||
.parse::<u64>()?;
|
.parse::<u64>()?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
progress="0%",
|
"Downloading monero-wallet-rpc ({}) from {}",
|
||||||
size=%content_length.big_byte(2),
|
content_length.big_byte(2),
|
||||||
download_url=DOWNLOAD_URL,
|
DOWNLOAD_URL
|
||||||
"Downloading monero-wallet-rpc",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
|
|
||||||
let byte_stream = response
|
let byte_stream = response
|
||||||
.bytes_stream()
|
.bytes_stream()
|
||||||
.map_ok(|bytes| {
|
|
||||||
hasher.update(&bytes);
|
|
||||||
bytes
|
|
||||||
})
|
|
||||||
.map_err(|err| std::io::Error::new(ErrorKind::Other, err));
|
.map_err(|err| std::io::Error::new(ErrorKind::Other, err));
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
@ -269,36 +139,12 @@ impl WalletRpc {
|
|||||||
let total = 3 * content_length;
|
let total = 3 * content_length;
|
||||||
let percent = 100 * received as u64 / total;
|
let percent = 100 * received as u64 / total;
|
||||||
if percent != notified && percent % 10 == 0 {
|
if percent != notified && percent % 10 == 0 {
|
||||||
tracing::info!(
|
tracing::debug!("{}%", percent);
|
||||||
progress=format!("{}%", percent),
|
|
||||||
size=%content_length.big_byte(2),
|
|
||||||
download_url=DOWNLOAD_URL,
|
|
||||||
"Downloading monero-wallet-rpc",
|
|
||||||
);
|
|
||||||
notified = percent;
|
notified = percent;
|
||||||
}
|
}
|
||||||
file.write_all(&bytes).await?;
|
file.write_all(&bytes).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
progress="100%",
|
|
||||||
size=%content_length.big_byte(2),
|
|
||||||
download_url=DOWNLOAD_URL,
|
|
||||||
"Downloading monero-wallet-rpc",
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = hasher.finalize();
|
|
||||||
let result_hash = HEXLOWER.encode(result.as_ref());
|
|
||||||
if result_hash != DOWNLOAD_HASH {
|
|
||||||
bail!(
|
|
||||||
"SHA256 of download ({}) does not match expected ({})!",
|
|
||||||
result_hash,
|
|
||||||
DOWNLOAD_HASH
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
tracing::debug!("Hashes match");
|
|
||||||
}
|
|
||||||
|
|
||||||
file.flush().await?;
|
file.flush().await?;
|
||||||
|
|
||||||
tracing::debug!("Extracting archive");
|
tracing::debug!("Extracting archive");
|
||||||
@ -307,23 +153,13 @@ impl WalletRpc {
|
|||||||
Ok(monero_wallet_rpc)
|
Ok(monero_wallet_rpc)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(&self, network: Network, daemon_address: &str) -> Result<WalletRpcProcess> {
|
||||||
&self,
|
|
||||||
network: Network,
|
|
||||||
daemon_address: Option<String>,
|
|
||||||
) -> Result<WalletRpcProcess> {
|
|
||||||
let port = tokio::net::TcpListener::bind("127.0.0.1:0")
|
let port = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||||||
.await?
|
.await?
|
||||||
.local_addr()?
|
.local_addr()?
|
||||||
.port();
|
.port();
|
||||||
|
|
||||||
let daemon_address = match daemon_address {
|
|
||||||
Some(daemon_address) => daemon_address,
|
|
||||||
None => choose_monero_daemon(network).await?.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
%daemon_address,
|
|
||||||
%port,
|
%port,
|
||||||
"Starting monero-wallet-rpc"
|
"Starting monero-wallet-rpc"
|
||||||
);
|
);
|
||||||
@ -352,7 +188,6 @@ impl WalletRpc {
|
|||||||
.arg("--disable-rpc-login")
|
.arg("--disable-rpc-login")
|
||||||
.arg("--wallet-dir")
|
.arg("--wallet-dir")
|
||||||
.arg(self.working_dir.join("monero-data"))
|
.arg(self.working_dir.join("monero-data"))
|
||||||
.arg("--no-initial-sync")
|
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
let stdout = child
|
let stdout = child
|
||||||
@ -370,7 +205,7 @@ impl WalletRpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we do not hear from the monero_wallet_rpc process for 3 seconds we assume
|
// If we do not hear from the monero_wallet_rpc process for 3 seconds we assume
|
||||||
// it is ready
|
// it is is ready
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
while let Ok(line) =
|
while let Ok(line) =
|
||||||
tokio::time::timeout(std::time::Duration::from_secs(3), reader.next_line()).await
|
tokio::time::timeout(std::time::Duration::from_secs(3), reader.next_line()).await
|
||||||
@ -397,6 +232,7 @@ impl WalletRpc {
|
|||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
async fn extract_archive(monero_wallet_rpc: &Self) -> Result<()> {
|
async fn extract_archive(monero_wallet_rpc: &Self) -> Result<()> {
|
||||||
|
use anyhow::bail;
|
||||||
use tokio_tar::Archive;
|
use tokio_tar::Archive;
|
||||||
|
|
||||||
let mut options = OpenOptions::new();
|
let mut options = OpenOptions::new();
|
||||||
@ -461,123 +297,3 @@ impl WalletRpc {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn extract_host_and_port(address: String) -> (&'static str, u16) {
|
|
||||||
let parts: Vec<&str> = address.split(':').collect();
|
|
||||||
|
|
||||||
if parts.len() == 2 {
|
|
||||||
let host = parts[0].to_string();
|
|
||||||
let port = parts[1].parse::<u16>().unwrap();
|
|
||||||
let static_str_host: &'static str = Box::leak(host.into_boxed_str());
|
|
||||||
return (static_str_host, port);
|
|
||||||
}
|
|
||||||
panic!("Could not extract host and port from address: {}", address)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_is_daemon_available_success() {
|
|
||||||
let mut server = mockito::Server::new_async().await;
|
|
||||||
|
|
||||||
let _ = server
|
|
||||||
.mock("GET", "/get_info")
|
|
||||||
.with_status(200)
|
|
||||||
.with_body(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"status": "OK",
|
|
||||||
"synchronized": true,
|
|
||||||
"mainnet": true,
|
|
||||||
"stagenet": false,
|
|
||||||
"testnet": false
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let (host, port) = extract_host_and_port(server.host_with_port());
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let result = MoneroDaemon::new(host, port, Network::Mainnet)
|
|
||||||
.is_available(&client)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(result.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_is_daemon_available_wrong_network_failure() {
|
|
||||||
let mut server = mockito::Server::new_async().await;
|
|
||||||
|
|
||||||
let _ = server
|
|
||||||
.mock("GET", "/get_info")
|
|
||||||
.with_status(200)
|
|
||||||
.with_body(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"status": "OK",
|
|
||||||
"synchronized": true,
|
|
||||||
"mainnet": true,
|
|
||||||
"stagenet": false,
|
|
||||||
"testnet": false
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let (host, port) = extract_host_and_port(server.host_with_port());
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let result = MoneroDaemon::new(host, port, Network::Stagenet)
|
|
||||||
.is_available(&client)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(!result.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_is_daemon_available_not_synced_failure() {
|
|
||||||
let mut server = mockito::Server::new_async().await;
|
|
||||||
|
|
||||||
let _ = server
|
|
||||||
.mock("GET", "/get_info")
|
|
||||||
.with_status(200)
|
|
||||||
.with_body(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"status": "OK",
|
|
||||||
"synchronized": false,
|
|
||||||
"mainnet": true,
|
|
||||||
"stagenet": false,
|
|
||||||
"testnet": false
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.create();
|
|
||||||
|
|
||||||
let (host, port) = extract_host_and_port(server.host_with_port());
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let result = MoneroDaemon::new(host, port, Network::Mainnet)
|
|
||||||
.is_available(&client)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(!result.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_is_daemon_available_network_error_failure() {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let result = MoneroDaemon::new("does.not.exist.com", 18081, Network::Mainnet)
|
|
||||||
.is_available(&client)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
mod impl_from_rr_event;
|
mod impl_from_rr_event;
|
||||||
|
|
||||||
pub mod cbor_request_response;
|
pub mod cbor_request_response;
|
||||||
pub mod cooperative_xmr_redeem_after_punish;
|
|
||||||
pub mod encrypted_signature;
|
pub mod encrypted_signature;
|
||||||
pub mod json_pull_codec;
|
pub mod json_pull_codec;
|
||||||
pub mod quote;
|
pub mod quote;
|
||||||
|
@ -19,7 +19,7 @@ pub struct CborCodec<P, Req, Res> {
|
|||||||
impl<P, Req, Res> Default for CborCodec<P, Req, Res> {
|
impl<P, Req, Res> Default for CborCodec<P, Req, Res> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
phantom: PhantomData,
|
phantom: PhantomData::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,113 +0,0 @@
|
|||||||
use crate::monero::Scalar;
|
|
||||||
use crate::network::cbor_request_response::CborCodec;
|
|
||||||
use crate::{asb, cli};
|
|
||||||
use libp2p::core::ProtocolName;
|
|
||||||
use libp2p::request_response::{
|
|
||||||
ProtocolSupport, RequestResponse, RequestResponseConfig, RequestResponseEvent,
|
|
||||||
RequestResponseMessage,
|
|
||||||
};
|
|
||||||
use libp2p::PeerId;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
const PROTOCOL: &str = "/comit/xmr/btc/cooperative_xmr_redeem_after_punish/1.0.0";
|
|
||||||
type OutEvent = RequestResponseEvent<Request, Response>;
|
|
||||||
type Message = RequestResponseMessage<Request, Response>;
|
|
||||||
|
|
||||||
pub type Behaviour = RequestResponse<CborCodec<CooperativeXmrRedeemProtocol, Request, Response>>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
|
||||||
pub struct CooperativeXmrRedeemProtocol;
|
|
||||||
|
|
||||||
impl ProtocolName for CooperativeXmrRedeemProtocol {
|
|
||||||
fn protocol_name(&self) -> &[u8] {
|
|
||||||
PROTOCOL.as_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum CooperativeXmrRedeemRejectReason {
|
|
||||||
#[error("Alice does not have a record of the swap")]
|
|
||||||
UnknownSwap,
|
|
||||||
#[error("Alice rejected the request because it deemed it malicious")]
|
|
||||||
MaliciousRequest,
|
|
||||||
#[error("Alice is in a state where a cooperative redeem is not possible")]
|
|
||||||
SwapInvalidState,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Request {
|
|
||||||
pub swap_id: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum Response {
|
|
||||||
Fullfilled {
|
|
||||||
swap_id: Uuid,
|
|
||||||
s_a: Scalar,
|
|
||||||
},
|
|
||||||
Rejected {
|
|
||||||
swap_id: Uuid,
|
|
||||||
reason: CooperativeXmrRedeemRejectReason,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pub fn alice() -> Behaviour {
|
|
||||||
Behaviour::new(
|
|
||||||
CborCodec::default(),
|
|
||||||
vec![(CooperativeXmrRedeemProtocol, ProtocolSupport::Inbound)],
|
|
||||||
RequestResponseConfig::default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bob() -> Behaviour {
|
|
||||||
Behaviour::new(
|
|
||||||
CborCodec::default(),
|
|
||||||
vec![(CooperativeXmrRedeemProtocol, ProtocolSupport::Outbound)],
|
|
||||||
RequestResponseConfig::default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(PeerId, Message)> for asb::OutEvent {
|
|
||||||
fn from((peer, message): (PeerId, Message)) -> Self {
|
|
||||||
match message {
|
|
||||||
Message::Request {
|
|
||||||
request, channel, ..
|
|
||||||
} => Self::CooperativeXmrRedeemRequested {
|
|
||||||
swap_id: request.swap_id,
|
|
||||||
channel,
|
|
||||||
peer,
|
|
||||||
},
|
|
||||||
Message::Response { .. } => Self::unexpected_response(peer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::impl_from_rr_event!(OutEvent, asb::OutEvent, PROTOCOL);
|
|
||||||
|
|
||||||
impl From<(PeerId, Message)> for cli::OutEvent {
|
|
||||||
fn from((peer, message): (PeerId, Message)) -> Self {
|
|
||||||
match message {
|
|
||||||
Message::Request { .. } => Self::unexpected_request(peer),
|
|
||||||
Message::Response {
|
|
||||||
response,
|
|
||||||
request_id,
|
|
||||||
} => match response {
|
|
||||||
Response::Fullfilled { swap_id, s_a } => Self::CooperativeXmrRedeemFulfilled {
|
|
||||||
id: request_id,
|
|
||||||
swap_id,
|
|
||||||
s_a,
|
|
||||||
},
|
|
||||||
Response::Rejected {
|
|
||||||
swap_id,
|
|
||||||
reason: error,
|
|
||||||
} => Self::CooperativeXmrRedeemRejected {
|
|
||||||
id: request_id,
|
|
||||||
swap_id,
|
|
||||||
reason: error,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::impl_from_rr_event!(OutEvent, cli::OutEvent, PROTOCOL);
|
|
@ -25,7 +25,7 @@ pub struct JsonPullCodec<P, Res> {
|
|||||||
impl<P, Res> Default for JsonPullCodec<P, Res> {
|
impl<P, Res> Default for JsonPullCodec<P, Res> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
phantom: PhantomData,
|
phantom: PhantomData::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use libp2p::rendezvous::Namespace;
|
use libp2p::rendezvous::Namespace;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
pub enum XmrBtcNamespace {
|
pub enum XmrBtcNamespace {
|
||||||
Mainnet,
|
Mainnet,
|
||||||
Testnet,
|
Testnet,
|
||||||
|
@ -37,7 +37,7 @@ pub mod protocol {
|
|||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct BlockchainNetwork {
|
pub struct BlockchainNetwork {
|
||||||
#[serde(with = "crate::bitcoin::network")]
|
#[serde(with = "crate::bitcoin::network")]
|
||||||
pub bitcoin: bitcoin::Network,
|
pub bitcoin: bitcoin::Network,
|
||||||
|
@ -155,16 +155,13 @@ impl ProtocolsHandler for Handler {
|
|||||||
let env_config = self.env_config;
|
let env_config = self.env_config;
|
||||||
|
|
||||||
let protocol = tokio::time::timeout(self.timeout, async move {
|
let protocol = tokio::time::timeout(self.timeout, async move {
|
||||||
write_cbor_message(
|
write_cbor_message(&mut substream, SpotPriceRequest {
|
||||||
&mut substream,
|
btc: info.btc,
|
||||||
SpotPriceRequest {
|
blockchain_network: BlockchainNetwork {
|
||||||
btc: info.btc,
|
bitcoin: env_config.bitcoin_network,
|
||||||
blockchain_network: BlockchainNetwork {
|
monero: env_config.monero_network,
|
||||||
bitcoin: env_config.bitcoin_network,
|
|
||||||
monero: env_config.monero_network,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let xmr = Result::from(read_cbor_message::<SpotPriceResponse>(&mut substream).await?)?;
|
let xmr = Result::from(read_cbor_message::<SpotPriceResponse>(&mut substream).await?)?;
|
||||||
@ -261,7 +258,7 @@ impl From<SpotPriceResponse> for Result<monero::Amount, Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
|
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Seller currently does not accept incoming swap requests, please try again later")]
|
#[error("Seller currently does not accept incoming swap requests, please try again later")]
|
||||||
NoSwapsAccepted,
|
NoSwapsAccepted,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use crate::asb::{LatestRate, RendezvousNode};
|
use crate::asb::LatestRate;
|
||||||
use crate::libp2p_ext::MultiAddrExt;
|
use crate::libp2p_ext::MultiAddrExt;
|
||||||
use crate::network::rendezvous::XmrBtcNamespace;
|
use crate::network::rendezvous::XmrBtcNamespace;
|
||||||
use crate::seed::Seed;
|
use crate::seed::Seed;
|
||||||
use crate::{asb, bitcoin, cli, env, tor};
|
use crate::{asb, bitcoin, cli, env, tor};
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder};
|
use libp2p::swarm::{NetworkBehaviour, SwarmBuilder};
|
||||||
use libp2p::{identity, Multiaddr, Swarm};
|
use libp2p::{identity, Multiaddr, Swarm};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
@ -17,23 +17,22 @@ pub fn asb<LR>(
|
|||||||
resume_only: bool,
|
resume_only: bool,
|
||||||
env_config: env::Config,
|
env_config: env::Config,
|
||||||
namespace: XmrBtcNamespace,
|
namespace: XmrBtcNamespace,
|
||||||
rendezvous_addrs: &[Multiaddr],
|
rendezvous_point: Option<Multiaddr>,
|
||||||
) -> Result<Swarm<asb::Behaviour<LR>>>
|
) -> Result<Swarm<asb::Behaviour<LR>>>
|
||||||
where
|
where
|
||||||
LR: LatestRate + Send + 'static + Debug + Clone,
|
LR: LatestRate + Send + 'static + Debug + Clone,
|
||||||
{
|
{
|
||||||
let identity = seed.derive_libp2p_identity();
|
let identity = seed.derive_libp2p_identity();
|
||||||
|
|
||||||
let rendezvous_nodes = rendezvous_addrs
|
let rendezvous_params = if let Some(address) = rendezvous_point {
|
||||||
.iter()
|
let peer_id = address
|
||||||
.map(|addr| {
|
.extract_peer_id()
|
||||||
let peer_id = addr
|
.context("Rendezvous node address must contain peer ID")?;
|
||||||
.extract_peer_id()
|
|
||||||
.expect("Rendezvous node address must contain peer ID");
|
|
||||||
|
|
||||||
RendezvousNode::new(addr, peer_id, namespace, None)
|
Some((identity.clone(), peer_id, address, namespace))
|
||||||
})
|
} else {
|
||||||
.collect();
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let behaviour = asb::Behaviour::new(
|
let behaviour = asb::Behaviour::new(
|
||||||
min_buy,
|
min_buy,
|
||||||
@ -42,7 +41,7 @@ where
|
|||||||
resume_only,
|
resume_only,
|
||||||
env_config,
|
env_config,
|
||||||
(identity.clone(), namespace),
|
(identity.clone(), namespace),
|
||||||
rendezvous_nodes,
|
rendezvous_params,
|
||||||
);
|
);
|
||||||
|
|
||||||
let transport = asb::transport::new(&identity)?;
|
let transport = asb::transport::new(&identity)?;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::stream::FusedStream;
|
use futures::stream::FusedStream;
|
||||||
use futures::{future, Future, StreamExt};
|
use futures::{future, Future, Stream, StreamExt};
|
||||||
use libp2p::core::muxing::StreamMuxerBox;
|
use libp2p::core::muxing::StreamMuxerBox;
|
||||||
use libp2p::core::transport::upgrade::Version;
|
use libp2p::core::transport::upgrade::Version;
|
||||||
use libp2p::core::transport::MemoryTransport;
|
use libp2p::core::transport::MemoryTransport;
|
||||||
@ -21,7 +21,7 @@ struct GlobalSpawnTokioExecutor;
|
|||||||
|
|
||||||
impl Executor for GlobalSpawnTokioExecutor {
|
impl Executor for GlobalSpawnTokioExecutor {
|
||||||
fn exec(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
|
fn exec(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
|
||||||
tokio::spawn(future);
|
let _ = tokio::spawn(future);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ where
|
|||||||
.expect("failed to create dh_keys");
|
.expect("failed to create dh_keys");
|
||||||
let noise = NoiseConfig::xx(dh_keys).into_authenticated();
|
let noise = NoiseConfig::xx(dh_keys).into_authenticated();
|
||||||
|
|
||||||
let transport = MemoryTransport
|
let transport = MemoryTransport::default()
|
||||||
.or_transport(TokioTcpConfig::new())
|
.or_transport(TokioTcpConfig::new())
|
||||||
.upgrade(Version::V1)
|
.upgrade(Version::V1)
|
||||||
.authenticate(noise)
|
.authenticate(noise)
|
||||||
@ -75,8 +75,8 @@ async fn get_local_tcp_address() -> Multiaddr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn await_events_or_timeout<A, B, E1, E2>(
|
pub async fn await_events_or_timeout<A, B, E1, E2>(
|
||||||
swarm_1: &mut (impl FusedStream<Item = SwarmEvent<A, E1>> + FusedStream + Unpin),
|
swarm_1: &mut (impl Stream<Item = SwarmEvent<A, E1>> + FusedStream + Unpin),
|
||||||
swarm_2: &mut (impl FusedStream<Item = SwarmEvent<B, E2>> + FusedStream + Unpin),
|
swarm_2: &mut (impl Stream<Item = SwarmEvent<B, E2>> + FusedStream + Unpin),
|
||||||
) -> (SwarmEvent<A, E1>, SwarmEvent<B, E2>)
|
) -> (SwarmEvent<A, E1>, SwarmEvent<B, E2>)
|
||||||
where
|
where
|
||||||
SwarmEvent<A, E1>: Debug,
|
SwarmEvent<A, E1>: Debug,
|
||||||
|
@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof};
|
use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof};
|
||||||
use sigma_fun::HashTranscript;
|
use sigma_fun::HashTranscript;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -102,11 +101,11 @@ impl From<BobState> for State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
|
||||||
#[error("Not in the role of Alice")]
|
#[error("Not in the role of Alice")]
|
||||||
pub struct NotAlice;
|
pub struct NotAlice;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
|
||||||
#[error("Not in the role of Bob")]
|
#[error("Not in the role of Bob")]
|
||||||
pub struct NotBob;
|
pub struct NotBob;
|
||||||
|
|
||||||
@ -140,19 +139,7 @@ pub trait Database {
|
|||||||
async fn get_monero_address(&self, swap_id: Uuid) -> Result<monero::Address>;
|
async fn get_monero_address(&self, swap_id: Uuid) -> Result<monero::Address>;
|
||||||
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>;
|
async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>;
|
||||||
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>>;
|
async fn get_addresses(&self, peer_id: PeerId) -> Result<Vec<Multiaddr>>;
|
||||||
async fn get_swap_start_date(&self, swap_id: Uuid) -> Result<String>;
|
|
||||||
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>;
|
async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>;
|
||||||
async fn get_state(&self, swap_id: Uuid) -> Result<State>;
|
async fn get_state(&self, swap_id: Uuid) -> Result<State>;
|
||||||
async fn get_states(&self, swap_id: Uuid) -> Result<Vec<State>>;
|
|
||||||
async fn all(&self) -> Result<Vec<(Uuid, State)>>;
|
async fn all(&self) -> Result<Vec<(Uuid, State)>>;
|
||||||
async fn raw_all(&self) -> Result<HashMap<Uuid, Vec<serde_json::Value>>>;
|
|
||||||
async fn insert_buffered_transfer_proof(
|
|
||||||
&self,
|
|
||||||
swap_id: Uuid,
|
|
||||||
proof: monero::TransferProof,
|
|
||||||
) -> Result<()>;
|
|
||||||
async fn get_buffered_transfer_proof(
|
|
||||||
&self,
|
|
||||||
swap_id: Uuid,
|
|
||||||
) -> Result<Option<monero::TransferProof>>;
|
|
||||||
}
|
}
|
||||||
|
@ -74,9 +74,7 @@ pub enum AliceState {
|
|||||||
transfer_proof: TransferProof,
|
transfer_proof: TransferProof,
|
||||||
state3: Box<State3>,
|
state3: Box<State3>,
|
||||||
},
|
},
|
||||||
BtcPunished {
|
BtcPunished,
|
||||||
state3: Box<State3>,
|
|
||||||
},
|
|
||||||
SafelyAborted,
|
SafelyAborted,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +98,7 @@ impl fmt::Display for AliceState {
|
|||||||
AliceState::BtcRedeemed => write!(f, "btc is redeemed"),
|
AliceState::BtcRedeemed => write!(f, "btc is redeemed"),
|
||||||
AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"),
|
AliceState::BtcCancelled { .. } => write!(f, "btc is cancelled"),
|
||||||
AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"),
|
AliceState::BtcRefunded { .. } => write!(f, "btc is refunded"),
|
||||||
AliceState::BtcPunished { .. } => write!(f, "btc is punished"),
|
AliceState::BtcPunished => write!(f, "btc is punished"),
|
||||||
AliceState::SafelyAborted => write!(f, "safely aborted"),
|
AliceState::SafelyAborted => write!(f, "safely aborted"),
|
||||||
AliceState::BtcPunishable { .. } => write!(f, "btc is punishable"),
|
AliceState::BtcPunishable { .. } => write!(f, "btc is punishable"),
|
||||||
AliceState::XmrRefunded => write!(f, "xmr is refunded"),
|
AliceState::XmrRefunded => write!(f, "xmr is refunded"),
|
||||||
@ -186,32 +184,29 @@ impl State0 {
|
|||||||
|
|
||||||
let v = self.v_a + msg.v_b;
|
let v = self.v_a + msg.v_b;
|
||||||
|
|
||||||
Ok((
|
Ok((msg.swap_id, State1 {
|
||||||
msg.swap_id,
|
a: self.a,
|
||||||
State1 {
|
B: msg.B,
|
||||||
a: self.a,
|
s_a: self.s_a,
|
||||||
B: msg.B,
|
S_a_monero: self.S_a_monero,
|
||||||
s_a: self.s_a,
|
S_a_bitcoin: self.S_a_bitcoin,
|
||||||
S_a_monero: self.S_a_monero,
|
S_b_monero: msg.S_b_monero,
|
||||||
S_a_bitcoin: self.S_a_bitcoin,
|
S_b_bitcoin: msg.S_b_bitcoin,
|
||||||
S_b_monero: msg.S_b_monero,
|
v,
|
||||||
S_b_bitcoin: msg.S_b_bitcoin,
|
v_a: self.v_a,
|
||||||
v,
|
dleq_proof_s_a: self.dleq_proof_s_a,
|
||||||
v_a: self.v_a,
|
btc: self.btc,
|
||||||
dleq_proof_s_a: self.dleq_proof_s_a,
|
xmr: self.xmr,
|
||||||
btc: self.btc,
|
cancel_timelock: self.cancel_timelock,
|
||||||
xmr: self.xmr,
|
punish_timelock: self.punish_timelock,
|
||||||
cancel_timelock: self.cancel_timelock,
|
refund_address: msg.refund_address,
|
||||||
punish_timelock: self.punish_timelock,
|
redeem_address: self.redeem_address,
|
||||||
refund_address: msg.refund_address,
|
punish_address: self.punish_address,
|
||||||
redeem_address: self.redeem_address,
|
tx_redeem_fee: self.tx_redeem_fee,
|
||||||
punish_address: self.punish_address,
|
tx_punish_fee: self.tx_punish_fee,
|
||||||
tx_redeem_fee: self.tx_redeem_fee,
|
tx_refund_fee: msg.tx_refund_fee,
|
||||||
tx_punish_fee: self.tx_punish_fee,
|
tx_cancel_fee: msg.tx_cancel_fee,
|
||||||
tx_refund_fee: msg.tx_refund_fee,
|
}))
|
||||||
tx_cancel_fee: msg.tx_cancel_fee,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,8 +307,7 @@ impl State2 {
|
|||||||
self.a.public(),
|
self.a.public(),
|
||||||
self.B,
|
self.B,
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)
|
);
|
||||||
.expect("valid cancel tx");
|
|
||||||
|
|
||||||
let tx_refund =
|
let tx_refund =
|
||||||
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
|
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
|
||||||
@ -338,7 +332,7 @@ impl State2 {
|
|||||||
self.a.public(),
|
self.a.public(),
|
||||||
self.B,
|
self.B,
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)?;
|
);
|
||||||
bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig)
|
bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig)
|
||||||
.context("Failed to verify cancel transaction")?;
|
.context("Failed to verify cancel transaction")?;
|
||||||
let tx_punish = bitcoin::TxPunish::new(
|
let tx_punish = bitcoin::TxPunish::new(
|
||||||
@ -379,13 +373,13 @@ impl State2 {
|
|||||||
pub struct State3 {
|
pub struct State3 {
|
||||||
a: bitcoin::SecretKey,
|
a: bitcoin::SecretKey,
|
||||||
B: bitcoin::PublicKey,
|
B: bitcoin::PublicKey,
|
||||||
pub s_a: monero::Scalar,
|
s_a: monero::Scalar,
|
||||||
S_b_monero: monero::PublicKey,
|
S_b_monero: monero::PublicKey,
|
||||||
S_b_bitcoin: bitcoin::PublicKey,
|
S_b_bitcoin: bitcoin::PublicKey,
|
||||||
pub v: monero::PrivateViewKey,
|
pub v: monero::PrivateViewKey,
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
pub btc: bitcoin::Amount,
|
btc: bitcoin::Amount,
|
||||||
pub xmr: monero::Amount,
|
xmr: monero::Amount,
|
||||||
pub cancel_timelock: CancelTimelock,
|
pub cancel_timelock: CancelTimelock,
|
||||||
pub punish_timelock: PunishTimelock,
|
pub punish_timelock: PunishTimelock,
|
||||||
refund_address: bitcoin::Address,
|
refund_address: bitcoin::Address,
|
||||||
@ -461,7 +455,6 @@ impl State3 {
|
|||||||
self.B,
|
self.B,
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)
|
)
|
||||||
.expect("valid cancel tx")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tx_refund(&self) -> TxRefund {
|
pub fn tx_refund(&self) -> TxRefund {
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
//! Run an XMR/BTC swap in the role of Alice.
|
//! Run an XMR/BTC swap in the role of Alice.
|
||||||
//! Alice holds XMR and wishes receive BTC.
|
//! Alice holds XMR and wishes receive BTC.
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::asb::{EventLoopHandle, LatestRate};
|
use crate::asb::{EventLoopHandle, LatestRate};
|
||||||
use crate::bitcoin::ExpiredTimelocks;
|
use crate::bitcoin::ExpiredTimelocks;
|
||||||
use crate::env::Config;
|
use crate::env::Config;
|
||||||
use crate::protocol::alice::{AliceState, Swap};
|
use crate::protocol::alice::{AliceState, Swap};
|
||||||
use crate::{bitcoin, monero};
|
use crate::{bitcoin, monero};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use backoff::ExponentialBackoffBuilder;
|
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -114,63 +111,23 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AliceState::BtcLocked { state3 } => {
|
AliceState::BtcLocked { state3 } => {
|
||||||
// We retry to lock the Monero wallet until we succeed or until the cancel timelock expires.
|
match state3.expired_timelocks(bitcoin_wallet).await? {
|
||||||
//
|
ExpiredTimelocks::None => {
|
||||||
// This is necessary because the monero-wallet-rpc can sometimes error out due to various reasons, such as
|
// Record the current monero wallet block height so we don't have to scan from
|
||||||
// - no connection to the daemon
|
// block 0 for scenarios where we create a refund wallet.
|
||||||
// - "failed to get output distribution"
|
let monero_wallet_restore_blockheight = monero_wallet.block_height().await?;
|
||||||
// See https://github.com/comit-network/xmr-btc-swap/issues/1726
|
|
||||||
let backoff = ExponentialBackoffBuilder::new()
|
|
||||||
.with_initial_interval(Duration::from_secs(5))
|
|
||||||
.with_max_interval(Duration::from_secs(60 * 3))
|
|
||||||
.with_max_elapsed_time(None)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let result = backoff::future::retry_notify(
|
let transfer_proof = monero_wallet
|
||||||
backoff,
|
.transfer(state3.lock_xmr_transfer_request())
|
||||||
|| async {
|
.await?;
|
||||||
match state3.expired_timelocks(bitcoin_wallet).await {
|
|
||||||
Ok(ExpiredTimelocks::None { .. }) => {
|
|
||||||
// Record the current monero wallet block height so we don't have to scan from
|
|
||||||
// block 0 for scenarios where we create a refund wallet.
|
|
||||||
let monero_wallet_restore_blockheight = monero_wallet
|
|
||||||
.block_height()
|
|
||||||
.await
|
|
||||||
.map_err(backoff::Error::transient)?;
|
|
||||||
|
|
||||||
let transfer_proof = monero_wallet
|
|
||||||
.transfer(state3.lock_xmr_transfer_request())
|
|
||||||
.await
|
|
||||||
.map_err(backoff::Error::transient)?;
|
|
||||||
|
|
||||||
Ok(Some((monero_wallet_restore_blockheight, transfer_proof)))
|
|
||||||
}
|
|
||||||
Ok(_) => Ok(None),
|
|
||||||
Err(e) => Err(backoff::Error::transient(e)),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|err, delay: Duration| {
|
|
||||||
tracing::warn!(
|
|
||||||
%err,
|
|
||||||
delay_secs = delay.as_secs(),
|
|
||||||
"Failed to lock XMR. We will retry after a delay"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(Some((monero_wallet_restore_blockheight, transfer_proof))) => {
|
|
||||||
AliceState::XmrLockTransactionSent {
|
AliceState::XmrLockTransactionSent {
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
transfer_proof,
|
transfer_proof,
|
||||||
state3,
|
state3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => AliceState::SafelyAborted,
|
_ => AliceState::SafelyAborted,
|
||||||
Err(e) => {
|
|
||||||
unreachable!("We should retry forever until the cancel timelock expires. But we got an error: {:#}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AliceState::XmrLockTransactionSent {
|
AliceState::XmrLockTransactionSent {
|
||||||
@ -178,7 +135,7 @@ where
|
|||||||
transfer_proof,
|
transfer_proof,
|
||||||
state3,
|
state3,
|
||||||
} => match state3.expired_timelocks(bitcoin_wallet).await? {
|
} => match state3.expired_timelocks(bitcoin_wallet).await? {
|
||||||
ExpiredTimelocks::None { .. } => {
|
ExpiredTimelocks::None => {
|
||||||
monero_wallet
|
monero_wallet
|
||||||
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1))
|
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1))
|
||||||
.await
|
.await
|
||||||
@ -264,7 +221,7 @@ where
|
|||||||
encrypted_signature,
|
encrypted_signature,
|
||||||
state3,
|
state3,
|
||||||
} => match state3.expired_timelocks(bitcoin_wallet).await? {
|
} => match state3.expired_timelocks(bitcoin_wallet).await? {
|
||||||
ExpiredTimelocks::None { .. } => {
|
ExpiredTimelocks::None => {
|
||||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
||||||
match state3.signed_redeem_transaction(*encrypted_signature) {
|
match state3.signed_redeem_transaction(*encrypted_signature) {
|
||||||
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
|
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
|
||||||
@ -405,7 +362,7 @@ where
|
|||||||
let punish = state3.punish_btc(bitcoin_wallet).await;
|
let punish = state3.punish_btc(bitcoin_wallet).await;
|
||||||
|
|
||||||
match punish {
|
match punish {
|
||||||
Ok(_) => AliceState::BtcPunished { state3 },
|
Ok(_) => AliceState::BtcPunished,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::warn!("Failed to publish punish transaction: {:#}", error);
|
tracing::warn!("Failed to publish punish transaction: {:#}", error);
|
||||||
|
|
||||||
@ -435,17 +392,17 @@ where
|
|||||||
}
|
}
|
||||||
AliceState::XmrRefunded => AliceState::XmrRefunded,
|
AliceState::XmrRefunded => AliceState::XmrRefunded,
|
||||||
AliceState::BtcRedeemed => AliceState::BtcRedeemed,
|
AliceState::BtcRedeemed => AliceState::BtcRedeemed,
|
||||||
AliceState::BtcPunished { state3 } => AliceState::BtcPunished { state3 },
|
AliceState::BtcPunished => AliceState::BtcPunished,
|
||||||
AliceState::SafelyAborted => AliceState::SafelyAborted,
|
AliceState::SafelyAborted => AliceState::SafelyAborted,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_complete(state: &AliceState) -> bool {
|
pub(crate) fn is_complete(state: &AliceState) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
state,
|
state,
|
||||||
AliceState::XmrRefunded
|
AliceState::XmrRefunded
|
||||||
| AliceState::BtcRedeemed
|
| AliceState::BtcRedeemed
|
||||||
| AliceState::BtcPunished { .. }
|
| AliceState::BtcPunished
|
||||||
| AliceState::SafelyAborted
|
| AliceState::SafelyAborted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,9 @@ use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum BobState {
|
pub enum BobState {
|
||||||
Started {
|
Started {
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
|
||||||
btc_amount: bitcoin::Amount,
|
btc_amount: bitcoin::Amount,
|
||||||
change_address: bitcoin::Address,
|
change_address: bitcoin::Address,
|
||||||
},
|
},
|
||||||
@ -48,7 +47,6 @@ pub enum BobState {
|
|||||||
tx_lock_id: bitcoin::Txid,
|
tx_lock_id: bitcoin::Txid,
|
||||||
},
|
},
|
||||||
BtcPunished {
|
BtcPunished {
|
||||||
state: State6,
|
|
||||||
tx_lock_id: bitcoin::Txid,
|
tx_lock_id: bitcoin::Txid,
|
||||||
},
|
},
|
||||||
SafelyAborted,
|
SafelyAborted,
|
||||||
@ -244,7 +242,7 @@ impl State1 {
|
|||||||
self.A,
|
self.A,
|
||||||
self.b.public(),
|
self.b.public(),
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)?;
|
);
|
||||||
let tx_refund =
|
let tx_refund =
|
||||||
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
|
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
|
||||||
|
|
||||||
@ -289,13 +287,13 @@ pub struct State2 {
|
|||||||
S_a_monero: monero::PublicKey,
|
S_a_monero: monero::PublicKey,
|
||||||
S_a_bitcoin: bitcoin::PublicKey,
|
S_a_bitcoin: bitcoin::PublicKey,
|
||||||
v: monero::PrivateViewKey,
|
v: monero::PrivateViewKey,
|
||||||
pub xmr: monero::Amount,
|
xmr: monero::Amount,
|
||||||
pub cancel_timelock: CancelTimelock,
|
cancel_timelock: CancelTimelock,
|
||||||
pub punish_timelock: PunishTimelock,
|
punish_timelock: PunishTimelock,
|
||||||
pub refund_address: bitcoin::Address,
|
refund_address: bitcoin::Address,
|
||||||
redeem_address: bitcoin::Address,
|
redeem_address: bitcoin::Address,
|
||||||
punish_address: bitcoin::Address,
|
punish_address: bitcoin::Address,
|
||||||
pub tx_lock: bitcoin::TxLock,
|
tx_lock: bitcoin::TxLock,
|
||||||
tx_cancel_sig_a: Signature,
|
tx_cancel_sig_a: Signature,
|
||||||
tx_refund_encsig: bitcoin::EncryptedSignature,
|
tx_refund_encsig: bitcoin::EncryptedSignature,
|
||||||
min_monero_confirmations: u64,
|
min_monero_confirmations: u64,
|
||||||
@ -304,9 +302,9 @@ pub struct State2 {
|
|||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
tx_punish_fee: bitcoin::Amount,
|
tx_punish_fee: bitcoin::Amount,
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
pub tx_refund_fee: bitcoin::Amount,
|
tx_refund_fee: bitcoin::Amount,
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
pub tx_cancel_fee: bitcoin::Amount,
|
tx_cancel_fee: bitcoin::Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State2 {
|
impl State2 {
|
||||||
@ -317,8 +315,7 @@ impl State2 {
|
|||||||
self.A,
|
self.A,
|
||||||
self.b.public(),
|
self.b.public(),
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)
|
);
|
||||||
.expect("valid cancel tx");
|
|
||||||
let tx_cancel_sig = self.b.sign(tx_cancel.digest());
|
let tx_cancel_sig = self.b.sign(tx_cancel.digest());
|
||||||
let tx_punish = bitcoin::TxPunish::new(
|
let tx_punish = bitcoin::TxPunish::new(
|
||||||
&tx_cancel,
|
&tx_cancel,
|
||||||
@ -369,7 +366,7 @@ pub struct State3 {
|
|||||||
S_a_monero: monero::PublicKey,
|
S_a_monero: monero::PublicKey,
|
||||||
S_a_bitcoin: bitcoin::PublicKey,
|
S_a_bitcoin: bitcoin::PublicKey,
|
||||||
v: monero::PrivateViewKey,
|
v: monero::PrivateViewKey,
|
||||||
pub xmr: monero::Amount,
|
xmr: monero::Amount,
|
||||||
pub cancel_timelock: CancelTimelock,
|
pub cancel_timelock: CancelTimelock,
|
||||||
punish_timelock: PunishTimelock,
|
punish_timelock: PunishTimelock,
|
||||||
refund_address: bitcoin::Address,
|
refund_address: bitcoin::Address,
|
||||||
@ -422,13 +419,11 @@ impl State3 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cancel(&self, monero_wallet_restore_blockheight: BlockHeight) -> State6 {
|
pub fn cancel(&self) -> State6 {
|
||||||
State6 {
|
State6 {
|
||||||
A: self.A,
|
A: self.A,
|
||||||
b: self.b.clone(),
|
b: self.b.clone(),
|
||||||
s_b: self.s_b,
|
s_b: self.s_b,
|
||||||
v: self.v,
|
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
cancel_timelock: self.cancel_timelock,
|
cancel_timelock: self.cancel_timelock,
|
||||||
punish_timelock: self.punish_timelock,
|
punish_timelock: self.punish_timelock,
|
||||||
refund_address: self.refund_address.clone(),
|
refund_address: self.refund_address.clone(),
|
||||||
@ -444,7 +439,7 @@ impl State3 {
|
|||||||
self.tx_lock.txid()
|
self.tx_lock.txid()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn expired_timelock(
|
pub async fn current_epoch(
|
||||||
&self,
|
&self,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<ExpiredTimelocks> {
|
) -> Result<ExpiredTimelocks> {
|
||||||
@ -454,7 +449,7 @@ impl State3 {
|
|||||||
self.A,
|
self.A,
|
||||||
self.b.public(),
|
self.b.public(),
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)?;
|
);
|
||||||
|
|
||||||
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
||||||
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
||||||
@ -466,19 +461,6 @@ impl State3 {
|
|||||||
tx_cancel_status,
|
tx_cancel_status,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
pub fn attempt_cooperative_redeem(
|
|
||||||
&self,
|
|
||||||
s_a: monero::PrivateKey,
|
|
||||||
monero_wallet_restore_blockheight: BlockHeight,
|
|
||||||
) -> State5 {
|
|
||||||
State5 {
|
|
||||||
s_a,
|
|
||||||
s_b: self.s_b,
|
|
||||||
v: self.v,
|
|
||||||
tx_lock: self.tx_lock.clone(),
|
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@ -505,27 +487,6 @@ pub struct State4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State4 {
|
impl State4 {
|
||||||
pub async fn check_for_tx_redeem(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<State5> {
|
|
||||||
let tx_redeem =
|
|
||||||
bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee);
|
|
||||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
|
|
||||||
|
|
||||||
let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?;
|
|
||||||
|
|
||||||
let tx_redeem_sig =
|
|
||||||
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
|
|
||||||
let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
|
|
||||||
let s_a = monero::private_key_from_secp256k1_scalar(s_a.into());
|
|
||||||
|
|
||||||
Ok(State5 {
|
|
||||||
s_a,
|
|
||||||
s_b: self.s_b,
|
|
||||||
v: self.v,
|
|
||||||
tx_lock: self.tx_lock.clone(),
|
|
||||||
monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature {
|
pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature {
|
||||||
let tx_redeem =
|
let tx_redeem =
|
||||||
bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee);
|
bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee);
|
||||||
@ -569,7 +530,7 @@ impl State4 {
|
|||||||
self.A,
|
self.A,
|
||||||
self.b.public(),
|
self.b.public(),
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)?;
|
);
|
||||||
|
|
||||||
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
||||||
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
||||||
@ -587,8 +548,6 @@ impl State4 {
|
|||||||
A: self.A,
|
A: self.A,
|
||||||
b: self.b,
|
b: self.b,
|
||||||
s_b: self.s_b,
|
s_b: self.s_b,
|
||||||
v: self.v,
|
|
||||||
monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight,
|
|
||||||
cancel_timelock: self.cancel_timelock,
|
cancel_timelock: self.cancel_timelock,
|
||||||
punish_timelock: self.punish_timelock,
|
punish_timelock: self.punish_timelock,
|
||||||
refund_address: self.refund_address,
|
refund_address: self.refund_address,
|
||||||
@ -601,7 +560,7 @@ impl State4 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct State5 {
|
pub struct State5 {
|
||||||
#[serde(with = "monero_private_key")]
|
#[serde(with = "monero_private_key")]
|
||||||
s_a: monero::PrivateKey,
|
s_a: monero::PrivateKey,
|
||||||
@ -622,43 +581,6 @@ impl State5 {
|
|||||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||||
self.tx_lock.txid()
|
self.tx_lock.txid()
|
||||||
}
|
}
|
||||||
pub async fn redeem_xmr(
|
|
||||||
&self,
|
|
||||||
monero_wallet: &monero::Wallet,
|
|
||||||
wallet_file_name: std::string::String,
|
|
||||||
monero_receive_address: monero::Address,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (spend_key, view_key) = self.xmr_keys();
|
|
||||||
|
|
||||||
tracing::info!(%wallet_file_name, "Generating and opening Monero wallet from the extracted keys to redeem the Monero");
|
|
||||||
if let Err(e) = monero_wallet
|
|
||||||
.create_from_and_load(
|
|
||||||
wallet_file_name.clone(),
|
|
||||||
spend_key,
|
|
||||||
view_key,
|
|
||||||
self.monero_wallet_restore_blockheight,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
// In case we failed to refresh/sweep, when resuming the wallet might already
|
|
||||||
// exist! This is a very unlikely scenario, but if we don't take care of it we
|
|
||||||
// might not be able to ever transfer the Monero.
|
|
||||||
tracing::warn!("Failed to generate monero wallet from keys: {:#}", e);
|
|
||||||
tracing::info!(%wallet_file_name,
|
|
||||||
"Falling back to trying to open the wallet if it already exists",
|
|
||||||
);
|
|
||||||
monero_wallet.open(wallet_file_name).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the generated wallet is synced so we have a proper balance
|
|
||||||
monero_wallet.refresh(20).await?;
|
|
||||||
// Sweep (transfer all funds) to the given address
|
|
||||||
let tx_hashes = monero_wallet.sweep_all(monero_receive_address).await?;
|
|
||||||
for tx_hash in tx_hashes {
|
|
||||||
tracing::info!(%monero_receive_address, txid=%tx_hash.0, "Successfully transferred XMR to wallet");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
@ -666,8 +588,6 @@ pub struct State6 {
|
|||||||
A: bitcoin::PublicKey,
|
A: bitcoin::PublicKey,
|
||||||
b: bitcoin::SecretKey,
|
b: bitcoin::SecretKey,
|
||||||
s_b: monero::Scalar,
|
s_b: monero::Scalar,
|
||||||
v: monero::PrivateViewKey,
|
|
||||||
pub monero_wallet_restore_blockheight: BlockHeight,
|
|
||||||
cancel_timelock: CancelTimelock,
|
cancel_timelock: CancelTimelock,
|
||||||
punish_timelock: PunishTimelock,
|
punish_timelock: PunishTimelock,
|
||||||
refund_address: bitcoin::Address,
|
refund_address: bitcoin::Address,
|
||||||
@ -691,7 +611,7 @@ impl State6 {
|
|||||||
self.A,
|
self.A,
|
||||||
self.b.public(),
|
self.b.public(),
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)?;
|
);
|
||||||
|
|
||||||
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
||||||
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
||||||
@ -703,20 +623,18 @@ impl State6 {
|
|||||||
tx_cancel_status,
|
tx_cancel_status,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
pub fn construct_tx_cancel(&self) -> Result<bitcoin::TxCancel> {
|
|
||||||
bitcoin::TxCancel::new(
|
pub async fn check_for_tx_cancel(
|
||||||
|
&self,
|
||||||
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
|
) -> Result<Transaction> {
|
||||||
|
let tx_cancel = bitcoin::TxCancel::new(
|
||||||
&self.tx_lock,
|
&self.tx_lock,
|
||||||
self.cancel_timelock,
|
self.cancel_timelock,
|
||||||
self.A,
|
self.A,
|
||||||
self.b.public(),
|
self.b.public(),
|
||||||
self.tx_cancel_fee,
|
self.tx_cancel_fee,
|
||||||
)
|
);
|
||||||
}
|
|
||||||
pub async fn check_for_tx_cancel(
|
|
||||||
&self,
|
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
|
||||||
) -> Result<Transaction> {
|
|
||||||
let tx_cancel = self.construct_tx_cancel()?;
|
|
||||||
|
|
||||||
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
|
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
|
||||||
|
|
||||||
@ -727,10 +645,15 @@ impl State6 {
|
|||||||
&self,
|
&self,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<(Txid, Subscription)> {
|
) -> Result<(Txid, Subscription)> {
|
||||||
let transaction = self
|
let transaction = bitcoin::TxCancel::new(
|
||||||
.construct_tx_cancel()?
|
&self.tx_lock,
|
||||||
.complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone())
|
self.cancel_timelock,
|
||||||
.context("Failed to complete Bitcoin cancel transaction")?;
|
self.A,
|
||||||
|
self.b.public(),
|
||||||
|
self.tx_cancel_fee,
|
||||||
|
)
|
||||||
|
.complete_as_bob(self.A, self.b.clone(), self.tx_cancel_sig_a.clone())
|
||||||
|
.context("Failed to complete Bitcoin cancel transaction")?;
|
||||||
|
|
||||||
let (tx_id, subscription) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
|
let (tx_id, subscription) = bitcoin_wallet.broadcast(transaction, "cancel").await?;
|
||||||
|
|
||||||
@ -745,7 +668,13 @@ impl State6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn signed_refund_transaction(&self) -> Result<Transaction> {
|
pub fn signed_refund_transaction(&self) -> Result<Transaction> {
|
||||||
let tx_cancel = self.construct_tx_cancel()?;
|
let tx_cancel = bitcoin::TxCancel::new(
|
||||||
|
&self.tx_lock,
|
||||||
|
self.cancel_timelock,
|
||||||
|
self.A,
|
||||||
|
self.b.public(),
|
||||||
|
self.tx_cancel_fee,
|
||||||
|
);
|
||||||
let tx_refund =
|
let tx_refund =
|
||||||
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
|
bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee);
|
||||||
|
|
||||||
@ -763,13 +692,4 @@ impl State6 {
|
|||||||
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
pub fn tx_lock_id(&self) -> bitcoin::Txid {
|
||||||
self.tx_lock.txid()
|
self.tx_lock.txid()
|
||||||
}
|
}
|
||||||
pub fn attempt_cooperative_redeem(&self, s_a: monero::PrivateKey) -> State5 {
|
|
||||||
State5 {
|
|
||||||
s_a,
|
|
||||||
s_b: self.s_b,
|
|
||||||
v: self.v,
|
|
||||||
tx_lock: self.tx_lock.clone(),
|
|
||||||
monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,23 @@
|
|||||||
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
|
||||||
use crate::cli::EventLoopHandle;
|
use crate::cli::EventLoopHandle;
|
||||||
use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected};
|
|
||||||
use crate::network::swap_setup::bob::NewSwap;
|
use crate::network::swap_setup::bob::NewSwap;
|
||||||
|
use crate::protocol::bob;
|
||||||
use crate::protocol::bob::state::*;
|
use crate::protocol::bob::state::*;
|
||||||
use crate::protocol::{bob, Database};
|
|
||||||
use crate::{bitcoin, monero};
|
use crate::{bitcoin, monero};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn is_complete(state: &BobState) -> bool {
|
pub fn is_complete(state: &BobState) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
state,
|
state,
|
||||||
BobState::BtcRefunded(..) | BobState::XmrRedeemed { .. } | BobState::SafelyAborted
|
BobState::BtcRefunded(..)
|
||||||
|
| BobState::XmrRedeemed { .. }
|
||||||
|
| BobState::BtcPunished { .. }
|
||||||
|
| BobState::SafelyAborted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identifies states that should be run at most once before exiting.
|
|
||||||
// This is used to prevent infinite retry loops while still allowing manual resumption.
|
|
||||||
//
|
|
||||||
// Currently, this applies to the BtcPunished state:
|
|
||||||
// - We want to attempt recovery via cooperative XMR redeem once.
|
|
||||||
// - If unsuccessful, we exit to avoid an infinite retry loop.
|
|
||||||
// - The swap can still be manually resumed later and retried if desired.
|
|
||||||
pub fn is_run_at_most_once(state: &BobState) -> bool {
|
|
||||||
matches!(state, BobState::BtcPunished { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn run(swap: bob::Swap) -> Result<BobState> {
|
pub async fn run(swap: bob::Swap) -> Result<BobState> {
|
||||||
run_until(swap, is_complete).await
|
run_until(swap, is_complete).await
|
||||||
@ -37,14 +27,13 @@ pub async fn run_until(
|
|||||||
mut swap: bob::Swap,
|
mut swap: bob::Swap,
|
||||||
is_target_state: fn(&BobState) -> bool,
|
is_target_state: fn(&BobState) -> bool,
|
||||||
) -> Result<BobState> {
|
) -> Result<BobState> {
|
||||||
let mut current_state = swap.state.clone();
|
let mut current_state = swap.state;
|
||||||
|
|
||||||
while !is_target_state(¤t_state) {
|
while !is_target_state(¤t_state) {
|
||||||
let next_state = next_state(
|
current_state = next_state(
|
||||||
swap.id,
|
swap.id,
|
||||||
current_state.clone(),
|
current_state.clone(),
|
||||||
&mut swap.event_loop_handle,
|
&mut swap.event_loop_handle,
|
||||||
swap.db.clone(),
|
|
||||||
swap.bitcoin_wallet.as_ref(),
|
swap.bitcoin_wallet.as_ref(),
|
||||||
swap.monero_wallet.as_ref(),
|
swap.monero_wallet.as_ref(),
|
||||||
swap.monero_receive_address,
|
swap.monero_receive_address,
|
||||||
@ -52,14 +41,8 @@ pub async fn run_until(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
swap.db
|
swap.db
|
||||||
.insert_latest_state(swap.id, next_state.clone().into())
|
.insert_latest_state(swap.id, current_state.clone().into())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if is_run_at_most_once(¤t_state) && next_state == current_state {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
current_state = next_state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(current_state)
|
Ok(current_state)
|
||||||
@ -69,7 +52,6 @@ async fn next_state(
|
|||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
state: BobState,
|
state: BobState,
|
||||||
event_loop_handle: &mut EventLoopHandle,
|
event_loop_handle: &mut EventLoopHandle,
|
||||||
db: Arc<dyn Database + Send + Sync>,
|
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
monero_wallet: &monero::Wallet,
|
monero_wallet: &monero::Wallet,
|
||||||
monero_receive_address: monero::Address,
|
monero_receive_address: monero::Address,
|
||||||
@ -135,29 +117,13 @@ async fn next_state(
|
|||||||
} => {
|
} => {
|
||||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
|
||||||
|
|
||||||
if let ExpiredTimelocks::None { .. } = state3.expired_timelock(bitcoin_wallet).await? {
|
if let ExpiredTimelocks::None = state3.current_epoch(bitcoin_wallet).await? {
|
||||||
tracing::info!("Waiting for Alice to lock Monero");
|
|
||||||
|
|
||||||
let buffered_transfer_proof = db
|
|
||||||
.get_buffered_transfer_proof(swap_id)
|
|
||||||
.await
|
|
||||||
.context("Failed to get buffered transfer proof")?;
|
|
||||||
|
|
||||||
if let Some(transfer_proof) = buffered_transfer_proof {
|
|
||||||
tracing::debug!(txid = %transfer_proof.tx_hash(), "Found buffered transfer proof");
|
|
||||||
tracing::info!(txid = %transfer_proof.tx_hash(), "Alice locked Monero");
|
|
||||||
|
|
||||||
return Ok(BobState::XmrLockProofReceived {
|
|
||||||
state: state3,
|
|
||||||
lock_transfer_proof: transfer_proof,
|
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
|
let transfer_proof_watcher = event_loop_handle.recv_transfer_proof();
|
||||||
let cancel_timelock_expires =
|
let cancel_timelock_expires =
|
||||||
tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock);
|
tx_lock_status.wait_until_confirmed_with(state3.cancel_timelock);
|
||||||
|
|
||||||
|
tracing::info!("Waiting for Alice to lock Monero");
|
||||||
|
|
||||||
select! {
|
select! {
|
||||||
transfer_proof = transfer_proof_watcher => {
|
transfer_proof = transfer_proof_watcher => {
|
||||||
let transfer_proof = transfer_proof?;
|
let transfer_proof = transfer_proof?;
|
||||||
@ -174,12 +140,12 @@ async fn next_state(
|
|||||||
result?;
|
result?;
|
||||||
tracing::info!("Alice took too long to lock Monero, cancelling the swap");
|
tracing::info!("Alice took too long to lock Monero, cancelling the swap");
|
||||||
|
|
||||||
let state4 = state3.cancel(monero_wallet_restore_blockheight);
|
let state4 = state3.cancel();
|
||||||
BobState::CancelTimelockExpired(state4)
|
BobState::CancelTimelockExpired(state4)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let state4 = state3.cancel(monero_wallet_restore_blockheight);
|
let state4 = state3.cancel();
|
||||||
BobState::CancelTimelockExpired(state4)
|
BobState::CancelTimelockExpired(state4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,7 +156,7 @@ async fn next_state(
|
|||||||
} => {
|
} => {
|
||||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await;
|
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await;
|
||||||
|
|
||||||
if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? {
|
if let ExpiredTimelocks::None = state.current_epoch(bitcoin_wallet).await? {
|
||||||
let watch_request = state.lock_xmr_watch_request(lock_transfer_proof);
|
let watch_request = state.lock_xmr_watch_request(lock_transfer_proof);
|
||||||
|
|
||||||
select! {
|
select! {
|
||||||
@ -203,30 +169,23 @@ async fn next_state(
|
|||||||
|
|
||||||
tx_lock_status.wait_until_confirmed_with(state.cancel_timelock).await?;
|
tx_lock_status.wait_until_confirmed_with(state.cancel_timelock).await?;
|
||||||
|
|
||||||
BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight))
|
BobState::CancelTimelockExpired(state.cancel())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => {
|
result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => {
|
||||||
result?;
|
result?;
|
||||||
BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight))
|
BobState::CancelTimelockExpired(state.cancel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight))
|
BobState::CancelTimelockExpired(state.cancel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BobState::XmrLocked(state) => {
|
BobState::XmrLocked(state) => {
|
||||||
// In case we send the encrypted signature to Alice, but she doesn't give us a confirmation
|
|
||||||
// We need to check if she still published the Bitcoin redeem transaction
|
|
||||||
// Otherwise we risk staying stuck in "XmrLocked"
|
|
||||||
if let Ok(state5) = state.check_for_tx_redeem(bitcoin_wallet).await {
|
|
||||||
return Ok(BobState::BtcRedeemed(state5));
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await;
|
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await;
|
||||||
|
|
||||||
if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? {
|
if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet).await? {
|
||||||
// Alice has locked Xmr
|
// Alice has locked Xmr
|
||||||
// Bob sends Alice his key
|
// Bob sends Alice his key
|
||||||
|
|
||||||
@ -248,16 +207,9 @@ async fn next_state(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BobState::EncSigSent(state) => {
|
BobState::EncSigSent(state) => {
|
||||||
// We need to make sure that Alice did not publish the redeem transaction while we were offline
|
|
||||||
// Even if the cancel timelock expired, if Alice published the redeem transaction while we were away we cannot miss it
|
|
||||||
// If we do we cannot refund and will never be able to leave the "CancelTimelockExpired" state
|
|
||||||
if let Ok(state5) = state.check_for_tx_redeem(bitcoin_wallet).await {
|
|
||||||
return Ok(BobState::BtcRedeemed(state5));
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await;
|
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await;
|
||||||
|
|
||||||
if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? {
|
if let ExpiredTimelocks::None = state.expired_timelock(bitcoin_wallet).await? {
|
||||||
select! {
|
select! {
|
||||||
state5 = state.watch_for_redeem_btc(bitcoin_wallet) => {
|
state5 = state.watch_for_redeem_btc(bitcoin_wallet) => {
|
||||||
BobState::BtcRedeemed(state5?)
|
BobState::BtcRedeemed(state5?)
|
||||||
@ -272,9 +224,36 @@ async fn next_state(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BobState::BtcRedeemed(state) => {
|
BobState::BtcRedeemed(state) => {
|
||||||
state
|
let (spend_key, view_key) = state.xmr_keys();
|
||||||
.redeem_xmr(monero_wallet, swap_id.to_string(), monero_receive_address)
|
|
||||||
.await?;
|
let wallet_file_name = swap_id.to_string();
|
||||||
|
if let Err(e) = monero_wallet
|
||||||
|
.create_from_and_load(
|
||||||
|
wallet_file_name.clone(),
|
||||||
|
spend_key,
|
||||||
|
view_key,
|
||||||
|
state.monero_wallet_restore_blockheight,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
// In case we failed to refresh/sweep, when resuming the wallet might already
|
||||||
|
// exist! This is a very unlikely scenario, but if we don't take care of it we
|
||||||
|
// might not be able to ever transfer the Monero.
|
||||||
|
tracing::warn!("Failed to generate monero wallet from keys: {:#}", e);
|
||||||
|
tracing::info!(%wallet_file_name,
|
||||||
|
"Falling back to trying to open the the wallet if it already exists",
|
||||||
|
);
|
||||||
|
monero_wallet.open(wallet_file_name).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the generated wallet is synced so we have a proper balance
|
||||||
|
monero_wallet.refresh().await?;
|
||||||
|
// Sweep (transfer all funds) to the given address
|
||||||
|
let tx_hashes = monero_wallet.sweep_all(monero_receive_address).await?;
|
||||||
|
|
||||||
|
for tx_hash in tx_hashes {
|
||||||
|
tracing::info!(%monero_receive_address, txid=%tx_hash.0, "Successfully transferred XMR to wallet");
|
||||||
|
}
|
||||||
|
|
||||||
BobState::XmrRedeemed {
|
BobState::XmrRedeemed {
|
||||||
tx_lock_id: state.tx_lock_id(),
|
tx_lock_id: state.tx_lock_id(),
|
||||||
@ -290,12 +269,12 @@ async fn next_state(
|
|||||||
BobState::BtcCancelled(state) => {
|
BobState::BtcCancelled(state) => {
|
||||||
// Bob has cancelled the swap
|
// Bob has cancelled the swap
|
||||||
match state.expired_timelock(bitcoin_wallet).await? {
|
match state.expired_timelock(bitcoin_wallet).await? {
|
||||||
ExpiredTimelocks::None { .. } => {
|
ExpiredTimelocks::None => {
|
||||||
bail!(
|
bail!(
|
||||||
"Internal error: canceled state reached before cancel timelock was expired"
|
"Internal error: canceled state reached before cancel timelock was expired"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ExpiredTimelocks::Cancel { .. } => {
|
ExpiredTimelocks::Cancel => {
|
||||||
state.publish_refund_btc(bitcoin_wallet).await?;
|
state.publish_refund_btc(bitcoin_wallet).await?;
|
||||||
BobState::BtcRefunded(state)
|
BobState::BtcRefunded(state)
|
||||||
}
|
}
|
||||||
@ -303,58 +282,12 @@ async fn next_state(
|
|||||||
tracing::info!("You have been punished for not refunding in time");
|
tracing::info!("You have been punished for not refunding in time");
|
||||||
BobState::BtcPunished {
|
BobState::BtcPunished {
|
||||||
tx_lock_id: state.tx_lock_id(),
|
tx_lock_id: state.tx_lock_id(),
|
||||||
state,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4),
|
BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4),
|
||||||
BobState::BtcPunished { state, tx_lock_id } => {
|
BobState::BtcPunished { tx_lock_id } => BobState::BtcPunished { tx_lock_id },
|
||||||
tracing::info!("Attempting to cooperatively redeem XMR after being punished");
|
|
||||||
let response = event_loop_handle
|
|
||||||
.request_cooperative_xmr_redeem(swap_id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(Fullfilled { s_a, .. }) => {
|
|
||||||
tracing::info!(
|
|
||||||
"Alice has accepted our request to cooperatively redeem the XMR"
|
|
||||||
);
|
|
||||||
|
|
||||||
let s_a = monero::PrivateKey { scalar: s_a };
|
|
||||||
let state5 = state.attempt_cooperative_redeem(s_a);
|
|
||||||
|
|
||||||
match state5
|
|
||||||
.redeem_xmr(monero_wallet, swap_id.to_string(), monero_receive_address)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
return Ok(BobState::XmrRedeemed { tx_lock_id });
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
return Err(error)
|
|
||||||
.context("Failed to redeem XMR with revealed XMR key");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Rejected { reason, .. }) => {
|
|
||||||
tracing::error!(
|
|
||||||
%reason,
|
|
||||||
"Alice rejected our request for cooperative XMR redeem"
|
|
||||||
);
|
|
||||||
return Err(reason)
|
|
||||||
.context("Alice rejected our request for cooperative XMR redeem");
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(
|
|
||||||
%error,
|
|
||||||
"Failed to request cooperative XMR redeem from Alice"
|
|
||||||
);
|
|
||||||
return Err(error)
|
|
||||||
.context("Failed to request cooperative XMR redeem from Alice");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
BobState::SafelyAborted => BobState::SafelyAborted,
|
BobState::SafelyAborted => BobState::SafelyAborted,
|
||||||
BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
|
BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
|
||||||
})
|
})
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
use crate::api::Context;
|
|
||||||
use jsonrpsee::server::{RpcModule, ServerBuilder, ServerHandle};
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub mod methods;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Could not parse key value from params")]
|
|
||||||
ParseError,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_server(
|
|
||||||
server_address: SocketAddr,
|
|
||||||
context: Arc<Context>,
|
|
||||||
) -> anyhow::Result<(SocketAddr, ServerHandle)> {
|
|
||||||
let server = ServerBuilder::default().build(server_address).await?;
|
|
||||||
let mut modules = RpcModule::new(());
|
|
||||||
{
|
|
||||||
modules
|
|
||||||
.merge(methods::register_modules(Arc::clone(&context))?)
|
|
||||||
.expect("Could not register RPC modules")
|
|
||||||
}
|
|
||||||
|
|
||||||
let addr = server.local_addr()?;
|
|
||||||
let server_handle = server.start(modules)?;
|
|
||||||
|
|
||||||
Ok((addr, server_handle))
|
|
||||||
}
|
|
@ -1,269 +0,0 @@
|
|||||||
use crate::api::request::{Method, Request};
|
|
||||||
use crate::api::Context;
|
|
||||||
use crate::bitcoin::bitcoin_address;
|
|
||||||
use crate::monero::monero_address;
|
|
||||||
use crate::{bitcoin, monero};
|
|
||||||
use anyhow::Result;
|
|
||||||
use jsonrpsee::server::RpcModule;
|
|
||||||
use jsonrpsee::types::Params;
|
|
||||||
use libp2p::core::Multiaddr;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub fn register_modules(context: Arc<Context>) -> Result<RpcModule<Arc<Context>>> {
|
|
||||||
let mut module = RpcModule::new(context);
|
|
||||||
|
|
||||||
module.register_async_method("suspend_current_swap", |params, context| async move {
|
|
||||||
execute_request(params, Method::SuspendCurrentSwap, &context).await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("get_swap_info", |params_raw, context| async move {
|
|
||||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
|
||||||
|
|
||||||
let swap_id = params
|
|
||||||
.get("swap_id")
|
|
||||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?;
|
|
||||||
|
|
||||||
let swap_id = as_uuid(swap_id)
|
|
||||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
|
|
||||||
|
|
||||||
execute_request(params_raw, Method::GetSwapInfo { swap_id }, &context).await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("get_bitcoin_balance", |params_raw, context| async move {
|
|
||||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
|
||||||
|
|
||||||
let force_refresh = params
|
|
||||||
.get("force_refresh")
|
|
||||||
.ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("Does not contain force_refresh".to_string())
|
|
||||||
})?
|
|
||||||
.as_bool()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("force_refesh is not a boolean".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
execute_request(params_raw, Method::Balance { force_refresh }, &context).await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("get_history", |params_raw, context| async move {
|
|
||||||
execute_request(params_raw, Method::History, &context).await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("get_logs", |params_raw, context| async move {
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
struct Params {
|
|
||||||
swap_id: Option<Uuid>,
|
|
||||||
logs_dir: Option<PathBuf>,
|
|
||||||
redact: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
let params: Params = params_raw.parse()?;
|
|
||||||
|
|
||||||
execute_request(
|
|
||||||
params_raw,
|
|
||||||
Method::Logs {
|
|
||||||
swap_id: params.swap_id,
|
|
||||||
logs_dir: params.logs_dir,
|
|
||||||
redact: params.redact,
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("get_raw_states", |params, context| async move {
|
|
||||||
execute_request(params, Method::GetRawStates, &context).await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("resume_swap", |params_raw, context| async move {
|
|
||||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
|
||||||
|
|
||||||
let swap_id = params
|
|
||||||
.get("swap_id")
|
|
||||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?;
|
|
||||||
|
|
||||||
let swap_id = as_uuid(swap_id)
|
|
||||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
|
|
||||||
|
|
||||||
execute_request(params_raw, Method::Resume { swap_id }, &context).await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("cancel_refund_swap", |params_raw, context| async move {
|
|
||||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
|
||||||
|
|
||||||
let swap_id = params
|
|
||||||
.get("swap_id")
|
|
||||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?;
|
|
||||||
|
|
||||||
let swap_id = as_uuid(swap_id)
|
|
||||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
|
|
||||||
|
|
||||||
execute_request(params_raw, Method::CancelAndRefund { swap_id }, &context).await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method(
|
|
||||||
"get_monero_recovery_info",
|
|
||||||
|params_raw, context| async move {
|
|
||||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
|
||||||
|
|
||||||
let swap_id = params.get("swap_id").ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let swap_id = as_uuid(swap_id).ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
execute_request(params_raw, Method::MoneroRecovery { swap_id }, &context).await
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
module.register_async_method("withdraw_btc", |params_raw, context| async move {
|
|
||||||
let params: HashMap<String, String> = params_raw.parse()?;
|
|
||||||
|
|
||||||
let amount = if let Some(amount_str) = params.get("amount") {
|
|
||||||
Some(
|
|
||||||
::bitcoin::Amount::from_str_in(amount_str, ::bitcoin::Denomination::Bitcoin)
|
|
||||||
.map_err(|_| {
|
|
||||||
jsonrpsee_core::Error::Custom("Unable to parse amount".to_string())
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let withdraw_address =
|
|
||||||
bitcoin::Address::from_str(params.get("address").ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("Does not contain address".to_string())
|
|
||||||
})?)
|
|
||||||
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?;
|
|
||||||
let withdraw_address =
|
|
||||||
bitcoin_address::validate(withdraw_address, context.config.env_config.bitcoin_network)?;
|
|
||||||
|
|
||||||
execute_request(
|
|
||||||
params_raw,
|
|
||||||
Method::WithdrawBtc {
|
|
||||||
amount,
|
|
||||||
address: withdraw_address,
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("buy_xmr", |params_raw, context| async move {
|
|
||||||
let params: HashMap<String, String> = params_raw.parse()?;
|
|
||||||
|
|
||||||
let bitcoin_change_address = params
|
|
||||||
.get("bitcoin_change_address")
|
|
||||||
.map(|addr_str| {
|
|
||||||
bitcoin::Address::from_str(addr_str)
|
|
||||||
.map_err(|err| {
|
|
||||||
jsonrpsee_core::Error::Custom(format!(
|
|
||||||
"Could not parse bitcoin address: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.and_then(|address| {
|
|
||||||
bitcoin_address::validate(
|
|
||||||
address,
|
|
||||||
context.config.env_config.bitcoin_network,
|
|
||||||
)
|
|
||||||
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
|
|
||||||
let monero_receive_address =
|
|
||||||
monero::Address::from_str(params.get("monero_receive_address").ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("Does not contain monero_receiveaddress".to_string())
|
|
||||||
})?)
|
|
||||||
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?;
|
|
||||||
|
|
||||||
let monero_receive_address = monero_address::validate(
|
|
||||||
monero_receive_address,
|
|
||||||
context.config.env_config.monero_network,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let seller =
|
|
||||||
Multiaddr::from_str(params.get("seller").ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("Does not contain seller".to_string())
|
|
||||||
})?)
|
|
||||||
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?;
|
|
||||||
|
|
||||||
execute_request(
|
|
||||||
params_raw,
|
|
||||||
Method::BuyXmr {
|
|
||||||
bitcoin_change_address,
|
|
||||||
monero_receive_address,
|
|
||||||
seller,
|
|
||||||
swap_id: Uuid::new_v4(),
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("list_sellers", |params_raw, context| async move {
|
|
||||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
|
||||||
|
|
||||||
let rendezvous_point = params.get("rendezvous_point").ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("Does not contain rendezvous_point".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let rendezvous_point = rendezvous_point
|
|
||||||
.as_str()
|
|
||||||
.and_then(|addr_str| Multiaddr::from_str(addr_str).ok())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
jsonrpsee_core::Error::Custom("Could not parse valid multiaddr".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
execute_request(
|
|
||||||
params_raw,
|
|
||||||
Method::ListSellers {
|
|
||||||
rendezvous_point: rendezvous_point.clone(),
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
module.register_async_method("get_current_swap", |params, context| async move {
|
|
||||||
execute_request(params, Method::GetCurrentSwap, &context).await
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(module)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_uuid(json_value: &serde_json::Value) -> Option<Uuid> {
|
|
||||||
if let Some(uuid_str) = json_value.as_str() {
|
|
||||||
Uuid::parse_str(uuid_str).ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_request(
|
|
||||||
params: Params<'static>,
|
|
||||||
cmd: Method,
|
|
||||||
context: &Arc<Context>,
|
|
||||||
) -> Result<serde_json::Value, jsonrpsee_core::Error> {
|
|
||||||
// If we fail to parse the params as a String HashMap, it's most likely because its an empty object
|
|
||||||
// In that case, we want to make sure not to fail the request, so we set the log_reference_id to None
|
|
||||||
// and swallow the error
|
|
||||||
let reference_id = params
|
|
||||||
.parse::<HashMap<String, serde_json::Value>>()
|
|
||||||
.ok()
|
|
||||||
.and_then(|params_parsed| params_parsed.get("log_reference_id").cloned());
|
|
||||||
|
|
||||||
let request = Request::with_id(cmd, reference_id.map(|log_ref| log_ref.to_string()));
|
|
||||||
request
|
|
||||||
.call(Arc::clone(context))
|
|
||||||
.await
|
|
||||||
.map_err(|err| jsonrpsee_core::Error::Custom(format!("{:#}", err)))
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
use crate::fs::ensure_directory_exists;
|
use crate::fs::ensure_directory_exists;
|
||||||
|
use ::bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
||||||
|
use ::bitcoin::secp256k1::{self, SecretKey};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
|
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
|
||||||
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
||||||
use bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
|
||||||
use bitcoin::secp256k1::{self, SecretKey};
|
|
||||||
use libp2p::identity;
|
use libp2p::identity;
|
||||||
use pem::{encode, Pem};
|
use pem::{encode, Pem};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
@ -16,7 +16,7 @@ use torut::onion::TorSecretKeyV3;
|
|||||||
|
|
||||||
pub const SEED_LENGTH: usize = 32;
|
pub const SEED_LENGTH: usize = 32;
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Eq, PartialEq)]
|
||||||
pub struct Seed([u8; SEED_LENGTH]);
|
pub struct Seed([u8; SEED_LENGTH]);
|
||||||
|
|
||||||
impl Seed {
|
impl Seed {
|
||||||
@ -61,7 +61,7 @@ impl Seed {
|
|||||||
let file_path = Path::new(&file_path_buf);
|
let file_path = Path::new(&file_path_buf);
|
||||||
|
|
||||||
if file_path.exists() {
|
if file_path.exists() {
|
||||||
return Self::from_file(file_path);
|
return Self::from_file(&file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("No seed file found, creating at {}", file_path.display());
|
tracing::debug!("No seed file found, creating at {}", file_path.display());
|
||||||
@ -106,12 +106,11 @@ impl Seed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn from_pem(pem: pem::Pem) -> Result<Self, Error> {
|
fn from_pem(pem: pem::Pem) -> Result<Self, Error> {
|
||||||
let contents = pem.contents();
|
if pem.contents.len() != SEED_LENGTH {
|
||||||
if contents.len() != SEED_LENGTH {
|
Err(Error::IncorrectLength(pem.contents.len()))
|
||||||
Err(Error::IncorrectLength(contents.len()))
|
|
||||||
} else {
|
} else {
|
||||||
let mut array = [0; SEED_LENGTH];
|
let mut array = [0; SEED_LENGTH];
|
||||||
for (i, b) in contents.iter().enumerate() {
|
for (i, b) in pem.contents.iter().enumerate() {
|
||||||
array[i] = *b;
|
array[i] = *b;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +122,10 @@ impl Seed {
|
|||||||
ensure_directory_exists(&seed_file)?;
|
ensure_directory_exists(&seed_file)?;
|
||||||
|
|
||||||
let data = self.bytes();
|
let data = self.bytes();
|
||||||
let pem = Pem::new("SEED", data);
|
let pem = Pem {
|
||||||
|
tag: String::from("SEED"),
|
||||||
|
contents: data.to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
let pem_string = encode(&pem);
|
let pem_string = encode(&pem);
|
||||||
|
|
||||||
@ -185,9 +187,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn seed_from_pem_works() {
|
fn seed_from_pem_works() {
|
||||||
use base64::engine::general_purpose;
|
|
||||||
use base64::Engine;
|
|
||||||
|
|
||||||
let payload: &str = "syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=";
|
let payload: &str = "syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=";
|
||||||
|
|
||||||
// 32 bytes base64 encoded.
|
// 32 bytes base64 encoded.
|
||||||
@ -196,7 +195,7 @@ syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM=
|
|||||||
-----END SEED-----
|
-----END SEED-----
|
||||||
";
|
";
|
||||||
|
|
||||||
let want = general_purpose::STANDARD.decode(payload).unwrap();
|
let want = base64::decode(payload).unwrap();
|
||||||
let pem = pem::parse(pem_string).unwrap();
|
let pem = pem::parse(pem_string).unwrap();
|
||||||
let got = Seed::from_pem(pem).unwrap();
|
let got = Seed::from_pem(pem).unwrap();
|
||||||
|
|
||||||
@ -222,20 +221,19 @@ VnZUNFZ4dlY=
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
fn seed_from_pem_fails_for_long_seed() {
|
fn seed_from_pem_fails_for_long_seed() {
|
||||||
let long = "-----BEGIN SEED-----
|
let long = "-----BEGIN SEED-----
|
||||||
MIIBPQIBAAJBAOsfi5AGYhdRs/x6q5H7kScxA0Kzzqe6WI6gf6+tc6IvKQJo5rQc
|
mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE=
|
||||||
dWWSQ0nRGt2hOPDO+35NKhQEjBQxPh/v7n0CAwEAAQJBAOGaBAyuw0ICyENy5NsO
|
mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE=
|
||||||
-----END SEED-----
|
-----END SEED-----
|
||||||
";
|
";
|
||||||
let pem = pem::parse(long).unwrap();
|
let pem = pem::parse(long).unwrap();
|
||||||
assert_eq!(pem.contents().len(), 96);
|
|
||||||
|
|
||||||
match Seed::from_pem(pem) {
|
match Seed::from_pem(pem) {
|
||||||
Ok(_) => panic!("should fail for long payload"),
|
Ok(_) => panic!("should fail for long payload"),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
match e {
|
match e {
|
||||||
Error::IncorrectLength(len) => assert_eq!(len, 96), // pass
|
Error::IncorrectLength(_) => {} // pass
|
||||||
_ => panic!("should fail with IncorrectLength error"),
|
_ => panic!("should fail with IncorrectLength error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ async fn given_alice_and_bob_manually_refund_after_funds_locked_both_refund() {
|
|||||||
|
|
||||||
// Bob manually cancels
|
// Bob manually cancels
|
||||||
bob_join_handle.abort();
|
bob_join_handle.abort();
|
||||||
let (_, state) = cli::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db).await?;
|
let (_, _, state) = cli::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db).await?;
|
||||||
assert!(matches!(state, BobState::BtcCancelled { .. }));
|
assert!(matches!(state, BobState::BtcCancelled { .. }));
|
||||||
|
|
||||||
let (bob_swap, bob_join_handle) = ctx
|
let (bob_swap, bob_join_handle) = ctx
|
||||||
|
@ -42,10 +42,10 @@ async fn given_alice_and_bob_manually_cancel_when_timelock_not_expired_errors()
|
|||||||
let error = cli::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db)
|
let error = cli::cancel(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
assert!(error
|
parse_rpc_error_code(&error).unwrap(),
|
||||||
.to_string()
|
i64::from(RpcErrorCode::RpcVerifyRejected)
|
||||||
.contains("Cannot cancel swap because the cancel timelock has not expired yet"));
|
);
|
||||||
|
|
||||||
ctx.restart_alice().await;
|
ctx.restart_alice().await;
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
let alice_swap = ctx.alice_next_swap().await;
|
||||||
@ -72,9 +72,10 @@ async fn given_alice_and_bob_manually_cancel_when_timelock_not_expired_errors()
|
|||||||
let error = cli::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db)
|
let error = cli::refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db)
|
||||||
.await
|
.await
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(error
|
assert_eq!(
|
||||||
.to_string()
|
parse_rpc_error_code(&error).unwrap(),
|
||||||
.contains("Cannot refund swap because the cancel timelock has not expired yet"));
|
i64::from(RpcErrorCode::RpcVerifyError)
|
||||||
|
);
|
||||||
|
|
||||||
let (bob_swap, _) = ctx
|
let (bob_swap, _) = ctx
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
.stop_and_resume_bob_from_db(bob_join_handle, swap_id)
|
||||||
|
@ -11,7 +11,7 @@ use swap::protocol::{alice, bob};
|
|||||||
|
|
||||||
/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice
|
/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice
|
||||||
/// the encsig and fail to refund or redeem. Alice punishes using the cancel and
|
/// the encsig and fail to refund or redeem. Alice punishes using the cancel and
|
||||||
/// punish command. Bob then cooperates with Alice and redeems XMR with her key.
|
/// punish command.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn alice_manually_punishes_after_bob_dead() {
|
async fn alice_manually_punishes_after_bob_dead() {
|
||||||
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
||||||
@ -78,7 +78,9 @@ async fn alice_manually_punishes_after_bob_dead() {
|
|||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||||
|
|
||||||
let bob_state = bob::run(bob_swap).await?;
|
let bob_state = bob::run(bob_swap).await?;
|
||||||
ctx.assert_bob_redeemed(bob_state).await;
|
|
||||||
|
ctx.assert_bob_punished(bob_state).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -1,98 +0,0 @@
|
|||||||
pub mod harness;
|
|
||||||
|
|
||||||
use harness::alice_run_until::is_xmr_lock_transaction_sent;
|
|
||||||
use harness::bob_run_until::is_btc_locked;
|
|
||||||
use harness::FastPunishConfig;
|
|
||||||
use swap::asb;
|
|
||||||
use swap::asb::FixedRate;
|
|
||||||
use swap::cli;
|
|
||||||
use swap::protocol::alice::AliceState;
|
|
||||||
use swap::protocol::bob::BobState;
|
|
||||||
use swap::protocol::{alice, bob};
|
|
||||||
/// Bob locks Btc and Alice locks Xmr. Bob does not act; he fails to send Alice
|
|
||||||
/// the encsig and fail to refund or redeem. Alice punishes using the cancel and
|
|
||||||
/// punish command. Then Bob tries to refund.
|
|
||||||
#[tokio::test]
|
|
||||||
async fn alice_manually_punishes_after_bob_dead_and_bob_cancels() {
|
|
||||||
harness::setup_test(FastPunishConfig, |mut ctx| async move {
|
|
||||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
|
||||||
let bob_swap_id = bob_swap.id;
|
|
||||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
|
||||||
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
|
||||||
let alice_bitcoin_wallet = alice_swap.bitcoin_wallet.clone();
|
|
||||||
|
|
||||||
let alice_swap = tokio::spawn(alice::run_until(
|
|
||||||
alice_swap,
|
|
||||||
is_xmr_lock_transaction_sent,
|
|
||||||
FixedRate::default(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let bob_state = bob_swap.await??;
|
|
||||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
|
||||||
|
|
||||||
let alice_state = alice_swap.await??;
|
|
||||||
|
|
||||||
// Ensure cancel timelock is expired
|
|
||||||
if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state {
|
|
||||||
alice_bitcoin_wallet
|
|
||||||
.subscribe_to(state3.tx_lock)
|
|
||||||
.await
|
|
||||||
.wait_until_confirmed_with(state3.cancel_timelock)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
panic!("Alice in unexpected state {}", alice_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// manual cancel (required to be able to punish)
|
|
||||||
|
|
||||||
ctx.restart_alice().await;
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
|
||||||
let (_, alice_state) =
|
|
||||||
asb::cancel(alice_swap.swap_id, alice_swap.bitcoin_wallet, alice_swap.db).await?;
|
|
||||||
|
|
||||||
// Ensure punish timelock is expired
|
|
||||||
if let AliceState::BtcCancelled { state3, .. } = alice_state {
|
|
||||||
alice_bitcoin_wallet
|
|
||||||
.subscribe_to(state3.tx_cancel())
|
|
||||||
.await
|
|
||||||
.wait_until_confirmed_with(state3.punish_timelock)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
panic!("Alice in unexpected state {}", alice_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// manual punish
|
|
||||||
|
|
||||||
ctx.restart_alice().await;
|
|
||||||
let alice_swap = ctx.alice_next_swap().await;
|
|
||||||
let (_, alice_state) =
|
|
||||||
asb::punish(alice_swap.swap_id, alice_swap.bitcoin_wallet, alice_swap.db).await?;
|
|
||||||
ctx.assert_alice_punished(alice_state).await;
|
|
||||||
// Bob is in wrong state.
|
|
||||||
let (bob_swap, bob_join_handle) = ctx
|
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
|
||||||
bob_join_handle.abort();
|
|
||||||
|
|
||||||
let (_, state) = cli::cancel(bob_swap_id, bob_swap.bitcoin_wallet, bob_swap.db).await?;
|
|
||||||
// Bob should be in BtcCancelled state now.
|
|
||||||
assert!(matches!(state, BobState::BtcCancelled { .. }));
|
|
||||||
|
|
||||||
let (bob_swap, _) = ctx
|
|
||||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcCancelled { .. }));
|
|
||||||
// Alice punished Bob, so he should be in the BtcPunished state.
|
|
||||||
let error = cli::refund(bob_swap_id, bob_swap.bitcoin_wallet, bob_swap.db)
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
error.to_string(),
|
|
||||||
"Cannot refund swap because we have already been punished"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user