diff --git a/.docker/test-nodes.yml b/.docker/test-nodes.yml new file mode 100644 index 0000000000..e5fc3e7d9b --- /dev/null +++ b/.docker/test-nodes.yml @@ -0,0 +1,222 @@ +# Docker Compose file for KDF test nodes +# +# Usage: +# Start all nodes: docker compose -f .docker/test-nodes.yml up -d +# Start specific: docker compose -f .docker/test-nodes.yml --profile utxo up -d +# Stop all: docker compose -f .docker/test-nodes.yml down -v +# View logs: docker compose -f .docker/test-nodes.yml logs -f +# +# Profiles: +# - utxo: MYCOIN, MYCOIN1 (basic UTXO testing) +# - slp: FORSLP (BCH/SLP token testing) +# - qrc20: QTUM (Qtum/QRC20 testing) +# - evm: GETH (Ethereum/ERC20 testing) +# - zombie: ZOMBIE (Zcash-based testing) +# - cosmos: NUCLEUS, ATOM, IBC-RELAYER (Tendermint/IBC testing) +# - sia: SIA (Sia testing) +# +# Node groups are controlled via compose profiles (see above). +# +# For CI/local reuse: +# KDF_DOCKER_COMPOSE_ENV=1 - Test harness attaches to running containers + +name: kdf-test-nodes + +services: + # ============================================================================ + # UTXO Test Nodes + # ============================================================================ + + mycoin: + image: docker.io/gleec/testblockchain:multiarch + profiles: ["utxo", "all"] + container_name: kdf-mycoin + ports: + - "8000:8000" + environment: + - CHAIN=MYCOIN + - CLIENTS=2 + - COIN=Komodo + - COIN_RPC_PORT=8000 + - DAEMON_URL=http://test:test@127.0.0.1:7000 + - TEST_ADDY=R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF + - TEST_WIF=UqqW7f766rADem9heD8vSBvvrdfJb3zg5r8du9rJxPtccjWf7RG9 + - TEST_PUBKEY=021607076d7a2cb148d542fb9644c04ffc22d2cca752f80755a0402a24c567b17a + volumes: + - ${ZCASH_PARAMS_PATH:-~/.zcash-params}:/root/.zcash-params:ro + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8000"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 10s + + mycoin1: + image: docker.io/gleec/testblockchain:multiarch + profiles: ["utxo", "all"] + container_name: kdf-mycoin1 + ports: + - "8001:8001" + environment: + - CHAIN=MYCOIN1 + - CLIENTS=2 + - COIN=Komodo + - COIN_RPC_PORT=8001 + - DAEMON_URL=http://test:test@127.0.0.1:7000 + - TEST_ADDY=R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF + - TEST_WIF=UqqW7f766rADem9heD8vSBvvrdfJb3zg5r8du9rJxPtccjWf7RG9 + - TEST_PUBKEY=021607076d7a2cb148d542fb9644c04ffc22d2cca752f80755a0402a24c567b17a + volumes: + - ${ZCASH_PARAMS_PATH:-~/.zcash-params}:/root/.zcash-params:ro + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8001"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 10s + + # ============================================================================ + # BCH/SLP Test Node + # ============================================================================ + + forslp: + image: docker.io/gleec/testblockchain:multiarch + profiles: ["slp", "all"] + container_name: kdf-forslp + ports: + - "10000:10000" + environment: + - CHAIN=FORSLP + - CLIENTS=2 + - COIN=Komodo + - COIN_RPC_PORT=10000 + - DAEMON_URL=http://test:test@127.0.0.1:7000 + - TEST_ADDY=R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF + - TEST_WIF=UqqW7f766rADem9heD8vSBvvrdfJb3zg5r8du9rJxPtccjWf7RG9 + - TEST_PUBKEY=021607076d7a2cb148d542fb9644c04ffc22d2cca752f80755a0402a24c567b17a + volumes: + - ${ZCASH_PARAMS_PATH:-~/.zcash-params}:/root/.zcash-params:ro + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:10000"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 10s + + # ============================================================================ + # Qtum/QRC20 Test Node + # ============================================================================ + + qtum: + image: docker.io/gleec/qtumregtest:latest + profiles: ["qrc20", "all"] + container_name: kdf-qtum + ports: + - "9000:9000" + environment: + - CLIENTS=2 + - COIN_RPC_PORT=9000 + - ADDRESS_LABEL=MM2_ADDRESS_LABEL + - FILL_MEMPOOL=true + healthcheck: + test: ["CMD-SHELL", "qtum-cli -rpcport=9000 getblockchaininfo || exit 1"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 15s + + # ============================================================================ + # Ethereum/Geth Dev Node + # ============================================================================ + + geth: + image: docker.io/ethereum/client-go:stable + profiles: ["evm", "all"] + container_name: kdf-geth + ports: + - "8545:8545" + command: ["--dev", "--http", "--http.addr=0.0.0.0", "--http.api=eth,net,web3,personal,debug", "--http.corsdomain=*"] + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8545 --post-data='{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"params\":[],\"id\":1}' --header='Content-Type: application/json' || exit 1"] + interval: 3s + timeout: 3s + retries: 30 + start_period: 5s + + # ============================================================================ + # Zcash-based (Zombie) Test Node + # ============================================================================ + + zombie: + image: docker.io/gleec/zombietestrunner:multiarch + profiles: ["zombie", "all"] + container_name: kdf-zombie + ports: + - "7090:7090" + environment: + - COIN_RPC_PORT=7090 + volumes: + - ${ZCASH_PARAMS_PATH:-~/.zcash-params}:/root/.zcash-params:ro + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:7090"] + interval: 5s + timeout: 3s + retries: 60 + start_period: 30s + + # ============================================================================ + # Cosmos/Tendermint Test Nodes (use host network for IBC) + # ============================================================================ + + nucleus: + image: docker.io/gleec/nucleusd:latest + profiles: ["cosmos", "all"] + container_name: kdf-nucleus + network_mode: host + volumes: + - ${KDF_CONTAINER_RUNTIME_DIR:-./container-runtime}/nucleus-testnet-data:/root/.nucleus + + atom: + image: docker.io/gleec/gaiad:kdf-ci + profiles: ["cosmos", "all"] + container_name: kdf-atom + network_mode: host + volumes: + - ${KDF_CONTAINER_RUNTIME_DIR:-./container-runtime}/atom-testnet-data:/root/.gaia + + ibc-relayer: + image: docker.io/gleec/ibc-relayer:kdf-ci + profiles: ["cosmos", "all"] + container_name: kdf-ibc-relayer + network_mode: host + volumes: + - ${KDF_CONTAINER_RUNTIME_DIR:-./container-runtime}/ibc-relayer-data:/root/.relayer + depends_on: + nucleus: + condition: service_started + atom: + condition: service_started + + # ============================================================================ + # Sia Test Node + # ============================================================================ + + sia: + image: ghcr.io/siafoundation/walletd:latest + profiles: ["sia", "all"] + container_name: kdf-sia + ports: + - "9980:9980" + environment: + - WALLETD_CONFIG_FILE=/config/walletd.yml + command: ["-network=/config/ci_network.json", "-debug"] + volumes: + - ${KDF_CONTAINER_RUNTIME_DIR:-./container-runtime}/sia-config:/config:ro + # No persistent volume for /data - use ephemeral storage like testcontainers + # to ensure fresh state each run and avoid address indexer lag issues + healthcheck: + test: ["CMD-SHELL", "wget -qO- --header='Authorization: Basic cGFzc3dvcmQ=' http://localhost:9980/api/state || exit 1"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 10s diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af01024734..15df0c4757 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,42 +12,37 @@ env: FROM_SHARED_RUNNER: true jobs: - linux-x86-64-unit: + unit: + name: Unit / ${{ matrix.os-name }} timeout-minutes: 90 - runs-on: ubuntu-latest - env: - BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_LINUX }} - BOB_USERPASS: ${{ secrets.BOB_USERPASS_LINUX }} - ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_LINUX }} - ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_LINUX }} - TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} - steps: - - uses: actions/checkout@v3 - - name: Install toolchain - run: | - rustup toolchain install stable --no-self-update --profile=minimal - rustup default stable - - - name: Install build deps - uses: ./.github/actions/deps-install - with: - deps: ('protoc') - - - name: Build cache - uses: ./.github/actions/build-cache - - - name: Test - run: | - cargo test --bins --lib --no-fail-fast - - mac-x86-64-unit: - timeout-minutes: 90 - runs-on: macos-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + os-name: Linux + bob-passphrase-secret: BOB_PASSPHRASE_LINUX + bob-userpass-secret: BOB_USERPASS_LINUX + alice-passphrase-secret: ALICE_PASSPHRASE_LINUX + alice-userpass-secret: ALICE_USERPASS_LINUX + - os: macos-latest + os-name: macOS + bob-passphrase-secret: BOB_PASSPHRASE_MACOS + bob-userpass-secret: BOB_USERPASS_MACOS + alice-passphrase-secret: ALICE_PASSPHRASE_MACOS + alice-userpass-secret: ALICE_USERPASS_MACOS + - os: windows-latest + os-name: Windows + bob-passphrase-secret: BOB_PASSPHRASE_WIN + bob-userpass-secret: BOB_USERPASS_WIN + alice-passphrase-secret: ALICE_PASSPHRASE_WIN + alice-userpass-secret: ALICE_USERPASS_WIN env: - BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_MACOS }} - BOB_USERPASS: ${{ secrets.BOB_USERPASS_MACOS }} - ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_MACOS }} - ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_MACOS }} + BOB_PASSPHRASE: ${{ secrets[matrix.bob-passphrase-secret] }} + BOB_USERPASS: ${{ secrets[matrix.bob-userpass-secret] }} + ALICE_PASSPHRASE: ${{ secrets[matrix.alice-passphrase-secret] }} + ALICE_USERPASS: ${{ secrets[matrix.alice-userpass-secret] }} TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} steps: - uses: actions/checkout@v3 @@ -68,42 +63,37 @@ jobs: run: | cargo test --bins --lib --no-fail-fast - win-x86-64-unit: + integration: + name: Integration / ${{ matrix.os-name }} timeout-minutes: 90 - runs-on: windows-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + os-name: Linux + bob-passphrase-secret: BOB_PASSPHRASE_LINUX + bob-userpass-secret: BOB_USERPASS_LINUX + alice-passphrase-secret: ALICE_PASSPHRASE_LINUX + alice-userpass-secret: ALICE_USERPASS_LINUX + - os: macos-latest + os-name: macOS + bob-passphrase-secret: BOB_PASSPHRASE_MACOS + bob-userpass-secret: BOB_USERPASS_MACOS + alice-passphrase-secret: ALICE_PASSPHRASE_MACOS + alice-userpass-secret: ALICE_USERPASS_MACOS + - os: windows-latest + os-name: Windows + bob-passphrase-secret: BOB_PASSPHRASE_WIN + bob-userpass-secret: BOB_USERPASS_WIN + alice-passphrase-secret: ALICE_PASSPHRASE_WIN + alice-userpass-secret: ALICE_USERPASS_WIN env: - BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_WIN }} - BOB_USERPASS: ${{ secrets.BOB_USERPASS_WIN }} - ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_WIN }} - ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_WIN }} - TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} - steps: - - uses: actions/checkout@v3 - - name: Install toolchain - run: | - rustup toolchain install stable --no-self-update --profile=minimal - rustup default stable - - - name: Install build deps - uses: ./.github/actions/deps-install - with: - deps: ('protoc') - - - name: Build cache - uses: ./.github/actions/build-cache - - - name: Test - run: | - cargo test --bins --lib --no-fail-fast - - linux-x86-64-kdf-integration: - timeout-minutes: 90 - runs-on: ubuntu-latest - env: - BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_LINUX }} - BOB_USERPASS: ${{ secrets.BOB_USERPASS_LINUX }} - ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_LINUX }} - ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_LINUX }} + BOB_PASSPHRASE: ${{ secrets[matrix.bob-passphrase-secret] }} + BOB_USERPASS: ${{ secrets[matrix.bob-userpass-secret] }} + ALICE_PASSPHRASE: ${{ secrets[matrix.alice-passphrase-secret] }} + ALICE_USERPASS: ${{ secrets[matrix.alice-userpass-secret] }} TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} steps: - uses: actions/checkout@v3 @@ -117,85 +107,143 @@ jobs: with: deps: ('protoc') - - name: Build cache - uses: ./.github/actions/build-cache - - - name: Test - run: | - wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/635112d590618165a152dfa0f31e95a9be39a8f6/zcutil/fetch-params-alt.sh | bash - cargo test --test 'mm2_tests_main' --no-fail-fast - - mac-x86-64-kdf-integration: - timeout-minutes: 90 - runs-on: macos-latest - env: - BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_MACOS }} - BOB_USERPASS: ${{ secrets.BOB_USERPASS_MACOS }} - ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_MACOS }} - ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_MACOS }} - TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} - steps: - - uses: actions/checkout@v3 - - name: Install toolchain - run: | - rustup toolchain install stable --no-self-update --profile=minimal - rustup default stable - - - name: Install build deps - uses: ./.github/actions/deps-install - with: - deps: ('protoc') - - - name: Set loopback address + - name: Set loopback address (macOS) + if: matrix.os == 'macos-latest' run: ./scripts/ci/lo0_config.sh - name: Build cache uses: ./.github/actions/build-cache - - name: Test - run: | - wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/635112d590618165a152dfa0f31e95a9be39a8f6/zcutil/fetch-params-alt.sh | bash - cargo test --test 'mm2_tests_main' --no-fail-fast - - win-x86-64-kdf-integration: - timeout-minutes: 90 - runs-on: windows-latest - env: - BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_WIN }} - BOB_USERPASS: ${{ secrets.BOB_USERPASS_WIN }} - ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_WIN }} - ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_WIN }} - TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} - steps: - - uses: actions/checkout@v3 - - name: Install toolchain - run: | - rustup toolchain install stable --no-self-update --profile=minimal - rustup default stable - - - name: Install build deps - uses: ./.github/actions/deps-install - with: - deps: ('protoc') - - - name: Build cache - uses: ./.github/actions/build-cache - - - name: Download wget64 + - name: Download wget64 (Windows) + if: matrix.os == 'windows-latest' uses: ./.github/actions/download-and-verify with: url: "https://github.com/KomodoPlatform/komodo/raw/d456be35acd1f8584e1e4f971aea27bd0644d5c5/zcutil/wget64.exe" output_file: "/wget64.exe" checksum: "d80719431dc22b0e4a070f61fab982b113a4ed9a6d4cf25e64b5be390dcadb94" + - name: Fetch zcash params (Linux/macOS) + if: matrix.os != 'windows-latest' + run: wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/635112d590618165a152dfa0f31e95a9be39a8f6/zcutil/fetch-params-alt.sh | bash + + - name: Fetch zcash params (Windows) + if: matrix.os == 'windows-latest' + run: Invoke-WebRequest -Uri https://raw.githubusercontent.com/KomodoPlatform/komodo/635112d590618165a152dfa0f31e95a9be39a8f6/zcutil/fetch-params-alt.bat -OutFile \cmd.bat && \cmd.bat + - name: Test - run: | - Invoke-WebRequest -Uri https://raw.githubusercontent.com/KomodoPlatform/komodo/635112d590618165a152dfa0f31e95a9be39a8f6/zcutil/fetch-params-alt.bat -OutFile \cmd.bat && \cmd.bat - cargo test --test 'mm2_tests_main' --no-fail-fast + run: cargo test --test 'mm2_tests_main' --no-fail-fast - docker-tests: - timeout-minutes: 90 + # ========================================================================== + # Docker Test Suites (matrix job for clean GitHub UI grouping) + # ========================================================================== + docker: + name: Docker / ${{ matrix.name }} runs-on: ubuntu-latest + timeout-minutes: ${{ matrix.timeout }} + strategy: + fail-fast: false + matrix: + include: + # SLP tests - BCH/SLP token tests + - name: SLP + features: docker-tests-slp + compose-profiles: "--profile slp" + timeout: 45 + needs-zcash-params: true + needs-nodes-setup: false + nodes-setup-args: "" + container-wait-time: 15 + + # Sia tests - Sia + UTXO for swaps + - name: Sia + features: docker-tests-sia + compose-profiles: "--profile sia --profile utxo" + timeout: 30 + needs-zcash-params: true + needs-nodes-setup: true + nodes-setup-args: "--skip-cosmos" + container-wait-time: 15 + + # ETH/EVM tests - Ethereum and ERC20 + - name: ETH + features: docker-tests-eth + compose-profiles: "--profile evm" + timeout: 60 + needs-zcash-params: false + needs-nodes-setup: false + nodes-setup-args: "" + container-wait-time: 15 + + # Ordermatching tests - UTXO order lifecycle + - name: Ordermatch (UTXO) + features: docker-tests-ordermatch + compose-profiles: "--profile utxo" + timeout: 60 + needs-zcash-params: true + needs-nodes-setup: false + nodes-setup-args: "" + container-wait-time: 15 + + # UTXO swap protocol tests + - name: Swaps (UTXO) + features: docker-tests-swaps + compose-profiles: "--profile utxo" + timeout: 60 + needs-zcash-params: true + needs-nodes-setup: false + nodes-setup-args: "" + container-wait-time: 20 + + # Watcher tests - UTXO watcher flows + - name: Watchers (UTXO) + features: docker-tests-watchers + compose-profiles: "--profile utxo" + timeout: 60 + needs-zcash-params: true + needs-nodes-setup: false + nodes-setup-args: "" + container-wait-time: 15 + + # QRC20/Qtum tests + - name: QRC20 + features: docker-tests-qrc20 + compose-profiles: "--profile qrc20 --profile utxo" + timeout: 45 + needs-zcash-params: true + needs-nodes-setup: false + nodes-setup-args: "" + container-wait-time: 20 + + # Tendermint/Cosmos tests + - name: Tendermint + features: docker-tests-tendermint + compose-profiles: "--profile cosmos" + timeout: 60 + needs-zcash-params: false + needs-nodes-setup: true + nodes-setup-args: "--skip-sia" + container-wait-time: 30 + + # ZCoin/Zombie tests + - name: ZCoin + features: docker-tests-zcoin + compose-profiles: "--profile zombie" + timeout: 60 + needs-zcash-params: true + needs-nodes-setup: false + nodes-setup-args: "" + container-wait-time: 30 + + # Cross-chain integration tests + - name: Integration (Cross-chain) + features: docker-tests-integration + compose-profiles: "--profile all" + timeout: 90 + needs-zcash-params: true + needs-nodes-setup: true + nodes-setup-args: "" + container-wait-time: 45 + env: BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_LINUX }} BOB_USERPASS: ${{ secrets.BOB_USERPASS_LINUX }} @@ -204,6 +252,7 @@ jobs: TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} steps: - uses: actions/checkout@v3 + - name: Install toolchain run: | rustup toolchain install stable --no-self-update --profile=minimal @@ -217,12 +266,32 @@ jobs: - name: Build cache uses: ./.github/actions/build-cache - - name: Test + - name: Fetch zcash params + if: ${{ matrix.needs-zcash-params }} + run: wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/v0.8.1/zcutil/fetch-params-alt.sh | bash + + - name: Prepare docker test environment + if: ${{ matrix.needs-nodes-setup }} + run: ./scripts/ci/docker-test-nodes-setup.sh ${{ matrix.nodes-setup-args }} + + - name: Start docker nodes run: | - wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/v0.8.1//zcutil/fetch-params-alt.sh | bash - cargo test --test 'docker_tests_main' --features run-docker-tests --no-fail-fast + docker compose -f .docker/test-nodes.yml ${{ matrix.compose-profiles }} up -d + echo "Waiting for containers to initialize..." + sleep ${{ matrix.container-wait-time }} + docker compose -f .docker/test-nodes.yml ps + + - name: Run tests + env: + KDF_DOCKER_COMPOSE_ENV: "1" + run: cargo test --test 'docker_tests_main' --features ${{ matrix.features }} --no-fail-fast + + - name: Stop docker nodes + if: always() + run: docker compose -f .docker/test-nodes.yml down -v wasm: + name: WASM timeout-minutes: 90 runs-on: ubuntu-latest env: diff --git a/.gitignore b/.gitignore index edccaf30e8..c908f46a19 100755 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,6 @@ hidden # Claude Code symlinks (generated from AGENTS.md) CLAUDE.md mm2src/*/CLAUDE.md + +# Claude Code configuration +.claude diff --git a/AGENTS.md b/AGENTS.md index 09bd91ea57..35adc49819 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,11 @@ # AGENTS.md — Komodo DeFi Framework +> **CURRENT STATUS & ROADMAP**: We use specific plan files to track large features, refactors, or complex fixes. +> +> 👉 **See [`docs/plans/`](docs/plans/) for active objectives.** +> +> *Always update the corresponding plan file when completing tasks. Delete the plan file when all tasks in the plan are complete.* + Guide for AI-assisted development. Keep changes small, follow patterns. ## Project Overview @@ -168,6 +174,15 @@ cargo clippy --all-targets --all-features -- -D warnings See `docs/DEV_ENVIRONMENT.md` for full setup and running specific tests. +### Test Environment Variables + +Set these environment variables before running docker or integration tests: + +```bash +export BOB_PASSPHRASE="also shoot benefit prefer juice shell elder veteran woman mimic image kidney" +export ALICE_PASSPHRASE="spice describe gravity federal blast come thank unfair canal monkey style afraid" +``` + ## Testing - **Bug fixes**: Prefer writing a failing test first, then fix the bug @@ -232,6 +247,9 @@ Update relevant AGENTS.md files when changing module structure, key types, patte | Wrote suboptimal code when efficient implementation existed in another crate | Search other crates for reusable functions; make private functions public if needed | | Large refactor done in one massive commit | Break into small, self-contained commits as you work | | Changed public API but didn't update AGENTS.md | Update documentation alongside code changes | +| Compared against wrong branch (e.g., deprecated `mm2.1`) | Use `git merge-base HEAD origin/dev origin/staging origin/main` to find the common ancestor, or ask the user which branch the feature is based on. Branch hierarchy: `main` ← `staging` ← `dev` ← feature branches | +| Forgot to run `cargo fmt` before committing | Always run `cargo fmt` before committing. CI will fail on unformatted code | +| Forgot to run clippy before committing | Run clippy on changed crates before committing. For speed, target only modified crate(s): `cargo clippy -p `. If code uses feature flags, include relevant features. If WASM-only code changed, also run: `cargo clippy -p --target wasm32-unknown-unknown` | ## Documentation diff --git a/docs/DOCKER_TESTS.md b/docs/DOCKER_TESTS.md new file mode 100644 index 0000000000..5cdfb3d9e9 --- /dev/null +++ b/docs/DOCKER_TESTS.md @@ -0,0 +1,88 @@ +# Docker Tests + +Docker tests run against local blockchain nodes to verify atomic swap functionality. + +## Prerequisites + +1. **Docker**: Install Docker Desktop or Docker Engine +2. **Zcash Parameters** (for UTXO nodes): + ```bash + wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/v0.8.1/zcutil/fetch-params-alt.sh | bash + ``` + +## Quick Start + +```bash +# Run all tests (testcontainers mode - starts containers automatically) +cargo test --test docker_tests_main --features docker-tests-all +``` + +## Running Specific Test Suites + +Tests are split by feature flag. Use the flag for the suite you want: + +| Feature | What it tests | +|---------|---------------| +| `docker-tests-eth` | ETH/ERC20/NFT | +| `docker-tests-slp` | BCH/SLP tokens | +| `docker-tests-sia` | Sia | +| `docker-tests-qrc20` | Qtum/QRC20 | +| `docker-tests-tendermint` | Cosmos/IBC | +| `docker-tests-zcoin` | ZCoin/Zombie | +| `docker-tests-swaps` | Swap protocol | +| `docker-tests-ordermatch` | Ordermatching | +| `docker-tests-watchers` | Watcher nodes | +| `docker-tests-integration` | Cross-chain swaps | +| `docker-tests-all` | Everything | + +```bash +# Example: run only ETH tests +cargo test --test docker_tests_main --features docker-tests-eth +``` + +## Docker Compose Mode (Faster Development) + +Keep nodes running between test runs for faster iteration: + +```bash +# 1. Prepare environment (needed for Cosmos & Sia tests) +./scripts/ci/docker-test-nodes-setup.sh + +# 2. Start nodes (use profile for specific chains) +docker compose -f .docker/test-nodes.yml --profile all up -d + +# 3. Run tests against running containers +KDF_DOCKER_COMPOSE_ENV=1 cargo test --test docker_tests_main --features docker-tests-eth + +# 4. Stop when done +docker compose -f .docker/test-nodes.yml --profile all down -v +``` + +**Profiles**: `utxo`, `slp`, `qrc20`, `evm`, `zombie`, `cosmos`, `sia`, `all` + +## Troubleshooting + +**Containers won't start**: Check Docker is running (`docker info`) + +**Port conflicts**: Stop existing containers (`docker compose -f .docker/test-nodes.yml down`) + +**Stale state**: Clean up and restart: +```bash +docker compose -f .docker/test-nodes.yml down -v +rm -rf .docker/container-runtime +./scripts/ci/docker-test-nodes-setup.sh +``` + +**UTXO nodes fail**: Ensure zcash params are downloaded (see Prerequisites) + +## Test Nodes + +| Node | Image | Port | +|------|-------|------| +| MYCOIN/MYCOIN1 | `gleec/testblockchain:multiarch` | 8000/8001 | +| FORSLP | `gleec/testblockchain:multiarch` | 10000 | +| QTUM | `gleec/qtumregtest:latest` | 9000 | +| GETH | `ethereum/client-go:stable` | 8545 | +| ZOMBIE | `gleec/zombietestrunner:multiarch` | 7090 | +| NUCLEUS/ATOM | `gleec/nucleusd:latest`, `gleec/gaiad:kdf-ci` | 26657/26658 | +| SIA | `ghcr.io/siafoundation/walletd:latest` | 9980 | diff --git a/mm2src/coins/AGENTS.md b/mm2src/coins/AGENTS.md index 3d6eedc006..6e3c7644dd 100644 --- a/mm2src/coins/AGENTS.md +++ b/mm2src/coins/AGENTS.md @@ -1,5 +1,7 @@ # coins — Multi-Protocol Coin Support +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + Abstraction layer for blockchain protocols. Defines traits for swaps, balances, and transactions. ## Responsibilities diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 0a02126de3..7f37f2861d 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -10,6 +10,10 @@ run-docker-tests = [] for-tests = ["dep:mocktopus"] new-db-arch = ["mm2_core/new-db-arch"] +# ETH/ERC20 watcher support - disabled by default because ETH watchers are +# unstable and not completed yet +enable-eth-watchers = [] + # Temporary feature for implementing IBC wrap/unwrap mechanism and will be removed # once we consider it as stable. ibc-routing-for-swaps = [] diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 7a9349d3f2..fda4d131b3 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -22,7 +22,7 @@ // use self::wallet_connect::{send_transaction_with_walletconnect, WcEthTxParams}; use super::eth::Action::{Call, Create}; -use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT}; +use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT, REWARD_OVERPAY_FACTOR}; use super::*; use crate::coin_balance::{ EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, @@ -34,6 +34,7 @@ use crate::hd_wallet::{ DisplayAddress, HDAccountOps, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, HDPathAccountToAddressId, HDWalletCoinOps, HDXPubExtractor, }; +#[cfg(feature = "enable-eth-watchers")] use crate::lp_price::get_base_price_in_rel; use crate::nft::nft_errors::ParseContractTypeError; use crate::nft::nft_structs::{ @@ -60,10 +61,13 @@ use crate::{ coin_balance, scan_for_new_addresses_impl, BalanceResult, CoinWithDerivationMethod, DerivationMethod, DexFee, Eip1559Ops, GasPriceRpcParam, MakerNftSwapOpsV2, ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyPolicy, RpcCommonOps, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, ToBytes, ValidateNftMakerPaymentArgs, - ValidateWatcherSpendInput, WatcherSpendType, }; +#[cfg(feature = "enable-eth-watchers")] +use crate::{ValidateWatcherSpendInput, WatcherSpendType}; use async_trait::async_trait; -use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; +#[cfg(feature = "enable-eth-watchers")] +use bitcrypto::dhash160; +use bitcrypto::{keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; use common::custom_futures::timeout::FutureTimerExt; use common::executor::{ @@ -95,6 +99,7 @@ use futures01::Future; use http::Uri; use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use mm2_core::mm_ctx::{MmArc, MmWeak}; +#[cfg(feature = "enable-eth-watchers")] use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, BigUint, MmNumber}; use num_traits::FromPrimitive; @@ -133,16 +138,19 @@ use super::{ PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundPaymentArgs, RewardTarget, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, - SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignEthTransactionParams, SignRawTransactionEnum, - SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, SwapGasFeePolicy, SwapOps, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, - WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, - WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, - INVALID_RECEIVER_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG, + SendPaymentArgs, SignEthTransactionParams, SignRawTransactionEnum, SignRawTransactionRequest, SignatureError, + SignatureResult, SpendPaymentArgs, SwapGasFeePolicy, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, + TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, + TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherRewardError, WeakSpawner, + WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, + INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_SENDER_ERR_LOG, +}; +#[cfg(feature = "enable-eth-watchers")] +use crate::{ + SendMakerPaymentSpendPreimageInput, TransactionFut, WatcherReward, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, INVALID_PAYMENT_STATE_ERR_LOG, INVALID_SWAP_ID_ERR_LOG, }; #[cfg(test)] pub(crate) use eth_utils::display_u256_with_decimal_point; @@ -1571,14 +1579,8 @@ impl SwapOps for EthCoin { input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { let swap_contract_address = try_s!(input.swap_contract_address.try_to_address()); - self.search_for_swap_tx_spend( - input.tx, - swap_contract_address, - input.secret_hash, - input.search_from_block, - input.watcher_reward, - ) - .await + self.search_for_swap_tx_spend(input.tx, swap_contract_address, input.search_from_block) + .await } async fn search_for_swap_tx_spend_other( @@ -1586,39 +1588,46 @@ impl SwapOps for EthCoin { input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { let swap_contract_address = try_s!(input.swap_contract_address.try_to_address()); - self.search_for_swap_tx_spend( - input.tx, - swap_contract_address, - input.secret_hash, - input.search_from_block, - input.watcher_reward, - ) - .await + self.search_for_swap_tx_spend(input.tx, swap_contract_address, input.search_from_block) + .await } - async fn extract_secret( - &self, - _secret_hash: &[u8], - spend_tx: &[u8], - watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(spend_tx)); - let function_name = get_function_name("receiverSpend", watcher_reward); - let function = try_s!(SWAP_CONTRACT.function(&function_name)); - - // Validate contract call; expected to be receiverSpend. - // https://www.4byte.directory/signatures/?bytes4_signature=02ed292b. - let expected_signature = function.short_signature(); - let actual_signature = &unverified.unsigned().data()[0..4]; - if actual_signature != expected_signature { + let tx_data = unverified.unsigned().data(); + if tx_data.len() < 4 { + return ERR!("Transaction data too short to contain function selector"); + } + let actual_signature = &tx_data[0..4]; + + // Auto-detect which receiverSpend variant was used by matching the function selector. + // Both variants have the secret at index 2, so we can use either for extraction. + // Note: receiverSpendReward may not exist until watcher-compatible contracts are deployed. + let receiver_spend = try_s!(SWAP_CONTRACT.function("receiverSpend")); + let receiver_spend_reward = SWAP_CONTRACT.function("receiverSpendReward").ok(); + + let function = if actual_signature == receiver_spend.short_signature() { + receiver_spend + } else if let Some(reward_func) = receiver_spend_reward.as_ref() { + if actual_signature == reward_func.short_signature() { + reward_func + } else { + return ERR!( + "Transaction is not a receiverSpend call. Expected signature {:?} or {:?}, found {:?}", + receiver_spend.short_signature(), + reward_func.short_signature(), + actual_signature + ); + } + } else { return ERR!( - "Expected 'receiverSpend' contract call signature: {:?}, found {:?}", - expected_signature, + "Transaction is not a receiverSpend call. Expected signature {:?}, found {:?}", + receiver_spend.short_signature(), actual_signature ); }; - let tokens = try_s!(decode_contract_call(function, unverified.unsigned().data())); + let tokens = try_s!(decode_contract_call(function, tx_data)); if tokens.len() < 3 { return ERR!("Invalid arguments in 'receiverSpend' call: {:?}", tokens); } @@ -1752,6 +1761,11 @@ impl SwapOps for EthCoin { } } +// ETH WatcherOps implementation - gated behind `enable-eth-watchers` feature +// because ETH watchers are unstable and not completed yet. +// When disabled, EthCoin uses the default WatcherOps implementation from lp_coins.rs +// which returns "not implemented" errors. +#[cfg(feature = "enable-eth-watchers")] #[async_trait] impl WatcherOps for EthCoin { fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { @@ -2357,14 +2371,8 @@ impl WatcherOps for EthCoin { Create => return Err(ERRL!("Invalid payment action: the payment action cannot be create")), }; - self.search_for_swap_tx_spend( - input.tx, - swap_contract_address, - input.secret_hash, - input.search_from_block, - true, - ) - .await + self.search_for_swap_tx_spend(input.tx, swap_contract_address, input.search_from_block) + .await } async fn get_taker_watcher_reward( @@ -2451,6 +2459,12 @@ impl WatcherOps for EthCoin { } } +// Fallback WatcherOps implementation when ETH watchers are disabled. +// Uses default implementations from the trait which return "not implemented" errors. +#[cfg(not(feature = "enable-eth-watchers"))] +#[async_trait] +impl WatcherOps for EthCoin {} + #[async_trait] #[cfg_attr(test, mockable)] impl MarketCoinOps for EthCoin { @@ -4079,7 +4093,10 @@ impl EthCoin { let mut value = trade_amount; let data = match &args.watcher_reward { Some(reward) => { - let reward_amount = try_tx_fus!(u256_from_big_decimal(&reward.amount, self.decimals)); + // Apply overpay factor to reward to handle gas price volatility between payment time and validation time until better things are in place. + let overpay_factor = BigDecimal::from_f64(REWARD_OVERPAY_FACTOR).unwrap_or(BigDecimal::from(1)); + let reward_with_overpay = &reward.amount * overpay_factor; + let reward_amount = try_tx_fus!(u256_from_big_decimal(&reward_with_overpay, self.decimals)); if !matches!(reward.reward_target, RewardTarget::None) || reward.send_contract_reward_on_spend { value += reward_amount; } @@ -4122,16 +4139,19 @@ impl EthCoin { let data = match args.watcher_reward { Some(reward) => { + // Apply overpay factor to reward to handle gas price volatility between payment time and validation time + let overpay_factor = BigDecimal::from_f64(REWARD_OVERPAY_FACTOR).unwrap_or(BigDecimal::from(1)); + let reward_with_overpay = &reward.amount * overpay_factor; let reward_amount = match reward.reward_target { RewardTarget::Contract | RewardTarget::PaymentSender => { let eth_reward_amount = - try_tx_fus!(u256_from_big_decimal(&reward.amount, ETH_DECIMALS)); + try_tx_fus!(u256_from_big_decimal(&reward_with_overpay, ETH_DECIMALS)); value += eth_reward_amount; eth_reward_amount }, RewardTarget::PaymentSpender => { let token_reward_amount = - try_tx_fus!(u256_from_big_decimal(&reward.amount, self.decimals)); + try_tx_fus!(u256_from_big_decimal(&reward_with_overpay, self.decimals)); amount += token_reward_amount; token_reward_amount }, @@ -4139,7 +4159,7 @@ impl EthCoin { // TODO tests passed without this change, need to research on how it worked if reward.send_contract_reward_on_spend { let eth_reward_amount = - try_tx_fus!(u256_from_big_decimal(&reward.amount, ETH_DECIMALS)); + try_tx_fus!(u256_from_big_decimal(&reward_with_overpay, ETH_DECIMALS)); value += eth_reward_amount; eth_reward_amount } else { @@ -4221,6 +4241,7 @@ impl EthCoin { } } + #[cfg(feature = "enable-eth-watchers")] fn watcher_spends_hash_time_locked_payment(&self, input: SendMakerPaymentSpendPreimageInput) -> EthTxFut { let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(input.preimage)); let payment = try_tx_fus!(SignedEthTx::new(tx)); @@ -4340,6 +4361,7 @@ impl EthCoin { } } + #[cfg(feature = "enable-eth-watchers")] fn watcher_refunds_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> EthTxFut { let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(args.payment_tx)); let payment = try_tx_fus!(SignedEthTx::new(tx)); @@ -5429,21 +5451,50 @@ impl EthCoin { &self, tx: &[u8], swap_contract_address: Address, - _secret_hash: &[u8], search_from_block: u64, - watcher_reward: bool, ) -> Result, String> { let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(tx)); let tx = try_s!(SignedEthTx::new(unverified)); - - let func_name = match self.coin_type { - EthCoinType::Eth => get_function_name("ethPayment", watcher_reward), - EthCoinType::Erc20 { .. } => get_function_name("erc20Payment", watcher_reward), + let tx_data = tx.unsigned().data(); + if tx_data.len() < 4 { + return ERR!("Transaction data too short to contain function selector"); + } + let actual_selector = &tx_data[0..4]; + + // Auto-detect which payment function variant was used by matching the function selector. + // The id (first argument) is at the same position in all variants. + // Note: Reward functions may not exist until watcher-compatible contracts are deployed. + let (payment_func_name, payment_func_reward_name) = match self.coin_type { + EthCoinType::Eth => ("ethPayment", "ethPaymentReward"), + EthCoinType::Erc20 { .. } => ("erc20Payment", "erc20PaymentReward"), EthCoinType::Nft { .. } => return ERR!("Nft Protocol is not supported yet!"), }; - let payment_func = try_s!(SWAP_CONTRACT.function(&func_name)); - let decoded = try_s!(decode_contract_call(payment_func, tx.unsigned().data())); + let payment_func = try_s!(SWAP_CONTRACT.function(payment_func_name)); + let payment_func_reward = SWAP_CONTRACT.function(payment_func_reward_name).ok(); + + let func_to_use = if actual_selector == payment_func.short_signature() { + payment_func + } else if let Some(reward_func) = payment_func_reward.as_ref() { + if actual_selector == reward_func.short_signature() { + reward_func + } else { + return ERR!( + "Transaction is not a payment call. Expected selector {:?} or {:?}, found {:?}", + payment_func.short_signature(), + reward_func.short_signature(), + actual_selector + ); + } + } else { + return ERR!( + "Transaction is not a payment call. Expected selector {:?}, found {:?}", + payment_func.short_signature(), + actual_selector + ); + }; + + let decoded = try_s!(decode_contract_call(func_to_use, tx_data)); let id = match decoded.first() { Some(Token::FixedBytes(bytes)) => bytes.clone(), invalid_token => return ERR!("Expected Token::FixedBytes, got {:?}", invalid_token), @@ -7721,7 +7772,9 @@ impl CommonSwapOpsV2 for EthCoin { #[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] impl EthCoin { - pub async fn set_coin_type(&self, new_coin_type: EthCoinType) -> EthCoin { + /// Creates a new EthCoin with a different coin type and decimals. + /// This is useful for tests that need to convert an ETH coin to ERC20. + pub async fn set_coin_type(&self, new_coin_type: EthCoinType, decimals: u8) -> EthCoin { let coin = EthCoinImpl { ticker: self.ticker.clone(), coin_type: new_coin_type, @@ -7734,7 +7787,7 @@ impl EthCoin { fallback_swap_contract: self.fallback_swap_contract, contract_supports_watchers: self.contract_supports_watchers, web3_instances: AsyncMutex::new(self.web3_instances.lock().await.clone()), - decimals: self.decimals, + decimals, history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), required_confirmations: AtomicU64::new( self.required_confirmations.load(std::sync::atomic::Ordering::SeqCst), diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index b4cea9f129..69d7c0f186 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -901,7 +901,7 @@ fn test_eth_extract_secret() { 100, 189, 72, 74, 221, 144, 66, 170, 68, 121, 29, 105, 19, 194, 35, 245, 196, 131, 236, 29, 105, 101, 30, ]; - let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice(), false)); + let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice())); assert!(secret.is_ok()); let expect_secret = &[ 168, 151, 11, 232, 224, 253, 63, 180, 26, 114, 23, 184, 27, 10, 161, 80, 178, 251, 73, 204, 80, 174, 97, 118, @@ -924,10 +924,10 @@ fn test_eth_extract_secret() { 6, 108, 165, 181, 188, 40, 56, 47, 211, 229, 221, 73, 5, 15, 89, 81, 117, 225, 216, 108, 98, 226, 119, 232, 94, 184, 42, 106, ]; - let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice(), false)) + let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice())) .err() .unwrap(); - assert!(secret.contains("Expected 'receiverSpend' contract call signature")); + assert!(secret.contains("Transaction is not a receiverSpend call")); } #[test] diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 8eb3107069..6da91b0536 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -791,12 +791,7 @@ impl SwapOps for LightningCoin { } } - async fn extract_secret( - &self, - _secret_hash: &[u8], - spend_tx: &[u8], - _watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { let payment_hash = payment_hash_from_slice(spend_tx).map_err(|e| e.to_string())?; let payment_hex = hex::encode(payment_hash.0); diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index fbdc914204..01e05c0756 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -26,7 +26,10 @@ clippy::swap_ptr_to_ref, clippy::forget_non_drop, clippy::doc_lazy_continuation, - clippy::needless_lifetimes // mocktopus requires explicit lifetimes + clippy::needless_lifetimes, // mocktopus requires explicit lifetimes + // TODO: Remove this allow when Rust 1.92 regression is fixed. + // See: https://github.com/rust-lang/rust/issues/147648 + unused_assignments )] #![allow(uncommon_codepoints)] @@ -187,7 +190,7 @@ macro_rules! try_tx_s { /// `TransactionErr:Plain` compatible `ERR` macro. macro_rules! TX_PLAIN_ERR { - ($format: expr, $($args: tt)+) => { Err(crate::TransactionErr::Plain((ERRL!($format, $($args)+)))) }; + ($format: expr, $($args: tt)+) => { Err(crate::TransactionErr::Plain(ERRL!($format, $($args)+))) }; ($format: expr) => { Err(crate::TransactionErr::Plain(ERRL!($format))) } } @@ -852,7 +855,6 @@ pub struct SearchForSwapTxSpendInput<'a> { pub search_from_block: u64, pub swap_contract_address: &'a Option, pub swap_unique_data: &'a [u8], - pub watcher_reward: bool, } #[derive(Copy, Clone, Debug)] @@ -1177,12 +1179,7 @@ pub trait SwapOps { input: SearchForSwapTxSpendInput<'_>, ) -> Result, String>; - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - watcher_reward: bool, - ) -> Result<[u8; 32], String>; + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String>; /// Whether the refund transaction can be sent now /// For example: there are no additional conditions for ETH, but for some UTXO coins we should wait for diff --git a/mm2src/coins/lp_price.rs b/mm2src/coins/lp_price.rs index 74537a31dc..0ccb939e01 100644 --- a/mm2src/coins/lp_price.rs +++ b/mm2src/coins/lp_price.rs @@ -298,6 +298,7 @@ pub async fn fetch_swap_coins_price(base: Option, rel: Option) - #[cfg(not(target_arch = "wasm32"))] mod tests { #[test] + #[ignore] // Requires external API access fn test_process_price_request() { use common::block_on; @@ -308,6 +309,7 @@ mod tests { } #[test] + #[ignore] // Requires external API access fn test_fetch_swap_coins_price() { use common::block_on; diff --git a/mm2src/coins/nft/nft_tests.rs b/mm2src/coins/nft/nft_tests.rs index aa7566c547..a432e1562a 100644 --- a/mm2src/coins/nft/nft_tests.rs +++ b/mm2src/coins/nft/nft_tests.rs @@ -1,36 +1,38 @@ use crate::hd_wallet::AddrToString; -use crate::nft::nft_structs::{ - Chain, NftFromMoralis, NftListFilters, NftTransferHistoryFilters, NftTransferHistoryFromMoralis, PhishingDomainReq, - PhishingDomainRes, SpamContractReq, SpamContractRes, TransferMeta, -}; +use crate::nft::nft_structs::{Chain, NftListFilters, NftTransferHistoryFilters, TransferMeta}; use crate::nft::storage::db_test_helpers::{get_nft_ctx, nft, nft_list, nft_transfer_history}; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps, RemoveNftResult}; use crate::nft::{ check_moralis_ipfs_bafy, get_domain_from_url, is_malicious, process_metadata_for_spam_link, process_text_for_spam_link, }; -use common::cross_test; -use ethereum_types::Address; -use mm2_net::transport::send_post_request_to_uri; +use common::{cfg_native, cfg_wasm32, cross_test}; use mm2_number::{BigDecimal, BigUint}; use std::num::NonZeroUsize; use std::str::FromStr; -const MORALIS_API_ENDPOINT_TEST: &str = "https://moralis.gleec.com/api/v2"; -const TEST_WALLET_ADDR_EVM: &str = "0x394d86994f954ed931b86791b62fe64f4c5dac37"; -const BLOCKLIST_API_ENDPOINT: &str = "https://nft-antispam.gleec.com"; const TOKEN_ADD: &str = "0xfd913a305d70a60aac4faac70c739563738e1f81"; const TOKEN_ID: &str = "214300044414"; const TX_HASH: &str = "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe"; const LOG_INDEX: u32 = 495; -#[cfg(not(target_arch = "wasm32"))] -use mm2_net::native_http::send_request_to_uri; +cfg_native! { + use crate::nft::nft_structs::{ + NftFromMoralis, NftTransferHistoryFromMoralis, PhishingDomainReq, PhishingDomainRes, SpamContractReq, + SpamContractRes, + }; + use ethereum_types::Address; + use mm2_net::native_http::send_request_to_uri; + use mm2_net::transport::send_post_request_to_uri; -common::cfg_wasm32! { + const MORALIS_API_ENDPOINT_TEST: &str = "https://moralis.gleec.com/api/v2"; + const TEST_WALLET_ADDR_EVM: &str = "0x394d86994f954ed931b86791b62fe64f4c5dac37"; + const BLOCKLIST_API_ENDPOINT: &str = "https://nft-antispam.gleec.com"; +} + +cfg_wasm32! { use wasm_bindgen_test::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use mm2_net::wasm::http::send_request_to_uri; } cross_test!(test_is_malicious, { @@ -92,7 +94,12 @@ cross_test!(test_check_for_spam_links, { assert_eq!(meta_redacted, nft.common.metadata.unwrap()) }); -cross_test!(test_moralis_requests, { +// Ignored: depends on external Moralis API which may be rate-limited or unavailable. +// Run manually with: cargo test test_moralis_requests -- --ignored +#[cfg(not(target_arch = "wasm32"))] +#[tokio::test(flavor = "multi_thread")] +#[ignore] +async fn test_moralis_requests() { let uri_nft_list = format!("{MORALIS_API_ENDPOINT_TEST}/{TEST_WALLET_ADDR_EVM}/nft?chain=POLYGON&format=decimal"); let response_nft_list = send_request_to_uri(uri_nft_list.as_str(), None).await.unwrap(); let nfts_list = response_nft_list["result"].as_array().unwrap(); @@ -119,9 +126,14 @@ cross_test!(test_moralis_requests, { let response_meta = send_request_to_uri(uri_meta.as_str(), None).await.unwrap(); let nft_moralis: NftFromMoralis = serde_json::from_str(&response_meta.to_string()).unwrap(); assert_eq!(42563567, nft_moralis.block_number.0); -}); +} -cross_test!(test_antispam_scan_endpoints, { +// Ignored: depends on external antispam API which may be unavailable. +// Run manually with: cargo test test_antispam_scan_endpoints -- --ignored +#[cfg(not(target_arch = "wasm32"))] +#[tokio::test(flavor = "multi_thread")] +#[ignore] +async fn test_antispam_scan_endpoints() { let req_spam = SpamContractReq { network: Chain::Eth, addresses: "0x0ded8542fc8b2b4e781b96e99fee6406550c9b7c,0x8d1355b65da254f2cc4611453adfa8b7a13f60ee".to_string(), @@ -130,14 +142,13 @@ cross_test!(test_antispam_scan_endpoints, { let req_json = serde_json::to_string(&req_spam).unwrap(); let contract_scan_res = send_post_request_to_uri(uri_contract.as_str(), req_json).await.unwrap(); let spam_res: SpamContractRes = serde_json::from_slice(&contract_scan_res).unwrap(); + // Only verify addresses are in the response; spam status may change over time assert!(spam_res .result - .get(&Address::from_str("0x0ded8542fc8b2b4e781b96e99fee6406550c9b7c").unwrap()) - .unwrap()); + .contains_key(&Address::from_str("0x0ded8542fc8b2b4e781b96e99fee6406550c9b7c").unwrap())); assert!(spam_res .result - .get(&Address::from_str("0x8d1355b65da254f2cc4611453adfa8b7a13f60ee").unwrap()) - .unwrap()); + .contains_key(&Address::from_str("0x8d1355b65da254f2cc4611453adfa8b7a13f60ee").unwrap())); let req_phishing = PhishingDomainReq { domains: "disposal-account-case-1f677.web.app,defi8090.vip".to_string(), @@ -146,27 +157,28 @@ cross_test!(test_antispam_scan_endpoints, { let uri_domain = format!("{BLOCKLIST_API_ENDPOINT}/api/blocklist/domain/scan"); let domain_scan_res = send_post_request_to_uri(uri_domain.as_str(), req_json).await.unwrap(); let phishing_res: PhishingDomainRes = serde_json::from_slice(&domain_scan_res).unwrap(); - assert!(phishing_res.result.get("disposal-account-case-1f677.web.app").unwrap()); -}); + // Only verify domain is in the response; phishing status may change over time + assert!(phishing_res.result.contains_key("disposal-account-case-1f677.web.app")); +} // Disabled on Linux: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2367 -cross_test!( - test_camo, - { - use crate::nft::nft_structs::UriMeta; - - let hex_token_uri = hex::encode("https://tikimetadata.s3.amazonaws.com/tiki_box.json"); - let uri_decode = format!("{BLOCKLIST_API_ENDPOINT}/url/decode/{hex_token_uri}"); - let decode_res = send_request_to_uri(&uri_decode, None).await.unwrap(); - let uri_meta: UriMeta = serde_json::from_value(decode_res).unwrap(); - assert_eq!( - uri_meta.raw_image_url.unwrap(), - "https://tikimetadata.s3.amazonaws.com/tiki_box.png" - ); - }, - target_os = "macos", - target_os = "windows" -); +// Ignored: depends on external antispam API which may be unavailable. +// Run manually with: cargo test test_camo -- --ignored +#[cfg(all(not(target_arch = "wasm32"), any(target_os = "macos", target_os = "windows")))] +#[tokio::test(flavor = "multi_thread")] +#[ignore] +async fn test_camo() { + use crate::nft::nft_structs::UriMeta; + + let hex_token_uri = hex::encode("https://tikimetadata.s3.amazonaws.com/tiki_box.json"); + let uri_decode = format!("{BLOCKLIST_API_ENDPOINT}/url/decode/{hex_token_uri}"); + let decode_res = send_request_to_uri(&uri_decode, None).await.unwrap(); + let uri_meta: UriMeta = serde_json::from_value(decode_res).unwrap(); + assert_eq!( + uri_meta.raw_image_url.unwrap(), + "https://tikimetadata.s3.amazonaws.com/tiki_box.png" + ); +} cross_test!(test_add_get_nfts, { let chain = Chain::Bsc; diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 2631f3724d..0669b023c8 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1035,12 +1035,7 @@ impl SwapOps for Qrc20Coin { } #[inline] - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - _watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { self.extract_secret_impl(secret_hash, spend_tx) } diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 335ab4320e..851ea65780 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -251,7 +251,9 @@ fn test_validate_maker_payment() { } } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] fn test_wait_for_confirmations_excepted() { // this priv_key corresponds to "taker_passphrase" passphrase let priv_key = [ @@ -484,7 +486,7 @@ fn test_extract_secret() { // taker spent maker payment - d3f5dab4d54c14b3d7ed8c7f5c8cc7f47ccf45ce589fdc7cd5140a3c1c3df6e1 let tx_hex = hex::decode("01000000033f56ecafafc8602fde083ba868d1192d6649b8433e42e1a2d79ba007ea4f7abb010000006b48304502210093404e90e40d22730013035d31c404c875646dcf2fad9aa298348558b6d65ba60220297d045eac5617c1a3eddb71d4bca9772841afa3c4c9d6c68d8d2d42ee6de3950121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff9cac7fe90d597922a1d92e05306c2215628e7ea6d5b855bfb4289c2944f4c73a030000006b483045022100b987da58c2c0c40ce5b6ef2a59e8124ed4ef7a8b3e60c7fb631139280019bc93022069649bcde6fe4dd5df9462a1fcae40598488d6af8c324cd083f5c08afd9568be0121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff70b9870f2b0c65d220a839acecebf80f5b44c3ca4c982fa2fdc5552c037f5610010000006a473044022071b34dd3ebb72d29ca24f3fa0fc96571c815668d3b185dd45cc46a7222b6843f02206c39c030e618d411d4124f7b3e7ca1dd5436775bd8083a85712d123d933a51300121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff020000000000000000c35403a0860101284ca402ed292b806a1835a1b514ad643f2acdb5c8db6b6a9714accff3275ea0d79a3f23be8fd00000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2c02288d4010000001976a914783cf0be521101942da509846ea476e683aad83288ac0f047f5f").unwrap(); - let secret = block_on(coin.extract_secret(secret_hash, &tx_hex, false)).unwrap(); + let secret = block_on(coin.extract_secret(secret_hash, &tx_hex)).unwrap(); assert_eq!(secret, expected_secret); } @@ -505,7 +507,7 @@ fn test_extract_secret_malicious() { let spend_tx = hex::decode("01000000022bc8299981ec0cea664cdf9df4f8306396a02e2067d6ac2d3770b34646d2bc2a010000006b483045022100eb13ef2d99ac1cd9984045c2365654b115dd8a7815b7fbf8e2a257f0b93d1592022060d648e73118c843e97f75fafc94e5ff6da70ec8ba36ae255f8c96e2626af6260121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffffd92a0a10ac6d144b36033916f67ae79889f40f35096629a5cd87be1a08f40ee7010000006b48304502210080cdad5c4770dfbeb760e215494c63cc30da843b8505e75e7bf9e8dad18568000220234c0b11c41bfbcdd50046c69059976aedabe17657fe43d809af71e9635678e20121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff030000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000202020202020202020202020202020202020202020202020202020202020202000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac20000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2b8ea82d3010000001976a914783cf0be521101942da509846ea476e683aad83288ac735d855f").unwrap(); let expected_secret = [1; 32]; let secret_hash = &*dhash160(&expected_secret); - let actual = block_on(coin.extract_secret(secret_hash, &spend_tx, false)); + let actual = block_on(coin.extract_secret(secret_hash, &spend_tx)); assert_eq!(actual, Ok(expected_secret)); } diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index cce1f0e02b..8cc52aba11 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1385,19 +1385,11 @@ impl SiaCoin { &self, expected_hash_slice: &[u8], spend_tx: &[u8], - watcher_reward: bool, ) -> Result<[u8; 32], SiaCoinSiaExtractSecretError> { // Parse arguments to Sia specific types let tx = SiaTransaction::try_from(spend_tx)?; let expected_hash = Hash256::try_from(expected_hash_slice)?; - // watcher_reward is irrelevant, but a true value indicates a bug within the swap protocol - // An error is not thrown as it would not be in the best interest of the swap participant - // if they are still able to extract the secret and spend the HTLC output - if watcher_reward { - debug!("SiaCoin::sia_extract_secret: expected watcher_reward false, found true"); - } - // iterate over all inputs and search for preimage that hashes to expected_hash let found_secret = tx.0.siacoin_inputs @@ -1848,13 +1840,8 @@ impl SwapOps for SiaCoin { unimplemented!() } - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - watcher_reward: bool, - ) -> Result<[u8; 32], String> { - self.sia_extract_secret(secret_hash, spend_tx, watcher_reward) + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { + self.sia_extract_secret(secret_hash, spend_tx) .map_err(|e| e.to_string()) } diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index c6e29883e4..14b21566ec 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -725,12 +725,7 @@ impl SwapOps for SolanaCoin { todo!() } - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, _secret_hash: &[u8], _spend_tx: &[u8]) -> Result<[u8; 32], String> { todo!() } diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index aa2d55182c..18bab84e3f 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -642,12 +642,7 @@ impl SwapOps for SolanaToken { todo!() } - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, _secret_hash: &[u8], _spend_tx: &[u8]) -> Result<[u8; 32], String> { todo!() } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 1bbc1489d0..b3676a2680 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -4203,12 +4203,7 @@ impl SwapOps for TendermintCoin { self.search_for_swap_tx_spend(input).await.map_err(|e| e.to_string()) } - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { let tx = try_s!(cosmrs::Tx::from_bytes(spend_tx)); let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); @@ -5141,7 +5136,6 @@ pub mod tests { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], - watcher_reward: false, }; let spend_tx = match block_on(coin.search_for_swap_tx_spend_my(input)).unwrap().unwrap() { @@ -5217,7 +5211,6 @@ pub mod tests { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], - watcher_reward: false, }; match block_on(coin.search_for_swap_tx_spend_my(input)).unwrap().unwrap() { diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 8863005576..81d5f9ad29 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -236,15 +236,8 @@ impl SwapOps for TendermintToken { self.platform_coin.search_for_swap_tx_spend_other(input).await } - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - watcher_reward: bool, - ) -> Result<[u8; 32], String> { - self.platform_coin - .extract_secret(secret_hash, spend_tx, watcher_reward) - .await + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { + self.platform_coin.extract_secret(secret_hash, spend_tx).await } fn negotiate_swap_contract_addr( diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index ad24b86c7c..deeb48860f 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -243,12 +243,7 @@ impl SwapOps for TestCoin { unimplemented!() } - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, _secret_hash: &[u8], _spend_tx: &[u8]) -> Result<[u8; 32], String> { unimplemented!() } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 891b0391e3..a9e3d16073 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1019,12 +1019,7 @@ impl SwapOps for BchCoin { } #[inline] - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - _watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index da98e1c8de..01d5d3394b 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -659,12 +659,7 @@ impl SwapOps for QtumCoin { } #[inline] - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - _watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 63966089f5..836efd55fc 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -1494,12 +1494,7 @@ impl SwapOps for SlpToken { } #[inline] - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - _watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index fa7d621e3d..f68d72b7a2 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -440,12 +440,7 @@ impl SwapOps for UtxoStandardCoin { } #[inline] - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - _watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 2ec224e48e..f03c5d6c6e 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -38,8 +38,11 @@ use crate::{ #[cfg(not(target_arch = "wasm32"))] use crate::{WaitForHTLCTxSpendArgs, WithdrawFee}; use chain::{BlockHeader, BlockHeaderBits, OutPoint}; +use common::custom_futures::repeatable::{Ready, Retry}; use common::executor::Timer; -use common::{block_on, block_on_f01, wait_until_sec, OrdRange, PagingOptionsEnum, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{ + block_on, block_on_f01, repeatable, wait_until_sec, OrdRange, PagingOptionsEnum, DEX_FEE_ADDR_RAW_PUBKEY, +}; use crypto::{privkey::key_pair_from_seed, Bip44Chain, HDPathToAccount, RpcDerivationPath, Secp256k1Secret}; #[cfg(not(target_arch = "wasm32"))] use db_common::sqlite::rusqlite::Connection; @@ -65,6 +68,7 @@ use std::convert::TryFrom; use std::iter; use std::num::NonZeroUsize; use std::str::FromStr; +use std::time::Duration; #[cfg(test)] use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; @@ -168,7 +172,7 @@ fn test_extract_secret() { let expected_secret = <[u8; 32]>::from_hex("5c62072b57b6473aeee6d35270c8b56d86975e6d6d4245b25425d771239fae32").unwrap(); let secret_hash = &*dhash160(&expected_secret); - let secret = block_on(coin.extract_secret(secret_hash, &tx_hex, false)).unwrap(); + let secret = block_on(coin.extract_secret(secret_hash, &tx_hex)).unwrap(); assert_eq!(secret, expected_secret); } @@ -551,7 +555,6 @@ fn test_search_for_swap_tx_spend_electrum_was_spent() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], - watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -586,7 +589,6 @@ fn test_search_for_swap_tx_spend_electrum_was_refunded() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], - watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -1680,7 +1682,9 @@ fn test_network_info_negative_time_offset() { let _info: NetworkInfo = json::from_str(info_str).unwrap(); } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] fn test_unavailable_electrum_proto_version() { ElectrumClientImpl::try_new_arc.mock_safe( |client_settings, block_headers_storage, streaming_manager, abortable_system, event_handlers, chain_variant| { @@ -1761,7 +1765,9 @@ fn test_spam_rick() { } } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] fn test_one_unavailable_electrum_proto_version() { // First mock with an unrealistically high version requirement that no server would support ElectrumClientImpl::try_new_arc.mock_safe( @@ -1850,7 +1856,9 @@ fn test_qtum_generate_pod() { assert_eq!(expected_res, res.to_string()); } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] fn test_qtum_add_delegation() { let keypair = key_pair_from_seed("asthma turtle lizard tone genuine tube hunt valley soap cloth urge alpha amazing frost faculty cycle mammal leaf normal bright topple avoid pulse buffalo").unwrap(); let conf = json!({"coin":"tQTUM","rpcport":13889,"pubtype":120,"p2shtype":110, "mature_confirmations":1}); @@ -1926,7 +1934,9 @@ fn test_qtum_add_delegation_on_already_delegating() { assert!(res.is_err()); } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] fn test_qtum_get_delegation_infos() { let keypair = key_pair_from_seed("federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron").unwrap(); @@ -5427,9 +5437,36 @@ fn test_block_header_utxo_loop() { let loop_fut = async move { block_header_utxo_loop(weak_client, loop_handle, spv_conf.unwrap()).await }; let test_fut = async move { + // Helper to poll until target height is reached and expected steps are consumed + async fn wait_for_height( + client: &ElectrumClient, + expected_steps: &Arc>>, + target_height: u64, + ) { + repeatable!(async { + let height = client + .block_headers_storage() + .get_last_block_height() + .await + .ok() + .flatten() + .unwrap_or(0); + let steps_empty = expected_steps.lock().unwrap().is_empty(); + if height >= target_height && steps_empty { + Ready(()) + } else { + Retry(()) + } + }) + .repeat_every(Duration::from_millis(100)) + .with_timeout_ms(30_000) + .await + .expect("Timed out waiting for block headers to sync"); + } + *expected_steps.lock().unwrap() = vec![(2, 5), (6, 9), (10, 13), (14, 14)]; CURRENT_BLOCK_COUNT.store(14, Ordering::Relaxed); - Timer::sleep(3.).await; + wait_for_height(&client, &expected_steps, 14).await; let get_headers_count = client .block_headers_storage() .get_last_block_height() @@ -5441,7 +5478,7 @@ fn test_block_header_utxo_loop() { *expected_steps.lock().unwrap() = vec![(15, 18)]; CURRENT_BLOCK_COUNT.store(18, Ordering::Relaxed); - Timer::sleep(2.).await; + wait_for_height(&client, &expected_steps, 18).await; let get_headers_count = client .block_headers_storage() .get_last_block_height() @@ -5453,7 +5490,7 @@ fn test_block_header_utxo_loop() { *expected_steps.lock().unwrap() = vec![(19, 19)]; CURRENT_BLOCK_COUNT.store(19, Ordering::Relaxed); - Timer::sleep(2.).await; + wait_for_height(&client, &expected_steps, 19).await; let get_headers_count = client .block_headers_storage() .get_last_block_height() @@ -5475,7 +5512,6 @@ fn test_block_header_utxo_loop() { assert_eq!(header, None); } - Timer::sleep(2.).await; }; if let Either::Left(_) = block_on(futures::future::select(loop_fut.boxed(), test_fut.boxed())) { diff --git a/mm2src/coins/watcher_common.rs b/mm2src/coins/watcher_common.rs index 41eb848158..e903312b48 100644 --- a/mm2src/coins/watcher_common.rs +++ b/mm2src/coins/watcher_common.rs @@ -1,9 +1,95 @@ use crate::ValidatePaymentError; use mm2_err_handle::prelude::MmError; -pub const REWARD_GAS_AMOUNT: u64 = 70000; +/// Gas amount used to calculate watcher reward. +/// +/// This value (150K) is set to cover actual watcher gas costs: +/// +/// 1. **Reward functions use more gas than non-reward functions:** +/// - `receiverSpendReward` / `senderRefundReward` have additional hashing (more parameters) +/// - Multiple external transfers (2-4 vs 1 in non-reward functions) +/// - ERC20 is ~2× more expensive (double token transfers + ETH transfers) +/// +/// # Watcher Economics +/// +/// Taker always benefits from watcher actions: +/// - `receiverSpendReward`: Watcher spends maker payment → taker gets coins +/// - `senderRefundReward`: Watcher refunds taker payment → taker gets refund +/// +/// Therefore taker should pay the watcher reward (which the current design does, +/// except for ETH/ETH maker payments which use a shared contract pool). +/// +/// # Future Improvements +/// +/// - Use operation-specific gas constants (measure actual `gasUsed` for each reward function) +/// - Dynamic reward adjustment based on network conditions +pub const REWARD_GAS_AMOUNT: u64 = 150000; + +/// Overpay factor for watcher reward calculation (1.5 = 50% overpay). +/// +/// When calculating watcher reward at payment time, multiply the gas cost by this factor +/// to account for: +/// +/// 1. **Gas price volatility:** Reward is set at payment time but validator checks it later. +/// Gas price can increase significantly between these times. +/// +/// 2. **Profit margin:** Provides buffer for watcher profit (~10%+). +/// +/// The 50% overpay ensures the reward remains valid even if gas price increases by 30-40% +/// between payment creation and validation. +pub const REWARD_OVERPAY_FACTOR: f64 = 1.5; + +/// Margin for reward validation (10%). +/// +/// When validating rewards in non-exact mode, actual reward must be at least +/// `expected_reward * (1 - REWARD_MARGIN)` to pass validation. const REWARD_MARGIN: f64 = 0.1; +/// Validates that the actual watcher reward in a payment transaction is acceptable. +/// +/// # Call sites +/// - `validate_payment` (eth.rs) - counterparty validates payment before proceeding with swap +/// - `watcher_validate_taker_payment` (eth.rs) - watcher validates taker payment before helping +/// +/// # Arguments +/// - `expected_reward`: Reward calculated by validator at validation time (based on current gas price) +/// - `actual_reward`: Reward encoded in the payment transaction (set by payer at payment time) +/// - `is_exact`: If true, requires exact match; if false, only enforces lower bound +/// +/// # Validation logic +/// +/// **Exact mode (`is_exact == true`):** +/// Requires `actual_reward == expected_reward`. Used when reward amount was pre-negotiated. +/// +/// **Non-exact mode (`is_exact == false`):** +/// Only enforces lower bound: `actual_reward >= expected_reward * (1 - REWARD_MARGIN)`. +/// +/// Upper bound is NOT enforced because: +/// 1. The payer of the reward chooses the amount when building the tx. +/// - Maker pays for maker payment reward, taker pays for taker payment reward. +/// - Their node computes `WatcherReward.amount` and encodes it in the contract call. +/// 2. No one can increase `_rewardAmount` after the fact - it's locked into `paymentHash`. +/// The contract's `receiverSpendReward`/`senderRefundReward` require exact hash match. +/// 3. Gas price volatility causes the reward (set at payment time) to exceed the +/// expected reward (calculated by validator at validation time). +/// 4. A higher reward benefits watchers and doesn't reduce what the counterparty receives. +/// +/// Lower bound provides a sanity check that the reward is in a reasonable range, +/// though note that due to gas price volatility, even this check can fail if gas prices +/// rise significantly between payment time and validation time. +/// +/// # Watcher Execution Flexibility +/// +/// - Watcher can execute with less gas than budgeted (they profit more) +/// - Watcher can wait if gas is high and retry later before locktime expires +/// - Multiple watchers can compete - first to call `receiverSpendReward`/`senderRefundReward` +/// gets the reward (reward goes to `msg.sender` in the contract) +/// +/// # Trade Amount Invariants +/// +/// `maker_amount` and `taker_amount` from ordermatching are **net trade amounts**, +/// independent of watcher rewards. The reward is funded separately (e.g., via `msg.value` +/// for ERC20 payments) and does not reduce what the counterparty receives. pub fn validate_watcher_reward( expected_reward: u64, actual_reward: u64, @@ -17,10 +103,9 @@ pub fn validate_watcher_reward( } } else { let min_acceptable_reward = get_reward_lower_boundary(expected_reward); - let max_acceptable_reward = get_reward_upper_boundary(expected_reward); - if actual_reward < min_acceptable_reward || actual_reward > max_acceptable_reward { + if actual_reward < min_acceptable_reward { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided watcher reward {actual_reward} is not within the expected interval {min_acceptable_reward} - {max_acceptable_reward}" + "Provided watcher reward {actual_reward} is less than minimum acceptable {min_acceptable_reward}" ))); } } @@ -30,7 +115,3 @@ pub fn validate_watcher_reward( fn get_reward_lower_boundary(reward: u64) -> u64 { (reward as f64 * (1. - REWARD_MARGIN)) as u64 } - -fn get_reward_upper_boundary(reward: u64) -> u64 { - (reward as f64 * (1. + REWARD_MARGIN)) as u64 -} diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index c72fc7fa4b..77a2cdacea 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1700,12 +1700,7 @@ impl SwapOps for ZCoin { } #[inline] - async fn extract_secret( - &self, - secret_hash: &[u8], - spend_tx: &[u8], - _watcher_reward: bool, - ) -> Result<[u8; 32], String> { + async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } diff --git a/mm2src/coins_activation/AGENTS.md b/mm2src/coins_activation/AGENTS.md index d50b98714e..3e95800de4 100644 --- a/mm2src/coins_activation/AGENTS.md +++ b/mm2src/coins_activation/AGENTS.md @@ -1,5 +1,7 @@ # coins_activation — Coin Activation Flows +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + Manages the lifecycle of cryptocurrency activation. Handles standalone coins, platform coins with tokens, and L2 layers. ## Responsibilities diff --git a/mm2src/common/AGENTS.md b/mm2src/common/AGENTS.md index 71f3b66bf5..23a3cc63e0 100644 --- a/mm2src/common/AGENTS.md +++ b/mm2src/common/AGENTS.md @@ -1,5 +1,7 @@ # common — Shared Utilities +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + Foundation crate providing utilities used across all KDF crates. Platform-aware (native/WASM). ## Responsibilities diff --git a/mm2src/crypto/AGENTS.md b/mm2src/crypto/AGENTS.md index 56d6561170..1991bbe092 100644 --- a/mm2src/crypto/AGENTS.md +++ b/mm2src/crypto/AGENTS.md @@ -1,5 +1,7 @@ # crypto — Key Management and HD Derivation +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + **Security-critical crate.** Handles mnemonics, seeds, key derivation, and hardware wallet integration. ## Security Rules (Non-Negotiable) diff --git a/mm2src/mm2_bin_lib/AGENTS.md b/mm2src/mm2_bin_lib/AGENTS.md index 5abfceb275..7e5b6d95f7 100644 --- a/mm2src/mm2_bin_lib/AGENTS.md +++ b/mm2src/mm2_bin_lib/AGENTS.md @@ -1,5 +1,7 @@ # mm2_bin_lib — Platform Entry Points +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + Thin wrapper providing platform-specific entry points for KDF. Bridges native, WASM, and mobile platforms to `mm2_main`. ## Responsibilities diff --git a/mm2src/mm2_bitcoin/AGENTS.md b/mm2src/mm2_bitcoin/AGENTS.md index 615dca7816..1bb79ef7b5 100644 --- a/mm2src/mm2_bitcoin/AGENTS.md +++ b/mm2src/mm2_bitcoin/AGENTS.md @@ -1,5 +1,7 @@ # mm2_bitcoin — UTXO Primitives +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + Low-level primitives for all UTXO-based coins (Bitcoin, Komodo, Litecoin, etc.). Named "bitcoin" historically but used across all UTXO protocols. **Note:** This is a workspace of sub-crates, not a single crate. Each sub-crate is published separately. diff --git a/mm2src/mm2_bitcoin/keys/src/lib.rs b/mm2src/mm2_bitcoin/keys/src/lib.rs index 3ce18fcb4e..ac087c480e 100644 --- a/mm2src/mm2_bitcoin/keys/src/lib.rs +++ b/mm2src/mm2_bitcoin/keys/src/lib.rs @@ -1,5 +1,9 @@ //! Bitcoin keys. +// TODO: Remove this allow when Rust 1.92 regression is fixed. +// See: https://github.com/rust-lang/rust/issues/147648 +#![allow(unused_assignments)] + extern crate bech32; extern crate bitcrypto as crypto; extern crate bs58; diff --git a/mm2src/mm2_bitcoin/primitives/src/lib.rs b/mm2src/mm2_bitcoin/primitives/src/lib.rs index d9cc678a1c..19cb627674 100644 --- a/mm2src/mm2_bitcoin/primitives/src/lib.rs +++ b/mm2src/mm2_bitcoin/primitives/src/lib.rs @@ -1,5 +1,4 @@ #![expect(clippy::assign_op_pattern)] -#![expect(clippy::ptr_offset_with_cast)] #![expect(clippy::manual_div_ceil)] extern crate bitcoin_hashes; diff --git a/mm2src/mm2_db/src/lib.rs b/mm2src/mm2_db/src/lib.rs index 0a69fa1484..27f5c59aae 100644 --- a/mm2src/mm2_db/src/lib.rs +++ b/mm2src/mm2_db/src/lib.rs @@ -1,3 +1,7 @@ +// TODO: Remove this allow when Rust 1.92 regression is fixed. +// See: https://github.com/rust-lang/rust/issues/147648 +#![allow(unused_assignments)] + #[cfg(target_arch = "wasm32")] #[path = "indexed_db/indexed_db.rs"] pub mod indexed_db; diff --git a/mm2src/mm2_main/AGENTS.md b/mm2src/mm2_main/AGENTS.md index 9fda438898..f9ccd93307 100644 --- a/mm2src/mm2_main/AGENTS.md +++ b/mm2src/mm2_main/AGENTS.md @@ -1,5 +1,7 @@ # mm2_main — RPC, Swaps, and Application Logic +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + Core application crate: RPC dispatcher, atomic swap engines, order matching, streaming. ## Responsibilities @@ -175,3 +177,77 @@ Enable via `stream::::enable`, disable via `stream::disable`: - Unit tests: `cargo test -p mm2_main --lib` - Integration: `cargo test --test mm2_tests_main` - Docker swaps: `cargo test --test docker_tests_main --features run-docker-tests` + +### Docker Test Infrastructure + +Docker tests run against local blockchain test nodes to verify atomic swap functionality. Tests are split into feature-gated modules for parallel CI execution. + +#### Test Module Structure + +``` +tests/docker_tests/ +├── helpers/ +│ ├── docker_ops.rs # CoinDockerOps trait (shared by utxo, zcoin) +│ ├── env.rs # MM_CTX, service constants, DockerNode +│ ├── eth.rs # Geth/ERC20 helpers (contracts, funding) +│ ├── mod.rs # Module index +│ ├── qrc20.rs # Qtum/QRC20 helpers +│ ├── sia.rs # Sia helpers +│ ├── swap.rs # Cross-chain swap orchestration (trade_base_rel) +│ ├── tendermint.rs # Tendermint/Cosmos/IBC helpers +│ ├── utxo.rs # UTXO coin helpers (MYCOIN, BCH/SLP) +│ └── zcoin.rs # ZCoin/Zombie helpers +├── swap_watcher_tests/ +│ ├── eth.rs # ETH watcher tests (disabled by default) +│ ├── mod.rs # Watcher test helpers +│ └── utxo.rs # UTXO watcher tests (stable) +├── docker_ordermatch_tests.rs # Cross-chain ordermatching +├── docker_tests_inner.rs # Mixed ETH/UTXO integration +├── eth_docker_tests.rs # ETH/ERC20/NFT coin & swap v2 tests +├── eth_inner_tests.rs # ETH-only ordermatching/wallet tests +├── qrc20_tests.rs # Qtum/QRC20 tests +├── runner.rs # Container startup/initialization +├── sia_docker_tests.rs # Sia tests +├── slp_tests.rs # SLP/BCH tests +├── swap_proto_v2_tests.rs # UTXO swap protocol v2 +├── swap_tests.rs # Cross-chain SLP swaps +├── swaps_confs_settings_sync_tests.rs +├── swaps_file_lock_tests.rs +├── tendermint_swap_tests.rs # Tendermint cross-chain swaps +├── tendermint_tests.rs # Cosmos/IBC tests +├── utxo_ordermatch_v1_tests.rs # UTXO-only ordermatching +├── utxo_swaps_v1_tests.rs # UTXO swap protocol v1 +└── z_coin_docker_tests.rs # ZCoin/Zombie tests +``` + +#### Feature Flags + +| Feature | Purpose | Containers | +|---------|---------|------------| +| `docker-tests-eth` | ETH/ERC20/NFT tests | Geth | +| `docker-tests-slp` | BCH/SLP token tests | FORSLP | +| `docker-tests-sia` | Sia tests + DSIA swaps | Sia + UTXO | +| `docker-tests-ordermatch` | Orderbook/matching tests | UTXO + Geth | +| `docker-tests-swaps` | Swap protocol tests | UTXO | +| `docker-tests-watchers` | UTXO watcher tests | UTXO | +| `docker-tests-watchers-eth` | ETH watcher tests (unstable) | UTXO + Geth | +| `docker-tests-qrc20` | Qtum/QRC20 tests | Qtum + UTXO | +| `docker-tests-tendermint` | Cosmos/IBC tests | Cosmos | +| `docker-tests-zcoin` | ZCoin/Zombie tests | Zombie | +| `docker-tests-integration` | Cross-chain swaps | ALL | +| `docker-tests-all` | All suites (local dev) | ALL | + +#### Running Tests + +```bash +# Single suite +cargo test --test docker_tests_main --features docker-tests-eth + +# All suites (local development) +cargo test --test docker_tests_main --features docker-tests-all + +# With docker-compose (faster iteration) +KDF_DOCKER_COMPOSE_ENV=1 cargo test --test docker_tests_main --features docker-tests-eth +``` + +See [`docs/DOCKER_TESTS.md`](../../../docs/DOCKER_TESTS.md) for full setup and troubleshooting. diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index b3b7698882..d423c3321a 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -18,11 +18,43 @@ native = [] # Deprecated track-ctx-pointer = ["common/track-ctx-pointer"] zhtlc-native-tests = ["coins/zhtlc-native-tests"] run-docker-tests = ["coins/run-docker-tests"] +# Split docker test features - organized by test category +# +# Chain-specific tests: Test coin implementations for specific blockchain protocols +# (consideration: move to coins::*/tests; far future: separate crates per chain) +docker-tests-slp = ["run-docker-tests"] # BCH-SLP coin tests +docker-tests-sia = ["run-docker-tests"] # Sia coin tests +docker-tests-eth = ["run-docker-tests"] # ETH/ERC20 coin tests +docker-tests-qrc20 = ["run-docker-tests"] # QRC20 coin tests +docker-tests-tendermint = ["run-docker-tests"] # Tendermint/IBC coin tests +docker-tests-zcoin = ["run-docker-tests"] # ZCoin/Zombie coin tests +# +# Protocol tests: Test cross-cutting protocol functionality (swaps, ordermatching, watchers). +# Protocol logic is coin-agnostic; these tests use UTXO coins to verify protocol correctness in isolation. +docker-tests-swaps = ["run-docker-tests"] # Swap protocol tests (v1, v2, confs, file-locks) +docker-tests-ordermatch = ["run-docker-tests"] # Orderbook and matching tests +docker-tests-watchers = ["run-docker-tests"] # Watcher node tests (UTXO-only, stable) +docker-tests-watchers-eth = ["docker-tests-watchers", "coins/enable-eth-watchers"] # Watcher tests (ETH, unstable) +# +# Cross-chain integration tests: Test interactions between different chain families +docker-tests-integration = ["run-docker-tests"] +# Aggregate feature for local development - runs all docker test suites +# Not recommended for CI (use split jobs instead for parallelism) +docker-tests-all = [ + "docker-tests-eth", + "docker-tests-slp", + "docker-tests-sia", + "docker-tests-ordermatch", + "docker-tests-swaps", + "docker-tests-watchers", + "docker-tests-qrc20", + "docker-tests-tendermint", + "docker-tests-zcoin", + "docker-tests-integration", +] default = [] trezor-udp = ["crypto/trezor-udp"] # use for tests to connect to trezor emulator over udp run-device-tests = [] -sepolia-maker-swap-v2-tests = [] -sepolia-taker-swap-v2-tests = [] test-ext-api = ["trading_api/test-ext-api"] new-db-arch = ["mm2_core/new-db-arch"] # A temporary feature to integrate the new db architecture incrementally # Temporary feature for implementing IBC wrap/unwrap mechanism and will be removed diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 9663350e1d..a3fb5e0b6a 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -1576,7 +1576,6 @@ impl MakerSwap { search_from_block: taker_coin_start_block, swap_contract_address: &taker_coin_swap_contract_address, swap_unique_data: &unique_data, - watcher_reward, }; // check if the taker payment is not spent yet match selfi.taker_coin.search_for_swap_tx_spend_other(search_input).await { @@ -1671,7 +1670,6 @@ impl MakerSwap { search_from_block: maker_coin_start_block, swap_contract_address: &maker_coin_swap_contract_address, swap_unique_data: &unique_data, - watcher_reward, }; // validate that maker payment is not spent match self.maker_coin.search_for_swap_tx_spend_my(search_input).await { diff --git a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs index 7ae7deb3e1..30b3823206 100644 --- a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs +++ b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs @@ -489,8 +489,7 @@ async fn convert_maker_to_taker_events( return events; }, MakerSwapEvent::TakerPaymentSpent(tx_ident) => { - //Is the watcher_reward argument important here? - let secret = match maker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex, false).await { + let secret = match maker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex).await { Ok(secret) => H256Json::from(secret), Err(e) => { push_event!(TakerSwapEvent::TakerPaymentWaitForSpendFailed(ERRL!("{}", e).into())); @@ -571,7 +570,7 @@ mod tests { #[test] fn test_recreate_taker_swap() { - TestCoin::extract_secret.mock_safe(|_coin, _secret_hash, _spend_tx, _watcher_reward| { + TestCoin::extract_secret.mock_safe(|_coin, _secret_hash, _spend_tx| { let secret = <[u8; 32]>::from_hex("23a6bb64bc0ab2cc14cb84277d8d25134b814e5f999c66e578c9bba3c5e2d3a4").unwrap(); MockResult::Return(Box::pin(async move { Ok(secret) })) diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 758ad57204..7861b06ea9 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -396,7 +396,7 @@ impl State for WaitForTakerPaymentSpend { let tx_hex = tx.tx_hex(); let secret = match watcher_ctx .taker_coin - .extract_secret(&watcher_ctx.data.secret_hash, &tx_hex, true) + .extract_secret(&watcher_ctx.data.secret_hash, &tx_hex) .await { Ok(secret) => H256Json::from(secret), diff --git a/mm2src/mm2_main/src/lp_swap/taker_restart.rs b/mm2src/mm2_main/src/lp_swap/taker_restart.rs index 6e9f3624d2..7d712e1931 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_restart.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_restart.rs @@ -91,7 +91,6 @@ pub async fn check_maker_payment_spend_and_add_event( None => return ERR!("No info about maker payment, swap is not recoverable"), }; let unique_data = swap.unique_swap_data(); - let watcher_reward = swap.r().watcher_reward; let maker_payment_spend_tx = match swap.maker_coin .search_for_swap_tx_spend_other(SearchForSwapTxSpendInput { @@ -102,7 +101,6 @@ pub async fn check_maker_payment_spend_and_add_event( search_from_block: maker_coin_start_block, swap_contract_address: &maker_coin_swap_contract_address, swap_unique_data: &unique_data, - watcher_reward, }) .await { Ok(Some(FoundSwapTxSpend::Spent(maker_payment_spend_tx))) => maker_payment_spend_tx, @@ -159,7 +157,6 @@ pub async fn check_taker_payment_spend(swap: &TakerSwap) -> Result Result Result<(), String> { let secret_hash = swap.r().secret_hash.0.clone(); - let watcher_reward = swap.r().watcher_reward; let tx_hash = taker_payment_spend_tx.tx_hash_as_bytes(); info!("Taker payment spend tx {:02x}", tx_hash); @@ -189,11 +184,7 @@ pub async fn add_taker_payment_spent_event( tx_hex: Bytes::from(taker_payment_spend_tx.tx_hex()), tx_hash, }; - let secret = match swap - .taker_coin - .extract_secret(&secret_hash, &tx_ident.tx_hex, watcher_reward) - .await - { + let secret = match swap.taker_coin.extract_secret(&secret_hash, &tx_ident.tx_hex).await { Ok(secret) => H256::from(secret), Err(_) => { return ERR!("Could not extract secret from taker payment spend transaction"); diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 62ebc2ceb0..c70a68b77d 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -597,7 +597,6 @@ pub struct TakerSwapMut { pub secret_hash: BytesJson, secret: H256Json, pub watcher_reward: bool, - reward_amount: Option, payment_instructions: Option, } @@ -839,6 +838,14 @@ impl TakerSwap { self.r().data.taker_payment_lock + 3700 } + #[inline] + fn watcher_reward_amount(&self) -> Option { + match &self.r().payment_instructions { + Some(PaymentInstructions::WatcherReward(reward)) => Some(reward.clone()), + _ => None, + } + } + pub(crate) fn apply_event(&self, event: TakerSwapEvent) { match event { TakerSwapEvent::Started(data) => { @@ -976,7 +983,6 @@ impl TakerSwap { secret_hash: BytesJson::default(), secret: H256Json::default(), watcher_reward: false, - reward_amount: None, payment_instructions: None, }), ctx, @@ -1551,7 +1557,7 @@ impl TakerSwap { } info!("After wait confirm"); - let reward_amount = self.r().reward_amount.clone(); + let reward_amount = self.watcher_reward_amount(); let wait_maker_payment_until = self.r().data.maker_payment_wait; let watcher_reward = if self.r().watcher_reward { match self @@ -1642,7 +1648,7 @@ impl TakerSwap { return Ok(None); } - let reward_amount = self.r().reward_amount.clone(); + let reward_amount = self.watcher_reward_amount(); self.taker_coin .get_taker_watcher_reward( &self.maker_coin, @@ -1940,11 +1946,7 @@ impl TakerSwap { tx_hash, }; - let secret = match self - .taker_coin - .extract_secret(&secret_hash.0, &tx_ident.tx_hex, watcher_reward) - .await - { + let secret = match self.taker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex).await { Ok(secret) => H256Json::from(secret), Err(e) => { return Ok(( @@ -2331,7 +2333,6 @@ impl TakerSwap { search_from_block: maker_coin_start_block, swap_contract_address: &maker_coin_swap_contract_address, swap_unique_data: &unique_data, - watcher_reward, }; match self.maker_coin.search_for_swap_tx_spend_other(search_input).await { @@ -2436,7 +2437,6 @@ impl TakerSwap { search_from_block: taker_coin_start_block, swap_contract_address: &taker_coin_swap_contract_address, swap_unique_data: &unique_data, - watcher_reward, }; let taker_payment_spend = try_s!(self.taker_coin.search_for_swap_tx_spend_my(search_input).await); @@ -2446,11 +2446,7 @@ impl TakerSwap { check_maker_payment_is_not_spent!(); let secret_hash = self.r().secret_hash.clone(); let tx_hex = tx.tx_hex(); - let secret = try_s!( - self.taker_coin - .extract_secret(&secret_hash.0, &tx_hex, watcher_reward) - .await - ); + let secret = try_s!(self.taker_coin.extract_secret(&secret_hash.0, &tx_hex).await); let taker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &maker_payment, @@ -3098,7 +3094,7 @@ mod taker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - TestCoin::extract_secret.mock_safe(|_, _, _, _| MockResult::Return(Box::pin(async move { Ok([0; 32]) }))); + TestCoin::extract_secret.mock_safe(|_, _, _| MockResult::Return(Box::pin(async move { Ok([0; 32]) }))); static MY_PAYMENT_SENT_CALLED: AtomicBool = AtomicBool::new(false); TestCoin::check_if_my_payment_sent.mock_safe(|_, _| { @@ -3225,7 +3221,7 @@ mod taker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - TestCoin::extract_secret.mock_safe(|_, _, _, _| MockResult::Return(Box::pin(async move { Ok([0; 32]) }))); + TestCoin::extract_secret.mock_safe(|_, _, _| MockResult::Return(Box::pin(async move { Ok([0; 32]) }))); static SEARCH_TX_SPEND_CALLED: AtomicBool = AtomicBool::new(false); TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _| { diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 9c36e9698a..77fc2f2d79 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -27,7 +27,10 @@ forgetting_copy_types, clippy::swap_ptr_to_ref, clippy::forget_non_drop, - clippy::let_unit_value + clippy::let_unit_value, + // TODO: Remove this allow when Rust 1.92 regression is fixed. + // See: https://github.com/rust-lang/rust/issues/147648 + unused_assignments )] #![cfg_attr(target_arch = "wasm32", allow(dead_code))] #![cfg_attr(target_arch = "wasm32", allow(unused_imports))] diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 7fd01eaf43..506cdf939a 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -2455,6 +2455,22 @@ fn remove_order(ctx: &MmArc, uuid: Uuid) { }; } +/// Remove multiple orders in a single batch to avoid multiple synchronous flushes. +/// This is faster than calling remove_order() in a loop. +fn remove_orders_batch(ctx: &MmArc, uuids: impl IntoIterator) { + let ordermatch_ctx = OrdermatchContext::from_ctx(ctx).unwrap(); + let ops: Vec<_> = { + let mut orderbook = ordermatch_ctx.orderbook.lock(); + uuids + .into_iter() + .filter_map(|uuid| orderbook.index_remove(uuid).map(|(_removed, op)| op)) + .collect() + }; + if !ops.is_empty() { + let _ = ordermatch_ctx.trie_ops_tx.unbounded_send(ops); + } +} + /// Wait until the background trie worker has applied all pending ops. fn flush_trie(ctx: &MmArc) { let ordermatch_ctx = OrdermatchContext::from_ctx(ctx).unwrap(); @@ -2557,12 +2573,11 @@ fn test_process_sync_pubkey_orderbook_state_after_orders_removed() { let mut old_mem_db = clone_orderbook_memory_db(&ctx); - // pick 10 orders at random and remove them + // pick 10 orders at random and remove them in a single batch + // (batching avoids 10 synchronous flushes which can exceed the 3s history timeout on slow CI) let mut rng = thread_rng(); - let to_remove = orders.choose_multiple(&mut rng, 10); - for order in to_remove { - remove_order(&ctx, order.uuid); - } + let to_remove: Vec = orders.choose_multiple(&mut rng, 10).map(|o| o.uuid).collect(); + remove_orders_batch(&ctx, to_remove); flush_trie(&ctx); let mut result = process_sync_pubkey_orderbook_state(ctx.clone(), pubkey.clone(), prev_pairs_state) diff --git a/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs b/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs index c852e2b638..df2604cab2 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs @@ -1,21 +1,16 @@ -use crate::docker_tests::docker_tests_common::{generate_utxo_coin_with_privkey, GETH_RPC_URL}; -use crate::docker_tests::eth_docker_tests::{fill_eth_erc20_with_private_key, swap_contract}; +use crate::docker_tests::helpers::utxo::generate_utxo_coin_with_random_privkey; use crate::integration_tests_common::enable_native; -use crate::{generate_utxo_coin_with_random_privkey, random_secp256k1_secret}; use common::block_on; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::OrderbookResponse; -use mm2_test_helpers::for_tests::{ - best_orders_v2, best_orders_v2_by_number, enable_eth_coin, eth_dev_conf, mm_dump, my_balance, mycoin1_conf, - mycoin_conf, MarketMakerIt, Mm2TestConf, -}; +use mm2_test_helpers::for_tests::{mm_dump, MarketMakerIt}; use mm2_test_helpers::structs::{ BestOrdersResponse, BestOrdersV2Response, BuyOrSellRpcResult, MyOrdersRpcResult, OrderbookDepthResponse, RpcV2Response, SetPriceResponse, }; -use serde_json::Value as Json; +use serde_json::{json, Value as Json}; use std::thread; use std::time::Duration; @@ -731,447 +726,10 @@ fn test_ordermatch_custom_orderbook_ticker_mixed_case_two() { block_on(mm_alice.stop()).unwrap(); } -fn get_bob_alice() -> (MarketMakerIt, MarketMakerIt) { - let bob_priv_key = random_secp256k1_secret(); - let alice_priv_key = random_secp256k1_secret(); - - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); - generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), bob_priv_key); - fill_eth_erc20_with_private_key(bob_priv_key); - - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); - generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), alice_priv_key); - fill_eth_erc20_with_private_key(alice_priv_key); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000), eth_dev_conf(),]); - - let bob_conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(bob_priv_key)), &coins); - let mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); - - let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); - log!("Bob log path: {}", mm_bob.log_path.display()); - - let alice_conf = Mm2TestConf::light_node( - &format!("0x{}", hex::encode(alice_priv_key)), - &coins, - &[&mm_bob.ip.to_string()], - ); - let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); - - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - dbg!(block_on(enable_eth_coin( - &mm_alice, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - - (mm_bob, mm_alice) -} - -#[test] -fn test_best_orders() { - let (mut mm_bob, mm_alice) = get_bob_alice(); - - // issue sell request on Bob side by setting base/rel price - log!("Issue bob sell requests"); - - let bob_orders = [ - // (base, rel, price, volume, min_volume) - ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), - ("MYCOIN", "MYCOIN1", "0.8", "0.9", None), - ("MYCOIN", "MYCOIN1", "0.7", "0.9", Some("0.9")), - ("MYCOIN", "ETH", "0.8", "0.9", None), - ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), - ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), - ("ETH", "MYCOIN", "0.8", "0.9", None), - ("MYCOIN1", "ETH", "0.8", "0.8", None), - ("MYCOIN1", "ETH", "0.7", "0.8", Some("0.8")), - ]; - for (base, rel, price, volume, min_volume) in bob_orders.iter() { - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": base, - "rel": rel, - "price": price, - "volume": volume, - "min_volume": min_volume.unwrap_or("0.00777"), - "cancel_previous": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - } - - block_on(mm_bob.wait_for_log(22., |log| { - log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") - })) - .unwrap(); - - let rc = block_on(mm_alice.rpc(&json! ({ - "userpass": mm_alice.userpass, - "method": "best_orders", - "coin": "MYCOIN", - "action": "buy", - "volume": "0.1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!best_orders: {}", rc.1); - let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); - let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap(); - assert_eq!(1, best_mycoin1_orders.len()); - let expected_price: BigDecimal = "0.8".parse().unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[0].price); - - let rc = block_on(mm_alice.rpc(&json! ({ - "userpass": mm_alice.userpass, - "method": "best_orders", - "coin": "MYCOIN", - "action": "buy", - "volume": "1.7", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!best_orders: {}", rc.1); - let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); - // MYCOIN1 - let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap(); - let expected_price: BigDecimal = "0.7".parse().unwrap(); - let bob_mycoin1_addr = block_on(my_balance(&mm_bob, "MYCOIN1")).address; - // let bob_mycoin1_addr = mm_bob.display_address("MYCOIN1").unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[0].price); - assert_eq!(bob_mycoin1_addr, best_mycoin1_orders[0].address); - let expected_price: BigDecimal = "0.8".parse().unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[1].price); - assert_eq!(bob_mycoin1_addr, best_mycoin1_orders[1].address); - // ETH - let expected_price: BigDecimal = "0.8".parse().unwrap(); - let best_eth_orders = response.result.get("ETH").unwrap(); - assert_eq!(expected_price, best_eth_orders[0].price); - - let rc = block_on(mm_alice.rpc(&json! ({ - "userpass": mm_alice.userpass, - "method": "best_orders", - "coin": "MYCOIN", - "action": "sell", - "volume": "0.1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!best_orders: {}", rc.1); - let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); - - let expected_price: BigDecimal = "1.25".parse().unwrap(); - - let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[0].price); - assert_eq!(1, best_mycoin1_orders.len()); - - let best_eth_orders = response.result.get("ETH").unwrap(); - assert_eq!(expected_price, best_eth_orders[0].price); - - let rc = block_on(mm_alice.rpc(&json! ({ - "userpass": mm_alice.userpass, - "method": "best_orders", - "coin": "ETH", - "action": "sell", - "volume": "0.1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!best_orders: {}", rc.1); - let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); - - let expected_price: BigDecimal = "1.25".parse().unwrap(); - - let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[0].price); - assert_eq!("MYCOIN1", best_mycoin1_orders[0].coin); - assert_eq!(1, best_mycoin1_orders.len()); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_best_orders_v2_by_number() { - let (mut mm_bob, mm_alice) = get_bob_alice(); - - // issue sell request on Bob side by setting base/rel price - log!("Issue bob sell requests"); - - let bob_orders = [ - // (base, rel, price, volume, min_volume) - ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), - ("MYCOIN", "MYCOIN1", "0.8", "0.9", None), - ("MYCOIN", "MYCOIN1", "0.7", "0.9", Some("0.9")), - ("MYCOIN", "ETH", "0.8", "0.9", None), - ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), - ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), - ("ETH", "MYCOIN", "0.8", "0.9", None), - ("MYCOIN1", "ETH", "0.8", "0.8", None), - ("MYCOIN1", "ETH", "0.7", "0.8", Some("0.8")), - ]; - for (base, rel, price, volume, min_volume) in bob_orders.iter() { - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": base, - "rel": rel, - "price": price, - "volume": volume, - "min_volume": min_volume.unwrap_or("0.00777"), - "cancel_previous": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - } - - block_on(mm_bob.wait_for_log(22., |log| { - log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") - })) - .unwrap(); - - let response = block_on(best_orders_v2_by_number(&mm_alice, "MYCOIN", "buy", 1, false)); - log!("response {response:?}"); - let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); - log!("Best MYCOIN1 orders when buy MYCOIN {:?}", [best_mycoin1_orders]); - assert_eq!(1, best_mycoin1_orders.len()); - let expected_price: BigDecimal = "0.7".parse().unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); - - let response = block_on(best_orders_v2_by_number(&mm_alice, "MYCOIN", "buy", 2, false)); - log!("response {response:?}"); - let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); - log!("Best MYCOIN1 orders when buy MYCOIN {:?}", [best_mycoin1_orders]); - assert_eq!(2, best_mycoin1_orders.len()); - let expected_price: BigDecimal = "0.7".parse().unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); - let expected_price: BigDecimal = "0.8".parse().unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[1].price.decimal); - - let response = block_on(best_orders_v2_by_number(&mm_alice, "MYCOIN", "sell", 1, false)); - log!("response {response:?}"); - let expected_price: BigDecimal = "1.25".parse().unwrap(); - let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); - log!("Best MYCOIN1 orders when sell MYCOIN {:?}", [best_mycoin1_orders]); - assert_eq!(1, best_mycoin1_orders.len()); - assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); - let best_eth_orders = response.result.orders.get("ETH").unwrap(); - log!("Best ETH orders when sell MYCOIN {:?}", [best_eth_orders]); - assert_eq!(1, best_eth_orders.len()); - assert_eq!(expected_price, best_eth_orders[0].price.decimal); - - let response = block_on(best_orders_v2_by_number(&mm_alice, "ETH", "sell", 1, false)); - log!("response {response:?}"); - let best_mycoin_orders = response.result.orders.get("MYCOIN").unwrap(); - log!("Best MYCOIN orders when sell ETH {:?}", [best_mycoin_orders]); - assert_eq!(1, best_mycoin_orders.len()); - let expected_price: BigDecimal = "1.25".parse().unwrap(); - assert_eq!(expected_price, best_mycoin_orders[0].price.decimal); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_best_orders_v2_by_volume() { - let (mut mm_bob, mm_alice) = get_bob_alice(); - - // issue sell request on Bob side by setting base/rel price - log!("Issue bob sell requests"); - - let bob_orders = [ - // (base, rel, price, volume, min_volume) - ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), - ("MYCOIN", "MYCOIN1", "0.8", "0.9", None), - ("MYCOIN", "MYCOIN1", "0.7", "0.9", Some("0.9")), - ("MYCOIN", "ETH", "0.8", "0.9", None), - ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), - ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), - ("ETH", "MYCOIN", "0.8", "0.9", None), - ("MYCOIN1", "ETH", "0.8", "0.8", None), - ("MYCOIN1", "ETH", "0.7", "0.8", Some("0.8")), - ]; - for (base, rel, price, volume, min_volume) in bob_orders.iter() { - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": base, - "rel": rel, - "price": price, - "volume": volume, - "min_volume": min_volume.unwrap_or("0.00777"), - "cancel_previous": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - } - - block_on(mm_bob.wait_for_log(22., |log| { - log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") - })) - .unwrap(); - - let response = block_on(best_orders_v2(&mm_alice, "MYCOIN", "buy", "1.7")); - log!("response {response:?}"); - // MYCOIN1 - let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); - log!("Best MYCOIN1 orders when buy MYCOIN {:?}", [best_mycoin1_orders]); - let expected_price: BigDecimal = "0.7".parse().unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); - let expected_price: BigDecimal = "0.8".parse().unwrap(); - assert_eq!(expected_price, best_mycoin1_orders[1].price.decimal); - // ETH - let expected_price: BigDecimal = "0.8".parse().unwrap(); - let best_eth_orders = response.result.orders.get("ETH").unwrap(); - log!("Best ETH orders when buy MYCOIN {:?}", [best_eth_orders]); - assert_eq!(expected_price, best_eth_orders[0].price.decimal); - - let response = block_on(best_orders_v2(&mm_alice, "MYCOIN", "sell", "0.1")); - log!("response {response:?}"); - let expected_price: BigDecimal = "1.25".parse().unwrap(); - let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); - log!("Best MYCOIN1 orders when sell MYCOIN {:?}", [best_mycoin1_orders]); - assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); - assert_eq!(1, best_mycoin1_orders.len()); - let best_eth_orders = response.result.orders.get("ETH").unwrap(); - log!("Best ETH orders when sell MYCOIN {:?}", [best_mycoin1_orders]); - assert_eq!(expected_price, best_eth_orders[0].price.decimal); - - let response = block_on(best_orders_v2(&mm_alice, "ETH", "sell", "0.1")); - log!("response {response:?}"); - let expected_price: BigDecimal = "1.25".parse().unwrap(); - let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); - log!("Best MYCOIN1 orders when sell ETH {:?}", [best_mycoin1_orders]); - assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); - assert_eq!("MYCOIN1", best_mycoin1_orders[0].coin); - assert_eq!(1, best_mycoin1_orders.len()); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_best_orders_filter_response() { - // alice defined MYCOIN1 as "wallet_only" in config - let alice_coins = json!([ - mycoin_conf(1000), - {"coin":"MYCOIN1","asset":"MYCOIN1","rpcport":11608,"wallet_only": true,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - eth_dev_conf(), - ]); - - let bob_priv_key = random_secp256k1_secret(); - let alice_priv_key = random_secp256k1_secret(); - - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); - generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), bob_priv_key); - fill_eth_erc20_with_private_key(bob_priv_key); - - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); - generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), alice_priv_key); - fill_eth_erc20_with_private_key(alice_priv_key); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000), eth_dev_conf(),]); - - let bob_conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(bob_priv_key)), &coins); - let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); - - let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); - log!("Bob log path: {}", mm_bob.log_path.display()); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - - // issue sell request on Bob side by setting base/rel price - log!("Issue bob sell requests"); - - let bob_orders = [ - // (base, rel, price, volume, min_volume) - ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), - ("MYCOIN", "MYCOIN1", "0.8", "0.9", None), - ("MYCOIN", "MYCOIN1", "0.7", "0.9", Some("0.9")), - ("MYCOIN", "ETH", "0.8", "0.9", None), - ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), - ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), - ("ETH", "MYCOIN", "0.8", "0.9", None), - ("MYCOIN1", "ETH", "0.8", "0.8", None), - ("MYCOIN1", "ETH", "0.7", "0.8", Some("0.8")), - ]; - for (base, rel, price, volume, min_volume) in bob_orders.iter() { - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": base, - "rel": rel, - "price": price, - "volume": volume, - "min_volume": min_volume.unwrap_or("0.00777"), - "cancel_previous": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - } - - let alice_conf = Mm2TestConf::light_node( - &format!("0x{}", hex::encode(alice_priv_key)), - &alice_coins, - &[&mm_bob.ip.to_string()], - ); - let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); - - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - - block_on(mm_bob.wait_for_log(22., |log| { - log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") - })) - .unwrap(); - - let rc = block_on(mm_alice.rpc(&json! ({ - "userpass": mm_alice.userpass, - "method": "best_orders", - "coin": "MYCOIN", - "action": "buy", - "volume": "0.1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!best_orders: {}", rc.1); - let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); - let empty_vec = Vec::new(); - let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap_or(&empty_vec); - assert_eq!(0, best_mycoin1_orders.len()); - let best_eth_orders = response.result.get("ETH").unwrap(); - assert_eq!(1, best_eth_orders.len()); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} +// ============================================================================= +// UTXO-only Orderbook Zombie Tests +// These tests verify order lifecycle and state management using only UTXO coins +// ============================================================================= // https://github.com/KomodoPlatform/atomicDEX-API/issues/1148 // here 'zombie' means 'unusable order' diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs deleted file mode 100644 index 514532f31f..0000000000 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ /dev/null @@ -1,1773 +0,0 @@ -use super::eth_docker_tests::{erc20_contract_checksum, fill_eth, fill_eth_erc20_with_private_key, swap_contract}; -use super::z_coin_docker_tests::z_coin_from_spending_key; -use bitcrypto::{dhash160, ChecksumType}; -use chain::TransactionOutput; -use coins::eth::addr_from_raw_pubkey; -use coins::qrc20::rpc_clients::for_tests::Qrc20NativeWalletOps; -use coins::qrc20::{qrc20_coin_with_priv_key, Qrc20ActivationParams, Qrc20Coin}; -use coins::utxo::bch::{bch_coin_with_priv_key, BchActivationRequest, BchCoin}; -use coins::utxo::qtum::{qtum_coin_with_priv_key, QtumBasedCoin, QtumCoin}; -use coins::utxo::rpc_clients::{NativeClient, UtxoRpcClientEnum, UtxoRpcClientOps}; -use coins::utxo::slp::{slp_genesis_output, SlpOutput, SlpToken}; -use coins::utxo::utxo_common::send_outputs_from_my_address; -use coins::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; -use coins::utxo::{ - coin_daemon_data_dir, sat_from_big_decimal, zcash_params_path, UtxoActivationParams, UtxoAddressFormat, - UtxoCoinFields, UtxoCommonOps, -}; -use coins::z_coin::ZCoin; -use coins::{ConfirmPaymentInput, MarketCoinOps, Transaction}; -use common::executor::Timer; -use common::Future01CompatExt; -pub use common::{block_on, block_on_f01, now_ms, now_sec, wait_until_ms, wait_until_sec}; -use crypto::privkey::{key_pair_from_secret, key_pair_from_seed}; -use crypto::Secp256k1Secret; -use ethabi::Token; -use ethereum_types::{H160 as H160Eth, U256}; -use futures::TryFutureExt; -use http::StatusCode; -use keys::{ - Address, AddressBuilder, AddressHashEnum, AddressPrefix, KeyPair, NetworkAddressPrefixes, - NetworkPrefix as CashAddrPrefix, -}; -use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; -use mm2_number::BigDecimal; -pub use mm2_number::MmNumber; -use mm2_rpc::data::legacy::BalanceResponse; -pub use mm2_test_helpers::for_tests::{ - check_my_swap_status, check_recent_swaps, enable_eth_coin, enable_native, enable_native_bch, erc20_dev_conf, - eth_dev_conf, mm_dump, wait_check_stats_swap_status, MarketMakerIt, -}; -use mm2_test_helpers::get_passphrase; -use mm2_test_helpers::structs::TransactionDetails; -use primitives::hash::{H160, H256}; -use script::Builder; -use secp256k1::Secp256k1; -pub use secp256k1::{PublicKey, SecretKey}; -use serde_json::{self as json, Value as Json}; -pub use std::cell::Cell; -use std::convert::TryFrom; -use std::process::{Command, Stdio}; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use std::str::FromStr; -pub use std::{env, thread}; -use std::{path::PathBuf, sync::Mutex, time::Duration}; -use testcontainers::core::Mount; -use testcontainers::runners::SyncRunner; -use testcontainers::{core::WaitFor, Container, GenericImage, RunnableImage}; -use tokio::sync::Mutex as AsyncMutex; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use web3::types::Address as EthAddress; -use web3::types::{BlockId, BlockNumber, TransactionRequest}; -use web3::{transports::Http, Web3}; - -lazy_static! { - static ref MY_COIN_LOCK: AsyncMutex<()> = AsyncMutex::new(()); - static ref MY_COIN1_LOCK: AsyncMutex<()> = AsyncMutex::new(()); - static ref QTUM_LOCK: AsyncMutex<()> = AsyncMutex::new(()); - static ref FOR_SLP_LOCK: AsyncMutex<()> = AsyncMutex::new(()); - pub static ref SLP_TOKEN_ID: Mutex = Mutex::new(H256::default()); - // Private keys supplied with 1000 SLP tokens on tests initialization. - // Due to the SLP protocol limitations only 19 outputs (18 + change) can be sent in one transaction, which is sufficient for now though. - // Supply more privkeys when 18 will be not enough. - pub static ref SLP_TOKEN_OWNERS: Mutex> = Mutex::new(Vec::with_capacity(18)); - pub static ref MM_CTX: MmArc = MmCtxBuilder::new().with_conf(json!({"coins":[eth_dev_conf()],"use_trading_proto_v2": true})).into_mm_arc(); - /// We need a second `MmCtx` instance when we use the same private keys for Maker and Taker across various tests. - /// When enabling coins for both Maker and Taker, two distinct coin instances are created. - /// This means that different instances of the same coin should have separate global nonce locks. - /// Utilizing different `MmCtx` instances allows us to assign Maker and Taker coins to separate `CoinsCtx`. - /// This approach addresses the `replacement transaction` issue, which occurs when different transactions share the same nonce. - pub static ref MM_CTX1: MmArc = MmCtxBuilder::new().with_conf(json!({"use_trading_proto_v2": true})).into_mm_arc(); - pub static ref GETH_WEB3: Web3 = Web3::new(Http::new(GETH_RPC_URL).unwrap()); - // Mutex used to prevent nonce re-usage during funding addresses used in tests - pub static ref GETH_NONCE_LOCK: Mutex<()> = Mutex::new(()); -} - -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -lazy_static! { - pub static ref SEPOLIA_WEB3: Web3 = Web3::new(Http::new(SEPOLIA_RPC_URL).unwrap()); - pub static ref SEPOLIA_NONCE_LOCK: Mutex<()> = Mutex::new(()); - pub static ref SEPOLIA_TESTS_LOCK: Mutex<()> = Mutex::new(()); -} - -pub static mut QICK_TOKEN_ADDRESS: Option = None; -pub static mut QORTY_TOKEN_ADDRESS: Option = None; -pub static mut QRC20_SWAP_CONTRACT_ADDRESS: Option = None; -pub static mut QTUM_CONF_PATH: Option = None; -/// The account supplied with ETH on Geth dev node creation -pub static mut GETH_ACCOUNT: H160Eth = H160Eth::zero(); -/// ERC20 token address on Geth dev node -pub static mut GETH_ERC20_CONTRACT: H160Eth = H160Eth::zero(); -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -pub static mut SEPOLIA_ERC20_CONTRACT: H160Eth = H160Eth::zero(); -/// Swap contract address on Geth dev node -pub static mut GETH_SWAP_CONTRACT: H160Eth = H160Eth::zero(); -/// Maker Swap V2 contract address on Geth dev node -pub static mut GETH_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); -/// Taker Swap V2 contract address on Geth dev node -pub static mut GETH_TAKER_SWAP_V2: H160Eth = H160Eth::zero(); -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -pub static mut SEPOLIA_TAKER_SWAP_V2: H160Eth = H160Eth::zero(); -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -pub static mut SEPOLIA_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); -/// Swap contract (with watchers support) address on Geth dev node -pub static mut GETH_WATCHERS_SWAP_CONTRACT: H160Eth = H160Eth::zero(); -/// ERC721 token address on Geth dev node -pub static mut GETH_ERC721_CONTRACT: H160Eth = H160Eth::zero(); -/// ERC1155 token address on Geth dev node -pub static mut GETH_ERC1155_CONTRACT: H160Eth = H160Eth::zero(); -/// NFT Maker Swap V2 contract address on Geth dev node -pub static mut GETH_NFT_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -/// NFT Maker Swap V2 contract address on Sepolia testnet -pub static mut SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2: H160Eth = H160Eth::zero(); -pub static GETH_RPC_URL: &str = "http://127.0.0.1:8545"; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -pub static SEPOLIA_RPC_URL: &str = "https://ethereum-sepolia-rpc.publicnode.com"; -/// SIA daemon RPC connection parameters -pub static SIA_RPC_PARAMS: (&str, u16, &str) = ("127.0.0.1", 9980, "password"); - -// use thread local to affect only the current running test -thread_local! { - /// Set test dex pubkey as Taker (to check DexFee::NoFee) - pub static SET_BURN_PUBKEY_TO_ALICE: Cell = const { Cell::new(false) }; -} - -pub const UTXO_ASSET_DOCKER_IMAGE: &str = "docker.io/artempikulin/testblockchain"; -pub const UTXO_ASSET_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/artempikulin/testblockchain:multiarch"; -pub const GETH_DOCKER_IMAGE: &str = "docker.io/ethereum/client-go"; -pub const GETH_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/ethereum/client-go:stable"; -pub const ZOMBIE_ASSET_DOCKER_IMAGE: &str = "docker.io/borngraced/zombietestrunner"; -pub const ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/borngraced/zombietestrunner:multiarch"; - -pub const SIA_DOCKER_IMAGE: &str = "ghcr.io/siafoundation/walletd"; -pub const SIA_DOCKER_IMAGE_WITH_TAG: &str = "ghcr.io/siafoundation/walletd:latest"; - -pub const NUCLEUS_IMAGE: &str = "docker.io/komodoofficial/nucleusd"; -pub const ATOM_IMAGE_WITH_TAG: &str = "docker.io/komodoofficial/gaiad:kdf-ci"; -pub const IBC_RELAYER_IMAGE_WITH_TAG: &str = "docker.io/komodoofficial/ibc-relayer:kdf-ci"; - -pub const QTUM_ADDRESS_LABEL: &str = "MM2_ADDRESS_LABEL"; - -/// ERC721_TEST_TOKEN has additional mint function -/// https://github.com/KomodoPlatform/etomic-swap/blob/public-mint-nft-functions/contracts/Erc721Token.sol (see public-mint-nft-functions branch) -pub const ERC721_TEST_ABI: &str = include_str!("../../../mm2_test_helpers/dummy_files/erc721_test_abi.json"); -/// ERC1155_TEST_TOKEN has additional mint function -/// https://github.com/KomodoPlatform/etomic-swap/blob/public-mint-nft-functions/contracts/Erc1155Token.sol (see public-mint-nft-functions branch) -pub const ERC1155_TEST_ABI: &str = include_str!("../../../mm2_test_helpers/dummy_files/erc1155_test_abi.json"); - -/// Ticker of MYCOIN dockerized blockchain. -pub const MYCOIN: &str = "MYCOIN"; -/// Ticker of MYCOIN1 dockerized blockchain. -pub const MYCOIN1: &str = "MYCOIN1"; - -pub const ERC20_TOKEN_BYTES: &str = include_str!("../../../mm2_test_helpers/contract_bytes/erc20_token_bytes"); -pub const SWAP_CONTRACT_BYTES: &str = include_str!("../../../mm2_test_helpers/contract_bytes/swap_contract_bytes"); -pub const WATCHERS_SWAP_CONTRACT_BYTES: &str = - include_str!("../../../mm2_test_helpers/contract_bytes/watchers_swap_contract_bytes"); -/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/Erc721Token.sol -pub const ERC721_TEST_TOKEN_BYTES: &str = - include_str!("../../../mm2_test_helpers/contract_bytes/erc721_test_token_bytes"); -/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/Erc1155Token.sol -pub const ERC1155_TEST_TOKEN_BYTES: &str = - include_str!("../../../mm2_test_helpers/contract_bytes/erc1155_test_token_bytes"); -/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/EtomicSwapMakerNftV2.sol -pub const NFT_MAKER_SWAP_V2_BYTES: &str = - include_str!("../../../mm2_test_helpers/contract_bytes/nft_maker_swap_v2_bytes"); -/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/EtomicSwapMakerV2.sol -pub const MAKER_SWAP_V2_BYTES: &str = include_str!("../../../mm2_test_helpers/contract_bytes/maker_swap_v2_bytes"); -/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/EtomicSwapTakerV2.sol -pub const TAKER_SWAP_V2_BYTES: &str = include_str!("../../../mm2_test_helpers/contract_bytes/taker_swap_v2_bytes"); - -pub trait CoinDockerOps { - fn rpc_client(&self) -> &UtxoRpcClientEnum; - - fn native_client(&self) -> &NativeClient { - match self.rpc_client() { - UtxoRpcClientEnum::Native(native) => native, - _ => panic!("UtxoRpcClientEnum::Native is expected"), - } - } - - fn wait_ready(&self, expected_tx_version: i32) { - let timeout = wait_until_ms(120000); - loop { - match block_on_f01(self.rpc_client().get_block_count()) { - Ok(n) => { - if n > 1 { - if let UtxoRpcClientEnum::Native(client) = self.rpc_client() { - let hash = block_on_f01(client.get_block_hash(n)).unwrap(); - let block = block_on_f01(client.get_block(hash)).unwrap(); - let coinbase = block_on_f01(client.get_verbose_transaction(&block.tx[0])).unwrap(); - log!("Coinbase tx {:?} in block {}", coinbase, n); - if coinbase.version == expected_tx_version { - break; - } - } - } - }, - Err(e) => log!("{:?}", e), - } - assert!(now_ms() < timeout, "Test timed out"); - thread::sleep(Duration::from_secs(1)); - } - } -} - -pub struct UtxoAssetDockerOps { - #[allow(dead_code)] - ctx: MmArc, - coin: UtxoStandardCoin, -} - -impl CoinDockerOps for UtxoAssetDockerOps { - fn rpc_client(&self) -> &UtxoRpcClientEnum { - &self.coin.as_ref().rpc_client - } -} - -impl UtxoAssetDockerOps { - pub fn from_ticker(ticker: &str) -> UtxoAssetDockerOps { - let conf = json!({"coin": ticker, "asset": ticker, "txfee": 1000, "network": "regtest"}); - let req = json!({"method":"enable"}); - let priv_key = Secp256k1Secret::from("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f"); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - - let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); - UtxoAssetDockerOps { ctx, coin } - } -} - -pub struct ZCoinAssetDockerOps { - #[allow(dead_code)] - ctx: MmArc, - coin: ZCoin, -} - -impl CoinDockerOps for ZCoinAssetDockerOps { - fn rpc_client(&self) -> &UtxoRpcClientEnum { - &self.coin.as_ref().rpc_client - } -} - -impl ZCoinAssetDockerOps { - pub fn new() -> ZCoinAssetDockerOps { - let (ctx, coin) = block_on(z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe")); - - ZCoinAssetDockerOps { ctx, coin } - } -} - -pub struct BchDockerOps { - #[allow(dead_code)] - ctx: MmArc, - coin: BchCoin, -} - -impl BchDockerOps { - pub fn from_ticker(ticker: &str) -> BchDockerOps { - let conf = - json!({"coin": ticker,"asset": ticker,"txfee":1000,"network": "regtest","txversion":4,"overwintered":1}); - let req = json!({"method":"enable", "bchd_urls": [], "allow_slp_unsafe_conf": true}); - let priv_key = Secp256k1Secret::from("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f"); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let params = BchActivationRequest::from_legacy_req(&req).unwrap(); - - let coin = block_on(bch_coin_with_priv_key( - &ctx, - ticker, - &conf, - params, - CashAddrPrefix::SlpTest, - priv_key, - )) - .unwrap(); - BchDockerOps { ctx, coin } - } - - pub fn initialize_slp(&self) { - fill_address(&self.coin, &self.coin.my_address().unwrap(), 100000.into(), 30); - let mut slp_privkeys = vec![]; - - let slp_genesis_op_ret = slp_genesis_output("ADEXSLP", "ADEXSLP", None, None, 8, None, 1000000_00000000); - let slp_genesis = TransactionOutput { - value: self.coin.as_ref().dust_amount, - script_pubkey: Builder::build_p2pkh(&self.coin.my_public_key().unwrap().address_hash().into()).to_bytes(), - }; - - let mut bch_outputs = vec![slp_genesis_op_ret, slp_genesis]; - let mut slp_outputs = vec![]; - - for _ in 0..18 { - let key_pair = KeyPair::random_compressed(); - let address = AddressBuilder::new( - Default::default(), - Default::default(), - self.coin.as_ref().conf.address_prefixes.clone(), - None, - ) - .as_pkh_from_pk(*key_pair.public()) - .build() - .expect("valid address props"); - - block_on_f01( - self.native_client() - .import_address(&address.to_string(), &address.to_string(), false), - ) - .unwrap(); - - let script_pubkey = Builder::build_p2pkh(&key_pair.public().address_hash().into()); - - bch_outputs.push(TransactionOutput { - value: 1000_00000000, - script_pubkey: script_pubkey.to_bytes(), - }); - - slp_outputs.push(SlpOutput { - amount: 1000_00000000, - script_pubkey: script_pubkey.to_bytes(), - }); - slp_privkeys.push(*key_pair.private_ref()); - } - - let slp_genesis_tx = block_on_f01(send_outputs_from_my_address(self.coin.clone(), bch_outputs)).unwrap(); - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: slp_genesis_tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: wait_until_sec(30), - check_every: 1, - }; - block_on_f01(self.coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let adex_slp = SlpToken::new( - 8, - "ADEXSLP".into(), - <&[u8; 32]>::try_from(slp_genesis_tx.tx_hash_as_bytes().as_slice()) - .unwrap() - .into(), - self.coin.clone(), - 1, - ) - .unwrap(); - - let tx = block_on(adex_slp.send_slp_outputs(slp_outputs)).unwrap(); - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: wait_until_sec(30), - check_every: 1, - }; - block_on_f01(self.coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - *SLP_TOKEN_OWNERS.lock().unwrap() = slp_privkeys; - *SLP_TOKEN_ID.lock().unwrap() = <[u8; 32]>::try_from(slp_genesis_tx.tx_hash_as_bytes().as_slice()) - .unwrap() - .into(); - } -} - -impl CoinDockerOps for BchDockerOps { - fn rpc_client(&self) -> &UtxoRpcClientEnum { - &self.coin.as_ref().rpc_client - } -} - -pub struct DockerNode { - #[allow(dead_code)] - pub container: Container, - #[allow(dead_code)] - pub ticker: String, - #[allow(dead_code)] - pub port: u16, -} - -pub fn random_secp256k1_secret() -> Secp256k1Secret { - let priv_key = SecretKey::new(&mut rand6::thread_rng()); - Secp256k1Secret::from(*priv_key.as_ref()) -} - -pub fn utxo_asset_docker_node(ticker: &'static str, port: u16) -> DockerNode { - let image = GenericImage::new(UTXO_ASSET_DOCKER_IMAGE, "multiarch") - .with_mount(Mount::bind_mount( - zcash_params_path().display().to_string(), - "/root/.zcash-params", - )) - .with_env_var("CLIENTS", "2") - .with_env_var("CHAIN", ticker) - .with_env_var("TEST_ADDY", "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF") - .with_env_var("TEST_WIF", "UqqW7f766rADem9heD8vSBvvrdfJb3zg5r8du9rJxPtccjWf7RG9") - .with_env_var( - "TEST_PUBKEY", - "021607076d7a2cb148d542fb9644c04ffc22d2cca752f80755a0402a24c567b17a", - ) - .with_env_var("DAEMON_URL", "http://test:test@127.0.0.1:7000") - .with_env_var("COIN", "Komodo") - .with_env_var("COIN_RPC_PORT", port.to_string()) - .with_wait_for(WaitFor::message_on_stdout("config is ready")); - let image = RunnableImage::from(image).with_mapped_port((port, port)); - let container = image.start().expect("Failed to start UTXO asset docker node"); - let mut conf_path = coin_daemon_data_dir(ticker, true); - std::fs::create_dir_all(&conf_path).unwrap(); - conf_path.push(format!("{ticker}.conf")); - Command::new("docker") - .arg("cp") - .arg(format!("{}:/data/node_0/{}.conf", container.id(), ticker)) - .arg(&conf_path) - .status() - .expect("Failed to execute docker command"); - let timeout = wait_until_ms(3000); - loop { - if conf_path.exists() { - break; - }; - assert!(now_ms() < timeout, "Test timed out"); - } - DockerNode { - container, - ticker: ticker.into(), - port, - } -} - -pub fn geth_docker_node(ticker: &'static str, port: u16) -> DockerNode { - let image = GenericImage::new(GETH_DOCKER_IMAGE, "stable"); - let args = vec!["--dev".into(), "--http".into(), "--http.addr=0.0.0.0".into()]; - let image = RunnableImage::from((image, args)).with_mapped_port((port, port)); - let container = image.start().expect("Failed to start Geth docker node"); - DockerNode { - container, - ticker: ticker.into(), - port, - } -} - -pub fn sia_docker_node(ticker: &'static str, port: u16) -> DockerNode { - use crate::sia_tests::utils::{WALLETD_CONFIG, WALLETD_NETWORK_CONFIG}; - - let config_dir = std::env::temp_dir() - .join(format!( - "sia-docker-tests-temp-{}", - chrono::Local::now().format("%Y-%m-%d_%H-%M-%S-%3f") - )) - .join("walletd_config"); - std::fs::create_dir_all(&config_dir).unwrap(); - - // Write walletd.yml - std::fs::write(config_dir.join("walletd.yml"), WALLETD_CONFIG).expect("failed to write walletd.yml"); - - // Write ci_network.json - std::fs::write(config_dir.join("ci_network.json"), WALLETD_NETWORK_CONFIG) - .expect("failed to write ci_network.json"); - - let image = GenericImage::new(SIA_DOCKER_IMAGE, "latest") - .with_env_var("WALLETD_CONFIG_FILE", "/config/walletd.yml") - .with_wait_for(WaitFor::message_on_stdout("node started")) - .with_mount(Mount::bind_mount( - config_dir.to_str().expect("config path is invalid"), - "/config", - )); - - let args = vec!["-network=/config/ci_network.json".to_string(), "-debug".to_string()]; - let image = RunnableImage::from(image) - .with_mapped_port((port, port)) - .with_args(args); - - let container = image.start().expect("Failed to start Sia docker node"); - DockerNode { - container, - ticker: ticker.into(), - port, - } -} - -pub fn nucleus_node(runtime_dir: PathBuf) -> DockerNode { - let nucleus_node_runtime_dir = runtime_dir.join("nucleus-testnet-data"); - assert!(nucleus_node_runtime_dir.exists()); - - let image = GenericImage::new(NUCLEUS_IMAGE, "latest").with_mount(Mount::bind_mount( - nucleus_node_runtime_dir.to_str().unwrap(), - "/root/.nucleus", - )); - let image = RunnableImage::from((image, vec![])).with_network("host"); - let container = image.start().expect("Failed to start Nucleus docker node"); - - DockerNode { - container, - ticker: "NUCLEUS-TEST".to_owned(), - port: Default::default(), // This doesn't need to be the correct value as we are using the host network. - } -} - -pub fn atom_node(runtime_dir: PathBuf) -> DockerNode { - let atom_node_runtime_dir = runtime_dir.join("atom-testnet-data"); - assert!(atom_node_runtime_dir.exists()); - - let (image, tag) = ATOM_IMAGE_WITH_TAG.rsplit_once(':').unwrap(); - let image = GenericImage::new(image, tag).with_mount(Mount::bind_mount( - atom_node_runtime_dir.to_str().unwrap(), - "/root/.gaia", - )); - let image = RunnableImage::from((image, vec![])).with_network("host"); - let container = image.start().expect("Failed to start Atom docker node"); - - DockerNode { - container, - ticker: "ATOM-TEST".to_owned(), - port: Default::default(), // This doesn't need to be the correct value as we are using the host network. - } -} - -pub fn ibc_relayer_node(runtime_dir: PathBuf) -> DockerNode { - let relayer_node_runtime_dir = runtime_dir.join("ibc-relayer-data"); - assert!(relayer_node_runtime_dir.exists()); - - let (image, tag) = IBC_RELAYER_IMAGE_WITH_TAG.rsplit_once(':').unwrap(); - let image = GenericImage::new(image, tag).with_mount(Mount::bind_mount( - relayer_node_runtime_dir.to_str().unwrap(), - "/root/.relayer", - )); - let image = RunnableImage::from((image, vec![])).with_network("host"); - let container = image.start().expect("Failed to start IBC Relayer docker node"); - - DockerNode { - container, - ticker: Default::default(), // This isn't an asset node. - port: Default::default(), // This doesn't need to be the correct value as we are using the host network. - } -} - -pub fn zombie_asset_docker_node(port: u16) -> DockerNode { - let image = GenericImage::new(ZOMBIE_ASSET_DOCKER_IMAGE, "multiarch") - .with_mount(Mount::bind_mount( - zcash_params_path().display().to_string(), - "/root/.zcash-params", - )) - .with_env_var("COIN_RPC_PORT", port.to_string()) - .with_wait_for(WaitFor::message_on_stdout("config is ready")); - - let image = RunnableImage::from(image).with_mapped_port((port, port)); - let container = image.start().expect("Failed to start Zombie asset docker node"); - let config_ticker = "ZOMBIE"; - let mut conf_path = coin_daemon_data_dir(config_ticker, true); - - std::fs::create_dir_all(&conf_path).unwrap(); - conf_path.push(format!("{config_ticker}.conf")); - Command::new("docker") - .arg("cp") - .arg(format!("{}:/data/node_0/{}.conf", container.id(), config_ticker)) - .arg(&conf_path) - .status() - .expect("Failed to execute docker command"); - - let timeout = wait_until_ms(3000); - while !conf_path.exists() { - assert!(now_ms() < timeout, "Test timed out"); - } - - DockerNode { - container, - ticker: config_ticker.into(), - port, - } -} - -pub fn rmd160_from_priv(privkey: Secp256k1Secret) -> H160 { - let secret = SecretKey::from_slice(privkey.as_slice()).unwrap(); - let public = PublicKey::from_secret_key(&Secp256k1::new(), &secret); - dhash160(&public.serialize()) -} - -pub fn get_prefilled_slp_privkey() -> [u8; 32] { - SLP_TOKEN_OWNERS.lock().unwrap().remove(0) -} - -pub fn get_slp_token_id() -> String { - hex::encode(SLP_TOKEN_ID.lock().unwrap().as_slice()) -} - -pub async fn import_address(coin: &T) -where - T: MarketCoinOps + AsRef, -{ - let mutex = match coin.ticker() { - "MYCOIN" => &*MY_COIN_LOCK, - "MYCOIN1" => &*MY_COIN1_LOCK, - "QTUM" | "QICK" | "QORTY" => &*QTUM_LOCK, - "FORSLP" => &*FOR_SLP_LOCK, - ticker => panic!("Unknown ticker {}", ticker), - }; - let _lock = mutex.lock().await; - - match coin.as_ref().rpc_client { - UtxoRpcClientEnum::Native(ref native) => { - let my_address = coin.my_address().unwrap(); - native - .import_address(&my_address, &my_address, false) - .compat() - .await - .unwrap(); - }, - UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), - } -} - -/// Build `Qrc20Coin` from ticker and privkey without filling the balance. -pub fn qrc20_coin_from_privkey(ticker: &str, priv_key: Secp256k1Secret) -> (MmArc, Qrc20Coin) { - let (contract_address, swap_contract_address) = unsafe { - let contract_address = match ticker { - "QICK" => QICK_TOKEN_ADDRESS.expect("QICK_TOKEN_ADDRESS must be set already"), - "QORTY" => QORTY_TOKEN_ADDRESS.expect("QORTY_TOKEN_ADDRESS must be set already"), - _ => panic!("Expected QICK or QORTY ticker"), - }; - ( - contract_address, - QRC20_SWAP_CONTRACT_ADDRESS.expect("QRC20_SWAP_CONTRACT_ADDRESS must be set already"), - ) - }; - let platform = "QTUM"; - let ctx = MmCtxBuilder::new().into_mm_arc(); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; - let conf = json!({ - "coin":ticker, - "decimals": 8, - "required_confirmations":0, - "pubtype":120, - "p2shtype":110, - "wiftype":128, - "mm2":1, - "mature_confirmations":500, - "network":"regtest", - "confpath": confpath, - "dust": 72800, - }); - let req = json!({ - "method": "enable", - "swap_contract_address": format!("{:#02x}", swap_contract_address), - }); - let params = Qrc20ActivationParams::from_legacy_req(&req).unwrap(); - - let coin = block_on(qrc20_coin_with_priv_key( - &ctx, - ticker, - platform, - &conf, - ¶ms, - priv_key, - contract_address, - )) - .unwrap(); - - block_on(import_address(&coin)); - (ctx, coin) -} - -fn qrc20_coin_conf_item(ticker: &str) -> Json { - let contract_address = unsafe { - match ticker { - "QICK" => QICK_TOKEN_ADDRESS.expect("QICK_TOKEN_ADDRESS must be set already"), - "QORTY" => QORTY_TOKEN_ADDRESS.expect("QORTY_TOKEN_ADDRESS must be set already"), - _ => panic!("Expected either QICK or QORTY ticker, found {}", ticker), - } - }; - let contract_address = format!("{contract_address:#02x}"); - - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; - json!({ - "coin":ticker, - "required_confirmations":1, - "pubtype":120, - "p2shtype":110, - "wiftype":128, - "mature_confirmations":500, - "confpath":confpath, - "network":"regtest", - "protocol":{"type":"QRC20","protocol_data":{"platform":"QTUM","contract_address":contract_address}}}) -} - -/// Build asset `UtxoStandardCoin` from ticker and privkey without filling the balance. -pub fn utxo_coin_from_privkey(ticker: &str, priv_key: Secp256k1Secret) -> (MmArc, UtxoStandardCoin) { - let ctx = MmCtxBuilder::new().into_mm_arc(); - let conf = json!({"coin":ticker,"asset":ticker,"txversion":4,"overwintered":1,"txfee":1000,"network":"regtest"}); - let req = json!({"method":"enable"}); - let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); - block_on(import_address(&coin)); - (ctx, coin) -} - -/// Create a UTXO coin for the given privkey and fill it's address with the specified balance. -pub fn generate_utxo_coin_with_privkey(ticker: &str, balance: BigDecimal, priv_key: Secp256k1Secret) { - let (_, coin) = utxo_coin_from_privkey(ticker, priv_key); - let timeout = 30; // timeout if test takes more than 30 seconds to run - let my_address = coin.my_address().expect("!my_address"); - fill_address(&coin, &my_address, balance, timeout); -} - -pub async fn fund_privkey_utxo(ticker: &str, balance: BigDecimal, priv_key: &Secp256k1Secret) { - let ctx = MmCtxBuilder::new().into_mm_arc(); - let conf = json!({"coin":ticker,"asset":ticker,"txversion":4,"overwintered":1,"txfee":1000,"network":"regtest"}); - let req = json!({"method":"enable"}); - let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = utxo_standard_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, *priv_key) - .await - .unwrap(); - let my_address = coin.my_address().expect("!my_address"); - fill_address_async(&coin, &my_address, balance, 30).await; -} - -/// Generate random privkey, create a UTXO coin and fill it's address with the specified balance. -pub fn generate_utxo_coin_with_random_privkey( - ticker: &str, - balance: BigDecimal, -) -> (MmArc, UtxoStandardCoin, Secp256k1Secret) { - let priv_key = random_secp256k1_secret(); - let (ctx, coin) = utxo_coin_from_privkey(ticker, priv_key); - let timeout = 30; // timeout if test takes more than 30 seconds to run - let my_address = coin.my_address().expect("!my_address"); - fill_address(&coin, &my_address, balance, timeout); - (ctx, coin, priv_key) -} - -/// Get only one address assigned the specified label. -pub fn get_address_by_label(coin: T, label: &str) -> String -where - T: AsRef, -{ - let native = match coin.as_ref().rpc_client { - UtxoRpcClientEnum::Native(ref native) => native, - UtxoRpcClientEnum::Electrum(_) => panic!("NativeClient expected"), - }; - let mut addresses = block_on_f01(native.get_addresses_by_label(label)) - .expect("!getaddressesbylabel") - .into_iter(); - match addresses.next() { - Some((addr, _purpose)) if addresses.next().is_none() => addr, - Some(_) => panic!("Expected only one address by {:?}", label), - None => panic!("Expected one address by {:?}", label), - } -} - -pub fn fill_qrc20_address(coin: &Qrc20Coin, amount: BigDecimal, timeout: u64) { - // prevent concurrent fill since daemon RPC returns errors if send_to_address - // is called concurrently (insufficient funds) and it also may return other errors - // if previous transaction is not confirmed yet - let _lock = block_on(QTUM_LOCK.lock()); - let timeout = wait_until_sec(timeout); - let client = match coin.as_ref().rpc_client { - UtxoRpcClientEnum::Native(ref client) => client, - UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), - }; - - let from_addr = get_address_by_label(coin, QTUM_ADDRESS_LABEL); - let to_addr = block_on_f01(coin.my_addr_as_contract_addr().compat()).unwrap(); - let satoshis = sat_from_big_decimal(&amount, coin.as_ref().decimals).expect("!sat_from_big_decimal"); - - let hash = block_on_f01(client.transfer_tokens( - &coin.contract_address, - &from_addr, - to_addr, - satoshis.into(), - coin.as_ref().decimals, - )) - .expect("!transfer_tokens") - .txid; - - let tx_bytes = block_on_f01(client.get_transaction_bytes(&hash)).unwrap(); - log!("{:02x}", tx_bytes); - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx_bytes.0, - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); -} - -/// Generate random privkey, create a QRC20 coin and fill it's address with the specified balance. -pub fn generate_qrc20_coin_with_random_privkey( - ticker: &str, - qtum_balance: BigDecimal, - qrc20_balance: BigDecimal, -) -> (MmArc, Qrc20Coin, Secp256k1Secret) { - let priv_key = random_secp256k1_secret(); - let (ctx, coin) = qrc20_coin_from_privkey(ticker, priv_key); - - let timeout = 30; // timeout if test takes more than 30 seconds to run - let my_address = coin.my_address().expect("!my_address"); - fill_address(&coin, &my_address, qtum_balance, timeout); - fill_qrc20_address(&coin, qrc20_balance, timeout); - (ctx, coin, priv_key) -} - -pub fn generate_qtum_coin_with_random_privkey( - ticker: &str, - balance: BigDecimal, - txfee: Option, -) -> (MmArc, QtumCoin, [u8; 32]) { - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; - let conf = json!({ - "coin":ticker, - "decimals":8, - "required_confirmations":0, - "pubtype":120, - "p2shtype": 110, - "wiftype":128, - "txfee": txfee, - "txfee_volatility_percent":0.1, - "mm2":1, - "mature_confirmations":500, - "network":"regtest", - "confpath": confpath, - "dust": 72800, - }); - let req = json!({"method": "enable"}); - let priv_key = random_secp256k1_secret(); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(qtum_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); - - let timeout = 30; // timeout if test takes more than 30 seconds to run - let my_address = coin.my_address().expect("!my_address"); - fill_address(&coin, &my_address, balance, timeout); - (ctx, coin, priv_key.take()) -} - -pub fn generate_segwit_qtum_coin_with_random_privkey( - ticker: &str, - balance: BigDecimal, - txfee: Option, -) -> (MmArc, QtumCoin, Secp256k1Secret) { - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; - let conf = json!({ - "coin":ticker, - "decimals":8, - "required_confirmations":0, - "pubtype":120, - "p2shtype": 110, - "wiftype":128, - "segwit":true, - "txfee": txfee, - "txfee_volatility_percent":0.1, - "mm2":1, - "mature_confirmations":500, - "network":"regtest", - "confpath": confpath, - "dust": 72800, - "bech32_hrp":"qcrt", - "address_format": { - "format": "segwit", - }, - }); - let req = json!({"method": "enable"}); - let priv_key = random_secp256k1_secret(); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(qtum_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); - - let timeout = 30; // timeout if test takes more than 30 seconds to run - let my_address = coin.my_address().expect("!my_address"); - fill_address(&coin, &my_address, balance, timeout); - (ctx, coin, priv_key) -} - -pub fn fill_address(coin: &T, address: &str, amount: BigDecimal, timeout: u64) -where - T: MarketCoinOps + AsRef, -{ - block_on(fill_address_async(coin, address, amount, timeout)); -} - -pub async fn fill_address_async(coin: &T, address: &str, amount: BigDecimal, timeout: u64) -where - T: MarketCoinOps + AsRef, -{ - // prevent concurrent fill since daemon RPC returns errors if send_to_address - // is called concurrently (insufficient funds) and it also may return other errors - // if previous transaction is not confirmed yet - let mutex = match coin.ticker() { - "MYCOIN" => &*MY_COIN_LOCK, - "MYCOIN1" => &*MY_COIN1_LOCK, - "QTUM" | "QICK" | "QORTY" => &*QTUM_LOCK, - "FORSLP" => &*FOR_SLP_LOCK, - ticker => panic!("Unknown ticker {}", ticker), - }; - let _lock = mutex.lock().await; - let timeout = wait_until_sec(timeout); - - if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { - client.import_address(address, address, false).compat().await.unwrap(); - let hash = client.send_to_address(address, &amount).compat().await.unwrap(); - let tx_bytes = client.get_transaction_bytes(&hash).compat().await.unwrap(); - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx_bytes.clone().0, - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - coin.wait_for_confirmations(confirm_payment_input) - .compat() - .await - .unwrap(); - log!("{:02x}", tx_bytes); - loop { - let unspents = client - .list_unspent_impl(0, i32::MAX, vec![address.to_string()]) - .compat() - .await - .unwrap(); - if !unspents.is_empty() { - break; - } - assert!(now_sec() < timeout, "Test timed out"); - Timer::sleep(1.0).await; - } - }; -} - -/// Wait for the `estimatesmartfee` returns no errors. -pub fn wait_for_estimate_smart_fee(timeout: u64) -> Result<(), String> { - enum EstimateSmartFeeState { - Idle, - Ok, - NotAvailable, - } - lazy_static! { - static ref LOCK: Mutex = Mutex::new(EstimateSmartFeeState::Idle); - } - - let state = &mut *LOCK.lock().unwrap(); - match state { - EstimateSmartFeeState::Ok => return Ok(()), - EstimateSmartFeeState::NotAvailable => return ERR!("estimatesmartfee not available"), - EstimateSmartFeeState::Idle => log!("Start wait_for_estimate_smart_fee"), - } - - let priv_key = random_secp256k1_secret(); - let (_ctx, coin) = qrc20_coin_from_privkey("QICK", priv_key); - let timeout = wait_until_sec(timeout); - let client = match coin.as_ref().rpc_client { - UtxoRpcClientEnum::Native(ref client) => client, - UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), - }; - while now_sec() < timeout { - if let Ok(res) = block_on_f01(client.estimate_smart_fee(&None, 1)) { - if res.errors.is_empty() { - *state = EstimateSmartFeeState::Ok; - return Ok(()); - } - } - thread::sleep(Duration::from_secs(1)); - } - - *state = EstimateSmartFeeState::NotAvailable; - ERR!("Waited too long for estimate_smart_fee to work") -} - -pub async fn enable_qrc20_native(mm: &MarketMakerIt, coin: &str) -> Json { - let swap_contract_address = - unsafe { QRC20_SWAP_CONTRACT_ADDRESS.expect("QRC20_SWAP_CONTRACT_ADDRESS must be set already") }; - - let native = mm - .rpc(&json! ({ - "userpass": mm.userpass, - "method": "enable", - "coin": coin, - "swap_contract_address": format!("{:#02x}", swap_contract_address), - "mm2": 1, - })) - .await - .unwrap(); - assert_eq!(native.0, StatusCode::OK, "'enable' failed: {}", native.1); - json::from_str(&native.1).unwrap() -} - -pub fn trade_base_rel((base, rel): (&str, &str)) { - /// Generate a wallet with the random private key and fill the wallet with Qtum (required by gas_fee) and specified in `ticker` coin. - fn generate_and_fill_priv_key(ticker: &str) -> Secp256k1Secret { - let timeout = 30; // timeout if test takes more than 30 seconds to run - - match ticker { - "QTUM" => { - //Segwit QTUM - wait_for_estimate_smart_fee(timeout).expect("!wait_for_estimate_smart_fee"); - let (_ctx, _coin, priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", 10.into(), Some(0)); - - priv_key - }, - "QICK" | "QORTY" => { - let priv_key = random_secp256k1_secret(); - let (_ctx, coin) = qrc20_coin_from_privkey(ticker, priv_key); - let my_address = coin.my_address().expect("!my_address"); - fill_address(&coin, &my_address, 10.into(), timeout); - fill_qrc20_address(&coin, 10.into(), timeout); - - priv_key - }, - "MYCOIN" | "MYCOIN1" => { - let priv_key = random_secp256k1_secret(); - let (_ctx, coin) = utxo_coin_from_privkey(ticker, priv_key); - let my_address = coin.my_address().expect("!my_address"); - fill_address(&coin, &my_address, 10.into(), timeout); - // also fill the Qtum - let (_ctx, coin) = qrc20_coin_from_privkey("QICK", priv_key); - let my_address = coin.my_address().expect("!my_address"); - fill_address(&coin, &my_address, 10.into(), timeout); - - priv_key - }, - "ADEXSLP" | "FORSLP" => Secp256k1Secret::from(get_prefilled_slp_privkey()), - "ETH" | "ERC20DEV" => { - let priv_key = random_secp256k1_secret(); - fill_eth_erc20_with_private_key(priv_key); - priv_key - }, - _ => panic!("Expected either QICK or QORTY or MYCOIN or MYCOIN1, found {}", ticker), - } - } - - let bob_priv_key = generate_and_fill_priv_key(base); - let alice_priv_key = generate_and_fill_priv_key(rel); - let alice_pubkey_str = hex::encode( - key_pair_from_secret(&alice_priv_key) - .expect("valid test key pair") - .public() - .to_vec(), - ); - - let mut envs = vec![]; - if SET_BURN_PUBKEY_TO_ALICE.get() { - envs.push(("TEST_BURN_ADDR_RAW_PUBKEY", alice_pubkey_str.as_str())); - } - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; - let coins = json! ([ - eth_dev_conf(), - erc20_dev_conf(&erc20_contract_checksum()), - qrc20_coin_conf_item("QICK"), - qrc20_coin_conf_item("QORTY"), - {"coin":"MYCOIN","asset":"MYCOIN","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, - {"coin":"MYCOIN1","asset":"MYCOIN1","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, - // TODO: check if we should fix protocol "type":"UTXO" to "QTUM" for this and other QTUM coin tests. - // Maybe we should use a different coin for "UTXO" protocol and make new tests for "QTUM" protocol - {"coin":"QTUM","asset":"QTUM","required_confirmations":0,"decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, "dust":72800, - "mm2":1,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"},"bech32_hrp":"qcrt","address_format":{"format":"segwit"}}, - {"coin":"FORSLP","asset":"FORSLP","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"BCH","protocol_data":{"slp_prefix":"slptest"}}}, - {"coin":"ADEXSLP","protocol":{"type":"SLPTOKEN","protocol_data":{"decimals":8,"token_id":get_slp_token_id(),"platform":"FORSLP"}}} - ]); - let mut mm_bob = block_on(MarketMakerIt::start_with_envs( - json! ({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - envs.as_slice(), - )) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - - let mut mm_alice = block_on(MarketMakerIt::start_with_envs( - json! ({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - envs.as_slice(), - )) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - log!("{:?}", block_on(enable_qrc20_native(&mm_bob, "QICK"))); - log!("{:?}", block_on(enable_qrc20_native(&mm_bob, "QORTY"))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "QTUM", &[], None))); - log!("{:?}", block_on(enable_native_bch(&mm_bob, "FORSLP", &[]))); - log!("{:?}", block_on(enable_native(&mm_bob, "ADEXSLP", &[], None))); - log!( - "{:?}", - block_on(enable_eth_coin( - &mm_bob, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - )) - ); - log!( - "{:?}", - block_on(enable_eth_coin( - &mm_bob, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - )) - ); - - log!("{:?}", block_on(enable_qrc20_native(&mm_alice, "QICK"))); - log!("{:?}", block_on(enable_qrc20_native(&mm_alice, "QORTY"))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "QTUM", &[], None))); - log!("{:?}", block_on(enable_native_bch(&mm_alice, "FORSLP", &[]))); - log!("{:?}", block_on(enable_native(&mm_alice, "ADEXSLP", &[], None))); - log!( - "{:?}", - block_on(enable_eth_coin( - &mm_alice, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - )) - ); - log!( - "{:?}", - block_on(enable_eth_coin( - &mm_alice, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - )) - ); - - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": base, - "rel": rel, - "price": 1, - "volume": "3", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - thread::sleep(Duration::from_secs(1)); - - log!("Issue alice {}/{} buy request", base, rel); - let rc = block_on(mm_alice.rpc(&json! ({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": base, - "rel": rel, - "price": 1, - "volume": "2", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - let buy_json: Json = serde_json::from_str(&rc.1).unwrap(); - let uuid = buy_json["result"]["uuid"].as_str().unwrap().to_owned(); - - // ensure the swaps are started - block_on(mm_bob.wait_for_log(22., |log| { - log.contains(&format!("Entering the maker_swap_loop {base}/{rel}")) - })) - .unwrap(); - block_on(mm_alice.wait_for_log(22., |log| { - log.contains(&format!("Entering the taker_swap_loop {base}/{rel}")) - })) - .unwrap(); - - // ensure the swaps are finished - block_on(mm_bob.wait_for_log(600., |log| log.contains(&format!("[swap uuid={uuid}] Finished")))).unwrap(); - block_on(mm_alice.wait_for_log(600., |log| log.contains(&format!("[swap uuid={uuid}] Finished")))).unwrap(); - - log!("Checking alice/taker status.."); - block_on(check_my_swap_status( - &mm_alice, - &uuid, - "2".parse().unwrap(), - "2".parse().unwrap(), - )); - - log!("Checking bob/maker status.."); - block_on(check_my_swap_status( - &mm_bob, - &uuid, - "2".parse().unwrap(), - "2".parse().unwrap(), - )); - - log!("Checking alice status.."); - block_on(wait_check_stats_swap_status(&mm_alice, &uuid, 240)); - - log!("Checking bob status.."); - block_on(wait_check_stats_swap_status(&mm_bob, &uuid, 240)); - - log!("Checking alice recent swaps.."); - block_on(check_recent_swaps(&mm_alice, 1)); - log!("Checking bob recent swaps.."); - block_on(check_recent_swaps(&mm_bob, 1)); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -pub fn slp_supplied_node() -> MarketMakerIt { - let coins = json! ([ - {"coin":"FORSLP","asset":"FORSLP","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"BCH","protocol_data":{"slp_prefix":"slptest"}}}, - {"coin":"ADEXSLP","protocol":{"type":"SLPTOKEN","protocol_data":{"decimals":8,"token_id":get_slp_token_id(),"platform":"FORSLP"}}} - ]); - - let priv_key = get_prefilled_slp_privkey(); - MarketMakerIt::start( - json! ({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap() -} - -pub fn get_balance(mm: &MarketMakerIt, coin: &str) -> BalanceResponse { - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "my_balance", - "coin": coin, - }))) - .unwrap(); - assert_eq!(rc.0, StatusCode::OK, "my_balance request failed {}", rc.1); - json::from_str(&rc.1).unwrap() -} - -pub fn utxo_burn_address() -> Address { - AddressBuilder::new( - UtxoAddressFormat::Standard, - ChecksumType::DSHA256, - NetworkAddressPrefixes { - p2pkh: [60].into(), - p2sh: AddressPrefix::default(), - }, - None, - ) - .as_pkh(AddressHashEnum::default_address_hash()) - .build() - .expect("valid address props") -} - -pub fn withdraw_max_and_send_v1(mm: &MarketMakerIt, coin: &str, to: &str) -> TransactionDetails { - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "withdraw", - "coin": coin, - "max": true, - "to": to, - }))) - .unwrap(); - assert_eq!(rc.0, StatusCode::OK, "withdraw request failed {}", rc.1); - let tx_details: TransactionDetails = json::from_str(&rc.1).unwrap(); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "send_raw_transaction", - "tx_hex": tx_details.tx_hex, - "coin": coin, - }))) - .unwrap(); - assert_eq!(rc.0, StatusCode::OK, "send_raw_transaction request failed {}", rc.1); - - tx_details -} - -async fn get_current_gas_limit(web3: &Web3) { - match web3.eth().block(BlockId::Number(BlockNumber::Latest)).await { - Ok(Some(block)) => { - log!("Current gas limit: {}", block.gas_limit); - }, - Ok(None) => log!("Latest block information is not available."), - Err(e) => log!("Failed to fetch the latest block: {}", e), - } -} - -pub fn prepare_ibc_channels(container_id: &str) { - let exec = |args: &[&str]| { - Command::new("docker") - .args(["exec", container_id]) - .args(args) - .output() - .unwrap(); - }; - - exec(&["rly", "transact", "clients", "nucleus-atom", "--override"]); - // It takes a couple of seconds for nodes to get into the right state after updating clients. - // Wait for 5 just to make sure. - thread::sleep(Duration::from_secs(5)); - - exec(&["rly", "transact", "link", "nucleus-atom"]); -} - -pub fn wait_until_relayer_container_is_ready(container_id: &str) { - const Q_RESULT: &str = "0: nucleus-atom -> chns(✔) clnts(✔) conn(✔) (nucleus-testnet<>cosmoshub-testnet)"; - - let mut attempts = 0; - loop { - let mut docker = Command::new("docker"); - docker.arg("exec").arg(container_id).args(["rly", "paths", "list"]); - - log!("Running <<{docker:?}>>."); - - let output = docker.stderr(Stdio::inherit()).output().unwrap(); - let output = String::from_utf8(output.stdout).unwrap(); - let output = output.trim(); - - if output == Q_RESULT { - break; - } - attempts += 1; - - log!("Expected output {Q_RESULT}, received {output}."); - if attempts > 10 { - panic!("Reached max attempts for <<{:?}>>.", docker); - } else { - log!("Asking for relayer node status again.."); - } - - thread::sleep(Duration::from_secs(2)); - } -} - -pub fn init_geth_node() { - unsafe { - block_on(get_current_gas_limit(&GETH_WEB3)); - let gas_price = block_on(GETH_WEB3.eth().gas_price()).unwrap(); - log!("Current gas price: {:?}", gas_price); - let accounts = block_on(GETH_WEB3.eth().accounts()).unwrap(); - GETH_ACCOUNT = accounts[0]; - log!("GETH ACCOUNT {:?}", GETH_ACCOUNT); - - let tx_request_deploy_erc20 = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(ERC20_TOKEN_BYTES).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - - let deploy_erc20_tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request_deploy_erc20)).unwrap(); - log!("Sent ERC20 deploy transaction {:?}", deploy_erc20_tx_hash); - - loop { - let deploy_tx_receipt = match block_on(GETH_WEB3.eth().transaction_receipt(deploy_erc20_tx_hash)) { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_tx_receipt { - GETH_ERC20_CONTRACT = receipt.contract_address.unwrap(); - log!("GETH_ERC20_CONTRACT {:?}", GETH_ERC20_CONTRACT); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - let tx_request_deploy_swap_contract = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(SWAP_CONTRACT_BYTES).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let deploy_swap_tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request_deploy_swap_contract)).unwrap(); - log!("Sent deploy swap contract transaction {:?}", deploy_swap_tx_hash); - - loop { - let deploy_swap_tx_receipt = match block_on(GETH_WEB3.eth().transaction_receipt(deploy_swap_tx_hash)) { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_swap_tx_receipt { - GETH_SWAP_CONTRACT = receipt.contract_address.unwrap(); - log!("GETH_SWAP_CONTRACT {:?}", GETH_SWAP_CONTRACT); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - let tx_request_deploy_maker_swap_contract_v2 = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(MAKER_SWAP_V2_BYTES).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let deploy_maker_swap_v2_tx_hash = block_on( - GETH_WEB3 - .eth() - .send_transaction(tx_request_deploy_maker_swap_contract_v2), - ) - .unwrap(); - log!( - "Sent deploy maker swap v2 contract transaction {:?}", - deploy_maker_swap_v2_tx_hash - ); - - loop { - let deploy_maker_swap_v2_tx_receipt = - match block_on(GETH_WEB3.eth().transaction_receipt(deploy_maker_swap_v2_tx_hash)) { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_maker_swap_v2_tx_receipt { - GETH_MAKER_SWAP_V2 = receipt.contract_address.unwrap(); - log!( - "GETH_MAKER_SWAP_V2 contract address: {:?}, receipt.status: {:?}", - GETH_MAKER_SWAP_V2, - receipt.status - ); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - let dex_fee_addr = Token::Address(GETH_ACCOUNT); - let params = ethabi::encode(&[dex_fee_addr]); - let taker_swap_v2_data = format!("{}{}", TAKER_SWAP_V2_BYTES, hex::encode(params)); - - let tx_request_deploy_taker_swap_contract_v2 = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(taker_swap_v2_data).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let deploy_taker_swap_v2_tx_hash = block_on( - GETH_WEB3 - .eth() - .send_transaction(tx_request_deploy_taker_swap_contract_v2), - ) - .unwrap(); - log!( - "Sent deploy taker swap v2 contract transaction {:?}", - deploy_taker_swap_v2_tx_hash - ); - - loop { - let deploy_taker_swap_v2_tx_receipt = - match block_on(GETH_WEB3.eth().transaction_receipt(deploy_taker_swap_v2_tx_hash)) { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_taker_swap_v2_tx_receipt { - GETH_TAKER_SWAP_V2 = receipt.contract_address.unwrap(); - log!( - "GETH_TAKER_SWAP_V2 contract address: {:?}, receipt.status: {:?}", - GETH_TAKER_SWAP_V2, - receipt.status - ); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - let tx_request_deploy_watchers_swap_contract = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(WATCHERS_SWAP_CONTRACT_BYTES).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let deploy_watchers_swap_tx_hash = block_on( - GETH_WEB3 - .eth() - .send_transaction(tx_request_deploy_watchers_swap_contract), - ) - .unwrap(); - log!( - "Sent deploy watchers swap contract transaction {:?}", - deploy_watchers_swap_tx_hash - ); - - loop { - let deploy_watchers_swap_tx_receipt = - match block_on(GETH_WEB3.eth().transaction_receipt(deploy_watchers_swap_tx_hash)) { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_watchers_swap_tx_receipt { - GETH_WATCHERS_SWAP_CONTRACT = receipt.contract_address.unwrap(); - log!("GETH_WATCHERS_SWAP_CONTRACT {:?}", GETH_WATCHERS_SWAP_CONTRACT); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - let tx_request_deploy_nft_maker_swap_v2_contract = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(NFT_MAKER_SWAP_V2_BYTES).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let deploy_nft_maker_swap_v2_tx_hash = block_on( - GETH_WEB3 - .eth() - .send_transaction(tx_request_deploy_nft_maker_swap_v2_contract), - ) - .unwrap(); - log!( - "Sent deploy nft maker swap v2 contract transaction {:?}", - deploy_nft_maker_swap_v2_tx_hash - ); - - loop { - let deploy_nft_maker_swap_v2_tx_receipt = - match block_on(GETH_WEB3.eth().transaction_receipt(deploy_nft_maker_swap_v2_tx_hash)) { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_nft_maker_swap_v2_tx_receipt { - GETH_NFT_MAKER_SWAP_V2 = receipt.contract_address.unwrap(); - log!( - "GETH_NFT_MAKER_SWAP_V2 contact address: {:?}, receipt.status: {:?}", - GETH_NFT_MAKER_SWAP_V2, - receipt.status - ); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - let tx_request_deploy_nft_maker_swap_v2_contract = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(NFT_MAKER_SWAP_V2_BYTES).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let deploy_nft_maker_swap_v2_tx_hash = block_on( - GETH_WEB3 - .eth() - .send_transaction(tx_request_deploy_nft_maker_swap_v2_contract), - ) - .unwrap(); - log!( - "Sent deploy nft maker swap v2 contract transaction {:?}", - deploy_nft_maker_swap_v2_tx_hash - ); - - loop { - let deploy_nft_maker_swap_v2_tx_receipt = - match block_on(GETH_WEB3.eth().transaction_receipt(deploy_nft_maker_swap_v2_tx_hash)) { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_nft_maker_swap_v2_tx_receipt { - GETH_NFT_MAKER_SWAP_V2 = receipt.contract_address.unwrap(); - log!( - "GETH_NFT_MAKER_SWAP_V2 {:?}, receipt.status {:?}", - GETH_NFT_MAKER_SWAP_V2, - receipt.status - ); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - let name = Token::String("MyNFT".into()); - let symbol = Token::String("MNFT".into()); - let params = ethabi::encode(&[name, symbol]); - let erc721_data = format!("{}{}", ERC721_TEST_TOKEN_BYTES, hex::encode(params)); - - let tx_request_deploy_erc721 = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(erc721_data).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let deploy_erc721_tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request_deploy_erc721)).unwrap(); - log!("Sent ERC721 deploy transaction {:?}", deploy_erc721_tx_hash); - - loop { - let deploy_erc721_tx_receipt = match block_on(GETH_WEB3.eth().transaction_receipt(deploy_erc721_tx_hash)) { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_erc721_tx_receipt { - GETH_ERC721_CONTRACT = receipt.contract_address.unwrap(); - log!("GETH_ERC721_CONTRACT {:?}", GETH_ERC721_CONTRACT); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - let uri = Token::String("MyNFTUri".into()); - let params = ethabi::encode(&[uri]); - let erc1155_data = format!("{}{}", ERC1155_TEST_TOKEN_BYTES, hex::encode(params)); - - let tx_request_deploy_erc1155 = TransactionRequest { - from: GETH_ACCOUNT, - to: None, - gas: None, - gas_price: None, - value: None, - data: Some(hex::decode(erc1155_data).unwrap().into()), - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let deploy_erc1155_tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request_deploy_erc1155)).unwrap(); - log!("Sent ERC1155 deploy transaction {:?}", deploy_erc721_tx_hash); - - loop { - let deploy_erc1155_tx_receipt = match block_on(GETH_WEB3.eth().transaction_receipt(deploy_erc1155_tx_hash)) - { - Ok(receipt) => receipt, - Err(_) => { - thread::sleep(Duration::from_millis(100)); - continue; - }, - }; - - if let Some(receipt) = deploy_erc1155_tx_receipt { - GETH_ERC1155_CONTRACT = receipt.contract_address.unwrap(); - log!("GETH_ERC1155_CONTRACT {:?}", GETH_ERC1155_CONTRACT); - break; - } - thread::sleep(Duration::from_millis(100)); - } - - #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] - { - SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 = - EthAddress::from_str("0x9eb88cd58605d8fb9b14652d6152727f7e95fb4d").unwrap(); - SEPOLIA_ERC20_CONTRACT = EthAddress::from_str("0xF7b5F8E8555EF7A743f24D3E974E23A3C6cB6638").unwrap(); - SEPOLIA_TAKER_SWAP_V2 = EthAddress::from_str("0x3B19873b81a6B426c8B2323955215F7e89CfF33F").unwrap(); - // deploy tx https://sepolia.etherscan.io/tx/0x6f743d79ecb806f5899a6a801083e33eba9e6f10726af0873af9f39883db7f11 - SEPOLIA_MAKER_SWAP_V2 = EthAddress::from_str("0xf9000589c66Df3573645B59c10aa87594Edc318F").unwrap(); - } - let alice_passphrase = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); - let alice_keypair = key_pair_from_seed(&alice_passphrase).unwrap(); - let alice_eth_addr = addr_from_raw_pubkey(alice_keypair.public()).unwrap(); - // 100 ETH - fill_eth(alice_eth_addr, U256::from(10).pow(U256::from(20))); - - let bob_passphrase = get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap(); - let bob_keypair = key_pair_from_seed(&bob_passphrase).unwrap(); - let bob_eth_addr = addr_from_raw_pubkey(bob_keypair.public()).unwrap(); - // 100 ETH - fill_eth(bob_eth_addr, U256::from(10).pow(U256::from(20))); - } -} diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index 4f7e40532e..9a4a43c8df 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -1,530 +1,55 @@ -use crate::docker_tests::docker_tests_common::{ - generate_utxo_coin_with_privkey, trade_base_rel, GETH_RPC_URL, MM_CTX, SET_BURN_PUBKEY_TO_ALICE, -}; -use crate::docker_tests::eth_docker_tests::{ - erc20_coin_with_random_privkey, erc20_contract_checksum, fill_eth_erc20_with_private_key, swap_contract, +// Docker Tests Inner - Cross-Chain Ordermatching Tests +// +// This module contains tests that require BOTH ETH and UTXO chains for ordermatching. +// These tests cannot be placed in either eth_inner_tests.rs or utxo_ordermatch_v1_tests.rs +// because they require cross-chain functionality. +// +// ETH-only tests have been extracted to: eth_inner_tests.rs +// UTXO-only ordermatching tests have been extracted to: utxo_ordermatch_v1_tests.rs +// +// Gated by: docker-tests-ordermatch (cross-chain ordermatching tests) + +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::eth::{ + erc20_contract_checksum, fill_eth_erc20_with_private_key, swap_contract_checksum, GETH_RPC_URL, }; +use crate::docker_tests::helpers::utxo::generate_utxo_coin_with_privkey; use crate::integration_tests_common::*; -use crate::{ - fill_address, generate_utxo_coin_with_random_privkey, random_secp256k1_secret, rmd160_from_priv, - utxo_coin_from_privkey, -}; -use bitcrypto::dhash160; -use chain::OutPoint; -use coins::utxo::rpc_clients::UnspentInfo; -use coins::utxo::{GetUtxoListOps, UtxoCommonOps}; -use coins::TxFeeDetails; -use coins::{ - ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, RefundPaymentArgs, SearchForSwapTxSpendInput, - SendPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TransactionEnum, WithdrawRequest, -}; -use common::{block_on, block_on_f01, executor::Timer, get_utc_timestamp, now_sec, wait_until_sec}; +use common::block_on; use crypto::privkey::key_pair_from_seed; -use crypto::{CryptoCtx, DerivationPath, KeyPairPolicy}; -use http::StatusCode; -use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; -use mm2_number::{BigDecimal, BigRational, MmNumber}; +use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::{ - check_my_swap_status_amounts, disable_coin, disable_coin_err, enable_eth_coin, erc20_dev_conf, eth_dev_conf, - get_locked_amount, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, - task_enable_eth_with_tokens, wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, MarketMakerIt, - Mm2TestConf, DEFAULT_RPC_PASSWORD, + best_orders_v2, best_orders_v2_by_number, enable_eth_coin, erc20_dev_conf, eth_dev_conf, mm_dump, my_balance, + mycoin1_conf, mycoin_conf, MarketMakerIt, Mm2TestConf, }; +use mm2_test_helpers::structs::BestOrdersResponse; use mm2_test_helpers::{get_passphrase, structs::*}; -use serde_json::Value as Json; -use std::collections::{HashMap, HashSet}; -use std::convert::TryInto; -use std::env; -use std::iter::FromIterator; -use std::str::FromStr; -use std::thread; -use std::time::Duration; - -#[test] -fn test_search_for_swap_tx_spend_native_was_refunded_taker() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let my_public_key = coin.my_public_key().unwrap(); - - let time_lock = now_sec() - 3600; - let taker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: my_public_key, - secret_hash: &[0; 20], - amount: 1u64.into(), - swap_contract_address: &None, - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = block_on(coin.send_taker_payment(taker_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - let maker_refunds_payment_args = RefundPaymentArgs { - payment_tx: &tx.tx_hex(), - time_lock, - other_pubkey: my_public_key, - tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { - maker_secret_hash: &[0; 20], - }, - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let refund_tx = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: refund_tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let search_input = SearchForSwapTxSpendInput { - time_lock, - other_pub: coin.my_public_key().unwrap(), - secret_hash: &[0; 20], - tx: &tx.tx_hex(), - search_from_block: 0, - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) - .unwrap() - .unwrap(); - assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); -} - -#[test] -fn test_for_non_existent_tx_hex_utxo() { - // This test shouldn't wait till timeout! - let timeout = wait_until_sec(120); - let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - // bad transaction hex - let tx = hex::decode("0400008085202f8902bf17bf7d1daace52e08f732a6b8771743ca4b1cb765a187e72fd091a0aabfd52000000006a47304402203eaaa3c4da101240f80f9c5e9de716a22b1ec6d66080de6a0cca32011cd77223022040d9082b6242d6acf9a1a8e658779e1c655d708379862f235e8ba7b8ca4e69c6012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffffff023ca13c0e9e085dd13f481f193e8a3e8fd609020936e98b5587342d994f4d020000006b483045022100c0ba56adb8de923975052312467347d83238bd8d480ce66e8b709a7997373994022048507bcac921fdb2302fa5224ce86e41b7efc1a2e20ae63aa738dfa99b7be826012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0300e1f5050000000017a9141ee6d4c38a3c078eab87ad1a5e4b00f21259b10d87000000000000000016611400000000000000000000000000000000000000001b94d736000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac2d08e35e000000000000000000000000000000").unwrap(); - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx, - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - let actual = block_on_f01(coin.wait_for_confirmations(confirm_payment_input)) - .err() - .unwrap(); - assert!(actual.contains( - "Tx d342ff9da528a2e262bddf2b6f9a27d1beb7aeb03f0fc8d9eac2987266447e44 was not found on chain after 10 tries" - )); -} - -#[test] -fn test_search_for_swap_tx_spend_native_was_refunded_maker() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let my_public_key = coin.my_public_key().unwrap(); - - let time_lock = now_sec() - 3600; - let maker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: my_public_key, - secret_hash: &[0; 20], - amount: 1u64.into(), - swap_contract_address: &None, - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = block_on(coin.send_maker_payment(maker_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - let maker_refunds_payment_args = RefundPaymentArgs { - payment_tx: &tx.tx_hex(), - time_lock, - other_pubkey: my_public_key, - tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { - maker_secret_hash: &[0; 20], - }, - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let refund_tx = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: refund_tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let search_input = SearchForSwapTxSpendInput { - time_lock, - other_pub: coin.my_public_key().unwrap(), - secret_hash: &[0; 20], - tx: &tx.tx_hex(), - search_from_block: 0, - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) - .unwrap() - .unwrap(); - assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); -} - -#[test] -fn test_search_for_taker_swap_tx_spend_native_was_spent_by_maker() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let secret = [0; 32]; - let my_pubkey = coin.my_public_key().unwrap(); - - let secret_hash = dhash160(&secret); - let time_lock = now_sec() - 3600; - let taker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: my_pubkey, - secret_hash: secret_hash.as_slice(), - amount: 1u64.into(), - swap_contract_address: &None, - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = block_on(coin.send_taker_payment(taker_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - let maker_spends_payment_args = SpendPaymentArgs { - other_payment_tx: &tx.tx_hex(), - time_lock, - other_pubkey: my_pubkey, - secret: &secret, - secret_hash: secret_hash.as_slice(), - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let spend_tx = block_on(coin.send_maker_spends_taker_payment(maker_spends_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: spend_tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let search_input = SearchForSwapTxSpendInput { - time_lock, - other_pub: coin.my_public_key().unwrap(), - secret_hash: &*dhash160(&secret), - tx: &tx.tx_hex(), - search_from_block: 0, - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) - .unwrap() - .unwrap(); - assert_eq!(FoundSwapTxSpend::Spent(spend_tx), found); -} - -#[test] -fn test_search_for_maker_swap_tx_spend_native_was_spent_by_taker() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let secret = [0; 32]; - let my_pubkey = coin.my_public_key().unwrap(); - - let time_lock = now_sec() - 3600; - let secret_hash = dhash160(&secret); - let maker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: my_pubkey, - secret_hash: secret_hash.as_slice(), - amount: 1u64.into(), - swap_contract_address: &None, - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = block_on(coin.send_maker_payment(maker_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - let taker_spends_payment_args = SpendPaymentArgs { - other_payment_tx: &tx.tx_hex(), - time_lock, - other_pubkey: my_pubkey, - secret: &secret, - secret_hash: secret_hash.as_slice(), - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let spend_tx = block_on(coin.send_taker_spends_maker_payment(taker_spends_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: spend_tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let search_input = SearchForSwapTxSpendInput { - time_lock, - other_pub: coin.my_public_key().unwrap(), - secret_hash: &*dhash160(&secret), - tx: &tx.tx_hex(), - search_from_block: 0, - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) - .unwrap() - .unwrap(); - assert_eq!(FoundSwapTxSpend::Spent(spend_tx), found); -} - -#[test] -fn test_one_hundred_maker_payments_in_a_row_native() { - let timeout = 30; // timeout if test takes more than 30 seconds to run - let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - let secret = [0; 32]; - let my_pubkey = coin.my_public_key().unwrap(); - - let time_lock = now_sec() - 3600; - let mut unspents = vec![]; - let mut sent_tx = vec![]; - for i in 0..100 { - let maker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock: time_lock + i, - other_pubkey: my_pubkey, - secret_hash: &*dhash160(&secret), - amount: 1.into(), - swap_contract_address: &coin.swap_contract_address(), - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = block_on(coin.send_maker_payment(maker_payment_args)).unwrap(); - if let TransactionEnum::UtxoTx(tx) = tx { - unspents.push(UnspentInfo { - outpoint: OutPoint { - hash: tx.hash(), - index: 2, - }, - value: tx.outputs[2].value, - height: None, - script: coin - .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) - .unwrap(), - }); - sent_tx.push(tx); - } - } - - let recently_sent = block_on(coin.as_ref().recently_spent_outpoints.lock()); +use serde_json::json; - unspents = recently_sent - .replace_spent_outputs_with_cache(unspents.into_iter().collect()) - .into_iter() - .collect(); - - let last_tx = sent_tx.last().unwrap(); - let expected_unspent = UnspentInfo { - outpoint: OutPoint { - hash: last_tx.hash(), - index: 2, - }, - value: last_tx.outputs[2].value, - height: None, - script: last_tx.outputs[2].script_pubkey.clone().into(), - }; - assert_eq!(vec![expected_unspent], unspents); -} +// ============================================================================= +// Cross-Chain Matching Tests (UTXO + ETH) +// These tests verify order matching between different chain types +// ============================================================================= #[test] -// https://github.com/KomodoPlatform/atomicDEX-API/issues/554 -fn order_should_be_cancelled_when_entire_balance_is_withdrawn() { - let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "myipaddr": env::var("BOB_TRADE_IP") .ok(), - "rpcip": env::var("BOB_TRADE_IP") .ok(), - "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": "999", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - let bob_uuid = json["result"]["uuid"].as_str().unwrap().to_owned(); - - log!("Get MYCOIN/MYCOIN1 orderbook"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {:?}", bob_orderbook); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - - let withdraw = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "withdraw", - "coin": "MYCOIN", - "max": true, - "to": "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF", - }))) - .unwrap(); - assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); - - let withdraw: Json = serde_json::from_str(&withdraw.1).unwrap(); - - let send_raw = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "send_raw_transaction", - "coin": "MYCOIN", - "tx_hex": withdraw["tx_hex"], - }))) - .unwrap(); - assert!(send_raw.0.is_success(), "!send_raw: {}", send_raw.1); - - thread::sleep(Duration::from_secs(32)); - - log!("Get MYCOIN/MYCOIN1 orderbook"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {}", serde_json::to_string(&bob_orderbook).unwrap()); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 0, "MYCOIN/MYCOIN1 orderbook must have exactly 0 asks"); - - log!("Get my orders"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let orders: Json = serde_json::from_str(&rc.1).unwrap(); - log!("my_orders {}", serde_json::to_string(&orders).unwrap()); - assert!( - orders["result"]["maker_orders"].as_object().unwrap().is_empty(), - "maker_orders must be empty" - ); +// https://github.com/KomodoPlatform/atomicDEX-API/issues/1074 +fn test_match_utxo_with_eth_taker_sell() { + let alice_passphrase = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); + let bob_passphrase = get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap(); + let alice_priv_key = key_pair_from_seed(&alice_passphrase).unwrap().private().secret; + let bob_priv_key = key_pair_from_seed(&bob_passphrase).unwrap().private().secret; - let rmd160 = rmd160_from_priv(priv_key); - let order_path = mm_bob.folder.join(format!( - "DB/{}/ORDERS/MY/MAKER/{}.json", - hex::encode(rmd160.take()), - bob_uuid, - )); - log!("Order path {}", order_path.display()); - assert!(!order_path.exists()); - block_on(mm_bob.stop()).unwrap(); -} + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); -#[test] -fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_after_update() { - let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let coins = json!([mycoin_conf(1000), eth_dev_conf()]); - let mm_bob = MarketMakerIt::start( + let mut mm_bob = MarketMakerIt::start( json!({ "gui": "nogui", "netid": 9000, "dht": "on", // Enable DHT without delay. - "myipaddr": env::var("BOB_TRADE_IP") .ok(), - "rpcip": env::var("BOB_TRADE_IP") .ok(), - "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), - "passphrase": format!("0x{}", hex::encode(priv_key)), + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), "coins": coins, "rpc_password": "pass", "i_am_seed": true, @@ -536,12 +61,12 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_after_upda .unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - let mm_alice = MarketMakerIt::start( + let mut mm_alice = MarketMakerIt::start( json!({ "gui": "nogui", "netid": 9000, "dht": "on", // Enable DHT without delay. - "passphrase": "alice passphrase", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), "coins": coins, "rpc_password": "pass", "seednodes": vec![format!("{}", mm_bob.ip)], @@ -553,112 +78,55 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_after_upda let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + block_on(enable_native(&mm_bob, "ETH", &[GETH_RPC_URL], None)); + block_on(enable_native(&mm_alice, "ETH", &[GETH_RPC_URL], None)); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", "base": "MYCOIN", - "rel": "MYCOIN1", + "rel": "ETH", "price": 1, - "volume": "999", + "volume": "0.0001", }))) .unwrap(); assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - log!("Get MYCOIN/MYCOIN1 orderbook"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {:?}", bob_orderbook); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - - let withdraw = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "withdraw", - "coin": "MYCOIN", - "amount": "499.99999481", - "to": "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF", - }))) - .unwrap(); - assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); - - let withdraw: Json = serde_json::from_str(&withdraw.1).unwrap(); - - let send_raw = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "send_raw_transaction", - "coin": "MYCOIN", - "tx_hex": withdraw["tx_hex"], - }))) - .unwrap(); - assert!(send_raw.0.is_success(), "!send_raw: {}", send_raw.1); - - thread::sleep(Duration::from_secs(32)); - - log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {}", serde_json::to_string(&bob_orderbook).unwrap()); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - - let order_volume = asks[0]["maxvolume"].as_str().unwrap(); - assert_eq!("500", order_volume); // 1000.0 - (499.99999481 + 0.00000274 txfee) = (500.0 + 0.00000274 txfee) - - log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", + "method": "sell", + "base": "ETH", + "rel": "MYCOIN", + "price": 1, + "volume": "0.0001", }))) .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {}", serde_json::to_string(&alice_orderbook).unwrap()); - let asks = alice_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Alice MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + assert!(rc.0.is_success(), "!sell: {}", rc.1); - let order_volume = asks[0]["maxvolume"].as_str().unwrap(); - assert_eq!("500", order_volume); + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/ETH"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/ETH"))).unwrap(); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } #[test] -fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_before_update() { - let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_bob = MarketMakerIt::start( +// https://github.com/KomodoPlatform/atomicDEX-API/issues/1074 +fn test_match_utxo_with_eth_taker_buy() { + let alice_passphrase = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); + let bob_passphrase = get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap(); + let alice_priv_key = key_pair_from_seed(&alice_passphrase).unwrap().private().secret; + let bob_priv_key = key_pair_from_seed(&bob_passphrase).unwrap().private().secret; + + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); + let coins = json!([mycoin_conf(1000), eth_dev_conf()]); + let mut mm_bob = MarketMakerIt::start( json!({ "gui": "nogui", "netid": 9000, "dht": "on", // Enable DHT without delay. - "myipaddr": env::var("BOB_TRADE_IP") .ok(), - "rpcip": env::var("BOB_TRADE_IP") .ok(), - "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), - "passphrase": format!("0x{}", hex::encode(priv_key)), + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), "coins": coins, "rpc_password": "pass", "i_am_seed": true, @@ -670,12 +138,12 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_before_upd .unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - let mm_alice = MarketMakerIt::start( + let mut mm_alice = MarketMakerIt::start( json!({ "gui": "nogui", "netid": 9000, "dht": "on", // Enable DHT without delay. - "passphrase": "alice passphrase", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), "coins": coins, "rpc_password": "pass", "seednodes": vec![format!("{}", mm_bob.ip)], @@ -687,4099 +155,120 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_before_upd let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + block_on(enable_native(&mm_bob, "ETH", &[GETH_RPC_URL], None)); + + block_on(enable_native(&mm_alice, "ETH", &[GETH_RPC_URL], None)); let rc = block_on(mm_bob.rpc(&json!({ "userpass": mm_bob.userpass, "method": "setprice", "base": "MYCOIN", - "rel": "MYCOIN1", + "rel": "ETH", "price": 1, - "volume": "999", + "volume": "0.0001", }))) .unwrap(); assert!(rc.0.is_success(), "!setprice: {}", rc.1); - log!("Get MYCOIN/MYCOIN1 orderbook"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {:?}", bob_orderbook); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - - thread::sleep(Duration::from_secs(2)); - log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); let rc = block_on(mm_alice.rpc(&json!({ "userpass": mm_alice.userpass, - "method": "orderbook", + "method": "buy", "base": "MYCOIN", - "rel": "MYCOIN1", + "rel": "ETH", + "price": 1, + "volume": "0.0001", }))) .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + assert!(rc.0.is_success(), "!buy: {}", rc.1); - let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {}", serde_json::to_string(&alice_orderbook).unwrap()); - let asks = alice_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Alice MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/ETH"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/ETH"))).unwrap(); - let withdraw = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "withdraw", - "coin": "MYCOIN", - "amount": "499.99999481", - "to": "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF", - }))) - .unwrap(); - assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} - let withdraw: Json = serde_json::from_str(&withdraw.1).unwrap(); +// ============================================================================= +// Cross-Chain Volume Validation Tests +// These tests check order volume constraints across ETH and UTXO coins +// ============================================================================= - let send_raw = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "send_raw_transaction", - "coin": "MYCOIN", - "tx_hex": withdraw["tx_hex"], +fn check_too_low_volume_order_creation_fails(mm: &MarketMakerIt, base: &str, rel: &str) { + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": "1", + "volume": "0.00000099", + "cancel_previous": false, }))) .unwrap(); - assert!(send_raw.0.is_success(), "!send_raw: {}", send_raw.1); - - thread::sleep(Duration::from_secs(32)); + assert!(!rc.0.is_success(), "setprice success, but should be error {}", rc.1); - log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": "0.00000000000000000099", + "volume": "1", + "cancel_previous": false, }))) .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {}", serde_json::to_string(&bob_orderbook).unwrap()); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - - let order_volume = asks[0]["maxvolume"].as_str().unwrap(); - assert_eq!("500", order_volume); // 1000.0 - (499.99999481 + 0.00000245 txfee) = (500.0 + 0.00000274 txfee) - - log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {}", serde_json::to_string(&alice_orderbook).unwrap()); - let asks = alice_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Alice MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - - let order_volume = asks[0]["maxvolume"].as_str().unwrap(); - assert_eq!("500", order_volume); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_order_should_be_updated_when_matched_partially() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": "1000", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": "500", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - - log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {}", serde_json::to_string(&bob_orderbook).unwrap()); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - - let order_volume = asks[0]["maxvolume"].as_str().unwrap(); - assert_eq!("500", order_volume); - - log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {}", serde_json::to_string(&alice_orderbook).unwrap()); - let asks = alice_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Alice MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -// https://github.com/KomodoPlatform/atomicDEX-API/issues/471 -fn test_match_and_trade_setprice_max() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - let bob_uuid = json["result"]["uuid"].as_str().unwrap().to_owned(); - - log!("Get MYCOIN/MYCOIN1 orderbook"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("orderbook {:?}", bob_orderbook); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); - assert_eq!(asks[0]["maxvolume"], Json::from("999.99999726")); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": "999.99999", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - - thread::sleep(Duration::from_secs(3)); - - let rmd160 = rmd160_from_priv(bob_priv_key); - let order_path = mm_bob.folder.join(format!( - "DB/{}/ORDERS/MY/MAKER/{}.json", - hex::encode(rmd160.take()), - bob_uuid, - )); - log!("Order path {}", order_path.display()); - assert!(!order_path.exists()); - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -// https://github.com/KomodoPlatform/atomicDEX-API/issues/888 -fn test_max_taker_vol_swap() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 50.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = block_on(MarketMakerIt::start_with_envs( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - &[("MYCOIN_FEE_DISCOUNT", "")], - )) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - - let mut mm_alice = block_on(MarketMakerIt::start_with_envs( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - &[("MYCOIN_FEE_DISCOUNT", "")], - )) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - let price = MmNumber::from((100, 1620)); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": price, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "orderbook", - "base": "MYCOIN1", - "rel": "MYCOIN", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - log!("{}", rc.1); - thread::sleep(Duration::from_secs(3)); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "max_taker_vol", - "coin": "MYCOIN1", - "trade_with": "MYCOIN", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); - let vol: MaxTakerVolResponse = serde_json::from_str(&rc.1).unwrap(); - let expected_vol = MmNumber::from((1294999865579, 25930000000)); - - let actual_vol = MmNumber::from(vol.result.clone()); - log!("actual vol {}", actual_vol.to_decimal()); - log!("expected vol {}", expected_vol.to_decimal()); - - assert_eq!(expected_vol, actual_vol); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "sell", - "base": "MYCOIN1", - "rel": "MYCOIN", - "price": "16", - "volume": vol.result, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - let sell_res: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - - thread::sleep(Duration::from_secs(3)); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "my_swap_status", - "params": { - "uuid": sell_res.result.uuid - } - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_swap_status: {}", rc.1); - - let status_response: Json = serde_json::from_str(&rc.1).unwrap(); - let events_array = status_response["result"]["events"].as_array().unwrap(); - let first_event_type = events_array[0]["event"]["type"].as_str().unwrap(); - assert_eq!("Started", first_event_type); - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_buy_when_coins_locked_by_other_swap() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - // the result of equation x + x / 777 + 0.00000274 dexfee_txfee + 0.00000245 payment_txfee = 1 - "volume": { - "numer":"77699596737", - "denom":"77800000000" - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - // TODO when buy call is made immediately swap might be not put into swap ctx yet so locked - // amount returns 0 - thread::sleep(Duration::from_secs(6)); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - // it is slightly more than previous volume so it should fail - // because the total sum of used funds will be slightly more than available 2 - "volume": { - "numer":"77699599999", // increase volume +0.00000001 - "denom":"77800000000" - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "buy success, but should fail: {}", rc.1); - assert!(rc.1.contains("Not enough MYCOIN1 for swap"), "{}", rc.1); - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_sell_when_coins_locked_by_other_swap() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "sell", - "base": "MYCOIN1", - "rel": "MYCOIN", - "price": 1, - // the result of equation x + x / 777 + 0.00000245 + 0.00000274 = 1 - "volume": { - "numer":"77699596737", - "denom":"77800000000" - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - // TODO when sell call is made immediately swap might be not put into swap ctx yet so locked - // amount returns 0 - // NOTE: in this test sometimes Alice has time to send only the taker fee, sometimes can send even the payment tx too - thread::sleep(Duration::from_secs(6)); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "sell", - "base": "MYCOIN1", - "rel": "MYCOIN", - "price": 1, - // it is slightly more than previous volume so it should fail - // because the total sum of used funds will be slightly more than available 2 - "volume": { - "numer":"77699599999", // ensure volume > 1.00000000 - "denom":"77800000000" - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "sell success, but should fail: {}", rc.1); - assert!(rc.1.contains("Not enough MYCOIN1 for swap"), "{}", rc.1); - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_buy_max() { - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - // the result of equation x + x / 777 + 0.00000274 dexfee_txfee + 0.00000245 payment_txfee = 1 - // (btw no need to add refund txfee - it's taken from the spend amount for utxo taker) - "volume": { - "numer":"77699596737", - "denom":"77800000000" - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - // it is slightly more than previous volume so it should fail - "volume": { - "numer":"77699596738", - "denom":"77800000000" - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "buy success, but should fail: {}", rc.1); - // assert! (rc.1.contains("MYCOIN1 balance 1 is too low")); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_maker_trade_preimage() { - let priv_key = random_secp256k1_secret(); - - let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); - let my_address = mycoin.my_address().expect("!my_address"); - fill_address(&mycoin, &my_address, 10.into(), 30); - - let (_ctx, mycoin1) = utxo_coin_from_privkey("MYCOIN1", priv_key); - let my_address = mycoin1.my_address().expect("!my_address"); - fill_address(&mycoin1, &my_address, 20.into(), 30); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); - let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "setprice", - "price": 1, - "max": true, - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000274", false); // txfee from get_sender_trade_fee - let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000992", true); - let volume = MmNumber::from("9.99999726"); // 1.0 - 0.00000274 from calc_max_maker_vol - - let my_coin_total = TotalTradeFeeForTest::new("MYCOIN", "0.00000274", "0.00000274"); - let my_coin1_total = TotalTradeFeeForTest::new("MYCOIN1", "0.00000992", "0"); - - let expected = TradePreimageResult::MakerPreimage(MakerPreimage { - base_coin_fee, - rel_coin_fee, - volume: Some(volume.to_decimal()), - volume_rat: Some(volume.to_ratio()), - volume_fraction: Some(volume.to_fraction()), - total_fees: vec![my_coin_total, my_coin1_total], - }); - - let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); - actual.result.sort_total_fees(); - assert_eq!(expected, actual.result); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN1", - "rel": "MYCOIN", - "swap_method": "setprice", - "price": 1, - "max": true, - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); - actual.result.sort_total_fees(); - - let base_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000548", false); - let rel_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000496", true); - let volume = MmNumber::from("19.99999452"); - - let my_coin_total = TotalTradeFeeForTest::new("MYCOIN", "0.00000496", "0"); - let my_coin1_total = TotalTradeFeeForTest::new("MYCOIN1", "0.00000548", "0.00000548"); - let expected = TradePreimageResult::MakerPreimage(MakerPreimage { - base_coin_fee, - rel_coin_fee, - volume: Some(volume.to_decimal()), - volume_rat: Some(volume.to_ratio()), - volume_fraction: Some(volume.to_fraction()), - total_fees: vec![my_coin_total, my_coin1_total], - }); - - actual.result.sort_total_fees(); - assert_eq!(expected, actual.result); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN1", - "rel": "MYCOIN", - "swap_method": "setprice", - "price": 1, - "volume": "19.99999109", // actually try max value (balance - txfee = 20.0 - 0.00000823) - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); - actual.result.sort_total_fees(); - - let base_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000891", false); // txfee updated for calculated max volume (not 616) - let rel_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000496", true); - - let total_my_coin = TotalTradeFeeForTest::new("MYCOIN", "0.00000496", "0"); - let total_my_coin1 = TotalTradeFeeForTest::new("MYCOIN1", "0.00000891", "0.00000891"); - - let expected = TradePreimageResult::MakerPreimage(MakerPreimage { - base_coin_fee, - rel_coin_fee, - volume: None, - volume_rat: None, - volume_fraction: None, - total_fees: vec![total_my_coin, total_my_coin1], - }); - - actual.result.sort_total_fees(); - assert_eq!(expected, actual.result); -} - -#[test] -fn test_taker_trade_preimage() { - let priv_key = random_secp256k1_secret(); - - let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); - let my_address = mycoin.my_address().expect("!my_address"); - fill_address(&mycoin, &my_address, 10.into(), 30); - - let (_ctx, mycoin1) = utxo_coin_from_privkey("MYCOIN1", priv_key); - let my_address = mycoin1.my_address().expect("!my_address"); - fill_address(&mycoin1, &my_address, 20.into(), 30); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); - let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - - // `max` field is not supported for `buy/sell` swap methods - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "sell", - "max": true, - "price": 1, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - - let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(actual.error_type, "InvalidParam", "Unexpected error_type: {}", rc.1); - let expected = trade_preimage_error::InvalidParam { - param: "max".to_owned(), - reason: "'max' cannot be used with 'sell' or 'buy' method".to_owned(), - }; - assert_eq!(actual.error_data, Some(expected)); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "sell", - "volume": "7.77", - "price": "2", - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - - let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); - actual.result.sort_total_fees(); - - let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000274", false); - let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000992", true); - let taker_fee = TradeFeeForTest::new("MYCOIN", "0.01", false); - let fee_to_send_taker_fee = TradeFeeForTest::new("MYCOIN", "0.00000245", false); - - let my_coin_total_fee = TotalTradeFeeForTest::new("MYCOIN", "0.01000519", "0.01000519"); - let my_coin1_total_fee = TotalTradeFeeForTest::new("MYCOIN1", "0.00000992", "0"); - - let expected = TradePreimageResult::TakerPreimage(TakerPreimage { - base_coin_fee, - rel_coin_fee, - taker_fee, - fee_to_send_taker_fee, - total_fees: vec![my_coin_total_fee, my_coin1_total_fee], - }); - assert_eq!(expected, actual.result); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "buy", - "volume": "7.77", - "price": "2", - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); - actual.result.sort_total_fees(); - - let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000496", true); - let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000548", false); // fee to send taker payment - let taker_fee = TradeFeeForTest::new("MYCOIN1", "0.02", false); - let fee_to_send_taker_fee = TradeFeeForTest::new("MYCOIN1", "0.0000049", false); - - let my_coin_total_fee = TotalTradeFeeForTest::new("MYCOIN", "0.00000496", "0"); - let my_coin1_total_fee = TotalTradeFeeForTest::new("MYCOIN1", "0.02001038", "0.02001038"); // taker_fee + rel_coin_fee + fee_to_send_taker_fee - - let expected = TradePreimageResult::TakerPreimage(TakerPreimage { - base_coin_fee, - rel_coin_fee, - taker_fee, - fee_to_send_taker_fee, - total_fees: vec![my_coin_total_fee, my_coin1_total_fee], - }); - assert_eq!(expected, actual.result); -} - -#[test] -fn test_trade_preimage_not_sufficient_balance() { - #[track_caller] - fn expect_not_sufficient_balance( - res: &str, - available: BigDecimal, - required: BigDecimal, - locked_by_swaps: Option, - ) { - let actual: RpcErrorResponse = serde_json::from_str(res).unwrap(); - assert_eq!(actual.error_type, "NotSufficientBalance"); - let expected = trade_preimage_error::NotSufficientBalance { - coin: "MYCOIN".to_owned(), - available, - required, - locked_by_swaps, - }; - assert_eq!(actual.error_data, Some(expected)); - } - - let priv_key = random_secp256k1_secret(); - let fill_balance_functor = |amount: BigDecimal| { - let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); - let my_address = mycoin.my_address().expect("!my_address"); - fill_address(&mycoin, &my_address, amount, 30); - }; - - let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); - let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - - fill_balance_functor(MmNumber::from("0.00001273").to_decimal()); // volume < txfee + dust = 274 + 1000 - // Try sell the max amount with the zero balance. - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "setprice", - "price": 1, - "max": true, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - let available = MmNumber::from("0.00001273").to_decimal(); - // Required at least 0.00001274 MYCOIN to pay the transaction_fee(0.00000274) and to send a value not less than dust(0.00001) and not less than min_trading_vol (10 * dust). - let required = MmNumber::from("0.00001274").to_decimal(); // TODO: this is not true actually: we can't create orders less that min_trading_vol = 10 * dust - expect_not_sufficient_balance(&rc.1, available, required, Some(MmNumber::from("0").to_decimal())); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "setprice", - "price": 1, - "volume": 0.1, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - // Required 0.00001 MYCOIN to pay the transaction fee and the specified 0.1 volume. - let available = MmNumber::from("0.00001273").to_decimal(); - let required = MmNumber::from("0.1000024").to_decimal(); - expect_not_sufficient_balance(&rc.1, available, required, None); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "setprice", - "price": 1, - "max": true, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - // balance(0.00001273) - let available = MmNumber::from("0.00001273").to_decimal(); - // required min_tx_amount(0.00001) + transaction_fee(0.00000274) - let required = MmNumber::from("0.00001274").to_decimal(); - expect_not_sufficient_balance(&rc.1, available, required, Some(MmNumber::from("0").to_decimal())); - - fill_balance_functor(MmNumber::from("7.770085").to_decimal()); - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "sell", - "price": 1, - "volume": 7.77, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - let available = MmNumber::from("7.77009773").to_decimal(); - // `required = volume + fee_to_send_taker_payment + dex_fee + fee_to_send_dex_fee`, - // where `volume = 7.77`, `fee_to_send_taker_payment = 0.00000393, fee_to_send_dex_fee = 0.00000422`, `dex_fee = 0.01`. - // Please note `dex_fee = 7.77 / 777` with dex_fee = 0.01 - // required = 7.77 + 0.01 (dex_fee) + (0.00000393 + 0.00000422) = 7.78000815 - let required = MmNumber::from("7.78000815"); - expect_not_sufficient_balance(&rc.1, available, required.to_decimal(), Some(BigDecimal::from(0))); -} - -/// This test ensures that `trade_preimage` will not succeed on input that will fail on `buy/sell/setprice`. -/// https://github.com/KomodoPlatform/atomicDEX-API/issues/902 -#[test] -fn test_trade_preimage_additional_validation() { - let priv_key = random_secp256k1_secret(); - - let (_ctx, mycoin1) = utxo_coin_from_privkey("MYCOIN1", priv_key); - let my_address = mycoin1.my_address().expect("!my_address"); - fill_address(&mycoin1, &my_address, 20.into(), 30); - - let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); - let my_address = mycoin.my_address().expect("!my_address"); - fill_address(&mycoin, &my_address, 10.into(), 30); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); - - let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - - // Price is too low - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "setprice", - "price": 0, - "volume": 0.1, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(actual.error_type, "PriceTooLow"); - // currently the minimum price is any value above 0 - let expected = trade_preimage_error::PriceTooLow { - price: BigDecimal::from(0), - threshold: BigDecimal::from(0), - }; - assert_eq!(actual.error_data, Some(expected)); - - // volume 0.00001 is too low, min trading volume 0.0001 - let low_volume = BigDecimal::from(1) / BigDecimal::from(100_000); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "setprice", - "price": 1, - "volume": low_volume, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(actual.error_type, "VolumeTooLow"); - // Min MYCOIN trading volume is 0.0001. - let volume_threshold = BigDecimal::from(1) / BigDecimal::from(10_000); - let expected = trade_preimage_error::VolumeTooLow { - coin: "MYCOIN".to_owned(), - volume: low_volume.clone(), - threshold: volume_threshold, - }; - assert_eq!(actual.error_data, Some(expected)); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "sell", - "price": 1, - "volume": low_volume, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(actual.error_type, "VolumeTooLow"); - // Min MYCOIN trading volume is 0.0001. - let volume_threshold = BigDecimal::from(1) / BigDecimal::from(10_000); - let expected = trade_preimage_error::VolumeTooLow { - coin: "MYCOIN".to_owned(), - volume: low_volume, - threshold: volume_threshold, - }; - assert_eq!(actual.error_data, Some(expected)); - - // rel volume is too low - // Min MYCOIN trading volume is 0.0001. - let volume = BigDecimal::from(1) / BigDecimal::from(10_000); - let low_price = BigDecimal::from(1) / BigDecimal::from(10); - // Min MYCOIN1 trading volume is 0.0001, but the actual volume is 0.00001 - let low_rel_volume = &volume * &low_price; - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "trade_preimage", - "params": { - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "sell", - "price": low_price, - "volume": volume, - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(actual.error_type, "VolumeTooLow"); - // Min MYCOIN1 trading volume is 0.0001. - let volume_threshold = BigDecimal::from(1) / BigDecimal::from(10_000); - let expected = trade_preimage_error::VolumeTooLow { - coin: "MYCOIN1".to_owned(), - volume: low_rel_volume, - threshold: volume_threshold, - }; - assert_eq!(actual.error_data, Some(expected)); -} - -#[test] -fn test_trade_preimage_legacy() { - let priv_key = random_secp256k1_secret(); - let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); - let my_address = mycoin.my_address().expect("!my_address"); - fill_address(&mycoin, &my_address, 10.into(), 30); - let (_ctx, mycoin1) = utxo_coin_from_privkey("MYCOIN1", priv_key); - let my_address = mycoin1.my_address().expect("!my_address"); - fill_address(&mycoin1, &my_address, 20.into(), 30); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); - let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "trade_preimage", - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "setprice", - "max": true, - "price": "1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let _: TradePreimageResponse = serde_json::from_str(&rc.1).unwrap(); - - // vvv test a taker method vvv - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "trade_preimage", - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "sell", - "volume": "7.77", - "price": "2", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); - let _: TradePreimageResponse = serde_json::from_str(&rc.1).unwrap(); - - // vvv test the error response vvv - - // `max` field is not supported for `buy/sell` swap methods - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "trade_preimage", - "base": "MYCOIN", - "rel": "MYCOIN1", - "swap_method": "sell", - "max": true, - "price": "1", - }))) - .unwrap(); - assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); - assert!(rc - .1 - .contains("Incorrect use of the 'max' parameter: 'max' cannot be used with 'sell' or 'buy' method")); -} - -#[test] -fn test_get_max_taker_vol() { - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "max_taker_vol", - "coin": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); - let json: MaxTakerVolResponse = serde_json::from_str(&rc.1).unwrap(); - // the result of equation `max_vol + max_vol / 777 + 0.00000274 + 0.00000245 = 1` - // derived from `max_vol = balance - locked - trade_fee - fee_to_send_taker_fee - dex_fee(max_vol)` - // where balance = 1, locked = 0, trade_fee = fee_to_send_taker_fee = 0.00001, dex_fee = max_vol / 777 - let expected = MmNumber::from((77699596737, 77800000000)).to_fraction(); - assert_eq!(json.result, expected); - assert_eq!(json.coin, "MYCOIN1"); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "sell", - "base": "MYCOIN1", - "rel": "MYCOIN", - "price": 1, - "volume": json.result, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - - block_on(mm_alice.stop()).unwrap(); -} - -// https://github.com/KomodoPlatform/atomicDEX-API/issues/733 -#[test] -fn test_get_max_taker_vol_dex_fee_min_tx_amount() { - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", "0.00532845".parse().unwrap()); - let coins = json!([mycoin_conf(10000), mycoin1_conf(10000)]); - let mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "max_taker_vol", - "coin": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - // the result of equation x + 0.00001 (dex fee) + 0.0000485 (miner fee 2740 + 2450) = 0.00532845 - assert_eq!(json["result"]["numer"], Json::from("105331")); - assert_eq!(json["result"]["denom"], Json::from("20000000")); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "sell", - "base": "MYCOIN1", - "rel": "MYCOIN", - "price": 1, - "volume": { - "numer": json["result"]["numer"], - "denom": json["result"]["denom"], - } - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - - block_on(mm_alice.stop()).unwrap(); -} - -/// Test if the `max_taker_vol` cannot return a volume less than the coin's dust. -/// The minimum required balance for trading can be obtained by solving the equation: -/// `volume + taker_fee + trade_fee + fee_to_send_taker_fee = x`. -/// Let `dust = 0.000728` like for Qtum, `trade_fee = 0.0001`, `fee_to_send_taker_fee = 0.0001` and `taker_fee` is the `0.000728` threshold, -/// therefore to find a minimum required balance, we should pass the `dust` as the `volume` into the equation above: -/// `2 * 0.000728 + 0.00002740 + 0.00002450 = x`, so `x = 0.0014041` -#[test] -fn test_get_max_taker_vol_dust_threshold() { - // first, try to test with the balance slightly less than required - let (_ctx, coin, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", "0.0014041".parse().unwrap()); - let coins = json!([ - mycoin_conf(10000), - {"coin":"MYCOIN1","asset":"MYCOIN1","txversion":4,"overwintered":1,"txfee":10000,"protocol":{"type":"UTXO"},"dust":72800} - ]); - let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); - - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "max_taker_vol", - "coin": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!max_taker_vol {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - let result: MmNumber = serde_json::from_value(json["result"].clone()).unwrap(); - assert!(result.is_zero()); - - fill_address(&coin, &coin.my_address().unwrap(), "0.0002".parse().unwrap(), 30); //00699910 - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "max_taker_vol", - "coin": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - // the result of equation x + 0.000728 (dex fee) + 0.00004220 + 0.00003930 (miner fees) = 0.0016041, x > dust - assert_eq!(json["result"]["numer"], Json::from("3973")); - assert_eq!(json["result"]["denom"], Json::from("5000000")); - - block_on(mm.stop()).unwrap(); -} - -#[test] -fn test_get_max_taker_vol_with_kmd() { - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); - let coins = json!([mycoin_conf(10000), mycoin1_conf(10000), kmd_conf(10000)]); - let mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - let electrum = block_on(enable_electrum( - &mm_alice, - "KMD", - false, - &[ - "electrum1.cipig.net:10001", - "electrum2.cipig.net:10001", - "electrum3.cipig.net:10001", - ], - )); - log!("{:?}", electrum); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "max_taker_vol", - "coin": "MYCOIN1", - "trade_with": "KMD", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - // the result of equation x + x * 9 / 7770 + 0.00002740 + 0.00002450 = 1 - assert_eq!(json["result"]["numer"], Json::from("2589865579")); - assert_eq!(json["result"]["denom"], Json::from("2593000000")); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "sell", - "base": "MYCOIN1", - "rel": "KMD", - "price": 1, - "volume": { - "numer": json["result"]["numer"], - "denom": json["result"]["denom"], - } - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_get_max_maker_vol() { - let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(priv_key)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - - // 1 - tx_fee (274) - let expected_volume = MmNumber::from("0.99999726"); - let expected = MaxMakerVolResponse { - coin: "MYCOIN1".to_string(), - volume: MmNumberMultiRepr::from(expected_volume.clone()), - balance: MmNumberMultiRepr::from(1), - locked_by_swaps: MmNumberMultiRepr::from(0), - }; - let actual = block_on(max_maker_vol(&mm, "MYCOIN1")).unwrap::(); - assert_eq!(actual, expected); - - let res = block_on(set_price(&mm, "MYCOIN1", "MYCOIN", "1", "0", true, None)); - assert_eq!(res.result.max_base_vol, expected_volume.to_decimal()); -} - -#[test] -fn test_get_max_maker_vol_error() { - let priv_key = random_secp256k1_secret(); - let coins = json!([mycoin_conf(1000)]); - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(priv_key)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); - - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - - let actual_error = block_on(max_maker_vol(&mm, "MYCOIN")).unwrap_err::(); - let expected_error = max_maker_vol_error::NotSufficientBalance { - coin: "MYCOIN".to_owned(), - available: 0.into(), - // tx_fee - required: BigDecimal::from(1000) / BigDecimal::from(100_000_000), - locked_by_swaps: None, - }; - assert_eq!(actual_error.error_type, "NotSufficientBalance"); - assert_eq!(actual_error.error_data, Some(expected_error)); -} - -#[test] -fn test_set_price_max() { - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - // the result of equation x + 0.00001 = 1 - "volume": { - "numer":"99999", - "denom":"100000" - }, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - // it is slightly more than previous volume so it should fail - "volume": { - "numer":"100000", - "denom":"100000" - }, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "setprice success, but should fail: {}", rc.1); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn swaps_should_stop_on_stop_rpc() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let mut uuids = Vec::with_capacity(3); - - for _ in 0..3 { - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": "1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - let buy: Json = serde_json::from_str(&rc.1).unwrap(); - uuids.push(buy["result"]["uuid"].as_str().unwrap().to_owned()); - } - for uuid in uuids.iter() { - block_on(mm_bob.wait_for_log(22., |log| { - log.contains(&format!( - "Entering the maker_swap_loop MYCOIN/MYCOIN1 with uuid: {uuid}" - )) - })) - .unwrap(); - block_on(mm_alice.wait_for_log(22., |log| { - log.contains(&format!( - "Entering the taker_swap_loop MYCOIN/MYCOIN1 with uuid: {uuid}" - )) - })) - .unwrap(); - } - thread::sleep(Duration::from_secs(3)); - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); - for uuid in uuids { - block_on(mm_bob.wait_for_log_after_stop(22., |log| log.contains(&format!("swap {uuid} stopped")))).unwrap(); - block_on(mm_alice.wait_for_log_after_stop(22., |log| log.contains(&format!("swap {uuid} stopped")))).unwrap(); - } -} - -#[test] -fn test_maker_order_should_kick_start_and_appear_in_orderbook_on_restart() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut bob_conf = json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }); - let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - // mm_bob using same DB dir that should kick start the order - bob_conf["dbdir"] = mm_bob.folder.join("DB").to_str().unwrap().into(); - bob_conf["log"] = mm_bob.folder.join("mm2_dup.log").to_str().unwrap().into(); - block_on(mm_bob.stop()).unwrap(); - - let mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); - let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); - - thread::sleep(Duration::from_secs(2)); - - log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); - let rc = block_on(mm_bob_dup.rpc(&json!({ - "userpass": mm_bob_dup.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("Bob orderbook {:?}", bob_orderbook); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 asks"); -} - -#[test] -fn test_maker_order_should_not_kick_start_and_appear_in_orderbook_if_balance_is_withdrawn() { - let (_ctx, coin, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut bob_conf = json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }); - let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let res: SetPriceResponse = serde_json::from_str(&rc.1).unwrap(); - let uuid = res.result.uuid; - - // mm_bob using same DB dir that should kick start the order - bob_conf["dbdir"] = mm_bob.folder.join("DB").to_str().unwrap().into(); - bob_conf["log"] = mm_bob.folder.join("mm2_dup.log").to_str().unwrap().into(); - block_on(mm_bob.stop()).unwrap(); - - let withdraw = block_on_f01(coin.withdraw(WithdrawRequest::new_max( - "MYCOIN".to_string(), - "RRYmiZSDo3UdHHqj1rLKf8cbJroyv9NxXw".to_string(), - ))) - .unwrap(); - block_on_f01(coin.send_raw_tx(&hex::encode(&withdraw.tx.tx_hex().unwrap().0))).unwrap(); - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: withdraw.tx.tx_hex().unwrap().0.to_owned(), - confirmations: 1, - requires_nota: false, - wait_until: wait_until_sec(10), - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); - let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); - - thread::sleep(Duration::from_secs(2)); - - log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); - let rc = block_on(mm_bob_dup.rpc(&json!({ - "userpass": mm_bob_dup.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("Bob orderbook {:?}", bob_orderbook); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert!(asks.is_empty(), "Bob MYCOIN/MYCOIN1 orderbook must not have asks"); - - let rc = block_on(mm_bob_dup.rpc(&json!({ - "userpass": mm_bob_dup.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - - let res: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); - assert!(res.result.maker_orders.is_empty(), "Bob maker orders must be empty"); - - let order_path = mm_bob.folder.join(format!( - "DB/{}/ORDERS/MY/MAKER/{}.json", - hex::encode(rmd160_from_priv(bob_priv_key).take()), - uuid - )); - - log!("Order path {}", order_path.display()); - assert!(!order_path.exists()); -} - -#[test] -fn test_maker_order_kick_start_should_trigger_subscription_and_match() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - - let relay_conf = json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": "relay", - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }); - let relay = MarketMakerIt::start(relay_conf, "pass".to_string(), None).unwrap(); - let (_relay_dump_log, _relay_dump_dashboard) = mm_dump(&relay.log_path); - - let mut bob_conf = json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", relay.ip)], - }); - let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", relay.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - // mm_bob using same DB dir that should kick start the order - bob_conf["dbdir"] = mm_bob.folder.join("DB").to_str().unwrap().into(); - bob_conf["log"] = mm_bob.folder.join("mm2_dup.log").to_str().unwrap().into(); - block_on(mm_bob.stop()).unwrap(); - - let mut mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); - let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); - - log!("Give restarted Bob 2 seconds to kickstart the order"); - thread::sleep(Duration::from_secs(2)); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 1, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob_dup.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); -} - -#[test] -fn test_orders_should_match_on_both_nodes_with_same_priv() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice_1 = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_1_dump_log, _alice_1_dump_dashboard) = mm_dump(&mm_alice_1.log_path); - - let mut mm_alice_2 = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_2_dump_log, _alice_2_dump_dashboard) = mm_dump(&mm_alice_2.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice_1, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice_1, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice_2, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice_2, "MYCOIN1", &[], None))); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_alice_1.rpc(&json!({ - "userpass": mm_alice_1.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": "1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_alice_1.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - - let rc = block_on(mm_alice_2.rpc(&json!({ - "userpass": mm_alice_2.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": "1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_alice_2.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice_1.stop()).unwrap(); - block_on(mm_alice_2.stop()).unwrap(); -} - -#[test] -fn test_maker_and_taker_order_created_with_same_priv_should_not_match() { - let (_ctx, coin, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, coin1, _) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1000.into()); - fill_address(&coin1, &coin.my_address().unwrap(), 1000.into(), 30); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_1_dump_log, _alice_1_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": "1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob.wait_for_log(5., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap_err(); - block_on(mm_alice.wait_for_log(5., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap_err(); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_taker_order_converted_to_maker_should_cancel_properly_when_matched() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "sell", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 1, - "timeout": 2, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - - log!("Give Bob 4 seconds to convert order to maker"); - block_on(Timer::sleep(4.)); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 1, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - - log!("Give Bob 2 seconds to cancel the order"); - thread::sleep(Duration::from_secs(2)); - log!("Get my_orders on Bob side"); - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let my_orders_json: Json = serde_json::from_str(&rc.1).unwrap(); - let maker_orders: HashMap = - serde_json::from_value(my_orders_json["result"]["maker_orders"].clone()).unwrap(); - assert!(maker_orders.is_empty()); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("Bob orderbook {:?}", bob_orderbook); - let asks = bob_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 0, "Bob MYCOIN/MYCOIN1 orderbook must be empty"); - - log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); - log!("Alice orderbook {:?}", alice_orderbook); - let asks = alice_orderbook["asks"].as_array().unwrap(); - assert_eq!(asks.len(), 0, "Alice MYCOIN/MYCOIN1 orderbook must be empty"); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_utxo_merge() { - let timeout = 30; // timeout if test takes more than 30 seconds to run - let (_ctx, coin, privkey) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - // fill several times to have more UTXOs on address - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(privkey)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let native = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "enable", - "coin": "MYCOIN", - "mm2": 1, - "utxo_merge_params": { - "merge_at": 2, - "check_every": 1, - } - }))) - .unwrap(); - assert!(native.0.is_success(), "'enable' failed: {}", native.1); - log!("Enable result {}", native.1); - - block_on(mm_bob.wait_for_log(4., |log| log.contains("Starting UTXO merge loop for coin MYCOIN"))).unwrap(); - - block_on(mm_bob.wait_for_log(4., |log| { - log.contains("UTXO merge of 5 outputs successful for coin=MYCOIN, tx_hash") - })) - .unwrap(); - - thread::sleep(Duration::from_secs(2)); - let address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); - let (unspents, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); - assert_eq!(unspents.len(), 1); -} - -#[test] -fn test_utxo_merge_max_merge_at_once() { - let timeout = 30; // timeout if test takes more than 30 seconds to run - let (_ctx, coin, privkey) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - // fill several times to have more UTXOs on address - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(privkey)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let native = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "enable", - "coin": "MYCOIN", - "mm2": 1, - "utxo_merge_params": { - "merge_at": 3, - "check_every": 1, - "max_merge_at_once": 4, - } - }))) - .unwrap(); - assert!(native.0.is_success(), "'enable' failed: {}", native.1); - log!("Enable result {}", native.1); - - block_on(mm_bob.wait_for_log(4., |log| log.contains("Starting UTXO merge loop for coin MYCOIN"))).unwrap(); - - block_on(mm_bob.wait_for_log(4., |log| { - log.contains("UTXO merge of 4 outputs successful for coin=MYCOIN, tx_hash") - })) - .unwrap(); - - thread::sleep(Duration::from_secs(2)); - let address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); - let (unspents, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); - // 4 utxos are merged of 5 so the resulting unspents len must be 2 - assert_eq!(unspents.len(), 2); -} - -#[test] -fn test_consolidate_utxos_rpc() { - let timeout = 30; // timeout if test takes more than 30 seconds to run - let utxos = 50; - let (_ctx, coin, privkey) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - - // fill several times to have more UTXOs on address - for i in 1..=utxos { - fill_address(&coin, &coin.my_address().unwrap(), i.into(), timeout); - } - - let coins = json!([mycoin_conf(1000)]); - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(privkey)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - - let consolidate_rpc = |merge_at: u32, merge_at_once: u32| { - block_on(mm_bob.rpc(&json!({ - "mmrpc": "2.0", - "userpass": mm_bob.userpass, - "method": "consolidate_utxos", - "params": { - "coin": "MYCOIN", - "merge_conditions": { - "merge_at": merge_at, - "max_merge_at_once": merge_at_once, - }, - "broadcast": true - } - }))) - .unwrap() - }; - - let res = consolidate_rpc(52, 4); - assert!(!res.0.is_success(), "Expected error for merge_at > utxos: {}", res.1); - - let res = consolidate_rpc(30, 4); - assert!(res.0.is_success(), "Consolidate utxos failed: {}", res.1); - - let res: RpcSuccessResponse = - serde_json::from_str(&res.1).expect("Expected 'RpcSuccessResponse'"); - // Assert that we respected `max_merge_at_once` and merged only 4 UTXOs. - assert_eq!(res.result.consolidated_utxos.len(), 4); - // Assert that we merged the smallest 4 UTXOs. - for i in 1..=4 { - assert_eq!(res.result.consolidated_utxos[i - 1].value, (i as u32).into()); - } - - thread::sleep(Duration::from_secs(2)); - let address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); - let (unspents, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); - // We have 51 utxos and merged 4 of them which resulted in an extra one. - assert_eq!(unspents.len(), 51 - 4 + 1); -} - -#[test] -fn test_fetch_utxos_rpc() { - let timeout = 30; // timeout if test takes more than 30 seconds to run - let (_ctx, coin, privkey) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - - // fill several times to have more UTXOs on address - for i in 1..=10 { - fill_address(&coin, &coin.my_address().unwrap(), i.into(), timeout); - } - - let coins = json!([mycoin_conf(1000)]); - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(privkey)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - - let fetch_utxo_rpc = || { - let res = block_on(mm_bob.rpc(&json!({ - "mmrpc": "2.0", - "userpass": mm_bob.userpass, - "method": "fetch_utxos", - "params": { - "coin": "MYCOIN" - } - }))) - .unwrap(); - assert!(res.0.is_success(), "Fetch UTXOs failed: {}", res.1); - let res: RpcSuccessResponse = - serde_json::from_str(&res.1).expect("Expected 'RpcSuccessResponse'"); - res.result - }; - - let res = fetch_utxo_rpc(); - assert!(res.total_count == 11); - - fill_address(&coin, &coin.my_address().unwrap(), 100.into(), timeout); - thread::sleep(Duration::from_secs(2)); - - let res = fetch_utxo_rpc(); - assert!(res.total_count == 12); - assert!(res.addresses[0].utxos.iter().any(|utxo| utxo.value == 100.into())); -} - -#[test] -fn test_withdraw_not_sufficient_balance() { - let privkey = random_secp256k1_secret(); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(privkey)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm.log_path); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - - // balance = 0, but amount = 1 - let amount = BigDecimal::from(1); - let withdraw = block_on(mm.rpc(&json!({ - "mmrpc": "2.0", - "userpass": mm.userpass, - "method": "withdraw", - "params": { - "coin": "MYCOIN", - "to": "RJTYiYeJ8eVvJ53n2YbrVmxWNNMVZjDGLh", - "amount": amount, - }, - "id": 0, - }))) - .unwrap(); - - assert!(withdraw.0.is_client_error(), "MYCOIN withdraw: {}", withdraw.1); - log!("error: {:?}", withdraw.1); - let error: RpcErrorResponse = - serde_json::from_str(&withdraw.1).expect("Expected 'RpcErrorResponse'"); - let expected_error = withdraw_error::NotSufficientBalance { - coin: "MYCOIN".to_owned(), - available: 0.into(), - required: amount, - }; - assert_eq!(error.error_type, "NotSufficientBalance"); - assert_eq!(error.error_data, Some(expected_error)); - - // fill the MYCOIN balance - let balance = BigDecimal::from(1) / BigDecimal::from(2); - let (_ctx, coin) = utxo_coin_from_privkey("MYCOIN", privkey); - fill_address(&coin, &coin.my_address().unwrap(), balance.clone(), 30); - - // txfee = 0.00000211, amount = 0.5 => required = 0.50000211 - // but balance = 0.5 - let txfee = BigDecimal::from_str("0.00000211").unwrap(); - let withdraw = block_on(mm.rpc(&json!({ - "mmrpc": "2.0", - "userpass": mm.userpass, - "method": "withdraw", - "params": { - "coin": "MYCOIN", - "to": "RJTYiYeJ8eVvJ53n2YbrVmxWNNMVZjDGLh", - "amount": balance, - }, - "id": 0, - }))) - .unwrap(); - - assert!(withdraw.0.is_client_error(), "MYCOIN withdraw: {}", withdraw.1); - log!("error: {:?}", withdraw.1); - let error: RpcErrorResponse = - serde_json::from_str(&withdraw.1).expect("Expected 'RpcErrorResponse'"); - let expected_error = withdraw_error::NotSufficientBalance { - coin: "MYCOIN".to_owned(), - available: balance.clone(), - required: balance + txfee, - }; - assert_eq!(error.error_type, "NotSufficientBalance"); - assert_eq!(error.error_data, Some(expected_error)); -} - -// https://github.com/KomodoPlatform/atomicDEX-API/issues/1053 -#[test] -fn test_taker_should_match_with_best_price_buy() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 4000.into()); - let (_ctx, _, eve_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - let mut mm_eve = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(eve_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_eve_dump_log, _eve_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN1", &[], None))); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 2, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_eve.rpc(&json!({ - "userpass": mm_eve.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - // subscribe alice to the orderbook topic to not miss eve's message - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!alice orderbook: {}", rc.1); - log!("alice orderbook {}", rc.1); - - thread::sleep(Duration::from_secs(1)); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 3, - "volume": "1000", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - let alice_buy: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); - - block_on(mm_eve.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - - thread::sleep(Duration::from_secs(2)); - - block_on(check_my_swap_status_amounts( - &mm_alice, - alice_buy.result.uuid, - 1000.into(), - 1000.into(), - )); - block_on(check_my_swap_status_amounts( - &mm_eve, - alice_buy.result.uuid, - 1000.into(), - 1000.into(), - )); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); - block_on(mm_eve.stop()).unwrap(); -} - -// https://github.com/KomodoPlatform/atomicDEX-API/issues/1053 -#[test] -fn test_taker_should_match_with_best_price_sell() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 4000.into()); - let (_ctx, _, eve_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - let mut mm_eve = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(eve_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_eve_dump_log, _eve_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN1", &[], None))); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 2, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_eve.rpc(&json!({ - "userpass": mm_eve.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "max": true, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - // subscribe alice to the orderbook topic to not miss eve's message - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!alice orderbook: {}", rc.1); - log!("alice orderbook {}", rc.1); - - thread::sleep(Duration::from_secs(1)); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "sell", - "base": "MYCOIN1", - "rel": "MYCOIN", - "price": "0.1", - "volume": "1000", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - let alice_buy: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); - - block_on(mm_eve.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); - - thread::sleep(Duration::from_secs(2)); - - block_on(check_my_swap_status_amounts( - &mm_alice, - alice_buy.result.uuid, - 1000.into(), - 1000.into(), - )); - block_on(check_my_swap_status_amounts( - &mm_eve, - alice_buy.result.uuid, - 1000.into(), - 1000.into(), - )); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); - block_on(mm_eve.stop()).unwrap(); -} - -#[test] -// https://github.com/KomodoPlatform/atomicDEX-API/issues/1074 -fn test_match_utxo_with_eth_taker_sell() { - let alice_passphrase = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); - let bob_passphrase = get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap(); - let alice_priv_key = key_pair_from_seed(&alice_passphrase).unwrap().private().secret; - let bob_priv_key = key_pair_from_seed(&bob_passphrase).unwrap().private().secret; - - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); - - let coins = json!([mycoin_conf(1000), eth_dev_conf()]); - - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - block_on(enable_native(&mm_bob, "ETH", &[GETH_RPC_URL], None)); - block_on(enable_native(&mm_alice, "ETH", &[GETH_RPC_URL], None)); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "ETH", - "price": 1, - "volume": "0.0001", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "sell", - "base": "ETH", - "rel": "MYCOIN", - "price": 1, - "volume": "0.0001", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/ETH"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/ETH"))).unwrap(); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -// https://github.com/KomodoPlatform/atomicDEX-API/issues/1074 -fn test_match_utxo_with_eth_taker_buy() { - let alice_passphrase = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); - let bob_passphrase = get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap(); - let alice_priv_key = key_pair_from_seed(&alice_passphrase).unwrap().private().secret; - let bob_priv_key = key_pair_from_seed(&bob_passphrase).unwrap().private().secret; - - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); - let coins = json!([mycoin_conf(1000), eth_dev_conf()]); - let mut mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(bob_priv_key)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let mut mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(alice_priv_key)), - "coins": coins, - "rpc_password": "pass", - "seednodes": vec![format!("{}", mm_bob.ip)], - }), - "pass".to_string(), - None, - ) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - block_on(enable_native(&mm_bob, "ETH", &[GETH_RPC_URL], None)); - - block_on(enable_native(&mm_alice, "ETH", &[GETH_RPC_URL], None)); - - let rc = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "ETH", - "price": 1, - "volume": "0.0001", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let rc = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "ETH", - "price": 1, - "volume": "0.0001", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/ETH"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/ETH"))).unwrap(); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_locked_amount() { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1000.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let bob_conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(bob_priv_key)), &coins); - let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - - let alice_conf = Mm2TestConf::light_node( - &format!("0x{}", hex::encode(alice_priv_key)), - &coins, - &[&mm_bob.ip.to_string()], - ); - let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); - - block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("MYCOIN", "MYCOIN1")], - 1., - 1., - 777., - )); - - let locked_bob = block_on(get_locked_amount(&mm_bob, "MYCOIN")); - assert_eq!(locked_bob.coin, "MYCOIN"); - - let expected_result: MmNumberMultiRepr = MmNumber::from("777.00000274").into(); // volume + txfee = 777 + 1 + 0.0000274 - assert_eq!(expected_result, locked_bob.locked_amount); - - let locked_alice = block_on(get_locked_amount(&mm_alice, "MYCOIN1")); - assert_eq!(locked_alice.coin, "MYCOIN1"); - - let expected_result: MmNumberMultiRepr = MmNumber::from("778.00000519").into(); // volume + dexfee + txfee + txfee = 777 + 1 + 0.0000245 + 0.00000274 - assert_eq!(expected_result, locked_alice.locked_amount); -} - -async fn enable_eth_with_tokens( - mm: &MarketMakerIt, - platform_coin: &str, - tokens: &[&str], - swap_contract_address: &str, - nodes: &[&str], - balance: bool, -) -> Json { - let erc20_tokens_requests: Vec<_> = tokens.iter().map(|ticker| json!({ "ticker": ticker })).collect(); - let nodes: Vec<_> = nodes.iter().map(|url| json!({ "url": url })).collect(); - - let enable = mm - .rpc(&json!({ - "userpass": mm.userpass, - "method": "enable_eth_with_tokens", - "mmrpc": "2.0", - "params": { - "ticker": platform_coin, - "erc20_tokens_requests": erc20_tokens_requests, - "swap_contract_address": swap_contract_address, - "nodes": nodes, - "tx_history": true, - "get_balances": balance, - } - })) - .await - .unwrap(); - assert_eq!( - enable.0, - StatusCode::OK, - "'enable_eth_with_tokens' failed: {}", - enable.1 - ); - serde_json::from_str(&enable.1).unwrap() -} - -#[test] -fn test_enable_eth_coin_with_token_then_disable() { - let coin = erc20_coin_with_random_privkey(swap_contract()); - - let priv_key = coin.display_priv_key().unwrap(); - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - - let conf = Mm2TestConf::seednode(&priv_key, &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("log path: {}", mm.log_path.display()); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - block_on(enable_eth_with_tokens( - &mm, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - true, - )); - - // Create setprice order - let req = json!({ - "userpass": mm.userpass, - "method": "buy", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.1, - "base_confs": 5, - "base_nota": false, - "rel_confs": 4, - "rel_nota": false, - }); - let make_test_order = block_on(mm.rpc(&req)).unwrap(); - assert_eq!(make_test_order.0, StatusCode::OK); - let order_uuid = Json::from_str(&make_test_order.1).unwrap(); - let order_uuid = order_uuid.get("result").unwrap().get("uuid").unwrap().as_str().unwrap(); - - // Passive ETH while having tokens enabled - let res = block_on(disable_coin(&mm, "ETH", false)); - assert!(res.passivized); - assert!(res.cancelled_orders.contains(order_uuid)); - - // Try to disable ERC20DEV token. - // This should work, because platform coin is still in the memory. - let res = block_on(disable_coin(&mm, "ERC20DEV", false)); - // We expected make_test_order to be cancelled - assert!(!res.passivized); - - // Because it's currently passive, default `disable_coin` should fail. - block_on(disable_coin_err(&mm, "ETH", false)); - // And forced `disable_coin` should not fail - let res = block_on(disable_coin(&mm, "ETH", true)); - assert!(!res.passivized); -} - -#[test] -fn test_platform_coin_mismatch() { - let coin = erc20_coin_with_random_privkey(swap_contract()); - - let priv_key = coin.display_priv_key().unwrap(); - let mut erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); - erc20_conf["protocol"]["protocol_data"]["platform"] = "MATIC".into(); // set a different platform coin - let coins = json!([eth_dev_conf(), erc20_conf]); - - let conf = Mm2TestConf::seednode(&priv_key, &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("log path: {}", mm.log_path.display()); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - let erc20_tokens_requests = vec![json!({ "ticker": "ERC20DEV" })]; - let nodes = vec![json!({ "url": GETH_RPC_URL })]; - - let enable = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "enable_eth_with_tokens", - "mmrpc": "2.0", - "params": { - "ticker": "ETH", - "erc20_tokens_requests": erc20_tokens_requests, - "swap_contract_address": swap_contract, - "nodes": nodes, - "tx_history": false, - "get_balances": false, - } - }))) - .unwrap(); - assert_eq!( - enable.0, - StatusCode::BAD_REQUEST, - "'enable_eth_with_tokens' must fail with PlatformCoinMismatch", - ); - assert_eq!( - serde_json::from_str::(&enable.1).unwrap()["error_type"] - .as_str() - .unwrap(), - "PlatformCoinMismatch", - ); -} - -#[test] -fn test_enable_eth_coin_with_token_without_balance() { - let coin = erc20_coin_with_random_privkey(swap_contract()); - - let priv_key = coin.display_priv_key().unwrap(); - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - - let conf = Mm2TestConf::seednode(&priv_key, &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("log path: {}", mm.log_path.display()); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - let enable_eth_with_tokens = block_on(enable_eth_with_tokens( - &mm, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - false, - )); - - let enable_eth_with_tokens: RpcV2Response = - serde_json::from_value(enable_eth_with_tokens).unwrap(); - - let (_, eth_balance) = enable_eth_with_tokens - .result - .eth_addresses_infos - .into_iter() - .next() - .unwrap(); - log!("{:?}", eth_balance); - assert!(eth_balance.balances.is_none()); - assert!(eth_balance.tickers.is_none()); - - let (_, erc20_balances) = enable_eth_with_tokens - .result - .erc20_addresses_infos - .into_iter() - .next() - .unwrap(); - assert!(erc20_balances.balances.is_none()); - assert_eq!( - erc20_balances.tickers.unwrap(), - HashSet::from_iter(vec!["ERC20DEV".to_string()]) - ); -} - -#[test] -fn test_eth_swap_contract_addr_negotiation_same_fallback() { - let bob_coin = erc20_coin_with_random_privkey(swap_contract()); - let alice_coin = erc20_coin_with_random_privkey(swap_contract()); - - let bob_priv_key = bob_coin.display_priv_key().unwrap(); - let alice_priv_key = alice_coin.display_priv_key().unwrap(); - - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - - let bob_conf = Mm2TestConf::seednode(&bob_priv_key, &coins); - let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); - - let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); - log!("Bob log path: {}", mm_bob.log_path.display()); - - let alice_conf = Mm2TestConf::light_node(&alice_priv_key, &coins, &[&mm_bob.ip.to_string()]); - let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); - - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ETH", - &[GETH_RPC_URL], - // using arbitrary address - "0x6c2858f6afac835c43ffda248aea167e1a58436c", - Some(&swap_contract), - false - ))); - - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ERC20DEV", - &[GETH_RPC_URL], - // using arbitrary address - "0x6c2858f6afac835c43ffda248aea167e1a58436c", - Some(&swap_contract), - false - ))); - - dbg!(block_on(enable_eth_coin( - &mm_alice, - "ETH", - &[GETH_RPC_URL], - // using arbitrary address - "0x24abe4c71fc658c01313b6552cd40cd808b3ea80", - Some(&swap_contract), - false - ))); - - dbg!(block_on(enable_eth_coin( - &mm_alice, - "ERC20DEV", - &[GETH_RPC_URL], - // using arbitrary address - "0x24abe4c71fc658c01313b6552cd40cd808b3ea80", - Some(&swap_contract), - false - ))); - - let uuids = block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("ETH", "ERC20DEV")], - 1., - 1., - 0.0001, - )); - - // give few seconds for swap statuses to be saved - thread::sleep(Duration::from_secs(3)); - - let wait_until = get_utc_timestamp() + 30; - let expected_contract = Json::from(swap_contract.trim_start_matches("0x")); - - block_on(wait_for_swap_contract_negotiation( - &mm_bob, - &uuids[0], - expected_contract.clone(), - wait_until, - )); - block_on(wait_for_swap_contract_negotiation( - &mm_alice, - &uuids[0], - expected_contract, - wait_until, - )); -} - -#[test] -fn test_eth_swap_negotiation_fails_maker_no_fallback() { - let bob_coin = erc20_coin_with_random_privkey(swap_contract()); - let alice_coin = erc20_coin_with_random_privkey(swap_contract()); - - let bob_priv_key = bob_coin.display_priv_key().unwrap(); - let alice_priv_key = alice_coin.display_priv_key().unwrap(); - - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - - let bob_conf = Mm2TestConf::seednode(&bob_priv_key, &coins); - let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); - - let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); - log!("Bob log path: {}", mm_bob.log_path.display()); - - let alice_conf = Mm2TestConf::light_node(&alice_priv_key, &coins, &[&mm_bob.ip.to_string()]); - let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); - - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ETH", - &[GETH_RPC_URL], - // using arbitrary address - "0x6c2858f6afac835c43ffda248aea167e1a58436c", - None, - false - ))); - - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ERC20DEV", - &[GETH_RPC_URL], - // using arbitrary address - "0x6c2858f6afac835c43ffda248aea167e1a58436c", - None, - false - ))); - - dbg!(block_on(enable_eth_coin( - &mm_alice, - "ETH", - &[GETH_RPC_URL], - // using arbitrary address - "0x24abe4c71fc658c01313b6552cd40cd808b3ea80", - Some(&swap_contract), - false - ))); - - dbg!(block_on(enable_eth_coin( - &mm_alice, - "ERC20DEV", - &[GETH_RPC_URL], - // using arbitrary address - "0x24abe4c71fc658c01313b6552cd40cd808b3ea80", - Some(&swap_contract), - false - ))); - - let uuids = block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("ETH", "ERC20DEV")], - 1., - 1., - 0.0001, - )); - - // give few seconds for swap statuses to be saved - thread::sleep(Duration::from_secs(3)); - - let wait_until = get_utc_timestamp() + 30; - block_on(wait_for_swap_negotiation_failure(&mm_bob, &uuids[0], wait_until)); - block_on(wait_for_swap_negotiation_failure(&mm_alice, &uuids[0], wait_until)); -} - -#[test] -fn test_trade_base_rel_eth_erc20_coins() { - trade_base_rel(("ETH", "ERC20DEV")); -} - -#[test] -fn test_trade_base_rel_mycoin_mycoin1_coins() { - trade_base_rel(("MYCOIN", "MYCOIN1")); -} - -// run swap with burn pubkey set to alice (no dex fee) -#[test] -fn test_trade_base_rel_mycoin_mycoin1_coins_burnkey_as_alice() { - SET_BURN_PUBKEY_TO_ALICE.set(true); - trade_base_rel(("MYCOIN", "MYCOIN1")); -} - -fn withdraw_and_send( - mm: &MarketMakerIt, - coin: &str, - from: Option, - to: &str, - from_addr: &str, - expected_bal_change: &str, - amount: f64, -) { - let withdraw = block_on(mm.rpc(&json! ({ - "mmrpc": "2.0", - "userpass": mm.userpass, - "method": "withdraw", - "params": { - "coin": coin, - "from": from, - "to": to, - "amount": amount, - }, - "id": 0, - }))) - .unwrap(); - - assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); - let res: RpcSuccessResponse = - serde_json::from_str(&withdraw.1).expect("Expected 'RpcSuccessResponse'"); - let tx_details = res.result; - - let mut expected_bal_change = BigDecimal::from_str(expected_bal_change).expect("!BigDecimal::from_str"); - - let fee_details: TxFeeDetails = serde_json::from_value(tx_details.fee_details).unwrap(); - - if let TxFeeDetails::Eth(fee_details) = fee_details { - if coin == "ETH" { - expected_bal_change -= fee_details.total_fee; - } - } - - assert_eq!(tx_details.to, vec![to.to_owned()]); - assert_eq!(tx_details.my_balance_change, expected_bal_change); - // Todo: Should check the from address for withdraws from another HD wallet address when there is an RPC method for addresses - if from.is_none() { - assert_eq!(tx_details.from, vec![from_addr.to_owned()]); - } - - let send = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "send_raw_transaction", - "coin": coin, - "tx_hex": tx_details.tx_hex, - }))) - .unwrap(); - assert!(send.0.is_success(), "!{} send: {}", coin, send.1); - let send_json: Json = serde_json::from_str(&send.1).unwrap(); - assert_eq!(tx_details.tx_hash, send_json["tx_hash"]); -} - -#[test] -fn test_withdraw_and_send_eth_erc20() { - let privkey = random_secp256k1_secret(); - fill_eth_erc20_with_private_key(privkey); - - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(privkey)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "pass".to_string(), - None, - ) - .unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("Alice log path: {}", mm.log_path.display()); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - let eth_enable = block_on(enable_eth_coin( - &mm, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false, - )); - let erc20_enable = block_on(enable_eth_coin( - &mm, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false, - )); - - withdraw_and_send( - &mm, - "ETH", - None, - "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", - eth_enable["address"].as_str().unwrap(), - "-0.001", - 0.001, - ); - - withdraw_and_send( - &mm, - "ERC20DEV", - None, - "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", - erc20_enable["address"].as_str().unwrap(), - "-0.001", - 0.001, - ); - - // must not allow to withdraw to invalid checksum address - let withdraw = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "mmrpc": "2.0", - "method": "withdraw", - "params": { - "coin": "ETH", - "to": "0x4b2d0d6c2c785217457b69b922a2A9cEA98f71E9", - "amount": "0.001", - }, - "id": 0, - }))) - .unwrap(); - - assert!(withdraw.0.is_client_error(), "ETH withdraw: {}", withdraw.1); - let res: RpcErrorResponse = serde_json::from_str(&withdraw.1).unwrap(); - assert_eq!(res.error_type, "InvalidAddress"); - assert!(res.error.contains("Invalid address checksum")); -} - -#[test] -fn test_withdraw_and_send_hd_eth_erc20() { - const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; - - let KeyPairPolicy::GlobalHDAccount(hd_acc) = CryptoCtx::init_with_global_hd_account(MM_CTX.clone(), PASSPHRASE) - .unwrap() - .key_pair_policy() - .clone() - else { - panic!("Expected 'KeyPairPolicy::GlobalHDAccount'"); - }; - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - - // Withdraw from HD account 0, change address 0, index 1 - let mut path_to_address = HDAccountAddressId { - account_id: 0, - chain: Bip44Chain::External, - address_id: 1, - }; - let path_to_addr_str = "/0'/0/1"; - let path_to_coin: String = serde_json::from_value(eth_dev_conf()["derivation_path"].clone()).unwrap(); - let derivation_path = path_to_coin.clone() + path_to_addr_str; - let derivation_path = DerivationPath::from_str(&derivation_path).unwrap(); - // Get the private key associated with this account and fill it with eth and erc20 token. - let priv_key = hd_acc.derive_secp256k1_secret(&derivation_path).unwrap(); - fill_eth_erc20_with_private_key(priv_key); - - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); - log!("Alice log path: {}", mm_hd.log_path.display()); - - let eth_enable = block_on(task_enable_eth_with_tokens( - &mm_hd, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - 60, - Some(path_to_address.clone()), - )); - let activation_result = match eth_enable { - EthWithTokensActivationResult::HD(hd) => hd, - _ => panic!("Expected EthWithTokensActivationResult::HD"), - }; - let balance = match activation_result.wallet_balance { - EnableCoinBalanceMap::HD(hd) => hd, - _ => panic!("Expected EnableCoinBalance::HD"), - }; - let account = balance.accounts.first().expect("Expected account at index 0"); - assert_eq!( - account.addresses[1].address, - "0xDe841899aB4A22E23dB21634e54920aDec402397" - ); - assert_eq!(account.addresses[1].balance.len(), 2); - assert_eq!(account.addresses[1].balance.get("ETH").unwrap().spendable, 100.into()); - assert_eq!( - account.addresses[1].balance.get("ERC20DEV").unwrap().spendable, - 100.into() - ); - - withdraw_and_send( - &mm_hd, - "ETH", - Some(path_to_address.clone()), - "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", - &account.addresses[1].address, - "-0.001", - 0.001, - ); - - withdraw_and_send( - &mm_hd, - "ERC20DEV", - Some(path_to_address.clone()), - "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", - &account.addresses[1].address, - "-0.001", - 0.001, - ); - - // Change the address index, the withdrawal should fail. - path_to_address.address_id = 0; - - let withdraw = block_on(mm_hd.rpc(&json! ({ - "mmrpc": "2.0", - "userpass": mm_hd.userpass, - "method": "withdraw", - "params": { - "coin": "ETH", - "from": path_to_address, - "to": "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", - "amount": 0.001, - }, - "id": 0, - }))) - .unwrap(); - assert!(!withdraw.0.is_success(), "!withdraw: {}", withdraw.1); - - // But if we fill it, we should be able to withdraw. - let path_to_addr_str = "/0'/0/0"; - let derivation_path = path_to_coin + path_to_addr_str; - let derivation_path = DerivationPath::from_str(&derivation_path).unwrap(); - let priv_key = hd_acc.derive_secp256k1_secret(&derivation_path).unwrap(); - fill_eth_erc20_with_private_key(priv_key); - - let withdraw = block_on(mm_hd.rpc(&json! ({ - "mmrpc": "2.0", - "userpass": mm_hd.userpass, - "method": "withdraw", - "params": { - "coin": "ETH", - "from": path_to_address, - "to": "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9", - "amount": 0.001, - }, - "id": 0, - }))) - .unwrap(); - assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); - - block_on(mm_hd.stop()).unwrap(); -} - -fn check_too_low_volume_order_creation_fails(mm: &MarketMakerIt, base: &str, rel: &str) { - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": base, - "rel": rel, - "price": "1", - "volume": "0.00000099", - "cancel_previous": false, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "setprice success, but should be error {}", rc.1); - - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": base, - "rel": rel, - "price": "0.00000000000000000099", - "volume": "1", - "cancel_previous": false, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "setprice success, but should be error {}", rc.1); - - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": base, - "rel": rel, - "price": "1", - "volume": "0.00000099", - }))) - .unwrap(); - assert!(!rc.0.is_success(), "sell success, but should be error {}", rc.1); - - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "buy", - "base": base, - "rel": rel, - "price": "1", - "volume": "0.00000099", - }))) - .unwrap(); - assert!(!rc.0.is_success(), "buy success, but should be error {}", rc.1); -} - -#[test] -// https://github.com/KomodoPlatform/atomicDEX-API/issues/481 -fn test_setprice_buy_sell_too_low_volume() { - let privkey = random_secp256k1_secret(); - - // Fill the addresses with coins. - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); - generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), privkey); - fill_eth_erc20_with_private_key(privkey); - - let coins = json!([ - mycoin_conf(1000), - mycoin1_conf(1000), - eth_dev_conf(), - erc20_dev_conf(&erc20_contract_checksum()) - ]); - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("Log path: {}", mm.log_path.display()); - - // Enable all the coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - dbg!(block_on(enable_eth_coin( - &mm, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - dbg!(block_on(enable_eth_coin( - &mm, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - - check_too_low_volume_order_creation_fails(&mm, "MYCOIN", "ETH"); - check_too_low_volume_order_creation_fails(&mm, "ETH", "MYCOIN"); - check_too_low_volume_order_creation_fails(&mm, "ERC20DEV", "MYCOIN1"); -} - -#[test] -fn test_fill_or_kill_taker_order_should_not_transform_to_maker() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); - - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - - log!("Issue bob MYCOIN/MYCOIN1 sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 0.1, - "order_type": { - "type": "FillOrKill" - }, - "timeout": 2, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - let sell_json: Json = serde_json::from_str(&rc.1).unwrap(); - let order_type = sell_json["result"]["order_type"]["type"].as_str(); - assert_eq!(order_type, Some("FillOrKill")); - - log!("Wait for 4 seconds for Bob order to be cancelled"); - block_on(Timer::sleep(4.)); - - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let my_orders: Json = serde_json::from_str(&rc.1).unwrap(); - let my_maker_orders: HashMap = - serde_json::from_value(my_orders["result"]["maker_orders"].clone()).unwrap(); - let my_taker_orders: HashMap = - serde_json::from_value(my_orders["result"]["taker_orders"].clone()).unwrap(); - assert!(my_maker_orders.is_empty(), "maker_orders must be empty"); - assert!(my_taker_orders.is_empty(), "taker_orders must be empty"); -} - -#[test] -fn test_gtc_taker_order_should_transform_to_maker() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); - - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - - log!("Issue bob MYCOIN/MYCOIN1 sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 0.1, - "order_type": { - "type": "GoodTillCancelled" - }, - "timeout": 2, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let rc_json: Json = serde_json::from_str(&rc.1).unwrap(); - let uuid: String = serde_json::from_value(rc_json["result"]["uuid"].clone()).unwrap(); - - log!("Wait for 4 seconds for Bob order to be converted to maker"); - block_on(Timer::sleep(4.)); - - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let my_orders: Json = serde_json::from_str(&rc.1).unwrap(); - let my_maker_orders: HashMap = - serde_json::from_value(my_orders["result"]["maker_orders"].clone()).unwrap(); - let my_taker_orders: HashMap = - serde_json::from_value(my_orders["result"]["taker_orders"].clone()).unwrap(); - assert_eq!(1, my_maker_orders.len(), "maker_orders must have exactly 1 order"); - assert!(my_taker_orders.is_empty(), "taker_orders must be empty"); - let order_path = mm.folder.join(format!( - "DB/{}/ORDERS/MY/MAKER/{}.json", - hex::encode(rmd160_from_passphrase(&format!("0x{}", hex::encode(privkey)))), - uuid - )); - log!("Order path {}", order_path.display()); - assert!(order_path.exists()); -} - -#[test] -fn test_set_price_must_save_order_to_db() { - let private_key_str = erc20_coin_with_random_privkey(swap_contract()) - .display_priv_key() - .unwrap(); - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - - let conf = Mm2TestConf::seednode(&private_key_str, &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - dbg!(block_on(enable_eth_coin( - &mm, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - dbg!(block_on(enable_eth_coin( - &mm, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - - log!("Issue bob ETH/ERC20DEV sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.1 - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let rc_json: Json = serde_json::from_str(&rc.1).unwrap(); - let uuid: String = serde_json::from_value(rc_json["result"]["uuid"].clone()).unwrap(); - let order_path = mm.folder.join(format!( - "DB/{}/ORDERS/MY/MAKER/{}.json", - hex::encode(rmd160_from_passphrase(&private_key_str)), - uuid - )); - assert!(order_path.exists()); -} - -#[test] -fn test_set_price_response_format() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); - - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + assert!(!rc.0.is_success(), "setprice success, but should be error {}", rc.1); - log!("Issue bob MYCOIN/MYCOIN1 sell request"); let rc = block_on(mm.rpc(&json! ({ "userpass": mm.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 0.1 + "method": "sell", + "base": base, + "rel": rel, + "price": "1", + "volume": "0.00000099", }))) .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let rc_json: Json = serde_json::from_str(&rc.1).unwrap(); - let _: BigDecimal = serde_json::from_value(rc_json["result"]["max_base_vol"].clone()).unwrap(); - let _: BigDecimal = serde_json::from_value(rc_json["result"]["min_base_vol"].clone()).unwrap(); - let _: BigDecimal = serde_json::from_value(rc_json["result"]["price"].clone()).unwrap(); - - let _: BigRational = serde_json::from_value(rc_json["result"]["max_base_vol_rat"].clone()).unwrap(); - let _: BigRational = serde_json::from_value(rc_json["result"]["min_base_vol_rat"].clone()).unwrap(); - let _: BigRational = serde_json::from_value(rc_json["result"]["price_rat"].clone()).unwrap(); -} - -#[test] -fn test_buy_response_format() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), privkey); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); - - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + assert!(!rc.0.is_success(), "sell success, but should be error {}", rc.1); - log!("Issue bob buy request"); let rc = block_on(mm.rpc(&json! ({ "userpass": mm.userpass, "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 0.1, - "base_confs": 5, - "base_nota": true, - "rel_confs": 4, - "rel_nota": false, + "base": base, + "rel": rel, + "price": "1", + "volume": "0.00000099", }))) .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - let _: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); + assert!(!rc.0.is_success(), "buy success, but should be error {}", rc.1); } #[test] -fn test_sell_response_format() { +// https://github.com/KomodoPlatform/atomicDEX-API/issues/481 +fn test_setprice_buy_sell_too_low_volume() { let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + // Fill the addresses with coins. + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), privkey); + fill_eth_erc20_with_private_key(privkey); + let coins = json!([ + mycoin_conf(1000), + mycoin1_conf(1000), + eth_dev_conf(), + erc20_dev_conf(&erc20_contract_checksum()) + ]); let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("Log path: {}", mm.log_path.display()); - // Enable coins + // Enable all the coins log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - log!("Issue bob sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 0.1, - "base_confs": 5, - "base_nota": true, - "rel_confs": 4, - "rel_nota": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - let _: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); -} - -#[test] -fn test_set_price_conf_settings() { - let private_key_str = erc20_coin_with_random_privkey(swap_contract()) - .display_priv_key() - .unwrap(); - - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); - - let conf = Mm2TestConf::seednode(&private_key_str, &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - let swap_contract = format!("0x{}", hex::encode(swap_contract())); + let swap_contract = swap_contract_checksum(); dbg!(block_on(enable_eth_coin( &mm, "ETH", @@ -4797,277 +286,82 @@ fn test_set_price_conf_settings() { false ))); - log!("Issue bob sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.1, - "base_confs": 5, - "base_nota": true, - "rel_confs": 4, - "rel_nota": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(5)); - assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(true)); - assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(4)); - assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); - - // must use coin config as defaults if not set in request - log!("Issue bob sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.1, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(1)); - assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(false)); - assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(2)); - assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); + check_too_low_volume_order_creation_fails(&mm, "MYCOIN", "ETH"); + check_too_low_volume_order_creation_fails(&mm, "ETH", "MYCOIN"); + check_too_low_volume_order_creation_fails(&mm, "ERC20DEV", "MYCOIN1"); } -#[test] -fn test_buy_conf_settings() { - let private_key_str = erc20_coin_with_random_privkey(swap_contract()) - .display_priv_key() - .unwrap(); - - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); - - let conf = Mm2TestConf::seednode(&private_key_str, &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - dbg!(block_on(enable_eth_coin( - &mm, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - dbg!(block_on(enable_eth_coin( - &mm, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - - log!("Issue bob buy request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "buy", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.1, - "base_confs": 5, - "base_nota": true, - "rel_confs": 4, - "rel_nota": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(5)); - assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(true)); - assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(4)); - assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); +// ============================================================================= +// Cross-Chain Orderbook Depth Tests +// These tests verify orderbook depth calculation across multiple chain types +// ============================================================================= - // must use coin config as defaults if not set in request - log!("Issue bob buy request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "buy", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.1, +fn request_and_check_orderbook_depth(mm_alice: &MarketMakerIt) { + let rc = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "orderbook_depth", + "pairs": [("MYCOIN", "MYCOIN1"), ("MYCOIN", "ETH"), ("MYCOIN1", "ETH")], }))) .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(1)); - assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(false)); - assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(2)); - assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); -} - -#[test] -fn test_sell_conf_settings() { - let private_key_str = erc20_coin_with_random_privkey(swap_contract()) - .display_priv_key() + assert!(rc.0.is_success(), "!orderbook_depth: {}", rc.1); + let response: OrderbookDepthResponse = serde_json::from_str(&rc.1).unwrap(); + let mycoin_mycoin1 = response + .result + .iter() + .find(|pair_depth| pair_depth.pair.0 == "MYCOIN" && pair_depth.pair.1 == "MYCOIN1") .unwrap(); + assert_eq!(3, mycoin_mycoin1.depth.asks); + assert_eq!(2, mycoin_mycoin1.depth.bids); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); - - let conf = Mm2TestConf::seednode(&private_key_str, &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - dbg!(block_on(enable_eth_coin( - &mm, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - dbg!(block_on(enable_eth_coin( - &mm, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); - - log!("Issue bob sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.1, - "base_confs": 5, - "base_nota": true, - "rel_confs": 4, - "rel_nota": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(5)); - assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(true)); - assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(4)); - assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); + let mycoin_eth = response + .result + .iter() + .find(|pair_depth| pair_depth.pair.0 == "MYCOIN" && pair_depth.pair.1 == "ETH") + .unwrap(); + assert_eq!(1, mycoin_eth.depth.asks); + assert_eq!(1, mycoin_eth.depth.bids); - // must use coin config as defaults if not set in request - log!("Issue bob sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.1, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - let json: Json = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(1)); - assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(false)); - assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(2)); - assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); + let mycoin1_eth = response + .result + .iter() + .find(|pair_depth| pair_depth.pair.0 == "MYCOIN1" && pair_depth.pair.1 == "ETH") + .unwrap(); + assert_eq!(0, mycoin1_eth.depth.asks); + assert_eq!(0, mycoin1_eth.depth.bids); } #[test] -fn test_my_orders_response_format() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN1", 10000.into(), privkey); - generate_utxo_coin_with_privkey("MYCOIN", 10000.into(), privkey); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); - - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - - log!("Issue bob buy request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 0.1, - "base_confs": 5, - "base_nota": true, - "rel_confs": 4, - "rel_nota": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - log!("Issue bob setprice request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": 1, - "volume": 0.1, - "base_confs": 5, - "base_nota": true, - "rel_confs": 4, - "rel_nota": false, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - log!("Issue bob my_orders request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); +fn test_orderbook_depth() { + let bob_priv_key = random_secp256k1_secret(); + let alice_priv_key = random_secp256k1_secret(); + let swap_contract = swap_contract_checksum(); - let _: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); -} + // Fill bob's addresses with coins. + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); + generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), bob_priv_key); + fill_eth_erc20_with_private_key(bob_priv_key); -#[test] -fn test_my_orders_after_matched() { - let bob_coin = erc20_coin_with_random_privkey(swap_contract()); - let alice_coin = erc20_coin_with_random_privkey(swap_contract()); + // Fill alice's addresses with coins. + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); + generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), alice_priv_key); + fill_eth_erc20_with_private_key(alice_priv_key); - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let coins = json!([ + mycoin_conf(1000), + mycoin1_conf(1000), + eth_dev_conf(), + erc20_dev_conf(&erc20_contract_checksum()) + ]); - let bob_conf = Mm2TestConf::seednode(&bob_coin.display_priv_key().unwrap(), &coins); + let bob_conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(bob_priv_key)), &coins); let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - let alice_conf = Mm2TestConf::light_node( - &alice_coin.display_priv_key().unwrap(), - &coins, - &[&mm_bob.ip.to_string()], - ); - let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); - - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); + // Enable all the coins for bob + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); dbg!(block_on(enable_eth_coin( &mm_bob, "ETH", @@ -5076,7 +370,6 @@ fn test_my_orders_after_matched() { None, false ))); - dbg!(block_on(enable_eth_coin( &mm_bob, "ERC20DEV", @@ -5086,472 +379,428 @@ fn test_my_orders_after_matched() { false ))); - dbg!(block_on(enable_eth_coin( - &mm_alice, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); + // issue sell request on Bob side by setting base/rel price + log!("Issue bob sell requests"); + let bob_orders = [ + // (base, rel, price, volume, min_volume) + ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), + ("MYCOIN", "MYCOIN1", "0.8", "0.9", None), + ("MYCOIN", "MYCOIN1", "0.7", "0.9", Some("0.9")), + ("MYCOIN", "ETH", "0.8", "0.9", None), + ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), + ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), + ("ETH", "MYCOIN", "0.8", "0.9", None), + ]; + for (base, rel, price, volume, min_volume) in bob_orders.iter() { + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": price, + "volume": volume, + "min_volume": min_volume.unwrap_or("0.00777"), + "cancel_previous": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + } - dbg!(block_on(enable_eth_coin( - &mm_alice, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); + let alice_conf = Mm2TestConf::light_node( + &format!("0x{}", hex::encode(alice_priv_key)), + &coins, + &[&mm_bob.ip.to_string()], + ); + let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.000001, - }))) + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + + block_on(mm_bob.wait_for_log(22., |log| { + log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") + })) .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); + request_and_check_orderbook_depth(&mm_alice); + // request MYCOIN/MYCOIN1 orderbook to subscribe Alice let rc = block_on(mm_alice.rpc(&json! ({ "userpass": mm_alice.userpass, - "method": "buy", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.000001, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", }))) .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop ETH/ERC20DEV"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop ETH/ERC20DEV"))).unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - log!("Issue bob my_orders request"); - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + request_and_check_orderbook_depth(&mm_alice); - let _: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } -#[test] -fn test_update_maker_order_after_matched() { - let bob_coin = erc20_coin_with_random_privkey(swap_contract()); - let alice_coin = erc20_coin_with_random_privkey(swap_contract()); +// ============================================================================= +// Cross-Chain Best Orders Tests +// These tests verify best_orders RPC across ETH and UTXO coins +// ============================================================================= - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); +fn get_bob_alice() -> (MarketMakerIt, MarketMakerIt) { + let bob_priv_key = random_secp256k1_secret(); + let alice_priv_key = random_secp256k1_secret(); - let bob_conf = Mm2TestConf::seednode(&bob_coin.display_priv_key().unwrap(), &coins); - let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); + generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), bob_priv_key); + fill_eth_erc20_with_private_key(bob_priv_key); + + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); + generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), alice_priv_key); + fill_eth_erc20_with_private_key(alice_priv_key); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000), eth_dev_conf(),]); + + let bob_conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(bob_priv_key)), &coins); + let mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); let alice_conf = Mm2TestConf::light_node( - &alice_coin.display_priv_key().unwrap(), + &format!("0x{}", hex::encode(alice_priv_key)), &coins, &[&mm_bob.ip.to_string()], ); - let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + let swap_contract = swap_contract_checksum(); dbg!(block_on(enable_eth_coin( - &mm_alice, + &mm_bob, "ETH", &[GETH_RPC_URL], &swap_contract, None, false ))); - dbg!(block_on(enable_eth_coin( &mm_alice, - "ERC20DEV", + "ETH", &[GETH_RPC_URL], &swap_contract, None, false ))); - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.00002, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let setprice_json: Json = serde_json::from_str(&rc.1).unwrap(); - let uuid: String = serde_json::from_value(setprice_json["result"]["uuid"].clone()).unwrap(); - - let rc = block_on(mm_alice.rpc(&json! ({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": "ETH", - "rel": "ERC20DEV", - "price": 1, - "volume": 0.00001, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - - block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop ETH/ERC20DEV"))).unwrap(); - block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop ETH/ERC20DEV"))).unwrap(); - - log!("Issue bob update maker order request that should fail because new volume is less than reserved amount"); - let update_maker_order = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "update_maker_order", - "uuid": uuid, - "volume_delta": -0.00002, - }))) - .unwrap(); - assert!( - !update_maker_order.0.is_success(), - "update_maker_order success, but should be error {}", - update_maker_order.1 - ); - - log!("Issue another bob update maker order request"); - let update_maker_order = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "update_maker_order", - "uuid": uuid, - "volume_delta": 0.00001, - }))) - .unwrap(); - assert!( - update_maker_order.0.is_success(), - "!update_maker_order: {}", - update_maker_order.1 - ); - let update_maker_order_json: Json = serde_json::from_str(&update_maker_order.1).unwrap(); - log!("{}", update_maker_order.1); - assert_eq!(update_maker_order_json["result"]["max_base_vol"], Json::from("0.00003")); - - log!("Issue bob my_orders request"); - let rc = block_on(mm_bob.rpc(&json! ({ - "userpass": mm_bob.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - - let _: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); -} - -#[test] -fn test_buy_min_volume() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), privkey); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); - - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); - - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); - - let min_volume: BigDecimal = "0.1".parse().unwrap(); - log!("Issue bob MYCOIN/MYCOIN1 buy request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "buy", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": "2", - "volume": "1", - "min_volume": min_volume, - "order_type": { - "type": "GoodTillCancelled" - }, - "timeout": 2, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - let response: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); - assert_eq!(min_volume, response.result.min_volume); - - log!("Wait for 4 seconds for Bob order to be converted to maker"); - block_on(Timer::sleep(4.)); - - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "my_orders", - }))) - .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let my_orders: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); - assert_eq!( - 1, - my_orders.result.maker_orders.len(), - "maker_orders must have exactly 1 order" - ); - assert!(my_orders.result.taker_orders.is_empty(), "taker_orders must be empty"); - let maker_order = my_orders.result.maker_orders.get(&response.result.uuid).unwrap(); - - let expected_min_volume: BigDecimal = "0.2".parse().unwrap(); - assert_eq!(expected_min_volume, maker_order.min_base_vol); + (mm_bob, mm_alice) } #[test] -fn test_sell_min_volume() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); - - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); - - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); +fn test_best_orders() { + let (mut mm_bob, mm_alice) = get_bob_alice(); - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + // issue sell request on Bob side by setting base/rel price + log!("Issue bob sell requests"); - let min_volume: BigDecimal = "0.1".parse().unwrap(); - log!("Issue bob MYCOIN/MYCOIN1 sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": "1", - "volume": "1", - "min_volume": min_volume, - "order_type": { - "type": "GoodTillCancelled" - }, - "timeout": 2, - }))) - .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - let rc_json: Json = serde_json::from_str(&rc.1).unwrap(); - let uuid: String = serde_json::from_value(rc_json["result"]["uuid"].clone()).unwrap(); - let min_volume_response: BigDecimal = serde_json::from_value(rc_json["result"]["min_volume"].clone()).unwrap(); - assert_eq!(min_volume, min_volume_response); + let bob_orders = [ + // (base, rel, price, volume, min_volume) + ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), + ("MYCOIN", "MYCOIN1", "0.8", "0.9", None), + ("MYCOIN", "MYCOIN1", "0.7", "0.9", Some("0.9")), + ("MYCOIN", "ETH", "0.8", "0.9", None), + ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), + ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), + ("ETH", "MYCOIN", "0.8", "0.9", None), + ("MYCOIN1", "ETH", "0.8", "0.8", None), + ("MYCOIN1", "ETH", "0.7", "0.8", Some("0.8")), + ]; + for (base, rel, price, volume, min_volume) in bob_orders.iter() { + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": price, + "volume": volume, + "min_volume": min_volume.unwrap_or("0.00777"), + "cancel_previous": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + } - log!("Wait for 4 seconds for Bob order to be converted to maker"); - block_on(Timer::sleep(4.)); + block_on(mm_bob.wait_for_log(22., |log| { + log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") + })) + .unwrap(); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "my_orders", + let rc = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "best_orders", + "coin": "MYCOIN", + "action": "buy", + "volume": "0.1", }))) .unwrap(); - assert!(rc.0.is_success(), "!my_orders: {}", rc.1); - let my_orders: Json = serde_json::from_str(&rc.1).unwrap(); - let my_maker_orders: HashMap = - serde_json::from_value(my_orders["result"]["maker_orders"].clone()).unwrap(); - let my_taker_orders: HashMap = - serde_json::from_value(my_orders["result"]["taker_orders"].clone()).unwrap(); - assert_eq!(1, my_maker_orders.len(), "maker_orders must have exactly 1 order"); - assert!(my_taker_orders.is_empty(), "taker_orders must be empty"); - let maker_order = my_maker_orders.get(&uuid).unwrap(); - let min_volume_maker: BigDecimal = serde_json::from_value(maker_order["min_base_vol"].clone()).unwrap(); - assert_eq!(min_volume, min_volume_maker); -} + assert!(rc.0.is_success(), "!best_orders: {}", rc.1); + let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); + let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap(); + assert_eq!(1, best_mycoin1_orders.len()); + let expected_price: BigDecimal = "0.8".parse().unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[0].price); -#[test] -fn test_setprice_min_volume_dust() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + let rc = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "best_orders", + "coin": "MYCOIN", + "action": "buy", + "volume": "1.7", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!best_orders: {}", rc.1); + let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); + // MYCOIN1 + let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap(); + let expected_price: BigDecimal = "0.7".parse().unwrap(); + let bob_mycoin1_addr = block_on(my_balance(&mm_bob, "MYCOIN1")).address; + // let bob_mycoin1_addr = mm_bob.display_address("MYCOIN1").unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[0].price); + assert_eq!(bob_mycoin1_addr, best_mycoin1_orders[0].address); + let expected_price: BigDecimal = "0.8".parse().unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[1].price); + assert_eq!(bob_mycoin1_addr, best_mycoin1_orders[1].address); + // ETH + let expected_price: BigDecimal = "0.8".parse().unwrap(); + let best_eth_orders = response.result.get("ETH").unwrap(); + assert_eq!(expected_price, best_eth_orders[0].price); - let coins = json! ([ - {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"dust":10000000,"protocol":{"type":"UTXO"}}, - mycoin1_conf(1000), - ]); + let rc = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "best_orders", + "coin": "MYCOIN", + "action": "sell", + "volume": "0.1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!best_orders: {}", rc.1); + let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let expected_price: BigDecimal = "1.25".parse().unwrap(); - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); + let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[0].price); + assert_eq!(1, best_mycoin1_orders.len()); - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + let best_eth_orders = response.result.get("ETH").unwrap(); + assert_eq!(expected_price, best_eth_orders[0].price); - log!("Issue bob MYCOIN/MYCOIN1 sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": "1", - "volume": "1", + let rc = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "best_orders", + "coin": "ETH", + "action": "sell", + "volume": "0.1", }))) .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - let response: SetPriceResponse = serde_json::from_str(&rc.1).unwrap(); - let expected_min = BigDecimal::from(1); - assert_eq!(expected_min, response.result.min_base_vol); + assert!(rc.0.is_success(), "!best_orders: {}", rc.1); + let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); - log!("Issue bob MYCOIN/MYCOIN1 sell request less than dust"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "setprice", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": "1", - // Less than dust, should fial - "volume": 0.01, - }))) - .unwrap(); - assert!(!rc.0.is_success(), "!setprice: {}", rc.1); -} + let expected_price: BigDecimal = "1.25".parse().unwrap(); -#[test] -fn test_sell_min_volume_dust() { - let privkey = random_secp256k1_secret(); - generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[0].price); + assert_eq!("MYCOIN1", best_mycoin1_orders[0].coin); + assert_eq!(1, best_mycoin1_orders.len()); - let coins = json! ([ - {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"dust":10000000,"protocol":{"type":"UTXO"}}, - mycoin1_conf(1000), - ]); + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} - let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); - let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); +#[test] +fn test_best_orders_v2_by_number() { + let (mut mm_bob, mm_alice) = get_bob_alice(); - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("MM log path: {}", mm.log_path.display()); + // issue sell request on Bob side by setting base/rel price + log!("Issue bob sell requests"); - // Enable coins - log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); - log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + let bob_orders = [ + // (base, rel, price, volume, min_volume) + ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), + ("MYCOIN", "MYCOIN1", "0.8", "0.9", None), + ("MYCOIN", "MYCOIN1", "0.7", "0.9", Some("0.9")), + ("MYCOIN", "ETH", "0.8", "0.9", None), + ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), + ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), + ("ETH", "MYCOIN", "0.8", "0.9", None), + ("MYCOIN1", "ETH", "0.8", "0.8", None), + ("MYCOIN1", "ETH", "0.7", "0.8", Some("0.8")), + ]; + for (base, rel, price, volume, min_volume) in bob_orders.iter() { + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": price, + "volume": volume, + "min_volume": min_volume.unwrap_or("0.00777"), + "cancel_previous": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + } - log!("Issue bob MYCOIN/MYCOIN1 sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": "1", - "volume": "1", - "order_type": { - "type": "FillOrKill" - } - }))) + block_on(mm_bob.wait_for_log(22., |log| { + log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") + })) .unwrap(); - assert!(rc.0.is_success(), "!sell: {}", rc.1); - let response: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); - let expected_min = BigDecimal::from(1); - assert_eq!(response.result.min_volume, expected_min); - log!("Issue bob MYCOIN/MYCOIN1 sell request"); - let rc = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "method": "sell", - "base": "MYCOIN", - "rel": "MYCOIN1", - "price": "1", - // Less than dust - "volume": 0.01, - "order_type": { - "type": "FillOrKill" - } - }))) - .unwrap(); - assert!(!rc.0.is_success(), "!sell: {}", rc.1); + let response = block_on(best_orders_v2_by_number(&mm_alice, "MYCOIN", "buy", 1, false)); + log!("response {response:?}"); + let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); + log!("Best MYCOIN1 orders when buy MYCOIN {:?}", [best_mycoin1_orders]); + assert_eq!(1, best_mycoin1_orders.len()); + let expected_price: BigDecimal = "0.7".parse().unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); + + let response = block_on(best_orders_v2_by_number(&mm_alice, "MYCOIN", "buy", 2, false)); + log!("response {response:?}"); + let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); + log!("Best MYCOIN1 orders when buy MYCOIN {:?}", [best_mycoin1_orders]); + assert_eq!(2, best_mycoin1_orders.len()); + let expected_price: BigDecimal = "0.7".parse().unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); + let expected_price: BigDecimal = "0.8".parse().unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[1].price.decimal); + + let response = block_on(best_orders_v2_by_number(&mm_alice, "MYCOIN", "sell", 1, false)); + log!("response {response:?}"); + let expected_price: BigDecimal = "1.25".parse().unwrap(); + let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); + log!("Best MYCOIN1 orders when sell MYCOIN {:?}", [best_mycoin1_orders]); + assert_eq!(1, best_mycoin1_orders.len()); + assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); + let best_eth_orders = response.result.orders.get("ETH").unwrap(); + log!("Best ETH orders when sell MYCOIN {:?}", [best_eth_orders]); + assert_eq!(1, best_eth_orders.len()); + assert_eq!(expected_price, best_eth_orders[0].price.decimal); + + let response = block_on(best_orders_v2_by_number(&mm_alice, "ETH", "sell", 1, false)); + log!("response {response:?}"); + let best_mycoin_orders = response.result.orders.get("MYCOIN").unwrap(); + log!("Best MYCOIN orders when sell ETH {:?}", [best_mycoin_orders]); + assert_eq!(1, best_mycoin_orders.len()); + let expected_price: BigDecimal = "1.25".parse().unwrap(); + assert_eq!(expected_price, best_mycoin_orders[0].price.decimal); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); } -fn request_and_check_orderbook_depth(mm_alice: &MarketMakerIt) { - let rc = block_on(mm_alice.rpc(&json! ({ - "userpass": mm_alice.userpass, - "method": "orderbook_depth", - "pairs": [("MYCOIN", "MYCOIN1"), ("MYCOIN", "ETH"), ("MYCOIN1", "ETH")], - }))) - .unwrap(); - assert!(rc.0.is_success(), "!orderbook_depth: {}", rc.1); - let response: OrderbookDepthResponse = serde_json::from_str(&rc.1).unwrap(); - let mycoin_mycoin1 = response - .result - .iter() - .find(|pair_depth| pair_depth.pair.0 == "MYCOIN" && pair_depth.pair.1 == "MYCOIN1") - .unwrap(); - assert_eq!(3, mycoin_mycoin1.depth.asks); - assert_eq!(2, mycoin_mycoin1.depth.bids); +#[test] +fn test_best_orders_v2_by_volume() { + let (mut mm_bob, mm_alice) = get_bob_alice(); - let mycoin_eth = response - .result - .iter() - .find(|pair_depth| pair_depth.pair.0 == "MYCOIN" && pair_depth.pair.1 == "ETH") - .unwrap(); - assert_eq!(1, mycoin_eth.depth.asks); - assert_eq!(1, mycoin_eth.depth.bids); + // issue sell request on Bob side by setting base/rel price + log!("Issue bob sell requests"); - let mycoin1_eth = response - .result - .iter() - .find(|pair_depth| pair_depth.pair.0 == "MYCOIN1" && pair_depth.pair.1 == "ETH") + let bob_orders = [ + // (base, rel, price, volume, min_volume) + ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), + ("MYCOIN", "MYCOIN1", "0.8", "0.9", None), + ("MYCOIN", "MYCOIN1", "0.7", "0.9", Some("0.9")), + ("MYCOIN", "ETH", "0.8", "0.9", None), + ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), + ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), + ("ETH", "MYCOIN", "0.8", "0.9", None), + ("MYCOIN1", "ETH", "0.8", "0.8", None), + ("MYCOIN1", "ETH", "0.7", "0.8", Some("0.8")), + ]; + for (base, rel, price, volume, min_volume) in bob_orders.iter() { + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": price, + "volume": volume, + "min_volume": min_volume.unwrap_or("0.00777"), + "cancel_previous": false, + }))) .unwrap(); - assert_eq!(0, mycoin1_eth.depth.asks); - assert_eq!(0, mycoin1_eth.depth.bids); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + } + + block_on(mm_bob.wait_for_log(22., |log| { + log.contains("DEBUG Handling IncludedTorelaysMesh message for peer") + })) + .unwrap(); + + let response = block_on(best_orders_v2(&mm_alice, "MYCOIN", "buy", "1.7")); + log!("response {response:?}"); + // MYCOIN1 + let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); + log!("Best MYCOIN1 orders when buy MYCOIN {:?}", [best_mycoin1_orders]); + let expected_price: BigDecimal = "0.7".parse().unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); + let expected_price: BigDecimal = "0.8".parse().unwrap(); + assert_eq!(expected_price, best_mycoin1_orders[1].price.decimal); + // ETH + let expected_price: BigDecimal = "0.8".parse().unwrap(); + let best_eth_orders = response.result.orders.get("ETH").unwrap(); + log!("Best ETH orders when buy MYCOIN {:?}", [best_eth_orders]); + assert_eq!(expected_price, best_eth_orders[0].price.decimal); + + let response = block_on(best_orders_v2(&mm_alice, "MYCOIN", "sell", "0.1")); + log!("response {response:?}"); + let expected_price: BigDecimal = "1.25".parse().unwrap(); + let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); + log!("Best MYCOIN1 orders when sell MYCOIN {:?}", [best_mycoin1_orders]); + assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); + assert_eq!(1, best_mycoin1_orders.len()); + let best_eth_orders = response.result.orders.get("ETH").unwrap(); + log!("Best ETH orders when sell MYCOIN {:?}", [best_mycoin1_orders]); + assert_eq!(expected_price, best_eth_orders[0].price.decimal); + + let response = block_on(best_orders_v2(&mm_alice, "ETH", "sell", "0.1")); + log!("response {response:?}"); + let expected_price: BigDecimal = "1.25".parse().unwrap(); + let best_mycoin1_orders = response.result.orders.get("MYCOIN1").unwrap(); + log!("Best MYCOIN1 orders when sell ETH {:?}", [best_mycoin1_orders]); + assert_eq!(expected_price, best_mycoin1_orders[0].price.decimal); + assert_eq!("MYCOIN1", best_mycoin1_orders[0].coin); + assert_eq!(1, best_mycoin1_orders.len()); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); } #[test] -fn test_orderbook_depth() { +fn test_best_orders_filter_response() { + // alice defined MYCOIN1 as "wallet_only" in config + let alice_coins = json!([ + mycoin_conf(1000), + {"coin":"MYCOIN1","asset":"MYCOIN1","rpcport":11608,"wallet_only": true,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, + eth_dev_conf(), + ]); + let bob_priv_key = random_secp256k1_secret(); let alice_priv_key = random_secp256k1_secret(); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - // Fill bob's addresses with coins. generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), bob_priv_key); generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), bob_priv_key); fill_eth_erc20_with_private_key(bob_priv_key); - // Fill alice's addresses with coins. generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), alice_priv_key); generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), alice_priv_key); fill_eth_erc20_with_private_key(alice_priv_key); - let coins = json!([ - mycoin_conf(1000), - mycoin1_conf(1000), - eth_dev_conf(), - erc20_dev_conf(&erc20_contract_checksum()) - ]); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000), eth_dev_conf(),]); let bob_conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(bob_priv_key)), &coins); let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); @@ -5559,9 +808,9 @@ fn test_orderbook_depth() { let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - // Enable all the coins for bob log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + let swap_contract = swap_contract_checksum(); dbg!(block_on(enable_eth_coin( &mm_bob, "ETH", @@ -5570,17 +819,10 @@ fn test_orderbook_depth() { None, false ))); - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false - ))); // issue sell request on Bob side by setting base/rel price log!("Issue bob sell requests"); + let bob_orders = [ // (base, rel, price, volume, min_volume) ("MYCOIN", "MYCOIN1", "0.9", "0.9", None), @@ -5590,6 +832,8 @@ fn test_orderbook_depth() { ("MYCOIN1", "MYCOIN", "0.8", "0.9", None), ("MYCOIN1", "MYCOIN", "0.9", "0.9", None), ("ETH", "MYCOIN", "0.8", "0.9", None), + ("MYCOIN1", "ETH", "0.8", "0.8", None), + ("MYCOIN1", "ETH", "0.7", "0.8", Some("0.8")), ]; for (base, rel, price, volume, min_volume) in bob_orders.iter() { let rc = block_on(mm_bob.rpc(&json! ({ @@ -5608,7 +852,7 @@ fn test_orderbook_depth() { let alice_conf = Mm2TestConf::light_node( &format!("0x{}", hex::encode(alice_priv_key)), - &coins, + &alice_coins, &[&mm_bob.ip.to_string()], ); let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); @@ -5621,173 +865,22 @@ fn test_orderbook_depth() { })) .unwrap(); - request_and_check_orderbook_depth(&mm_alice); - // request MYCOIN/MYCOIN1 orderbook to subscribe Alice let rc = block_on(mm_alice.rpc(&json! ({ "userpass": mm_alice.userpass, - "method": "orderbook", - "base": "MYCOIN", - "rel": "MYCOIN1", + "method": "best_orders", + "coin": "MYCOIN", + "action": "buy", + "volume": "0.1", }))) .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - request_and_check_orderbook_depth(&mm_alice); + assert!(rc.0.is_success(), "!best_orders: {}", rc.1); + let response: BestOrdersResponse = serde_json::from_str(&rc.1).unwrap(); + let empty_vec = Vec::new(); + let best_mycoin1_orders = response.result.get("MYCOIN1").unwrap_or(&empty_vec); + assert_eq!(0, best_mycoin1_orders.len()); + let best_eth_orders = response.result.get("ETH").unwrap(); + assert_eq!(1, best_eth_orders.len()); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } - -#[test] -fn test_approve_erc20() { - let privkey = random_secp256k1_secret(); - fill_eth_erc20_with_private_key(privkey); - - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - let mm = MarketMakerIt::start( - Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins).conf, - DEFAULT_RPC_PASSWORD.to_string(), - None, - ) - .unwrap(); - - let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("Node log path: {}", mm.log_path.display()); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - let _eth_enable = block_on(enable_eth_coin( - &mm, - "ETH", - &[GETH_RPC_URL], - &swap_contract, - None, - false, - )); - let _erc20_enable = block_on(enable_eth_coin( - &mm, - "ERC20DEV", - &[GETH_RPC_URL], - &swap_contract, - None, - false, - )); - - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method":"approve_token", - "mmrpc":"2.0", - "id": 0, - "params":{ - "coin": "ERC20DEV", - "spender": swap_contract, - "amount": BigDecimal::from_str("11.0").unwrap(), - } - }))) - .unwrap(); - assert!(rc.0.is_success(), "approve_token error: {}", rc.1); - let res = serde_json::from_str::(&rc.1).unwrap(); - assert!( - hex::decode(str_strip_0x!(res["result"].as_str().unwrap())).is_ok(), - "approve_token result incorrect" - ); - thread::sleep(Duration::from_secs(5)); - let rc = block_on(mm.rpc(&json!({ - "userpass": mm.userpass, - "method":"get_token_allowance", - "mmrpc":"2.0", - "id": 0, - "params":{ - "coin": "ERC20DEV", - "spender": swap_contract, - } - }))) - .unwrap(); - assert!(rc.0.is_success(), "get_token_allowance error: {}", rc.1); - let res = serde_json::from_str::(&rc.1).unwrap(); - assert_eq!( - BigDecimal::from_str(res["result"].as_str().unwrap()).unwrap(), - BigDecimal::from_str("11.0").unwrap(), - "get_token_allowance result incorrect" - ); - - block_on(mm.stop()).unwrap(); -} - -#[test] -fn test_peer_time_sync_validation() { - let timeoffset_tolerable = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() - 1; - let timeoffset_too_big = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() + 1; - - let start_peers_with_time_offset = |offset: i64| -> (Json, Json) { - let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 10.into()); - let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 10.into()); - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let bob_conf = Mm2TestConf::seednode(&hex::encode(bob_priv_key), &coins); - let mut mm_bob = block_on(MarketMakerIt::start_with_envs( - bob_conf.conf, - bob_conf.rpc_password, - None, - &[], - )) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); - block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - let alice_conf = - Mm2TestConf::light_node(&hex::encode(alice_priv_key), &coins, &[mm_bob.ip.to_string().as_str()]); - let mut mm_alice = block_on(MarketMakerIt::start_with_envs( - alice_conf.conf, - alice_conf.rpc_password, - None, - &[("TEST_TIMESTAMP_OFFSET", offset.to_string().as_str())], - )) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - - let res_bob = block_on(mm_bob.rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "get_directly_connected_peers", - }))) - .unwrap(); - assert!(res_bob.0.is_success(), "!get_directly_connected_peers: {}", res_bob.1); - let bob_peers = serde_json::from_str::(&res_bob.1).unwrap(); - - let res_alice = block_on(mm_alice.rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "get_directly_connected_peers", - }))) - .unwrap(); - assert!( - res_alice.0.is_success(), - "!get_directly_connected_peers: {}", - res_alice.1 - ); - let alice_peers = serde_json::from_str::(&res_alice.1).unwrap(); - - block_on(mm_bob.stop()).unwrap(); - block_on(mm_alice.stop()).unwrap(); - (bob_peers, alice_peers) - }; - - // check with small time offset: - let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_tolerable); - assert!( - bob_peers["result"].as_object().unwrap().len() == 1, - "bob must have one peer" - ); - assert!( - alice_peers["result"].as_object().unwrap().len() == 1, - "alice must have one peer" - ); - - // check with too big time offset: - let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_too_big); - assert!( - bob_peers["result"].as_object().unwrap().is_empty(), - "bob must have no peers" - ); - assert!( - alice_peers["result"].as_object().unwrap().is_empty(), - "alice must have no peers" - ); -} diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 8cc759ffb8..b22456dd1f 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -1,37 +1,33 @@ -use super::docker_tests_common::{ - random_secp256k1_secret, ERC1155_TEST_ABI, ERC721_TEST_ABI, GETH_ACCOUNT, GETH_ERC1155_CONTRACT, - GETH_ERC20_CONTRACT, GETH_ERC721_CONTRACT, GETH_MAKER_SWAP_V2, GETH_NFT_MAKER_SWAP_V2, GETH_NONCE_LOCK, - GETH_RPC_URL, GETH_SWAP_CONTRACT, GETH_TAKER_SWAP_V2, GETH_WATCHERS_SWAP_CONTRACT, GETH_WEB3, MM_CTX, MM_CTX1, -}; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use super::docker_tests_common::{ - SEPOLIA_ERC20_CONTRACT, SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2, SEPOLIA_MAKER_SWAP_V2, SEPOLIA_NONCE_LOCK, - SEPOLIA_RPC_URL, SEPOLIA_TAKER_SWAP_V2, SEPOLIA_TESTS_LOCK, SEPOLIA_WEB3, +use super::helpers::env::random_secp256k1_secret; +use super::helpers::eth::{ + erc20_coin_with_random_privkey, erc20_contract, erc20_contract_checksum, eth_coin_with_random_privkey, + eth_coin_with_random_privkey_using_urls, fill_erc20, fill_eth, geth_account, geth_erc1155_contract, + geth_erc721_contract, geth_maker_swap_v2, geth_nft_maker_swap_v2, geth_taker_swap_v2, swap_contract, + swap_contract_checksum, GETH_DEV_CHAIN_ID, GETH_NONCE_LOCK, GETH_RPC_URL, GETH_WEB3, MM_CTX, MM_CTX1, }; use crate::common::Future01CompatExt; use bitcrypto::{dhash160, sha256}; use coins::eth::gas_limit::ETH_MAX_TRADE_GAS; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivationV2Request, EthNode}; use coins::eth::{ - checksum_address, eth_coin_from_conf_and_request, ChainSpec, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, - SignedEthTx, SwapV2Contracts, ERC20_ABI, + eth_coin_from_conf_and_request, ChainSpec, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, SignedEthTx, + SwapV2Contracts, }; use coins::hd_wallet::AddrToString; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use coins::{ - lp_coinfind, CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, - MakerCoinSwapOpsV2, MmCoinStruct, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, - RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, SendMakerPaymentArgs, SendTakerFundingArgs, - SpendMakerPaymentArgs, TakerCoinSwapOpsV2, TxPreimageWithSig, ValidateMakerPaymentArgs, ValidateTakerFundingArgs, -}; use coins::{ - lp_register_coin, CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, DerivationMethod, - Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, MmCoinEnum, NftSwapInfo, ParseCoinAssocTypes, + lp_register_coin, CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, Eip1559Ops, + FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, MmCoinEnum, NftSwapInfo, ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, RegisterCoinParams, SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, SpendPaymentArgs, SwapGasFeePolicy, SwapOps, SwapTxTypeWithSecretHash, ToBytes, Transaction, ValidateNftMakerPaymentArgs, }; +use coins::{ + DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, + RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, + SendMakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, TakerCoinSwapOpsV2, TxPreimageWithSig, + ValidateMakerPaymentArgs, ValidateTakerFundingArgs, +}; use common::{block_on, block_on_f01, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256; @@ -41,118 +37,31 @@ use mm2_test_helpers::for_tests::{ account_balance, active_swaps, check_recent_swaps, coins_needed_for_kickstart, disable_coin, enable_erc20_token_v2, enable_eth_coin_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, get_new_address, get_token_info, mm_dump, my_balance, my_swap_status, nft_dev_conf, start_swaps, task_enable_eth_with_tokens, - wait_for_swap_finished, MarketMakerIt, Mm2TestConf, SwapV2TestContracts, TestNode, ETH_SEPOLIA_CHAIN_ID, + wait_for_swap_finished, MarketMakerIt, Mm2TestConf, SwapV2TestContracts, TestNode, }; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; use mm2_test_helpers::structs::{ Bip44Chain, EnableCoinBalanceMap, EthWithTokensActivationResult, HDAccountAddressId, TokenInfo, }; use num_traits::FromPrimitive; -use serde_json::Value as Json; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use serde_json::{json, Value as Json}; use std::str::FromStr; use std::thread; use std::time::Duration; use uuid::Uuid; use web3::contract::{Contract, Options}; use web3::ethabi::Token; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use web3::types::BlockNumber; -use web3::types::{Address, TransactionRequest, H256}; - -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -const SEPOLIA_MAKER_PRIV: &str = "6e2f3a6223b928a05a3a3622b0c3f3573d03663b704a61a6eb73326de0487928"; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba2110bddd281e711608f16"; +use web3::types::{Address, H256}; + const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; -const GETH_DEV_CHAIN_ID: u64 = 1337; - -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -const ERC20: &str = "ERC20DEV"; - -/// # Safety -/// -/// GETH_ACCOUNT is set once during initialization before tests start -pub fn geth_account() -> Address { - unsafe { GETH_ACCOUNT } -} -/// # Safety -/// -/// GETH_SWAP_CONTRACT is set once during initialization before tests start -pub fn swap_contract() -> Address { - unsafe { GETH_SWAP_CONTRACT } -} -/// # Safety -/// -/// GETH_MAKER_SWAP_V2 is set once during initialization before tests start -pub fn maker_swap_v2() -> Address { - unsafe { GETH_MAKER_SWAP_V2 } -} -/// # Safety -/// -/// GETH_TAKER_SWAP_V2 is set once during initialization before tests start -pub fn taker_swap_v2() -> Address { - unsafe { GETH_TAKER_SWAP_V2 } -} -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -pub fn sepolia_taker_swap_v2() -> Address { - unsafe { SEPOLIA_TAKER_SWAP_V2 } -} -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -pub fn sepolia_maker_swap_v2() -> Address { - unsafe { SEPOLIA_MAKER_SWAP_V2 } -} -/// # Safety -/// -/// GETH_NFT_MAKER_SWAP_V2 is set once during initialization before tests start -pub fn geth_nft_maker_swap_v2() -> Address { - unsafe { GETH_NFT_MAKER_SWAP_V2 } -} -/// # Safety -/// -/// GETH_WATCHERS_SWAP_CONTRACT is set once during initialization before tests start -pub fn watchers_swap_contract() -> Address { - unsafe { GETH_WATCHERS_SWAP_CONTRACT } -} -/// # Safety -/// -/// GETH_ERC20_CONTRACT is set once during initialization before tests start -pub fn erc20_contract() -> Address { - unsafe { GETH_ERC20_CONTRACT } -} -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -pub fn sepolia_erc20_contract() -> Address { - unsafe { SEPOLIA_ERC20_CONTRACT } -} -/// Return ERC20 dev token contract address in checksum format -pub fn erc20_contract_checksum() -> String { - checksum_address(&format!("{:02x}", erc20_contract())) -} -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -pub fn sepolia_erc20_contract_checksum() -> String { - checksum_address(&format!("{:02x}", sepolia_erc20_contract())) -} -/// # Safety -/// -/// GETH_ERC721_CONTRACT is set once during initialization before tests start -pub fn geth_erc721_contract() -> Address { - unsafe { GETH_ERC721_CONTRACT } -} -/// # Safety -/// -/// GETH_ERC1155_CONTRACT is set once during initialization before tests start -pub fn geth_erc1155_contract() -> Address { - unsafe { GETH_ERC1155_CONTRACT } -} -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -/// # Safety -/// -/// SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 address is set once during initialization before tests start -pub fn sepolia_etomic_maker_nft() -> Address { - unsafe { SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 } -} +const ERC20DEV: &str = "ERC20DEV"; + +/// ERC721_TEST_TOKEN has additional mint function +/// https://github.com/KomodoPlatform/etomic-swap/blob/public-mint-nft-functions/contracts/Erc721Token.sol (see public-mint-nft-functions branch) +const ERC721_TEST_ABI: &str = include_str!("../../../mm2_test_helpers/dummy_files/erc721_test_abi.json"); +/// ERC1155_TEST_TOKEN has additional mint function +/// https://github.com/KomodoPlatform/etomic-swap/blob/public-mint-nft-functions/contracts/Erc1155Token.sol (see public-mint-nft-functions branch) +const ERC1155_TEST_ABI: &str = include_str!("../../../mm2_test_helpers/dummy_files/erc1155_test_abi.json"); fn wait_for_confirmation(tx_hash: H256) { thread::sleep(Duration::from_millis(2000)); @@ -169,40 +78,6 @@ fn wait_for_confirmation(tx_hash: H256) { } } -pub fn fill_eth(to_addr: Address, amount: U256) { - let _guard = GETH_NONCE_LOCK.lock().unwrap(); - let tx_request = TransactionRequest { - from: geth_account(), - to: Some(to_addr), - gas: None, - gas_price: None, - value: Some(amount), - data: None, - nonce: None, - condition: None, - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }; - let tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request)).unwrap(); - wait_for_confirmation(tx_hash); -} - -fn fill_erc20(to_addr: Address, amount: U256) { - let _guard = GETH_NONCE_LOCK.lock().unwrap(); - let erc20_contract = Contract::from_json(GETH_WEB3.eth(), erc20_contract(), ERC20_ABI.as_bytes()).unwrap(); - - let tx_hash = block_on(erc20_contract.call( - "transfer", - (Token::Address(to_addr), Token::Uint(amount)), - geth_account(), - Options::default(), - )) - .unwrap(); - wait_for_confirmation(tx_hash); -} - fn mint_erc721(to_addr: Address, token_id: U256) { let _guard = GETH_NONCE_LOCK.lock().unwrap(); let erc721_contract = @@ -322,81 +197,6 @@ pub(crate) async fn fill_erc721_info(eth_coin: &EthCoin, token_address: Address, nft_infos.insert(erc721_key, erc721_nft_info); } -/// Creates ETH protocol coin supplied with 100 ETH -pub fn eth_coin_with_random_privkey_using_urls(swap_contract_address: Address, urls: &[&str]) -> EthCoin { - let eth_conf = eth_dev_conf(); - let req = json!({ - "method": "enable", - "coin": "ETH", - "swap_contract_address": swap_contract_address, - "urls": urls, - }); - - let secret = random_secp256k1_secret(); - let eth_coin = block_on(eth_coin_from_conf_and_request( - &MM_CTX, - "ETH", - ð_conf, - &req, - CoinProtocol::ETH { - chain_id: GETH_DEV_CHAIN_ID, - }, - PrivKeyBuildPolicy::IguanaPrivKey(secret), - )) - .unwrap(); - - let my_address = match eth_coin.derivation_method() { - DerivationMethod::SingleAddress(addr) => *addr, - _ => panic!("Expected single address"), - }; - - // 100 ETH - fill_eth(my_address, U256::from(10).pow(U256::from(20))); - - eth_coin -} - -/// Creates ETH protocol coin supplied with 100 ETH, using the default GETH_RPC_URL -pub fn eth_coin_with_random_privkey(swap_contract_address: Address) -> EthCoin { - eth_coin_with_random_privkey_using_urls(swap_contract_address, &[GETH_RPC_URL]) -} - -/// Creates ERC20 protocol coin supplied with 1 ETH and 100 token -pub fn erc20_coin_with_random_privkey(swap_contract_address: Address) -> EthCoin { - let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); - let req = json!({ - "method": "enable", - "coin": "ERC20DEV", - "swap_contract_address": swap_contract_address, - "urls": [GETH_RPC_URL], - }); - - let erc20_coin = block_on(eth_coin_from_conf_and_request( - &MM_CTX, - "ERC20DEV", - &erc20_conf, - &req, - CoinProtocol::ERC20 { - platform: "ETH".to_string(), - contract_address: checksum_address(&format!("{:02x}", erc20_contract())), - }, - PrivKeyBuildPolicy::IguanaPrivKey(random_secp256k1_secret()), - )) - .unwrap(); - - let my_address = match erc20_coin.derivation_method() { - DerivationMethod::SingleAddress(addr) => *addr, - _ => panic!("Expected single address"), - }; - - // 1 ETH - fill_eth(my_address, U256::from(10).pow(U256::from(18))); - // 100 tokens (it has 8 decimals) - fill_erc20(my_address, U256::from(10000000000u64)); - - erc20_coin -} - #[derive(Clone, Copy, Debug)] pub enum TestNftType { Erc1155 { token_id: u32, amount: u32 }, @@ -449,7 +249,8 @@ fn global_nft_with_random_privkey( let coin_type = EthCoinType::Nft { platform: platform_ticker, }; - let global_nft = block_on(coin.set_coin_type(coin_type)); + // NFT coins use ETH decimals (18) + let global_nft = block_on(coin.set_coin_type(coin_type, 18)); let my_address = block_on(coin.my_addr()); fill_eth(my_address, U256::from(10).pow(U256::from(20))); @@ -473,133 +274,6 @@ fn global_nft_with_random_privkey( global_nft } -// Todo: This shouldn't be part of docker tests, move it to a separate module or stop relying on it -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -/// Can be used to generate coin from Sepolia Maker/Taker priv keys. -fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, conf: &Json, erc20: bool) -> EthCoin { - let swap_addr = SwapAddresses { - swap_v2_contracts: SwapV2Contracts { - maker_swap_v2_contract: sepolia_maker_swap_v2(), - taker_swap_v2_contract: sepolia_taker_swap_v2(), - nft_maker_swap_v2_contract: sepolia_etomic_maker_nft(), - }, - swap_contract_address: sepolia_taker_swap_v2(), - fallback_swap_contract_address: sepolia_taker_swap_v2(), - }; - - let priv_key = Secp256k1Secret::from(secret); - let build_policy = EthPrivKeyBuildPolicy::IguanaPrivKey(priv_key); - - let node = EthNode { - url: SEPOLIA_RPC_URL.to_string(), - komodo_proxy: false, - }; - let platform_request = EthActivationV2Request { - nodes: vec![node], - rpc_mode: Default::default(), - swap_contract_address: swap_addr.swap_contract_address, - swap_v2_contracts: Some(swap_addr.swap_v2_contracts), - fallback_swap_contract: Some(swap_addr.fallback_swap_contract_address), - contract_supports_watchers: false, - mm2: None, - required_confirmations: None, - priv_key_policy: Default::default(), - enable_params: Default::default(), - path_to_address: Default::default(), - gap_limit: None, - swap_gas_fee_policy: None, - }; - let coin = block_on(eth_coin_from_conf_and_request_v2( - ctx, - ticker, - conf, - platform_request, - build_policy, - ChainSpec::Evm { - chain_id: ETH_SEPOLIA_CHAIN_ID, - }, - )) - .unwrap(); - let coin = if erc20 { - let coin_type = EthCoinType::Erc20 { - platform: ETH.to_string(), - token_addr: sepolia_erc20_contract(), - }; - block_on(coin.set_coin_type(coin_type)) - } else { - coin - }; - - let coins_ctx = CoinsContext::from_ctx(ctx).unwrap(); - let mut coins = block_on(coins_ctx.lock_coins()); - coins.insert( - coin.ticker().into(), - MmCoinStruct::new(MmCoinEnum::EthCoinVariant(coin.clone())), - ); - coin -} - -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -fn get_or_create_sepolia_coin(ctx: &MmArc, priv_key: &'static str, ticker: &str, conf: &Json, erc20: bool) -> EthCoin { - match block_on(lp_coinfind(ctx, ticker)).unwrap() { - None => sepolia_coin_from_privkey(ctx, priv_key, ticker, conf, erc20), - Some(mm_coin) => match mm_coin { - MmCoinEnum::EthCoinVariant(coin) => coin, - _ => panic!("Unexpected coin type found. Expected MmCoinEnum::EthCoin"), - }, - } -} - -/// Fills the private key's public address with ETH and ERC20 tokens -pub fn fill_eth_erc20_with_private_key(priv_key: Secp256k1Secret) { - let eth_conf = eth_dev_conf(); - let req = json!({ - "coin": "ETH", - "urls": [GETH_RPC_URL], - "swap_contract_address": swap_contract(), - }); - - let eth_coin = block_on(eth_coin_from_conf_and_request( - &MM_CTX, - "ETH", - ð_conf, - &req, - CoinProtocol::ETH { - chain_id: GETH_DEV_CHAIN_ID, - }, - PrivKeyBuildPolicy::IguanaPrivKey(priv_key), - )) - .unwrap(); - let my_address = block_on(eth_coin.derivation_method().single_addr_or_err()).unwrap(); - - // 100 ETH - fill_eth(my_address, U256::from(10).pow(U256::from(20))); - - let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); - let req = json!({ - "method": "enable", - "coin": "ERC20DEV", - "urls": [GETH_RPC_URL], - "swap_contract_address": swap_contract(), - }); - - let _erc20_coin = block_on(eth_coin_from_conf_and_request( - &MM_CTX, - "ERC20DEV", - &erc20_conf, - &req, - CoinProtocol::ERC20 { - platform: "ETH".to_string(), - contract_address: erc20_contract_checksum(), - }, - PrivKeyBuildPolicy::IguanaPrivKey(priv_key), - )) - .unwrap(); - - // 100 tokens (it has 8 decimals) - fill_erc20(my_address, U256::from(10000000000u64)); -} - fn send_and_refund_eth_maker_payment_impl(swap_txfee_policy: SwapGasFeePolicy) { thread::sleep(Duration::from_secs(3)); let eth_coin = eth_coin_with_random_privkey(swap_contract()); @@ -665,7 +339,6 @@ fn send_and_refund_eth_maker_payment_impl(swap_txfee_policy: SwapGasFeePolicy) { search_from_block: 0, swap_contract_address: &Some(swap_contract().as_bytes().into()), swap_unique_data: &[], - watcher_reward: false, }; let search_tx = block_on(eth_coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -752,7 +425,6 @@ fn send_and_spend_eth_maker_payment_impl(swap_txfee_policy: SwapGasFeePolicy) { search_from_block: 0, swap_contract_address: &Some(swap_contract().as_bytes().into()), swap_unique_data: &[], - watcher_reward: false, }; let search_tx = block_on(maker_eth_coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -838,7 +510,6 @@ fn send_and_refund_erc20_maker_payment_impl(swap_txfee_policy: SwapGasFeePolicy) search_from_block: 0, swap_contract_address: &Some(swap_contract().as_bytes().into()), swap_unique_data: &[], - watcher_reward: false, }; let search_tx = block_on(erc20_coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -926,7 +597,6 @@ fn send_and_spend_erc20_maker_payment_impl(swap_txfee_policy: SwapGasFeePolicy) search_from_block: 0, swap_contract_address: &Some(swap_contract().as_bytes().into()), swap_unique_data: &[], - watcher_reward: false, }; let search_tx = block_on(maker_erc20_coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -946,33 +616,6 @@ fn send_and_spend_erc20_maker_payment_priority_fee() { send_and_spend_erc20_maker_payment_impl(SwapGasFeePolicy::Medium); } -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -/// Wait for all pending transactions for the given address to be confirmed -fn wait_pending_transactions(wallet_address: Address) { - let _guard = SEPOLIA_NONCE_LOCK.lock().unwrap(); - let web3 = SEPOLIA_WEB3.clone(); - - loop { - let latest_nonce = block_on(web3.eth().transaction_count(wallet_address, Some(BlockNumber::Latest))).unwrap(); - let pending_nonce = block_on(web3.eth().transaction_count(wallet_address, Some(BlockNumber::Pending))).unwrap(); - - if latest_nonce == pending_nonce { - log!( - "All pending transactions have been confirmed. Latest nonce: {}", - latest_nonce - ); - break; - } else { - log!( - "Waiting for pending transactions to confirm... Current nonce: {}, Pending nonce: {}", - latest_nonce, - pending_nonce - ); - thread::sleep(Duration::from_secs(1)); - } - } -} - #[test] fn send_and_spend_erc721_maker_payment() { let token_id = 1u32; @@ -1368,8 +1011,8 @@ impl NftActivationV2Args { swap_contract_address: swap_contract(), fallback_swap_contract_address: swap_contract(), swap_v2_contracts: SwapV2Contracts { - maker_swap_v2_contract: maker_swap_v2(), - taker_swap_v2_contract: taker_swap_v2(), + maker_swap_v2_contract: geth_maker_swap_v2(), + taker_swap_v2_contract: geth_taker_swap_v2(), nft_maker_swap_v2_contract: geth_nft_maker_swap_v2(), }, nft_ticker: NFT_ETH.to_string(), @@ -1543,14 +1186,17 @@ impl SwapAddresses { swap_contract_address: swap_contract(), fallback_swap_contract_address: swap_contract(), swap_v2_contracts: SwapV2Contracts { - maker_swap_v2_contract: maker_swap_v2(), - taker_swap_v2_contract: taker_swap_v2(), + maker_swap_v2_contract: geth_maker_swap_v2(), + taker_swap_v2_contract: geth_taker_swap_v2(), nft_maker_swap_v2_contract: geth_nft_maker_swap_v2(), }, } } } +/// ERC20 test token decimals (our test token has 8 decimals) +const ERC20_TOKEN_DECIMALS: u8 = 8; + /// Needed for eth or erc20 v2 activation in Geth tests fn eth_coin_v2_activation_with_random_privkey( ctx: &MmArc, @@ -1599,19 +1245,17 @@ fn eth_coin_v2_activation_with_random_privkey( platform: ETH.to_string(), token_addr: erc20_contract(), }; - let coin = block_on(coin.set_coin_type(coin_type)); + let coin = block_on(coin.set_coin_type(coin_type, ERC20_TOKEN_DECIMALS)); return (coin, priv_key); } (coin, priv_key) } -#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_by_secret_eth() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + let swap_addr = SwapAddresses::init(); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH, ð_dev_conf(), swap_addr, false); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addr, false); let taker_secret = &[0; 32]; let taker_secret_hash = sha256(taker_secret).to_vec(); @@ -1620,8 +1264,6 @@ fn send_and_refund_taker_funding_by_secret_eth() { let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() + 1000; - let taker_address = block_on(taker_coin.my_addr()); - let dex_fee = &DexFee::Standard("0.00001".into()); let trading_amount = BigDecimal::from_str("0.0001").unwrap(); @@ -1638,8 +1280,6 @@ fn send_and_refund_taker_funding_by_secret_eth() { swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); - thread::sleep(Duration::from_secs(2)); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ETH funding, tx hash: {:02x}", funding_tx.tx_hash()); @@ -1657,32 +1297,27 @@ fn send_and_refund_taker_funding_by_secret_eth() { swap_unique_data: &[], watcher_reward: false, }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx_refund = block_on(taker_coin.refund_taker_funding_secret(refund_args)).unwrap(); log!( "Taker refunded ETH funding by secret, tx hash: {:02x}", funding_tx_refund.tx_hash() ); - wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); + wait_for_confirmations(&taker_coin, &funding_tx_refund, 30); } -#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_by_secret_erc20() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + let swap_addr = SwapAddresses::init(); + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ERC20DEV, &erc20_conf, swap_addr, true); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ERC20DEV, &erc20_conf, swap_addr, true); let taker_secret = &[0; 32]; let taker_secret_hash = sha256(taker_secret).to_vec(); let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); - let taker_address = block_on(taker_coin.my_addr()); - let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() + 1000; @@ -1702,16 +1337,15 @@ fn send_and_refund_taker_funding_by_secret_erc20() { swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ERC20 funding, tx hash: {:02x}", funding_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &funding_tx, 200); + wait_for_confirmations(&taker_coin, &funding_tx, 30); let refund_args = RefundFundingSecretArgs { funding_tx: &funding_tx, funding_time_lock, payment_time_lock, - maker_pubkey: &taker_coin.derive_htlc_pubkey_v2(&[]), + maker_pubkey: maker_pub, taker_secret, taker_secret_hash: &taker_secret_hash, maker_secret_hash: &maker_secret_hash, @@ -1721,30 +1355,25 @@ fn send_and_refund_taker_funding_by_secret_erc20() { swap_unique_data: &[], watcher_reward: false, }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx_refund = block_on(taker_coin.refund_taker_funding_secret(refund_args)).unwrap(); log!( "Taker refunded ERC20 funding by secret, tx hash: {:02x}", funding_tx_refund.tx_hash() ); - wait_for_confirmations(&taker_coin, &funding_tx_refund, 200); + wait_for_confirmations(&taker_coin, &funding_tx_refund, 30); } -#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_pre_approve_timelock_eth() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + let swap_addr = SwapAddresses::init(); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH, ð_dev_conf(), swap_addr, false); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addr, false); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); - let taker_address = block_on(taker_coin.my_addr()); - // if TakerPaymentState is `PaymentSent` then timestamp should exceed payment pre-approve lock time (funding_time_lock) let funding_time_lock = now_sec() - 3000; let payment_time_lock = now_sec() + 1000; @@ -1764,10 +1393,9 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_eth() { trading_amount: trading_amount.clone(), swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ETH funding, tx hash: {:02x}", funding_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &funding_tx, 100); + wait_for_confirmations(&taker_coin, &funding_tx, 30); let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::TakerPaymentV2 { maker_secret_hash: &maker_secret_hash, @@ -1785,23 +1413,20 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_eth() { premium_amount: BigDecimal::default(), trading_amount, }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx_refund = block_on(taker_coin.refund_taker_funding_timelock(refund_args)).unwrap(); log!( "Taker refunded ETH funding after pre-approval lock time was exceeded, tx hash: {:02x}", funding_tx_refund.tx_hash() ); - wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); + wait_for_confirmations(&taker_coin, &funding_tx_refund, 30); } -#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn taker_send_approve_and_spend_eth() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + let swap_addr = SwapAddresses::init(); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH, ð_dev_conf(), swap_addr, false); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addr, false); let taker_secret = &[0; 32]; let taker_secret_hash = sha256(taker_secret).to_vec(); @@ -1810,7 +1435,6 @@ fn taker_send_approve_and_spend_eth() { let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() + 600; - let taker_address = block_on(taker_coin.my_addr()); let maker_address = block_on(maker_coin.my_addr()); let dex_fee = &DexFee::Standard("0.00001".into()); @@ -1828,11 +1452,10 @@ fn taker_send_approve_and_spend_eth() { trading_amount: trading_amount.clone(), swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let taker_coin_start_block = block_on(taker_coin.current_block().compat()).unwrap(); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ETH funding, tx hash: {:02x}", funding_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &funding_tx, 100); + wait_for_confirmations(&taker_coin, &funding_tx, 30); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); let validate = ValidateTakerFundingArgs { @@ -1862,16 +1485,14 @@ fn taker_send_approve_and_spend_eth() { preimage: funding_tx.clone(), signature: taker_coin.parse_signature(&[0u8; 65]).unwrap(), }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let taker_approve_tx = block_on(taker_coin.sign_and_send_taker_funding_spend(&preimage, &approve_args, &[])).unwrap(); log!( "Taker approved ETH payment, tx hash: {:02x}", taker_approve_tx.tx_hash() ); - wait_for_confirmations(&taker_coin, &taker_approve_tx, 100); + wait_for_confirmations(&taker_coin, &taker_approve_tx, 30); - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let check_taker_approved_tx = block_on(maker_coin.search_for_taker_funding_spend(&funding_tx, 0u64, &[])) .unwrap() .unwrap(); @@ -1895,11 +1516,10 @@ fn taker_send_approve_and_spend_eth() { premium_amount: Default::default(), trading_amount, }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let spend_tx = block_on(maker_coin.sign_and_broadcast_taker_payment_spend(None, &spend_args, maker_secret, &[])).unwrap(); log!("Maker spent ETH payment, tx hash: {:02x}", spend_tx.tx_hash()); - wait_for_confirmations(&maker_coin, &spend_tx, 100); + wait_for_confirmations(&maker_coin, &spend_tx, 30); let found_spend_tx = block_on(taker_coin.find_taker_payment_spend_tx(&taker_approve_tx, taker_coin_start_block, payment_time_lock)) .unwrap(); @@ -1907,14 +1527,12 @@ fn taker_send_approve_and_spend_eth() { assert_eq!(maker_secret, &extracted_maker_secret); } -#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn taker_send_approve_and_spend_erc20() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + let swap_addr = SwapAddresses::init(); + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ERC20DEV, &erc20_conf, swap_addr, true); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ERC20DEV, &erc20_conf, swap_addr, true); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); @@ -1923,10 +1541,9 @@ fn taker_send_approve_and_spend_erc20() { let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() + 600; - let taker_address = block_on(taker_coin.my_addr()); let maker_address = block_on(maker_coin.my_addr()); - let dex_fee = &DexFee::NoFee; + let dex_fee = &DexFee::Standard("0.00001".into()); let trading_amount = BigDecimal::from_str("0.0001").unwrap(); let maker_pub = &maker_coin.derive_htlc_pubkey_v2(&[]); @@ -1941,11 +1558,10 @@ fn taker_send_approve_and_spend_erc20() { trading_amount: trading_amount.clone(), swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let taker_coin_start_block = block_on(taker_coin.current_block().compat()).unwrap(); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ERC20 funding, tx hash: {:02x}", funding_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &funding_tx, 100); + wait_for_confirmations(&taker_coin, &funding_tx, 30); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); let validate = ValidateTakerFundingArgs { @@ -1975,16 +1591,14 @@ fn taker_send_approve_and_spend_erc20() { preimage: funding_tx.clone(), signature: taker_coin.parse_signature(&[0u8; 65]).unwrap(), }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let taker_approve_tx = block_on(taker_coin.sign_and_send_taker_funding_spend(&preimage, &approve_args, &[])).unwrap(); log!( "Taker approved ERC20 payment, tx hash: {:02x}", taker_approve_tx.tx_hash() ); - wait_for_confirmations(&taker_coin, &taker_approve_tx, 100); + wait_for_confirmations(&taker_coin, &taker_approve_tx, 30); - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let check_taker_approved_tx = block_on(maker_coin.search_for_taker_funding_spend(&funding_tx, 0u64, &[])) .unwrap() .unwrap(); @@ -2008,7 +1622,6 @@ fn taker_send_approve_and_spend_erc20() { premium_amount: Default::default(), trading_amount, }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let spend_tx = block_on(maker_coin.sign_and_broadcast_taker_payment_spend(None, &spend_args, &maker_secret, &[])).unwrap(); log!("Maker spent ERC20 payment, tx hash: {:02x}", spend_tx.tx_hash()); @@ -2019,13 +1632,11 @@ fn taker_send_approve_and_spend_erc20() { assert_eq!(maker_secret, extracted_maker_secret); } -#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + let swap_addr = SwapAddresses::init(); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH, ð_dev_conf(), swap_addr, false); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addr, false); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); @@ -2034,8 +1645,6 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() - 1000; - let taker_address = block_on(taker_coin.my_addr()); - let dex_fee = &DexFee::Standard("0.00001".into()); let trading_amount = BigDecimal::from_str("0.0001").unwrap(); @@ -2051,10 +1660,9 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { trading_amount: trading_amount.clone(), swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ETH funding, tx hash: {:02x}", funding_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &funding_tx, 100); + wait_for_confirmations(&taker_coin, &funding_tx, 30); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); let approve_args = GenTakerFundingSpendArgs { @@ -2070,14 +1678,13 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { preimage: funding_tx.clone(), signature: taker_coin.parse_signature(&[0u8; 65]).unwrap(), }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let taker_approve_tx = block_on(taker_coin.sign_and_send_taker_funding_spend(&preimage, &approve_args, &[])).unwrap(); log!( "Taker approved ETH payment, tx hash: {:02x}", taker_approve_tx.tx_hash() ); - wait_for_confirmations(&taker_coin, &taker_approve_tx, 100); + wait_for_confirmations(&taker_coin, &taker_approve_tx, 30); let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::TakerPaymentV2 { maker_secret_hash: &maker_secret_hash, @@ -2094,23 +1701,20 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { premium_amount: BigDecimal::default(), trading_amount, }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx_refund = block_on(taker_coin.refund_taker_funding_timelock(refund_args)).unwrap(); log!( "Taker refunded ETH funding after payment lock time was exceeded, tx hash: {:02x}", funding_tx_refund.tx_hash() ); - wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); + wait_for_confirmations(&taker_coin, &funding_tx_refund, 30); } -#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + let swap_addr = SwapAddresses::init(); + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ERC20DEV, &erc20_conf, swap_addr, true); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ERC20DEV, &erc20_conf, swap_addr, true); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); @@ -2119,8 +1723,6 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { let funding_time_lock = now_sec() + 29; let payment_time_lock = now_sec() + 15; - let taker_address = block_on(taker_coin.my_addr()); - let dex_fee = &DexFee::Standard("0.00001".into()); let trading_amount = BigDecimal::from_str("0.0001").unwrap(); @@ -2136,10 +1738,9 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { trading_amount: trading_amount.clone(), swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ERC20 funding, tx hash: {:02x}", funding_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &funding_tx, 100); + wait_for_confirmations(&taker_coin, &funding_tx, 30); thread::sleep(Duration::from_secs(16)); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); @@ -2156,14 +1757,13 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { preimage: funding_tx.clone(), signature: taker_coin.parse_signature(&[0u8; 65]).unwrap(), }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let taker_approve_tx = block_on(taker_coin.sign_and_send_taker_funding_spend(&preimage, &approve_args, &[])).unwrap(); log!( "Taker approved ERC20 payment, tx hash: {:02x}", taker_approve_tx.tx_hash() ); - wait_for_confirmations(&taker_coin, &taker_approve_tx, 100); + wait_for_confirmations(&taker_coin, &taker_approve_tx, 30); let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::TakerPaymentV2 { maker_secret_hash: &maker_secret_hash, @@ -2180,31 +1780,26 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { premium_amount: BigDecimal::default(), trading_amount, }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx_refund = block_on(taker_coin.refund_taker_funding_timelock(refund_args)).unwrap(); log!( "Taker refunded ERC20 funding after payment lock time was exceeded, tx hash: {:02x}", funding_tx_refund.tx_hash() ); - wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); + wait_for_confirmations(&taker_coin, &funding_tx_refund, 30); } -#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + let swap_addr = SwapAddresses::init(); + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ERC20DEV, &erc20_conf, swap_addr, true); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ERC20DEV, &erc20_conf, swap_addr, true); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); - let taker_address = block_on(taker_coin.my_addr()); - // if TakerPaymentState is `PaymentSent` then timestamp should exceed payment pre-approve lock time (funding_time_lock) let funding_time_lock = now_sec() + 29; let payment_time_lock = now_sec() + 1000; @@ -2224,10 +1819,9 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { trading_amount: trading_amount.clone(), swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ERC20 funding, tx hash: {:02x}", funding_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &funding_tx, 150); + wait_for_confirmations(&taker_coin, &funding_tx, 30); thread::sleep(Duration::from_secs(29)); let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::TakerPaymentV2 { @@ -2246,22 +1840,19 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { premium_amount: BigDecimal::default(), trading_amount, }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let funding_tx_refund = block_on(taker_coin.refund_taker_funding_timelock(refund_args)).unwrap(); log!( "Taker refunded ERC20 funding after pre-approval lock time was exceeded, tx hash: {:02x}", funding_tx_refund.tx_hash() ); - wait_for_confirmations(&taker_coin, &funding_tx_refund, 150); + wait_for_confirmations(&taker_coin, &funding_tx_refund, 30); } -#[cfg(feature = "sepolia-maker-swap-v2-tests")] #[test] fn send_maker_payment_and_refund_timelock_eth() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + let swap_addr = SwapAddresses::init(); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH, ð_dev_conf(), swap_addr, false); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addr, false); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); @@ -2269,7 +1860,6 @@ fn send_maker_payment_and_refund_timelock_eth() { let maker_secret_hash = sha256(&maker_secret).to_vec(); let payment_time_lock = now_sec() - 1000; - let maker_address = block_on(maker_coin.my_addr()); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); let trading_amount = BigDecimal::from_str("0.0001").unwrap(); @@ -2282,10 +1872,9 @@ fn send_maker_payment_and_refund_timelock_eth() { taker_pub, swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); - wait_for_confirmations(&maker_coin, &payment_tx, 100); + wait_for_confirmations(&maker_coin, &payment_tx, 30); let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::MakerPaymentV2 { maker_secret_hash: &maker_secret_hash, @@ -2300,23 +1889,20 @@ fn send_maker_payment_and_refund_timelock_eth() { watcher_reward: false, amount: trading_amount, }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_timelock(refund_args)).unwrap(); log!( "Maker refunded ETH payment after timelock, tx hash: {:02x}", payment_tx_refund.tx_hash() ); - wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 30); } -#[cfg(feature = "sepolia-maker-swap-v2-tests")] #[test] fn send_maker_payment_and_refund_timelock_erc20() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + let swap_addr = SwapAddresses::init(); + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ERC20DEV, &erc20_conf, swap_addr, true); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ERC20DEV, &erc20_conf, swap_addr, true); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); @@ -2324,11 +1910,17 @@ fn send_maker_payment_and_refund_timelock_erc20() { let maker_secret_hash = sha256(&maker_secret).to_vec(); let payment_time_lock = now_sec() - 1000; - let maker_address = block_on(maker_coin.my_addr()); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + // Pre-approve the ERC20 token for maker swap v2 contract since the payment time_lock + // is in the past (for refund testing) and handle_allowance would timeout immediately. + let approve_tx = + block_on_f01(maker_coin.approve(swap_addr.swap_v2_contracts.maker_swap_v2_contract, U256::max_value())) + .unwrap(); + wait_for_confirmations(&maker_coin, &approve_tx, 30); + let payment_args = SendMakerPaymentArgs { time_lock: payment_time_lock, taker_secret_hash: &taker_secret_hash, @@ -2337,10 +1929,9 @@ fn send_maker_payment_and_refund_timelock_erc20() { taker_pub, swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); - wait_for_confirmations(&maker_coin, &payment_tx, 100); + wait_for_confirmations(&maker_coin, &payment_tx, 30); let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::MakerPaymentV2 { maker_secret_hash: &maker_secret_hash, @@ -2355,22 +1946,19 @@ fn send_maker_payment_and_refund_timelock_erc20() { watcher_reward: false, amount: trading_amount, }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_timelock(refund_args)).unwrap(); log!( "Maker refunded ERC20 payment after timelock, tx hash: {:02x}", payment_tx_refund.tx_hash() ); - wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 30); } -#[cfg(feature = "sepolia-maker-swap-v2-tests")] #[test] fn send_maker_payment_and_refund_secret_eth() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + let swap_addr = SwapAddresses::init(); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH, ð_dev_conf(), swap_addr, false); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addr, false); let taker_secret = &[0; 32]; let taker_secret_hash = sha256(taker_secret).to_vec(); @@ -2378,7 +1966,6 @@ fn send_maker_payment_and_refund_secret_eth() { let maker_secret_hash = sha256(maker_secret).to_vec(); let payment_time_lock = now_sec() + 1000; - let maker_address = block_on(maker_coin.my_addr()); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); let trading_amount = BigDecimal::from_str("0.0001").unwrap(); @@ -2391,10 +1978,9 @@ fn send_maker_payment_and_refund_secret_eth() { taker_pub, swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); - wait_for_confirmations(&maker_coin, &payment_tx, 100); + wait_for_confirmations(&maker_coin, &payment_tx, 30); let refund_args = RefundMakerPaymentSecretArgs { maker_payment_tx: &payment_tx, @@ -2406,23 +1992,20 @@ fn send_maker_payment_and_refund_secret_eth() { swap_unique_data: &[], amount: trading_amount, }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_secret(refund_args)).unwrap(); log!( "Maker refunded ETH payment using taker secret, tx hash: {:02x}", payment_tx_refund.tx_hash() ); - wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 30); } -#[cfg(feature = "sepolia-maker-swap-v2-tests")] #[test] fn send_maker_payment_and_refund_secret_erc20() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + let swap_addr = SwapAddresses::init(); + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ERC20DEV, &erc20_conf, swap_addr, true); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ERC20DEV, &erc20_conf, swap_addr, true); let taker_secret = &[0; 32]; let taker_secret_hash = sha256(taker_secret).to_vec(); @@ -2430,7 +2013,6 @@ fn send_maker_payment_and_refund_secret_erc20() { let maker_secret_hash = sha256(maker_secret).to_vec(); let payment_time_lock = now_sec() + 1000; - let maker_address = block_on(maker_coin.my_addr()); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); let trading_amount = BigDecimal::from_str("0.0001").unwrap(); @@ -2443,10 +2025,9 @@ fn send_maker_payment_and_refund_secret_erc20() { taker_pub, swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); - wait_for_confirmations(&maker_coin, &payment_tx, 100); + wait_for_confirmations(&maker_coin, &payment_tx, 30); let refund_args = RefundMakerPaymentSecretArgs { maker_payment_tx: &payment_tx, @@ -2458,22 +2039,19 @@ fn send_maker_payment_and_refund_secret_erc20() { swap_unique_data: &[], amount: trading_amount, }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_secret(refund_args)).unwrap(); log!( "Maker refunded ERC20 payment using taker secret, tx hash: {:02x}", payment_tx_refund.tx_hash() ); - wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 30); } -#[cfg(feature = "sepolia-maker-swap-v2-tests")] #[test] fn send_and_spend_maker_payment_eth() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + let swap_addr = SwapAddresses::init(); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH, ð_dev_conf(), swap_addr, false); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addr, false); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); @@ -2481,8 +2059,6 @@ fn send_and_spend_maker_payment_eth() { let maker_secret_hash = sha256(&maker_secret).to_vec(); let payment_time_lock = now_sec() + 1000; - let maker_address = block_on(maker_coin.my_addr()); - let taker_address = block_on(taker_coin.my_addr()); let maker_pub = &maker_coin.derive_htlc_pubkey_v2(&[]); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); @@ -2496,10 +2072,9 @@ fn send_and_spend_maker_payment_eth() { taker_pub, swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); - wait_for_confirmations(&maker_coin, &payment_tx, 100); + wait_for_confirmations(&maker_coin, &payment_tx, 30); let validation_args = ValidateMakerPaymentArgs { maker_payment_tx: &payment_tx, @@ -2523,20 +2098,17 @@ fn send_and_spend_maker_payment_eth() { swap_unique_data: &[], amount: trading_amount, }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let spend_tx = block_on(taker_coin.spend_maker_payment_v2(spend_args)).unwrap(); log!("Taker spent maker ETH payment, tx hash: {:02x}", spend_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &spend_tx, 100); + wait_for_confirmations(&taker_coin, &spend_tx, 30); } -#[cfg(feature = "sepolia-maker-swap-v2-tests")] #[test] fn send_and_spend_maker_payment_erc20() { - let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); - - let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); - let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); - let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + let swap_addr = SwapAddresses::init(); + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let (taker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ERC20DEV, &erc20_conf, swap_addr, true); + let (maker_coin, _) = eth_coin_v2_activation_with_random_privkey(&MM_CTX, ERC20DEV, &erc20_conf, swap_addr, true); let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); @@ -2544,8 +2116,6 @@ fn send_and_spend_maker_payment_erc20() { let maker_secret_hash = sha256(&maker_secret).to_vec(); let payment_time_lock = now_sec() + 1000; - let maker_address = block_on(maker_coin.my_addr()); - let taker_address = block_on(taker_coin.my_addr()); let maker_pub = &maker_coin.derive_htlc_pubkey_v2(&[]); let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); @@ -2559,10 +2129,9 @@ fn send_and_spend_maker_payment_erc20() { taker_pub, swap_unique_data: &[], }; - wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); - wait_for_confirmations(&maker_coin, &payment_tx, 100); + wait_for_confirmations(&maker_coin, &payment_tx, 30); let validation_args = ValidateMakerPaymentArgs { maker_payment_tx: &payment_tx, @@ -2586,10 +2155,9 @@ fn send_and_spend_maker_payment_erc20() { swap_unique_data: &[], amount: trading_amount, }; - wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); let spend_tx = block_on(taker_coin.spend_maker_payment_v2(spend_args)).unwrap(); log!("Taker spent maker ERC20 payment, tx hash: {:02x}", spend_tx.tx_hash()); - wait_for_confirmations(&taker_coin, &spend_tx, 100); + wait_for_confirmations(&taker_coin, &spend_tx, 30); } #[test] @@ -2597,7 +2165,7 @@ fn test_eth_erc20_hd() { const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); + let swap_contract = swap_contract_checksum(); // Withdraw from HD account 0, change address 0, index 0 let path_to_address = HDAccountAddressId::default(); @@ -2733,7 +2301,7 @@ fn test_enable_custom_erc20() { const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; let coins = json!([eth_dev_conf()]); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); + let swap_contract = swap_contract_checksum(); let path_to_address = HDAccountAddressId::default(); let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); @@ -2817,7 +2385,7 @@ fn test_enable_custom_erc20_with_duplicate_contract_in_config() { let erc20_dev_conf = erc20_dev_conf(&erc20_contract_checksum()); let coins = json!([eth_dev_conf(), erc20_dev_conf]); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); + let swap_contract = swap_contract_checksum(); let path_to_address = HDAccountAddressId::default(); let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); diff --git a/mm2src/mm2_main/tests/docker_tests/eth_inner_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_inner_tests.rs new file mode 100644 index 0000000000..81de0cde47 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/eth_inner_tests.rs @@ -0,0 +1,1295 @@ +// ETH Inner Tests +// +// This module contains ETH-only tests that were extracted from docker_tests_inner.rs. +// These tests focus on ETH/ERC20 coin functionality including: +// - ETH/ERC20 activation and disable flows +// - Swap contract address negotiation +// - ETH/ERC20 withdraw and send operations +// - ETH/ERC20 orderbook and order management +// - ERC20 token approval +// +// Gated by: docker-tests-eth + +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::eth::{ + erc20_coin_with_random_privkey, erc20_contract_checksum, fill_eth_erc20_with_private_key, swap_contract, + swap_contract_checksum, GETH_RPC_URL, MM_CTX, +}; +use crate::docker_tests::helpers::swap::trade_base_rel; +use crate::integration_tests_common::rmd160_from_passphrase; +use coins::{MarketCoinOps, TxFeeDetails}; +use common::{block_on, get_utc_timestamp}; +use crypto::{CryptoCtx, DerivationPath, KeyPairPolicy}; +use http::StatusCode; +use mm2_number::BigDecimal; +use mm2_test_helpers::for_tests::{ + disable_coin, disable_coin_err, enable_eth_coin, erc20_dev_conf, eth_dev_conf, start_swaps, + task_enable_eth_with_tokens, wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, MarketMakerIt, + Mm2TestConf, DEFAULT_RPC_PASSWORD, +}; +use mm2_test_helpers::structs::*; +use serde_json::{json, Value as Json}; +use std::collections::HashSet; +use std::iter::FromIterator; +use std::str::FromStr; +use std::thread; +use std::time::Duration; + +// ============================================================================= +// Test address constants +// ============================================================================= + +/// Arbitrary address used for swap contract negotiation tests (maker side) +const TEST_ARBITRARY_SWAP_ADDR_1: &str = "0x6c2858f6afac835c43ffda248aea167e1a58436c"; +/// Arbitrary address used for swap contract negotiation tests (taker side) +const TEST_ARBITRARY_SWAP_ADDR_2: &str = "0x24abe4c71fc658c01313b6552cd40cd808b3ea80"; +/// Valid checksummed ETH address used as withdraw destination in tests +const TEST_WITHDRAW_DEST_ADDR: &str = "0x4b2d0d6c2c785217457B69B922A2A9cEA98f71E9"; +/// Invalid checksum variant of the withdraw destination (for checksum validation tests) +const TEST_WITHDRAW_DEST_ADDR_INVALID_CHECKSUM: &str = "0x4b2d0d6c2c785217457b69b922a2A9cEA98f71E9"; + +// ============================================================================= +// ETH Activation Helper +// ============================================================================= + +async fn enable_eth_with_tokens( + mm: &MarketMakerIt, + platform_coin: &str, + tokens: &[&str], + swap_contract_address: &str, + nodes: &[&str], + balance: bool, +) -> Json { + let erc20_tokens_requests: Vec<_> = tokens.iter().map(|ticker| json!({ "ticker": ticker })).collect(); + let nodes: Vec<_> = nodes.iter().map(|url| json!({ "url": url })).collect(); + + let enable = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "enable_eth_with_tokens", + "mmrpc": "2.0", + "params": { + "ticker": platform_coin, + "erc20_tokens_requests": erc20_tokens_requests, + "swap_contract_address": swap_contract_address, + "nodes": nodes, + "tx_history": true, + "get_balances": balance, + } + })) + .await + .unwrap(); + assert_eq!( + enable.0, + StatusCode::OK, + "'enable_eth_with_tokens' failed: {}", + enable.1 + ); + serde_json::from_str(&enable.1).unwrap() +} + +// ============================================================================= +// ETH/ERC20 Activation and Disable Tests +// ============================================================================= + +#[test] +fn test_enable_eth_coin_with_token_then_disable() { + let coin = erc20_coin_with_random_privkey(swap_contract()); + + let priv_key = coin.display_priv_key().unwrap(); + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + + let conf = Mm2TestConf::seednode(&priv_key, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("log path: {}", mm.log_path.display()); + + let swap_contract = swap_contract_checksum(); + block_on(enable_eth_with_tokens( + &mm, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + true, + )); + + // Create setprice order + let req = json!({ + "userpass": mm.userpass, + "method": "buy", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.1, + "base_confs": 5, + "base_nota": false, + "rel_confs": 4, + "rel_nota": false, + }); + let make_test_order = block_on(mm.rpc(&req)).unwrap(); + assert_eq!(make_test_order.0, StatusCode::OK); + let order_uuid = Json::from_str(&make_test_order.1).unwrap(); + let order_uuid = order_uuid.get("result").unwrap().get("uuid").unwrap().as_str().unwrap(); + + // Passive ETH while having tokens enabled + let res = block_on(disable_coin(&mm, "ETH", false)); + assert!(res.passivized); + assert!(res.cancelled_orders.contains(order_uuid)); + + // Try to disable ERC20DEV token. + // This should work, because platform coin is still in the memory. + let res = block_on(disable_coin(&mm, "ERC20DEV", false)); + // We expected make_test_order to be cancelled + assert!(!res.passivized); + + // Because it's currently passive, default `disable_coin` should fail. + block_on(disable_coin_err(&mm, "ETH", false)); + // And forced `disable_coin` should not fail + let res = block_on(disable_coin(&mm, "ETH", true)); + assert!(!res.passivized); +} + +#[test] +fn test_platform_coin_mismatch() { + let coin = erc20_coin_with_random_privkey(swap_contract()); + + let priv_key = coin.display_priv_key().unwrap(); + let mut erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + erc20_conf["protocol"]["protocol_data"]["platform"] = "MATIC".into(); // set a different platform coin + let coins = json!([eth_dev_conf(), erc20_conf]); + + let conf = Mm2TestConf::seednode(&priv_key, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("log path: {}", mm.log_path.display()); + + let swap_contract = swap_contract_checksum(); + let erc20_tokens_requests = vec![json!({ "ticker": "ERC20DEV" })]; + let nodes = vec![json!({ "url": GETH_RPC_URL })]; + + let enable = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "enable_eth_with_tokens", + "mmrpc": "2.0", + "params": { + "ticker": "ETH", + "erc20_tokens_requests": erc20_tokens_requests, + "swap_contract_address": swap_contract, + "nodes": nodes, + "tx_history": false, + "get_balances": false, + } + }))) + .unwrap(); + assert_eq!( + enable.0, + StatusCode::BAD_REQUEST, + "'enable_eth_with_tokens' must fail with PlatformCoinMismatch", + ); + assert_eq!( + serde_json::from_str::(&enable.1).unwrap()["error_type"] + .as_str() + .unwrap(), + "PlatformCoinMismatch", + ); +} + +#[test] +fn test_enable_eth_coin_with_token_without_balance() { + let coin = erc20_coin_with_random_privkey(swap_contract()); + + let priv_key = coin.display_priv_key().unwrap(); + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + + let conf = Mm2TestConf::seednode(&priv_key, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("log path: {}", mm.log_path.display()); + + let swap_contract = swap_contract_checksum(); + let enable_eth_with_tokens = block_on(enable_eth_with_tokens( + &mm, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + false, + )); + + let enable_eth_with_tokens: RpcV2Response = + serde_json::from_value(enable_eth_with_tokens).unwrap(); + + let (_, eth_balance) = enable_eth_with_tokens + .result + .eth_addresses_infos + .into_iter() + .next() + .unwrap(); + log!("{:?}", eth_balance); + assert!(eth_balance.balances.is_none()); + assert!(eth_balance.tickers.is_none()); + + let (_, erc20_balances) = enable_eth_with_tokens + .result + .erc20_addresses_infos + .into_iter() + .next() + .unwrap(); + assert!(erc20_balances.balances.is_none()); + assert_eq!( + erc20_balances.tickers.unwrap(), + HashSet::from_iter(vec!["ERC20DEV".to_string()]) + ); +} + +// ============================================================================= +// Swap Contract Negotiation Tests +// ============================================================================= + +#[test] +fn test_eth_swap_contract_addr_negotiation_same_fallback() { + let bob_coin = erc20_coin_with_random_privkey(swap_contract()); + let alice_coin = erc20_coin_with_random_privkey(swap_contract()); + + let bob_priv_key = bob_coin.display_priv_key().unwrap(); + let alice_priv_key = alice_coin.display_priv_key().unwrap(); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + + let bob_conf = Mm2TestConf::seednode(&bob_priv_key, &coins); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + let alice_conf = Mm2TestConf::light_node(&alice_priv_key, &coins, &[&mm_bob.ip.to_string()]); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + + let swap_contract = swap_contract_checksum(); + + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ETH", + &[GETH_RPC_URL], + // using arbitrary address + TEST_ARBITRARY_SWAP_ADDR_1, + Some(&swap_contract), + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ERC20DEV", + &[GETH_RPC_URL], + // using arbitrary address + TEST_ARBITRARY_SWAP_ADDR_1, + Some(&swap_contract), + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ETH", + &[GETH_RPC_URL], + // using arbitrary address + TEST_ARBITRARY_SWAP_ADDR_2, + Some(&swap_contract), + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ERC20DEV", + &[GETH_RPC_URL], + // using arbitrary address + TEST_ARBITRARY_SWAP_ADDR_2, + Some(&swap_contract), + false + ))); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("ETH", "ERC20DEV")], + 1., + 1., + 0.0001, + )); + + // give few seconds for swap statuses to be saved + thread::sleep(Duration::from_secs(3)); + + let wait_until = get_utc_timestamp() + 30; + // Expected contract should be lowercase since swap status stores addresses in lowercase format + let expected_contract = Json::from(swap_contract.trim_start_matches("0x").to_lowercase()); + + block_on(wait_for_swap_contract_negotiation( + &mm_bob, + &uuids[0], + expected_contract.clone(), + wait_until, + )); + block_on(wait_for_swap_contract_negotiation( + &mm_alice, + &uuids[0], + expected_contract, + wait_until, + )); +} + +#[test] +fn test_eth_swap_negotiation_fails_maker_no_fallback() { + let bob_coin = erc20_coin_with_random_privkey(swap_contract()); + let alice_coin = erc20_coin_with_random_privkey(swap_contract()); + + let bob_priv_key = bob_coin.display_priv_key().unwrap(); + let alice_priv_key = alice_coin.display_priv_key().unwrap(); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + + let bob_conf = Mm2TestConf::seednode(&bob_priv_key, &coins); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + let alice_conf = Mm2TestConf::light_node(&alice_priv_key, &coins, &[&mm_bob.ip.to_string()]); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + + let swap_contract = swap_contract_checksum(); + + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ETH", + &[GETH_RPC_URL], + // using arbitrary address + TEST_ARBITRARY_SWAP_ADDR_1, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ERC20DEV", + &[GETH_RPC_URL], + // using arbitrary address + TEST_ARBITRARY_SWAP_ADDR_1, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ETH", + &[GETH_RPC_URL], + // using arbitrary address + TEST_ARBITRARY_SWAP_ADDR_2, + Some(&swap_contract), + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ERC20DEV", + &[GETH_RPC_URL], + // using arbitrary address + TEST_ARBITRARY_SWAP_ADDR_2, + Some(&swap_contract), + false + ))); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("ETH", "ERC20DEV")], + 1., + 1., + 0.0001, + )); + + // give few seconds for swap statuses to be saved + thread::sleep(Duration::from_secs(3)); + + let wait_until = get_utc_timestamp() + 30; + block_on(wait_for_swap_negotiation_failure(&mm_bob, &uuids[0], wait_until)); + block_on(wait_for_swap_negotiation_failure(&mm_alice, &uuids[0], wait_until)); +} + +// ============================================================================= +// ETH/ERC20 Swap Tests +// ============================================================================= + +#[test] +fn test_trade_base_rel_eth_erc20_coins() { + trade_base_rel(("ETH", "ERC20DEV")); +} + +// ============================================================================= +// ETH/ERC20 Withdraw and Send Tests +// ============================================================================= + +fn withdraw_and_send( + mm: &MarketMakerIt, + coin: &str, + from: Option, + to: &str, + from_addr: &str, + expected_bal_change: &str, + amount: f64, +) { + let withdraw = block_on(mm.rpc(&json! ({ + "mmrpc": "2.0", + "userpass": mm.userpass, + "method": "withdraw", + "params": { + "coin": coin, + "from": from, + "to": to, + "amount": amount, + }, + "id": 0, + }))) + .unwrap(); + + assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); + let res: RpcSuccessResponse = + serde_json::from_str(&withdraw.1).expect("Expected 'RpcSuccessResponse'"); + let tx_details = res.result; + + let mut expected_bal_change = BigDecimal::from_str(expected_bal_change).expect("!BigDecimal::from_str"); + + let fee_details: TxFeeDetails = serde_json::from_value(tx_details.fee_details).unwrap(); + + if let TxFeeDetails::Eth(fee_details) = fee_details { + if coin == "ETH" { + expected_bal_change -= fee_details.total_fee; + } + } + + assert_eq!(tx_details.to, vec![to.to_owned()]); + assert_eq!(tx_details.my_balance_change, expected_bal_change); + // Todo: Should check the from address for withdraws from another HD wallet address when there is an RPC method for addresses + if from.is_none() { + assert_eq!(tx_details.from, vec![from_addr.to_owned()]); + } + + let send = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "send_raw_transaction", + "coin": coin, + "tx_hex": tx_details.tx_hex, + }))) + .unwrap(); + assert!(send.0.is_success(), "!{} send: {}", coin, send.1); + let send_json: Json = serde_json::from_str(&send.1).unwrap(); + assert_eq!(tx_details.tx_hash, send_json["tx_hash"]); +} + +#[test] +fn test_withdraw_and_send_eth_erc20() { + let privkey = random_secp256k1_secret(); + fill_eth_erc20_with_private_key(privkey); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(privkey)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("Alice log path: {}", mm.log_path.display()); + + let swap_contract = swap_contract_checksum(); + let eth_enable = block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + let erc20_enable = block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + + withdraw_and_send( + &mm, + "ETH", + None, + TEST_WITHDRAW_DEST_ADDR, + eth_enable["address"].as_str().unwrap(), + "-0.001", + 0.001, + ); + + withdraw_and_send( + &mm, + "ERC20DEV", + None, + TEST_WITHDRAW_DEST_ADDR, + erc20_enable["address"].as_str().unwrap(), + "-0.001", + 0.001, + ); + + // must not allow to withdraw to invalid checksum address + let withdraw = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "withdraw", + "params": { + "coin": "ETH", + "to": TEST_WITHDRAW_DEST_ADDR_INVALID_CHECKSUM, + "amount": "0.001", + }, + "id": 0, + }))) + .unwrap(); + + assert!(withdraw.0.is_client_error(), "ETH withdraw: {}", withdraw.1); + let res: RpcErrorResponse = serde_json::from_str(&withdraw.1).unwrap(); + assert_eq!(res.error_type, "InvalidAddress"); + assert!(res.error.contains("Invalid address checksum")); +} + +#[test] +fn test_withdraw_and_send_hd_eth_erc20() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let KeyPairPolicy::GlobalHDAccount(hd_acc) = CryptoCtx::init_with_global_hd_account(MM_CTX.clone(), PASSPHRASE) + .unwrap() + .key_pair_policy() + .clone() + else { + panic!("Expected 'KeyPairPolicy::GlobalHDAccount'"); + }; + + let swap_contract = swap_contract_checksum(); + + // Withdraw from HD account 0, change address 0, index 1 + let mut path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + }; + let path_to_addr_str = "/0'/0/1"; + let path_to_coin: String = serde_json::from_value(eth_dev_conf()["derivation_path"].clone()).unwrap(); + let derivation_path = path_to_coin.clone() + path_to_addr_str; + let derivation_path = DerivationPath::from_str(&derivation_path).unwrap(); + // Get the private key associated with this account and fill it with eth and erc20 token. + let priv_key = hd_acc.derive_secp256k1_secret(&derivation_path).unwrap(); + fill_eth_erc20_with_private_key(priv_key); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + let eth_enable = block_on(task_enable_eth_with_tokens( + &mm_hd, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address.clone()), + )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.first().expect("Expected account at index 0"); + assert_eq!( + account.addresses[1].address, + "0xDe841899aB4A22E23dB21634e54920aDec402397" + ); + assert_eq!(account.addresses[1].balance.len(), 2); + assert_eq!(account.addresses[1].balance.get("ETH").unwrap().spendable, 100.into()); + assert_eq!( + account.addresses[1].balance.get("ERC20DEV").unwrap().spendable, + 100.into() + ); + + withdraw_and_send( + &mm_hd, + "ETH", + Some(path_to_address.clone()), + TEST_WITHDRAW_DEST_ADDR, + &account.addresses[1].address, + "-0.001", + 0.001, + ); + + withdraw_and_send( + &mm_hd, + "ERC20DEV", + Some(path_to_address.clone()), + TEST_WITHDRAW_DEST_ADDR, + &account.addresses[1].address, + "-0.001", + 0.001, + ); + + // Change the address index, the withdrawal should fail. + path_to_address.address_id = 0; + + let withdraw = block_on(mm_hd.rpc(&json! ({ + "mmrpc": "2.0", + "userpass": mm_hd.userpass, + "method": "withdraw", + "params": { + "coin": "ETH", + "from": path_to_address, + "to": TEST_WITHDRAW_DEST_ADDR, + "amount": 0.001, + }, + "id": 0, + }))) + .unwrap(); + assert!(!withdraw.0.is_success(), "!withdraw: {}", withdraw.1); + + // But if we fill it, we should be able to withdraw. + let path_to_addr_str = "/0'/0/0"; + let derivation_path = path_to_coin + path_to_addr_str; + let derivation_path = DerivationPath::from_str(&derivation_path).unwrap(); + let priv_key = hd_acc.derive_secp256k1_secret(&derivation_path).unwrap(); + fill_eth_erc20_with_private_key(priv_key); + + let withdraw = block_on(mm_hd.rpc(&json! ({ + "mmrpc": "2.0", + "userpass": mm_hd.userpass, + "method": "withdraw", + "params": { + "coin": "ETH", + "from": path_to_address, + "to": TEST_WITHDRAW_DEST_ADDR, + "amount": 0.001, + }, + "id": 0, + }))) + .unwrap(); + assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); + + block_on(mm_hd.stop()).unwrap(); +} + +// ============================================================================= +// ETH/ERC20 Order DB Persistence and Conf Settings Tests +// ============================================================================= + +#[test] +fn test_set_price_must_save_order_to_db() { + let private_key_str = erc20_coin_with_random_privkey(swap_contract()) + .display_priv_key() + .unwrap(); + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + + let conf = Mm2TestConf::seednode(&private_key_str, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + let swap_contract = swap_contract_checksum(); + dbg!(block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + dbg!(block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + log!("Issue bob ETH/ERC20DEV sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.1 + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let rc_json: Json = serde_json::from_str(&rc.1).unwrap(); + let uuid: String = serde_json::from_value(rc_json["result"]["uuid"].clone()).unwrap(); + let order_path = mm.folder.join(format!( + "DB/{}/ORDERS/MY/MAKER/{}.json", + hex::encode(rmd160_from_passphrase(&private_key_str)), + uuid + )); + assert!(order_path.exists()); +} + +#[test] +fn test_set_price_conf_settings() { + let private_key_str = erc20_coin_with_random_privkey(swap_contract()) + .display_priv_key() + .unwrap(); + + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); + + let conf = Mm2TestConf::seednode(&private_key_str, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + let swap_contract = swap_contract_checksum(); + dbg!(block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + dbg!(block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + log!("Issue bob sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.1, + "base_confs": 5, + "base_nota": true, + "rel_confs": 4, + "rel_nota": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(5)); + assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(true)); + assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(4)); + assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); + + // must use coin config as defaults if not set in request + log!("Issue bob sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.1, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(1)); + assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(false)); + assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(2)); + assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); +} + +#[test] +fn test_buy_conf_settings() { + let private_key_str = erc20_coin_with_random_privkey(swap_contract()) + .display_priv_key() + .unwrap(); + + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); + + let conf = Mm2TestConf::seednode(&private_key_str, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + let swap_contract = swap_contract_checksum(); + dbg!(block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + dbg!(block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + log!("Issue bob buy request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "buy", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.1, + "base_confs": 5, + "base_nota": true, + "rel_confs": 4, + "rel_nota": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(5)); + assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(true)); + assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(4)); + assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); + + // must use coin config as defaults if not set in request + log!("Issue bob buy request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "buy", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.1, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(1)); + assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(false)); + assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(2)); + assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); +} + +#[test] +fn test_sell_conf_settings() { + let private_key_str = erc20_coin_with_random_privkey(swap_contract()) + .display_priv_key() + .unwrap(); + + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); + + let conf = Mm2TestConf::seednode(&private_key_str, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + let swap_contract = swap_contract_checksum(); + dbg!(block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + dbg!(block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + log!("Issue bob sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "sell", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.1, + "base_confs": 5, + "base_nota": true, + "rel_confs": 4, + "rel_nota": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(5)); + assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(true)); + assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(4)); + assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); + + // must use coin config as defaults if not set in request + log!("Issue bob sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "sell", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.1, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["conf_settings"]["base_confs"], Json::from(1)); + assert_eq!(json["result"]["conf_settings"]["base_nota"], Json::from(false)); + assert_eq!(json["result"]["conf_settings"]["rel_confs"], Json::from(2)); + assert_eq!(json["result"]["conf_settings"]["rel_nota"], Json::from(false)); +} + +// ============================================================================= +// ETH/ERC20 Order Matching and my_orders Tests +// ============================================================================= + +#[test] +fn test_my_orders_after_matched() { + let bob_coin = erc20_coin_with_random_privkey(swap_contract()); + let alice_coin = erc20_coin_with_random_privkey(swap_contract()); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + + let bob_conf = Mm2TestConf::seednode(&bob_coin.display_priv_key().unwrap(), &coins); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + let alice_conf = Mm2TestConf::light_node( + &alice_coin.display_priv_key().unwrap(), + &coins, + &[&mm_bob.ip.to_string()], + ); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + + let swap_contract = swap_contract_checksum(); + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.000001, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.000001, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop ETH/ERC20DEV"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop ETH/ERC20DEV"))).unwrap(); + + log!("Issue bob my_orders request"); + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + + let _: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +fn test_update_maker_order_after_matched() { + let bob_coin = erc20_coin_with_random_privkey(swap_contract()); + let alice_coin = erc20_coin_with_random_privkey(swap_contract()); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + + let bob_conf = Mm2TestConf::seednode(&bob_coin.display_priv_key().unwrap(), &coins); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + let alice_conf = Mm2TestConf::light_node( + &alice_coin.display_priv_key().unwrap(), + &coins, + &[&mm_bob.ip.to_string()], + ); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + + let swap_contract = swap_contract_checksum(); + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.00002, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let setprice_json: Json = serde_json::from_str(&rc.1).unwrap(); + let uuid: String = serde_json::from_value(setprice_json["result"]["uuid"].clone()).unwrap(); + + let rc = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "ETH", + "rel": "ERC20DEV", + "price": 1, + "volume": 0.00001, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop ETH/ERC20DEV"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop ETH/ERC20DEV"))).unwrap(); + + log!("Issue bob update maker order request that should fail because new volume is less than reserved amount"); + let update_maker_order = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "update_maker_order", + "uuid": uuid, + "volume_delta": -0.00002, + }))) + .unwrap(); + assert!( + !update_maker_order.0.is_success(), + "update_maker_order success, but should be error {}", + update_maker_order.1 + ); + + log!("Issue another bob update maker order request"); + let update_maker_order = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "update_maker_order", + "uuid": uuid, + "volume_delta": 0.00001, + }))) + .unwrap(); + assert!( + update_maker_order.0.is_success(), + "!update_maker_order: {}", + update_maker_order.1 + ); + let update_maker_order_json: Json = serde_json::from_str(&update_maker_order.1).unwrap(); + log!("{}", update_maker_order.1); + assert_eq!(update_maker_order_json["result"]["max_base_vol"], Json::from("0.00003")); + + log!("Issue bob my_orders request"); + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + + let _: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +// ============================================================================= +// ERC20 Token Approval Tests +// ============================================================================= + +#[test] +fn test_approve_erc20() { + let privkey = random_secp256k1_secret(); + fill_eth_erc20_with_private_key(privkey); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let mm = MarketMakerIt::start( + Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins).conf, + DEFAULT_RPC_PASSWORD.to_string(), + None, + ) + .unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("Node log path: {}", mm.log_path.display()); + + let swap_contract = swap_contract_checksum(); + let _eth_enable = block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + let _erc20_enable = block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"approve_token", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + "amount": BigDecimal::from_str("11.0").unwrap(), + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "approve_token error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert!( + hex::decode(str_strip_0x!(res["result"].as_str().unwrap())).is_ok(), + "approve_token result incorrect" + ); + thread::sleep(Duration::from_secs(5)); + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"get_token_allowance", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "get_token_allowance error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert_eq!( + BigDecimal::from_str(res["result"].as_str().unwrap()).unwrap(), + BigDecimal::from_str("11.0").unwrap(), + "get_token_allowance result incorrect" + ); + + block_on(mm.stop()).unwrap(); +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/docker_ops.rs b/mm2src/mm2_main/tests/docker_tests/helpers/docker_ops.rs new file mode 100644 index 0000000000..70f5177e03 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/docker_ops.rs @@ -0,0 +1,111 @@ +//! Docker operations for docker tests. +//! +//! This module provides shared infrastructure for docker test helpers: +//! - `CoinDockerOps` trait for coins running in docker containers +//! - Docker compose utilities for container management + +use coins::utxo::coin_daemon_data_dir; +#[cfg(any(feature = "docker-tests-slp", feature = "docker-tests-integration"))] +use coins::utxo::rpc_clients::NativeClient; +use coins::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcClientOps}; +use common::{block_on_f01, now_ms, wait_until_ms}; +use std::process::Command; +use std::thread; +use std::time::Duration; + +use super::env::resolve_compose_container_id; + +// ============================================================================= +// CoinDockerOps trait +// ============================================================================= + +/// Trait for docker coin operations. +/// +/// Provides common functionality for coins running in docker containers, +/// including RPC client access and readiness waiting. +/// +/// Implemented by: +/// - `UtxoAssetDockerOps` (in `helpers::utxo`) +/// - `BchDockerOps` (in `helpers::utxo`) +/// - `ZCoinAssetDockerOps` (in `helpers::zcoin`) +pub trait CoinDockerOps { + /// Get the RPC client for this coin. + fn rpc_client(&self) -> &UtxoRpcClientEnum; + + /// Get the native RPC client, panicking if not native. + /// Only used by BchDockerOps::initialize_slp for SLP token setup. + #[cfg(any(feature = "docker-tests-slp", feature = "docker-tests-integration"))] + fn native_client(&self) -> &NativeClient { + match self.rpc_client() { + UtxoRpcClientEnum::Native(native) => native, + _ => panic!("UtxoRpcClientEnum::Native is expected"), + } + } + + /// Wait until the coin node is ready with expected transaction version. + fn wait_ready(&self, expected_tx_version: i32) { + let timeout = wait_until_ms(120000); + loop { + match block_on_f01(self.rpc_client().get_block_count()) { + Ok(n) => { + if n > 1 { + if let UtxoRpcClientEnum::Native(client) = self.rpc_client() { + let hash = block_on_f01(client.get_block_hash(n)).unwrap(); + let block = block_on_f01(client.get_block(hash)).unwrap(); + let coinbase = block_on_f01(client.get_verbose_transaction(&block.tx[0])).unwrap(); + log!("Coinbase tx {:?} in block {}", coinbase, n); + if coinbase.version == expected_tx_version { + break; + } + } + } + }, + Err(e) => log!("{:?}", e), + } + assert!(now_ms() < timeout, "Test timed out"); + thread::sleep(Duration::from_secs(1)); + } + } +} + +// ============================================================================= +// Docker Compose Utilities +// ============================================================================= + +/// Copy a file from a compose container to the host. +pub fn docker_cp_from_container(container_id: &str, src: &str, dst: &std::path::Path) { + Command::new("docker") + .arg("cp") + .arg(format!("{}:{}", container_id, src)) + .arg(dst) + .status() + .expect("Failed to copy file from compose container"); +} + +/// Wait for a file to exist on the filesystem. +pub fn wait_for_file(path: &std::path::Path, timeout_ms: u64) { + let timeout = wait_until_ms(timeout_ms); + loop { + if path.exists() { + break; + } + assert!(now_ms() < timeout, "Timed out waiting for {:?}", path); + thread::sleep(Duration::from_millis(100)); + } +} + +/// Setup UTXO coin configuration from a docker-compose container. +/// +/// Copies the coin configuration file from the compose container to the local +/// daemon data directory. Used when tests run against pre-started compose nodes +/// rather than testcontainers. +pub fn setup_utxo_conf_for_compose(ticker: &str, service_name: &str) { + let mut conf_path = coin_daemon_data_dir(ticker, true); + std::fs::create_dir_all(&conf_path).unwrap(); + conf_path.push(format!("{ticker}.conf")); + + let container_id = resolve_compose_container_id(service_name); + let src = format!("/data/node_0/{ticker}.conf"); + docker_cp_from_container(&container_id, &src, &conf_path); + wait_for_file(&conf_path, 3000); +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/env.rs b/mm2src/mm2_main/tests/docker_tests/helpers/env.rs new file mode 100644 index 0000000000..a4950b7022 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/env.rs @@ -0,0 +1,192 @@ +//! Environment helpers for docker tests. +//! +//! This module provides: +//! - Docker-compose service name constants +//! - Generic docker node helpers and types + +use testcontainers::{Container, GenericImage}; + +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-watchers-eth", + feature = "docker-tests-qrc20", + feature = "docker-tests-eth", + feature = "docker-tests-sia", + feature = "docker-tests-integration" +))] +use crypto::Secp256k1Secret; +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-watchers-eth", + feature = "docker-tests-qrc20", + feature = "docker-tests-eth", + feature = "docker-tests-sia", + feature = "docker-tests-integration" +))] +use secp256k1::SecretKey; + +// Cell import only needed for SET_BURN_PUBKEY_TO_ALICE +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-slp", + feature = "docker-tests-eth", + feature = "docker-tests-integration" +))] +use std::cell::Cell; + +// ============================================================================= +// Thread-local test flags +// ============================================================================= + +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-slp", + feature = "docker-tests-eth", + feature = "docker-tests-integration" +))] +thread_local! { + /// Set test dex pubkey as Taker (to check DexFee::NoFee) + pub static SET_BURN_PUBKEY_TO_ALICE: Cell = const { Cell::new(false) }; +} + +// ============================================================================= +// Docker-compose service name constants +// ============================================================================= + +// Docker-compose service names (see `.docker/test-nodes.yml`). +// Use service names rather than container names to enable label-based lookup, +// making the code resilient to compose project name changes. + +/// docker-compose service name for Qtum/QRC20 node +#[cfg(feature = "docker-tests-qrc20")] +pub const KDF_QTUM_SERVICE: &str = "qtum"; + +/// docker-compose service name for primary UTXO node MYCOIN +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-sia", + feature = "docker-tests-integration" +))] +pub const KDF_MYCOIN_SERVICE: &str = "mycoin"; + +/// docker-compose service name for secondary UTXO node MYCOIN1 +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-integration" +))] +pub const KDF_MYCOIN1_SERVICE: &str = "mycoin1"; + +/// docker-compose service name for BCH/SLP node FORSLP +#[cfg(any(feature = "docker-tests-slp", feature = "docker-tests-integration"))] +pub const KDF_FORSLP_SERVICE: &str = "forslp"; + +/// docker-compose service name for Zcash-based Zombie node +#[cfg(feature = "docker-tests-zcoin")] +pub const KDF_ZOMBIE_SERVICE: &str = "zombie"; + +/// docker-compose service name for IBC relayer node +#[cfg(any(feature = "docker-tests-tendermint", feature = "docker-tests-integration"))] +pub const KDF_IBC_RELAYER_SERVICE: &str = "ibc-relayer"; + +// ============================================================================= +// Generic docker node struct +// ============================================================================= + +/// A running docker container for testing. +pub struct DockerNode { + #[allow(dead_code)] + pub container: Container, + #[allow(dead_code)] + pub ticker: String, + #[allow(dead_code)] + pub port: u16, +} + +// ============================================================================= +// Utility functions +// ============================================================================= + +/// Generate a random secp256k1 secret key for testing. +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-watchers-eth", + feature = "docker-tests-qrc20", + feature = "docker-tests-eth", + feature = "docker-tests-sia", + feature = "docker-tests-integration" +))] +pub fn random_secp256k1_secret() -> Secp256k1Secret { + let priv_key = SecretKey::new(&mut rand6::thread_rng()); + Secp256k1Secret::from(*priv_key.as_ref()) +} + +// ============================================================================= +// Docker Compose Utilities +// ============================================================================= + +/// Find the container ID for a docker-compose service, independent of project name. +/// +/// Uses label-based lookup (`com.docker.compose.service=`) which works +/// regardless of project name or container_name settings. +/// +/// Note: kept in `helpers::env` so both docker-compose setup helpers and Tendermint helpers +/// can reuse it without extra dependencies. +#[cfg(any( + feature = "docker-tests-tendermint", + feature = "docker-tests-integration", + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-sia", + feature = "docker-tests-slp", + feature = "docker-tests-zcoin" +))] +pub fn resolve_compose_container_id(service_name: &str) -> String { + use std::process::Command; + + let output = Command::new("docker") + .args([ + "ps", + "-q", + "--filter", + &format!("label=com.docker.compose.service={}", service_name), + "--filter", + "status=running", + ]) + .output() + .expect("failed to execute `docker ps`"); + + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .lines() + .next() + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(String::from) + .unwrap_or_else(|| { + panic!( + "No running container found for docker-compose service '{}'. \ + Make sure `.docker/test-nodes.yml` is up and containers are started.", + service_name + ) + }) +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/eth.rs b/mm2src/mm2_main/tests/docker_tests/helpers/eth.rs new file mode 100644 index 0000000000..6356d6f508 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/eth.rs @@ -0,0 +1,861 @@ +//! Shared ETH/ERC20 helper functions for docker tests. +//! +//! This module provides: +//! - Global state for Geth contracts and accounts +//! - Address getters and checksum helpers +//! - Funding utilities for ETH and ERC20 tokens +//! - Coin creation helpers +//! - Geth initialization with contract deployment + +#[cfg(any(feature = "docker-tests-eth", feature = "docker-tests-watchers-eth"))] +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::env::DockerNode; +use coins::eth::addr_from_raw_pubkey; +#[cfg(any(feature = "docker-tests-eth", feature = "docker-tests-watchers-eth"))] +use coins::eth::EthCoin; +use coins::eth::{checksum_address, eth_coin_from_conf_and_request, ERC20_ABI}; +#[cfg(any(feature = "docker-tests-eth", feature = "docker-tests-watchers-eth"))] +use coins::DerivationMethod; +use coins::{CoinProtocol, CoinWithDerivationMethod, PrivKeyBuildPolicy}; +use common::block_on; +use common::custom_futures::timeout::FutureTimerExt; +use crypto::privkey::key_pair_from_seed; +#[cfg(any(feature = "docker-tests-eth", feature = "docker-tests-integration"))] +use crypto::Secp256k1Secret; +use ethabi::Token; +use ethereum_types::{H160 as H160Eth, U256}; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_test_helpers::for_tests::{erc20_dev_conf, eth_dev_conf}; +use mm2_test_helpers::get_passphrase; +use serde_json::json; +use std::sync::{Mutex, OnceLock}; +use std::thread; +use std::time::Duration; +use testcontainers::runners::SyncRunner; +use testcontainers::{GenericImage, RunnableImage}; +use web3::contract::{Contract, Options}; +use web3::types::{Address, BlockId, BlockNumber, TransactionRequest, H256}; +use web3::{transports::Http, Web3}; + +// ============================================================================= +// Global state - statics for Geth node +// ============================================================================= + +lazy_static! { + /// Web3 instance connected to the Geth dev node + pub static ref GETH_WEB3: Web3 = Web3::new(Http::new(GETH_RPC_URL).unwrap()); + /// Mutex used to prevent nonce re-usage during funding addresses used in tests + pub static ref GETH_NONCE_LOCK: Mutex<()> = Mutex::new(()); + + /// Shared MmArc context for single-instance tests + pub static ref MM_CTX: MmArc = MmCtxBuilder::new() + .with_conf(json!({"coins":[eth_dev_conf()],"use_trading_proto_v2": true})) + .into_mm_arc(); + + /// Second MmCtx instance for Maker/Taker tests using same private keys. + /// + /// When enabling coins for both Maker and Taker, two distinct coin instances are created. + /// Different instances of the same coin should have separate global nonce locks. + /// Using different MmCtx instances assigns Maker and Taker coins to separate CoinsCtx, + /// addressing the "replacement transaction" issue (same nonce for different transactions). + pub static ref MM_CTX1: MmArc = MmCtxBuilder::new() + .with_conf(json!({"use_trading_proto_v2": true})) + .into_mm_arc(); +} + +// ============================================================================= +// OnceLock contract addresses (initialized once in init_geth_node) +// ============================================================================= + +/// The account supplied with ETH on Geth dev node creation +static GETH_ACCOUNT: OnceLock = OnceLock::new(); +/// ERC20 token address on Geth dev node +static GETH_ERC20_CONTRACT: OnceLock = OnceLock::new(); +/// Swap contract address on Geth dev node +static GETH_SWAP_CONTRACT: OnceLock = OnceLock::new(); +/// Maker Swap V2 contract address on Geth dev node +static GETH_MAKER_SWAP_V2: OnceLock = OnceLock::new(); +/// Taker Swap V2 contract address on Geth dev node +static GETH_TAKER_SWAP_V2: OnceLock = OnceLock::new(); +/// Swap contract (with watchers support) address on Geth dev node +static GETH_WATCHERS_SWAP_CONTRACT: OnceLock = OnceLock::new(); +/// ERC721 token address on Geth dev node +static GETH_ERC721_CONTRACT: OnceLock = OnceLock::new(); +/// ERC1155 token address on Geth dev node +static GETH_ERC1155_CONTRACT: OnceLock = OnceLock::new(); +/// NFT Maker Swap V2 contract address on Geth dev node +static GETH_NFT_MAKER_SWAP_V2: OnceLock = OnceLock::new(); + +/// Geth RPC URL +pub static GETH_RPC_URL: &str = "http://127.0.0.1:8545"; + +// ============================================================================= +// Docker image constants +// ============================================================================= + +/// Geth docker image +pub const GETH_DOCKER_IMAGE: &str = "docker.io/ethereum/client-go"; +/// Geth docker image with tag +pub const GETH_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/ethereum/client-go:stable"; + +// ============================================================================= +// Contract bytecode constants +// ============================================================================= + +pub const ERC20_TOKEN_BYTES: &str = include_str!("../../../../mm2_test_helpers/contract_bytes/erc20_token_bytes"); +pub const SWAP_CONTRACT_BYTES: &str = include_str!("../../../../mm2_test_helpers/contract_bytes/swap_contract_bytes"); +pub const WATCHERS_SWAP_CONTRACT_BYTES: &str = + include_str!("../../../../mm2_test_helpers/contract_bytes/watchers_swap_contract_bytes"); +/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/Erc721Token.sol +pub const ERC721_TEST_TOKEN_BYTES: &str = + include_str!("../../../../mm2_test_helpers/contract_bytes/erc721_test_token_bytes"); +/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/Erc1155Token.sol +pub const ERC1155_TEST_TOKEN_BYTES: &str = + include_str!("../../../../mm2_test_helpers/contract_bytes/erc1155_test_token_bytes"); +/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/EtomicSwapMakerNftV2.sol +pub const NFT_MAKER_SWAP_V2_BYTES: &str = + include_str!("../../../../mm2_test_helpers/contract_bytes/nft_maker_swap_v2_bytes"); +/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/EtomicSwapMakerV2.sol +pub const MAKER_SWAP_V2_BYTES: &str = include_str!("../../../../mm2_test_helpers/contract_bytes/maker_swap_v2_bytes"); +/// https://github.com/KomodoPlatform/etomic-swap/blob/7d4eafd4a408188a95aee78a41f0bf5f9116ffa2/contracts/EtomicSwapTakerV2.sol +pub const TAKER_SWAP_V2_BYTES: &str = include_str!("../../../../mm2_test_helpers/contract_bytes/taker_swap_v2_bytes"); + +/// Geth dev chain ID used for testing +pub const GETH_DEV_CHAIN_ID: u64 = 1337; + +// ============================================================================= +// Address getters - safe OnceCell access +// ============================================================================= + +/// Get the Geth coinbase account address. +/// Panics if called before `init_geth_node()`. +pub fn geth_account() -> Address { + *GETH_ACCOUNT + .get() + .expect("GETH_ACCOUNT not initialized - call init_geth_node() first") +} + +/// Get the swap contract address. +/// Panics if called before `init_geth_node()`. +pub fn swap_contract() -> Address { + *GETH_SWAP_CONTRACT + .get() + .expect("GETH_SWAP_CONTRACT not initialized - call init_geth_node() first") +} + +/// Get the watchers swap contract address. +/// Panics if called before `init_geth_node()`. +pub fn watchers_swap_contract() -> Address { + *GETH_WATCHERS_SWAP_CONTRACT + .get() + .expect("GETH_WATCHERS_SWAP_CONTRACT not initialized - call init_geth_node() first") +} + +/// Get the ERC20 contract address. +/// Panics if called before `init_geth_node()`. +pub fn erc20_contract() -> Address { + *GETH_ERC20_CONTRACT + .get() + .expect("GETH_ERC20_CONTRACT not initialized - call init_geth_node() first") +} + +/// Get the Maker Swap V2 contract address. +/// Panics if called before `init_geth_node()`. +pub fn geth_maker_swap_v2() -> Address { + *GETH_MAKER_SWAP_V2 + .get() + .expect("GETH_MAKER_SWAP_V2 not initialized - call init_geth_node() first") +} + +/// Get the Taker Swap V2 contract address. +/// Panics if called before `init_geth_node()`. +pub fn geth_taker_swap_v2() -> Address { + *GETH_TAKER_SWAP_V2 + .get() + .expect("GETH_TAKER_SWAP_V2 not initialized - call init_geth_node() first") +} + +/// Get the ERC721 contract address. +/// Panics if called before `init_geth_node()`. +pub fn geth_erc721_contract() -> Address { + *GETH_ERC721_CONTRACT + .get() + .expect("GETH_ERC721_CONTRACT not initialized - call init_geth_node() first") +} + +/// Get the ERC1155 contract address. +/// Panics if called before `init_geth_node()`. +pub fn geth_erc1155_contract() -> Address { + *GETH_ERC1155_CONTRACT + .get() + .expect("GETH_ERC1155_CONTRACT not initialized - call init_geth_node() first") +} + +/// Get the NFT Maker Swap V2 contract address. +/// Panics if called before `init_geth_node()`. +pub fn geth_nft_maker_swap_v2() -> Address { + *GETH_NFT_MAKER_SWAP_V2 + .get() + .expect("GETH_NFT_MAKER_SWAP_V2 not initialized - call init_geth_node() first") +} + +/// Return ERC20 dev token contract address in checksum format +pub fn erc20_contract_checksum() -> String { + checksum_address(&format!("{:02x}", erc20_contract())) +} + +/// Return swap contract address in checksum format (with 0x prefix) +#[cfg(any( + feature = "docker-tests-eth", + feature = "docker-tests-tendermint", + feature = "docker-tests-integration" +))] +pub fn swap_contract_checksum() -> String { + checksum_address(&format!("{:02x}", swap_contract())) +} + +/// Return watchers swap contract address in checksum format (with 0x prefix) +#[cfg(feature = "docker-tests-watchers")] +pub fn watchers_swap_contract_checksum() -> String { + checksum_address(&format!("{:02x}", watchers_swap_contract())) +} + +// ============================================================================= +// Docker node helpers +// ============================================================================= + +/// Start a Geth docker node for testing. +pub fn geth_docker_node(ticker: &'static str, port: u16) -> DockerNode { + let image = GenericImage::new(GETH_DOCKER_IMAGE, "stable"); + let args = vec!["--dev".into(), "--http".into(), "--http.addr=0.0.0.0".into()]; + let image = RunnableImage::from((image, args)).with_mapped_port((port, port)); + let container = image.start().expect("Failed to start Geth docker node"); + DockerNode { + container, + ticker: ticker.into(), + port, + } +} + +/// Wait for the Geth node to be ready to accept connections. +/// +/// Polls the node's block number endpoint until it responds successfully. +/// Used in compose mode where the node may still be starting up. +pub fn wait_for_geth_node_ready() { + let mut attempts = 0; + loop { + if attempts >= 5 { + panic!("Failed to connect to Geth node after several attempts."); + } + match block_on(GETH_WEB3.eth().block_number().timeout(Duration::from_secs(6))) { + Ok(Ok(block_number)) => { + log!("Geth node is ready, latest block number: {:?}", block_number); + break; + }, + Ok(Err(e)) => { + log!("Failed to connect to Geth node: {:?}, retrying...", e); + }, + Err(_) => { + log!("Connection to Geth node timed out, retrying..."); + }, + } + attempts += 1; + thread::sleep(Duration::from_secs(1)); + } +} + +// ============================================================================= +// Funding utilities - fill test wallets with ETH and tokens +// ============================================================================= + +fn wait_for_confirmation(tx_hash: H256) { + thread::sleep(Duration::from_millis(2000)); + loop { + match block_on(GETH_WEB3.eth().transaction_receipt(tx_hash)) { + Ok(Some(r)) => match r.block_hash { + Some(_) => break, + None => thread::sleep(Duration::from_millis(100)), + }, + _ => { + thread::sleep(Duration::from_millis(100)); + }, + } + } +} + +/// Fill an address with ETH from the Geth coinbase account +pub fn fill_eth(to_addr: Address, amount: U256) { + let _guard = GETH_NONCE_LOCK.lock().unwrap(); + let tx_request = TransactionRequest { + from: geth_account(), + to: Some(to_addr), + gas: None, + gas_price: None, + value: Some(amount), + data: None, + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + let tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request)).unwrap(); + wait_for_confirmation(tx_hash); +} + +/// Fill an address with ERC20 tokens from the Geth coinbase account +pub fn fill_erc20(to_addr: Address, amount: U256) { + let _guard = GETH_NONCE_LOCK.lock().unwrap(); + let erc20 = Contract::from_json(GETH_WEB3.eth(), erc20_contract(), ERC20_ABI.as_bytes()).unwrap(); + + let tx_hash = block_on(erc20.call( + "transfer", + (Token::Address(to_addr), Token::Uint(amount)), + geth_account(), + Options::default(), + )) + .unwrap(); + wait_for_confirmation(tx_hash); +} + +// ============================================================================= +// Coin creation utilities - create test coins with random keys +// Only used by docker-tests-eth and docker-tests-watchers-eth (not integration) +// ============================================================================= + +/// Creates ETH protocol coin supplied with 100 ETH +#[cfg(any(feature = "docker-tests-eth", feature = "docker-tests-watchers-eth"))] +pub fn eth_coin_with_random_privkey_using_urls(swap_contract_address: Address, urls: &[&str]) -> EthCoin { + let eth_conf = eth_dev_conf(); + let req = json!({ + "method": "enable", + "coin": "ETH", + "swap_contract_address": swap_contract_address, + "urls": urls, + }); + + let secret = random_secp256k1_secret(); + let eth_coin = block_on(eth_coin_from_conf_and_request( + &MM_CTX, + "ETH", + ð_conf, + &req, + CoinProtocol::ETH { + chain_id: GETH_DEV_CHAIN_ID, + }, + PrivKeyBuildPolicy::IguanaPrivKey(secret), + )) + .unwrap(); + + let my_address = match eth_coin.derivation_method() { + DerivationMethod::SingleAddress(addr) => *addr, + _ => panic!("Expected single address"), + }; + + // 100 ETH + fill_eth(my_address, U256::from(10).pow(U256::from(20))); + + eth_coin +} + +/// Creates ETH protocol coin supplied with 100 ETH, using the default GETH_RPC_URL +#[cfg(any(feature = "docker-tests-eth", feature = "docker-tests-watchers-eth"))] +pub fn eth_coin_with_random_privkey(swap_contract_address: Address) -> EthCoin { + eth_coin_with_random_privkey_using_urls(swap_contract_address, &[GETH_RPC_URL]) +} + +/// Creates ERC20 protocol coin supplied with 1 ETH and 100 tokens +#[cfg(any(feature = "docker-tests-eth", feature = "docker-tests-watchers-eth"))] +pub fn erc20_coin_with_random_privkey(swap_contract_address: Address) -> EthCoin { + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let req = json!({ + "method": "enable", + "coin": "ERC20DEV", + "swap_contract_address": swap_contract_address, + "urls": [GETH_RPC_URL], + }); + + let erc20_coin = block_on(eth_coin_from_conf_and_request( + &MM_CTX, + "ERC20DEV", + &erc20_conf, + &req, + CoinProtocol::ERC20 { + platform: "ETH".to_string(), + contract_address: checksum_address(&format!("{:02x}", erc20_contract())), + }, + PrivKeyBuildPolicy::IguanaPrivKey(random_secp256k1_secret()), + )) + .unwrap(); + + let my_address = match erc20_coin.derivation_method() { + DerivationMethod::SingleAddress(addr) => *addr, + _ => panic!("Expected single address"), + }; + + // 1 ETH + fill_eth(my_address, U256::from(10).pow(U256::from(18))); + // 100 tokens (it has 8 decimals) + fill_erc20(my_address, U256::from(10000000000u64)); + + erc20_coin +} + +/// Fills the private key's public address with ETH and ERC20 tokens +#[cfg(any(feature = "docker-tests-eth", feature = "docker-tests-integration"))] +pub fn fill_eth_erc20_with_private_key(priv_key: Secp256k1Secret) { + let eth_conf = eth_dev_conf(); + let req = json!({ + "coin": "ETH", + "urls": [GETH_RPC_URL], + "swap_contract_address": swap_contract(), + }); + + let eth_coin = block_on(eth_coin_from_conf_and_request( + &MM_CTX, + "ETH", + ð_conf, + &req, + CoinProtocol::ETH { + chain_id: GETH_DEV_CHAIN_ID, + }, + PrivKeyBuildPolicy::IguanaPrivKey(priv_key), + )) + .unwrap(); + let my_address = block_on(eth_coin.derivation_method().single_addr_or_err()).unwrap(); + + // 100 ETH + fill_eth(my_address, U256::from(10).pow(U256::from(20))); + + let erc20_conf = erc20_dev_conf(&erc20_contract_checksum()); + let req = json!({ + "method": "enable", + "coin": "ERC20DEV", + "urls": [GETH_RPC_URL], + "swap_contract_address": swap_contract(), + }); + + let _erc20_coin = block_on(eth_coin_from_conf_and_request( + &MM_CTX, + "ERC20DEV", + &erc20_conf, + &req, + CoinProtocol::ERC20 { + platform: "ETH".to_string(), + contract_address: erc20_contract_checksum(), + }, + PrivKeyBuildPolicy::IguanaPrivKey(priv_key), + )) + .unwrap(); + + // 100 tokens (it has 8 decimals) + fill_erc20(my_address, U256::from(10000000000u64)); +} + +// ============================================================================= +// Geth initialization +// ============================================================================= + +async fn get_current_gas_limit(web3: &Web3) { + match web3.eth().block(BlockId::Number(BlockNumber::Latest)).await { + Ok(Some(block)) => { + log!("Current gas limit: {}", block.gas_limit); + }, + Ok(None) => log!("Latest block information is not available."), + Err(e) => log!("Failed to fetch the latest block: {}", e), + } +} + +/// Initialize the Geth node by deploying all test contracts. +/// +/// This function deploys: +/// - ERC20 test token +/// - Swap contract +/// - Maker/Taker Swap V2 contracts +/// - Watchers swap contract +/// - NFT Maker Swap V2 contract +/// - ERC721 and ERC1155 test tokens +/// +/// It also funds the Alice and Bob test accounts with ETH. +pub fn init_geth_node() { + block_on(get_current_gas_limit(&GETH_WEB3)); + let gas_price = block_on(GETH_WEB3.eth().gas_price()).unwrap(); + log!("Current gas price: {:?}", gas_price); + let accounts = block_on(GETH_WEB3.eth().accounts()).unwrap(); + let geth_account = accounts[0]; + GETH_ACCOUNT + .set(geth_account) + .expect("GETH_ACCOUNT already initialized"); + log!("GETH ACCOUNT {:?}", geth_account); + + let tx_request_deploy_erc20 = TransactionRequest { + from: geth_account, + to: None, + gas: None, + gas_price: None, + value: None, + data: Some(hex::decode(ERC20_TOKEN_BYTES).unwrap().into()), + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + + let deploy_erc20_tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request_deploy_erc20)).unwrap(); + log!("Sent ERC20 deploy transaction {:?}", deploy_erc20_tx_hash); + + let geth_erc20_contract = loop { + let deploy_tx_receipt = match block_on(GETH_WEB3.eth().transaction_receipt(deploy_erc20_tx_hash)) { + Ok(receipt) => receipt, + Err(_) => { + thread::sleep(Duration::from_millis(100)); + continue; + }, + }; + + if let Some(receipt) = deploy_tx_receipt { + let addr = receipt.contract_address.unwrap(); + log!("GETH_ERC20_CONTRACT {:?}", addr); + break addr; + } + thread::sleep(Duration::from_millis(100)); + }; + GETH_ERC20_CONTRACT + .set(geth_erc20_contract) + .expect("GETH_ERC20_CONTRACT already initialized"); + + let tx_request_deploy_swap_contract = TransactionRequest { + from: geth_account, + to: None, + gas: None, + gas_price: None, + value: None, + data: Some(hex::decode(SWAP_CONTRACT_BYTES).unwrap().into()), + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + let deploy_swap_tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request_deploy_swap_contract)).unwrap(); + log!("Sent deploy swap contract transaction {:?}", deploy_swap_tx_hash); + + let geth_swap_contract = loop { + let deploy_swap_tx_receipt = match block_on(GETH_WEB3.eth().transaction_receipt(deploy_swap_tx_hash)) { + Ok(receipt) => receipt, + Err(_) => { + thread::sleep(Duration::from_millis(100)); + continue; + }, + }; + + if let Some(receipt) = deploy_swap_tx_receipt { + let addr = receipt.contract_address.unwrap(); + log!("GETH_SWAP_CONTRACT {:?}", addr); + break addr; + } + thread::sleep(Duration::from_millis(100)); + }; + GETH_SWAP_CONTRACT + .set(geth_swap_contract) + .expect("GETH_SWAP_CONTRACT already initialized"); + + let tx_request_deploy_maker_swap_contract_v2 = TransactionRequest { + from: geth_account, + to: None, + gas: None, + gas_price: None, + value: None, + data: Some(hex::decode(MAKER_SWAP_V2_BYTES).unwrap().into()), + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + let deploy_maker_swap_v2_tx_hash = block_on( + GETH_WEB3 + .eth() + .send_transaction(tx_request_deploy_maker_swap_contract_v2), + ) + .unwrap(); + log!( + "Sent deploy maker swap v2 contract transaction {:?}", + deploy_maker_swap_v2_tx_hash + ); + + let geth_maker_swap_v2 = loop { + let deploy_maker_swap_v2_tx_receipt = + match block_on(GETH_WEB3.eth().transaction_receipt(deploy_maker_swap_v2_tx_hash)) { + Ok(receipt) => receipt, + Err(_) => { + thread::sleep(Duration::from_millis(100)); + continue; + }, + }; + + if let Some(receipt) = deploy_maker_swap_v2_tx_receipt { + let addr = receipt.contract_address.unwrap(); + log!( + "GETH_MAKER_SWAP_V2 contract address: {:?}, receipt.status: {:?}", + addr, + receipt.status + ); + break addr; + } + thread::sleep(Duration::from_millis(100)); + }; + GETH_MAKER_SWAP_V2 + .set(geth_maker_swap_v2) + .expect("GETH_MAKER_SWAP_V2 already initialized"); + + let dex_fee_addr = Token::Address(geth_account); + let params = ethabi::encode(&[dex_fee_addr]); + let taker_swap_v2_data = format!("{}{}", TAKER_SWAP_V2_BYTES, hex::encode(params)); + + let tx_request_deploy_taker_swap_contract_v2 = TransactionRequest { + from: geth_account, + to: None, + gas: None, + gas_price: None, + value: None, + data: Some(hex::decode(taker_swap_v2_data).unwrap().into()), + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + let deploy_taker_swap_v2_tx_hash = block_on( + GETH_WEB3 + .eth() + .send_transaction(tx_request_deploy_taker_swap_contract_v2), + ) + .unwrap(); + log!( + "Sent deploy taker swap v2 contract transaction {:?}", + deploy_taker_swap_v2_tx_hash + ); + + let geth_taker_swap_v2 = loop { + let deploy_taker_swap_v2_tx_receipt = + match block_on(GETH_WEB3.eth().transaction_receipt(deploy_taker_swap_v2_tx_hash)) { + Ok(receipt) => receipt, + Err(_) => { + thread::sleep(Duration::from_millis(100)); + continue; + }, + }; + + if let Some(receipt) = deploy_taker_swap_v2_tx_receipt { + let addr = receipt.contract_address.unwrap(); + log!( + "GETH_TAKER_SWAP_V2 contract address: {:?}, receipt.status: {:?}", + addr, + receipt.status + ); + break addr; + } + thread::sleep(Duration::from_millis(100)); + }; + GETH_TAKER_SWAP_V2 + .set(geth_taker_swap_v2) + .expect("GETH_TAKER_SWAP_V2 already initialized"); + + let tx_request_deploy_watchers_swap_contract = TransactionRequest { + from: geth_account, + to: None, + gas: None, + gas_price: None, + value: None, + data: Some(hex::decode(WATCHERS_SWAP_CONTRACT_BYTES).unwrap().into()), + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + let deploy_watchers_swap_tx_hash = block_on( + GETH_WEB3 + .eth() + .send_transaction(tx_request_deploy_watchers_swap_contract), + ) + .unwrap(); + log!( + "Sent deploy watchers swap contract transaction {:?}", + deploy_watchers_swap_tx_hash + ); + + let geth_watchers_swap_contract = loop { + let deploy_watchers_swap_tx_receipt = + match block_on(GETH_WEB3.eth().transaction_receipt(deploy_watchers_swap_tx_hash)) { + Ok(receipt) => receipt, + Err(_) => { + thread::sleep(Duration::from_millis(100)); + continue; + }, + }; + + if let Some(receipt) = deploy_watchers_swap_tx_receipt { + let addr = receipt.contract_address.unwrap(); + log!("GETH_WATCHERS_SWAP_CONTRACT {:?}", addr); + break addr; + } + thread::sleep(Duration::from_millis(100)); + }; + GETH_WATCHERS_SWAP_CONTRACT + .set(geth_watchers_swap_contract) + .expect("GETH_WATCHERS_SWAP_CONTRACT already initialized"); + + let tx_request_deploy_nft_maker_swap_v2_contract = TransactionRequest { + from: geth_account, + to: None, + gas: None, + gas_price: None, + value: None, + data: Some(hex::decode(NFT_MAKER_SWAP_V2_BYTES).unwrap().into()), + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + let deploy_nft_maker_swap_v2_tx_hash = block_on( + GETH_WEB3 + .eth() + .send_transaction(tx_request_deploy_nft_maker_swap_v2_contract), + ) + .unwrap(); + log!( + "Sent deploy nft maker swap v2 contract transaction {:?}", + deploy_nft_maker_swap_v2_tx_hash + ); + + let geth_nft_maker_swap_v2 = loop { + let deploy_nft_maker_swap_v2_tx_receipt = + match block_on(GETH_WEB3.eth().transaction_receipt(deploy_nft_maker_swap_v2_tx_hash)) { + Ok(receipt) => receipt, + Err(_) => { + thread::sleep(Duration::from_millis(100)); + continue; + }, + }; + + if let Some(receipt) = deploy_nft_maker_swap_v2_tx_receipt { + let addr = receipt.contract_address.unwrap(); + log!( + "GETH_NFT_MAKER_SWAP_V2 contact address: {:?}, receipt.status: {:?}", + addr, + receipt.status + ); + break addr; + } + thread::sleep(Duration::from_millis(100)); + }; + GETH_NFT_MAKER_SWAP_V2 + .set(geth_nft_maker_swap_v2) + .expect("GETH_NFT_MAKER_SWAP_V2 already initialized"); + + let name = Token::String("MyNFT".into()); + let symbol = Token::String("MNFT".into()); + let params = ethabi::encode(&[name, symbol]); + let erc721_data = format!("{}{}", ERC721_TEST_TOKEN_BYTES, hex::encode(params)); + + let tx_request_deploy_erc721 = TransactionRequest { + from: geth_account, + to: None, + gas: None, + gas_price: None, + value: None, + data: Some(hex::decode(erc721_data).unwrap().into()), + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + let deploy_erc721_tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request_deploy_erc721)).unwrap(); + log!("Sent ERC721 deploy transaction {:?}", deploy_erc721_tx_hash); + + let geth_erc721_contract = loop { + let deploy_erc721_tx_receipt = match block_on(GETH_WEB3.eth().transaction_receipt(deploy_erc721_tx_hash)) { + Ok(receipt) => receipt, + Err(_) => { + thread::sleep(Duration::from_millis(100)); + continue; + }, + }; + + if let Some(receipt) = deploy_erc721_tx_receipt { + let addr = receipt.contract_address.unwrap(); + log!("GETH_ERC721_CONTRACT {:?}", addr); + break addr; + } + thread::sleep(Duration::from_millis(100)); + }; + GETH_ERC721_CONTRACT + .set(geth_erc721_contract) + .expect("GETH_ERC721_CONTRACT already initialized"); + + let uri = Token::String("MyNFTUri".into()); + let params = ethabi::encode(&[uri]); + let erc1155_data = format!("{}{}", ERC1155_TEST_TOKEN_BYTES, hex::encode(params)); + + let tx_request_deploy_erc1155 = TransactionRequest { + from: geth_account, + to: None, + gas: None, + gas_price: None, + value: None, + data: Some(hex::decode(erc1155_data).unwrap().into()), + nonce: None, + condition: None, + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + }; + let deploy_erc1155_tx_hash = block_on(GETH_WEB3.eth().send_transaction(tx_request_deploy_erc1155)).unwrap(); + log!("Sent ERC1155 deploy transaction {:?}", deploy_erc1155_tx_hash); + + let geth_erc1155_contract = loop { + let deploy_erc1155_tx_receipt = match block_on(GETH_WEB3.eth().transaction_receipt(deploy_erc1155_tx_hash)) { + Ok(receipt) => receipt, + Err(_) => { + thread::sleep(Duration::from_millis(100)); + continue; + }, + }; + + if let Some(receipt) = deploy_erc1155_tx_receipt { + let addr = receipt.contract_address.unwrap(); + log!("GETH_ERC1155_CONTRACT {:?}", addr); + break addr; + } + thread::sleep(Duration::from_millis(100)); + }; + GETH_ERC1155_CONTRACT + .set(geth_erc1155_contract) + .expect("GETH_ERC1155_CONTRACT already initialized"); + + let alice_passphrase = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); + let alice_keypair = key_pair_from_seed(&alice_passphrase).unwrap(); + let alice_eth_addr = addr_from_raw_pubkey(alice_keypair.public()).unwrap(); + // 100 ETH + fill_eth(alice_eth_addr, U256::from(10).pow(U256::from(20))); + + let bob_passphrase = get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap(); + let bob_keypair = key_pair_from_seed(&bob_passphrase).unwrap(); + let bob_eth_addr = addr_from_raw_pubkey(bob_keypair.public()).unwrap(); + // 100 ETH + fill_eth(bob_eth_addr, U256::from(10).pow(U256::from(20))); +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/mod.rs b/mm2src/mm2_main/tests/docker_tests/helpers/mod.rs new file mode 100644 index 0000000000..7680138527 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/mod.rs @@ -0,0 +1,85 @@ +//! Shared helper functions for docker tests. +//! +//! These helpers are organized by chain type and are gated on `run-docker-tests`. +//! +//! ## Module organization +//! +//! - `docker_ops` - Docker operations trait and funding locks for coins in containers +//! - `env` - Environment setup: shared contexts, service constants +//! - `eth` - Ethereum/ERC20: Geth initialization, contract deployment, funding +//! - `utxo` - UTXO coins: MYCOIN, MYCOIN1, BCH/SLP helpers +//! - `qrc20` - Qtum/QRC20: contract initialization, coin creation +//! - `sia` - Sia: node setup, RPC configuration +//! - `swap` - Cross-chain swap orchestration helpers +//! - `tendermint` - Cosmos/Tendermint: node setup, IBC channels +//! - `zcoin` - ZCoin/Zombie: sapling cache, node setup + +// Docker-specific helpers, only needed when docker tests are enabled. +// Gated on specific features to avoid unused code warnings. + +// docker_ops - CoinDockerOps trait and UTXO compose utilities +// (tendermint uses resolve_compose_container_id from env.rs instead) +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-sia", + feature = "docker-tests-slp", + feature = "docker-tests-zcoin", + feature = "docker-tests-integration" +))] +pub mod docker_ops; + +// Environment helpers +#[cfg(feature = "run-docker-tests")] +pub mod env; + +// ETH helpers +#[cfg(any( + feature = "docker-tests-eth", + feature = "docker-tests-watchers-eth", + feature = "docker-tests-integration", +))] +pub mod eth; + +// QRC20 helpers (Qtum/QRC20 docker nodes & contracts). +#[cfg(feature = "docker-tests-qrc20")] +pub mod qrc20; + +// Sia helpers (Sia docker nodes). +#[cfg(feature = "docker-tests-sia")] +pub mod sia; + +// SLP helpers (BCH/SLP tokens). +#[cfg(any(feature = "docker-tests-slp", feature = "docker-tests-integration"))] +pub mod slp; + +// Cross-chain swap orchestration helpers. +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-eth", + feature = "docker-tests-qrc20", + feature = "docker-tests-slp" +))] +pub mod swap; + +// Tendermint / IBC helpers. +#[cfg(any(feature = "docker-tests-tendermint", feature = "docker-tests-integration"))] +pub mod tendermint; + +// UTXO helpers (MYCOIN, MYCOIN1). +// Note: SLP has its own self-contained module (slp.rs) and doesn't need utxo. +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-sia", + feature = "docker-tests-integration" +))] +pub mod utxo; + +// ZCoin/Zombie helpers. +#[cfg(feature = "docker-tests-zcoin")] +pub mod zcoin; diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/qrc20.rs b/mm2src/mm2_main/tests/docker_tests/helpers/qrc20.rs new file mode 100644 index 0000000000..db73709145 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/qrc20.rs @@ -0,0 +1,524 @@ +//! Qtum/QRC20 helpers for docker tests. +//! +//! This module provides: +//! - QRC20 coin creation and funding utilities +//! - Qtum docker node helpers +//! - QRC20 contract initialization + +use crate::docker_tests::helpers::docker_ops::{docker_cp_from_container, wait_for_file}; +use crate::docker_tests::helpers::env::{ + random_secp256k1_secret, resolve_compose_container_id, DockerNode, KDF_QTUM_SERVICE, +}; +use crate::docker_tests::helpers::utxo::fill_address; +use crate::docker_tests::helpers::utxo::QTUM_LOCK; +use coins::qrc20::rpc_clients::for_tests::Qrc20NativeWalletOps; +use coins::qrc20::{qrc20_coin_with_priv_key, Qrc20ActivationParams, Qrc20Coin}; +use coins::utxo::qtum::QtumBasedCoin; +use coins::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin}; +use coins::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcClientOps}; +use coins::utxo::{sat_from_big_decimal, UtxoActivationParams, UtxoCoinFields}; +use coins::{ConfirmPaymentInput, MarketCoinOps}; +use common::{block_on, block_on_f01, now_ms, now_sec, temp_dir, wait_until_ms, wait_until_sec}; +use crypto::Secp256k1Secret; +use ethereum_types::H160 as H160Eth; +use http::StatusCode; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_number::BigDecimal; +use mm2_test_helpers::for_tests::MarketMakerIt; +use serde_json::{self as json, json, Value as Json}; +use std::path::PathBuf; +use std::process::Command; +use std::str::FromStr; +use std::sync::{Mutex, OnceLock}; +use std::thread; +use std::time::Duration; +use testcontainers::core::WaitFor; +use testcontainers::runners::SyncRunner; +use testcontainers::{GenericImage, RunnableImage}; + +// ============================================================================= +// Docker image constants +// ============================================================================= + +/// Qtum regtest docker image +pub const QTUM_REGTEST_DOCKER_IMAGE: &str = "docker.io/gleec/qtumregtest"; +/// Qtum regtest docker image with tag +pub const QTUM_REGTEST_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/gleec/qtumregtest:latest"; + +// ============================================================================= +// Global state (OnceLock for contract addresses) +// ============================================================================= + +/// QICK token contract address +static QICK_TOKEN_ADDRESS: OnceLock = OnceLock::new(); +/// QORTY token contract address +static QORTY_TOKEN_ADDRESS: OnceLock = OnceLock::new(); +/// QRC20 swap contract address +static QRC20_SWAP_CONTRACT_ADDRESS: OnceLock = OnceLock::new(); +/// Path to Qtum config file +static QTUM_CONF_PATH: OnceLock = OnceLock::new(); + +/// Get the QICK token contract address. +/// Panics if called before initialization. +pub fn qick_token_address() -> H160Eth { + *QICK_TOKEN_ADDRESS + .get() + .expect("QICK_TOKEN_ADDRESS not initialized - ensure QRC20 init has run") +} + +/// Get the QORTY token contract address. +/// Panics if called before initialization. +pub fn qorty_token_address() -> H160Eth { + *QORTY_TOKEN_ADDRESS + .get() + .expect("QORTY_TOKEN_ADDRESS not initialized - ensure QRC20 init has run") +} + +/// Get the QRC20 swap contract address. +/// Panics if called before initialization. +pub fn qrc20_swap_contract_address() -> H160Eth { + *QRC20_SWAP_CONTRACT_ADDRESS + .get() + .expect("QRC20_SWAP_CONTRACT_ADDRESS not initialized - ensure QRC20 init has run") +} + +/// Get the Qtum config file path. +/// Panics if called before initialization. +pub fn qtum_conf_path() -> &'static PathBuf { + QTUM_CONF_PATH + .get() + .expect("QTUM_CONF_PATH not initialized - ensure QRC20 init has run") +} + +/// Set the QICK token contract address (for initialization). +pub fn set_qick_token_address(addr: H160Eth) { + QICK_TOKEN_ADDRESS + .set(addr) + .expect("QICK_TOKEN_ADDRESS already initialized"); +} + +/// Set the QORTY token contract address (for initialization). +pub fn set_qorty_token_address(addr: H160Eth) { + QORTY_TOKEN_ADDRESS + .set(addr) + .expect("QORTY_TOKEN_ADDRESS already initialized"); +} + +/// Set the QRC20 swap contract address (for initialization). +pub fn set_qrc20_swap_contract_address(addr: H160Eth) { + QRC20_SWAP_CONTRACT_ADDRESS + .set(addr) + .expect("QRC20_SWAP_CONTRACT_ADDRESS already initialized"); +} + +/// Set the Qtum config file path (for initialization). +pub fn set_qtum_conf_path(path: PathBuf) { + QTUM_CONF_PATH.set(path).expect("QTUM_CONF_PATH already initialized"); +} + +/// Setup Qtum configuration from a docker-compose container. +/// +/// Copies the Qtum configuration file from the compose container to the local +/// daemon data directory. Used when tests run against pre-started compose nodes. +pub fn setup_qtum_conf_for_compose() { + use coins::utxo::coin_daemon_data_dir; + + let mut conf_path = coin_daemon_data_dir("qtum", false); + std::fs::create_dir_all(&conf_path).unwrap(); + conf_path.push("qtum.conf"); + + let container_id = resolve_compose_container_id(KDF_QTUM_SERVICE); + docker_cp_from_container(&container_id, "/data/node_0/qtum.conf", &conf_path); + wait_for_file(&conf_path, 3000); + + set_qtum_conf_path(conf_path); +} + +// ============================================================================= +// Constants +// ============================================================================= + +/// Qtum address label used in tests +pub const QTUM_ADDRESS_LABEL: &str = "MM2_ADDRESS_LABEL"; + +// ============================================================================= +// Utility functions +// ============================================================================= + +/// Get only one address assigned the specified label. +pub fn get_address_by_label(coin: T, label: &str) -> String +where + T: AsRef, +{ + let native = match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref native) => native, + UtxoRpcClientEnum::Electrum(_) => panic!("NativeClient expected"), + }; + let mut addresses = block_on_f01(native.get_addresses_by_label(label)) + .expect("!getaddressesbylabel") + .into_iter(); + match addresses.next() { + Some((addr, _purpose)) if addresses.next().is_none() => addr, + Some(_) => panic!("Expected only one address by {:?}", label), + None => panic!("Expected one address by {:?}", label), + } +} + +/// Build `Qrc20Coin` from ticker and privkey without filling the balance. +pub fn qrc20_coin_from_privkey(ticker: &str, priv_key: Secp256k1Secret) -> (MmArc, Qrc20Coin) { + use crate::docker_tests::helpers::utxo::import_address; + + let contract_address = match ticker { + "QICK" => qick_token_address(), + "QORTY" => qorty_token_address(), + _ => panic!("Expected QICK or QORTY ticker"), + }; + let swap_contract_address = qrc20_swap_contract_address(); + let platform = "QTUM"; + let ctx = MmCtxBuilder::new().into_mm_arc(); + let confpath = qtum_conf_path(); + let conf = json!({ + "coin":ticker, + "decimals": 8, + "required_confirmations":0, + "pubtype":120, + "p2shtype":110, + "wiftype":128, + "mm2":1, + "mature_confirmations":500, + "network":"regtest", + "confpath": confpath, + "dust": 72800, + }); + let req = json!({ + "method": "enable", + "swap_contract_address": format!("{:#02x}", swap_contract_address), + }); + let params = Qrc20ActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(qrc20_coin_with_priv_key( + &ctx, + ticker, + platform, + &conf, + ¶ms, + priv_key, + contract_address, + )) + .unwrap(); + + block_on(import_address(&coin)); + (ctx, coin) +} + +/// Get the QRC20 coin config item for MM2 config. +pub fn qrc20_coin_conf_item(ticker: &str) -> Json { + let contract_address = match ticker { + "QICK" => qick_token_address(), + "QORTY" => qorty_token_address(), + _ => panic!("Expected either QICK or QORTY ticker, found {}", ticker), + }; + let contract_address = format!("{contract_address:#02x}"); + + let confpath = qtum_conf_path(); + json!({ + "coin":ticker, + "required_confirmations":1, + "pubtype":120, + "p2shtype":110, + "wiftype":128, + "mature_confirmations":500, + "confpath":confpath, + "network":"regtest", + "protocol":{"type":"QRC20","protocol_data":{"platform":"QTUM","contract_address":contract_address}}}) +} + +/// Fill a QRC20 address with tokens. +pub fn fill_qrc20_address(coin: &Qrc20Coin, amount: BigDecimal, timeout: u64) { + // prevent concurrent fill since daemon RPC returns errors if send_to_address + // is called concurrently (insufficient funds) and it also may return other errors + // if previous transaction is not confirmed yet + let _lock = block_on(QTUM_LOCK.lock()); + let timeout = wait_until_sec(timeout); + let client = match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref client) => client, + UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), + }; + + use futures::TryFutureExt; + let from_addr = get_address_by_label(coin, QTUM_ADDRESS_LABEL); + let to_addr = block_on_f01(coin.my_addr_as_contract_addr().compat()).unwrap(); + let satoshis = sat_from_big_decimal(&amount, coin.as_ref().decimals).expect("!sat_from_big_decimal"); + + let hash = block_on_f01(client.transfer_tokens( + &coin.contract_address, + &from_addr, + to_addr, + satoshis.into(), + coin.as_ref().decimals, + )) + .expect("!transfer_tokens") + .txid; + + let tx_bytes = block_on_f01(client.get_transaction_bytes(&hash)).unwrap(); + log!("{:02x}", tx_bytes); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx_bytes.0, + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); +} + +/// Generate random privkey, create a QRC20 coin and fill its address with the specified balance. +pub fn generate_qrc20_coin_with_random_privkey( + ticker: &str, + qtum_balance: BigDecimal, + qrc20_balance: BigDecimal, +) -> (MmArc, Qrc20Coin, Secp256k1Secret) { + let priv_key = random_secp256k1_secret(); + let (ctx, coin) = qrc20_coin_from_privkey(ticker, priv_key); + + let timeout = 30; // timeout if test takes more than 30 seconds to run + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, qtum_balance, timeout); + fill_qrc20_address(&coin, qrc20_balance, timeout); + (ctx, coin, priv_key) +} + +/// Generate a Qtum coin with random privkey. +pub fn generate_qtum_coin_with_random_privkey( + ticker: &str, + balance: BigDecimal, + txfee: Option, +) -> (MmArc, QtumCoin, [u8; 32]) { + let confpath = qtum_conf_path(); + let conf = json!({ + "coin":ticker, + "decimals":8, + "required_confirmations":0, + "pubtype":120, + "p2shtype": 110, + "wiftype":128, + "txfee": txfee, + "txfee_volatility_percent":0.1, + "mm2":1, + "mature_confirmations":500, + "network":"regtest", + "confpath": confpath, + "dust": 72800, + }); + let req = json!({"method": "enable"}); + let priv_key = random_secp256k1_secret(); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); + + let timeout = 30; // timeout if test takes more than 30 seconds to run + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, balance, timeout); + (ctx, coin, priv_key.take()) +} + +/// Generate a SegWit Qtum coin with random privkey. +pub fn generate_segwit_qtum_coin_with_random_privkey( + ticker: &str, + balance: BigDecimal, + txfee: Option, +) -> (MmArc, QtumCoin, Secp256k1Secret) { + let confpath = qtum_conf_path(); + let conf = json!({ + "coin":ticker, + "decimals":8, + "required_confirmations":0, + "pubtype":120, + "p2shtype": 110, + "wiftype":128, + "segwit":true, + "txfee": txfee, + "txfee_volatility_percent":0.1, + "mm2":1, + "mature_confirmations":500, + "network":"regtest", + "confpath": confpath, + "dust": 72800, + "bech32_hrp":"qcrt", + "address_format": { + "format": "segwit", + }, + }); + let req = json!({"method": "enable"}); + let priv_key = random_secp256k1_secret(); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); + + let timeout = 30; // timeout if test takes more than 30 seconds to run + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, balance, timeout); + (ctx, coin, priv_key) +} + +/// Wait for the `estimatesmartfee` returns no errors. +pub fn wait_for_estimate_smart_fee(timeout: u64) -> Result<(), String> { + enum EstimateSmartFeeState { + Idle, + Ok, + NotAvailable, + } + lazy_static! { + static ref LOCK: Mutex = Mutex::new(EstimateSmartFeeState::Idle); + } + + let state = &mut *LOCK.lock().unwrap(); + match state { + EstimateSmartFeeState::Ok => return Ok(()), + EstimateSmartFeeState::NotAvailable => return ERR!("estimatesmartfee not available"), + EstimateSmartFeeState::Idle => log!("Start wait_for_estimate_smart_fee"), + } + + let priv_key = random_secp256k1_secret(); + let (_ctx, coin) = qrc20_coin_from_privkey("QICK", priv_key); + let timeout = wait_until_sec(timeout); + let client = match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref client) => client, + UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), + }; + while now_sec() < timeout { + if let Ok(res) = block_on_f01(client.estimate_smart_fee(&None, 1)) { + if res.errors.is_empty() { + *state = EstimateSmartFeeState::Ok; + return Ok(()); + } + } + thread::sleep(Duration::from_secs(1)); + } + + *state = EstimateSmartFeeState::NotAvailable; + ERR!("Waited too long for estimate_smart_fee to work") +} + +/// Enable QRC20 coin in MarketMaker. +pub async fn enable_qrc20_native(mm: &MarketMakerIt, coin: &str) -> Json { + let swap_contract_address = qrc20_swap_contract_address(); + + let native = mm + .rpc(&json! ({ + "userpass": mm.userpass, + "method": "enable", + "coin": coin, + "swap_contract_address": format!("{:#02x}", swap_contract_address), + "mm2": 1, + })) + .await + .unwrap(); + assert_eq!(native.0, StatusCode::OK, "'enable' failed: {}", native.1); + json::from_str(&native.1).unwrap() +} + +// ============================================================================= +// Docker node setup +// ============================================================================= + +// ============================================================================= +// QtumDockerOps - Docker ops for Qtum initialization +// ============================================================================= + +use crate::docker_tests::helpers::docker_ops::CoinDockerOps; + +const QRC20_TOKEN_BYTES: &str = "6080604052600860ff16600a0a633b9aca000260005534801561002157600080fd5b50600054600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610c69806100776000396000f3006080604052600436106100a4576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100a9578063095ea7b31461013957806318160ddd1461019e57806323b872dd146101c9578063313ce5671461024e5780635a3b7e421461027f57806370a082311461030f57806395d89b4114610366578063a9059cbb146103f6578063dd62ed3e1461045b575b600080fd5b3480156100b557600080fd5b506100be6104d2565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100fe5780820151818401526020810190506100e3565b50505050905090810190601f16801561012b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561014557600080fd5b50610184600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061050b565b604051808215151515815260200191505060405180910390f35b3480156101aa57600080fd5b506101b36106bb565b6040518082815260200191505060405180910390f35b3480156101d557600080fd5b50610234600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506106c1565b604051808215151515815260200191505060405180910390f35b34801561025a57600080fd5b506102636109a1565b604051808260ff1660ff16815260200191505060405180910390f35b34801561028b57600080fd5b506102946109a6565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156102d45780820151818401526020810190506102b9565b50505050905090810190601f1680156103015780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561031b57600080fd5b50610350600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506109df565b6040518082815260200191505060405180910390f35b34801561037257600080fd5b5061037b6109f7565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103bb5780820151818401526020810190506103a0565b50505050905090810190601f1680156103e85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561040257600080fd5b50610441600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610a30565b604051808215151515815260200191505060405180910390f35b34801561046757600080fd5b506104bc600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610be1565b6040518082815260200191505060405180910390f35b6040805190810160405280600881526020017f515243205445535400000000000000000000000000000000000000000000000081525081565b60008260008173ffffffffffffffffffffffffffffffffffffffff161415151561053457600080fd5b60008314806105bf57506000600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054145b15156105ca57600080fd5b82600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925856040518082815260200191505060405180910390a3600191505092915050565b60005481565b60008360008173ffffffffffffffffffffffffffffffffffffffff16141515156106ea57600080fd5b8360008173ffffffffffffffffffffffffffffffffffffffff161415151561071157600080fd5b610797600260008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c06565b600260008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610860600160008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c06565b600160008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506108ec600160008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c1f565b600160008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508473ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef866040518082815260200191505060405180910390a36001925050509392505050565b600881565b6040805190810160405280600981526020017f546f6b656e20302e31000000000000000000000000000000000000000000000081525081565b60016020528060005260406000206000915090505481565b6040805190810160405280600381526020017f515443000000000000000000000000000000000000000000000000000000000081525081565b60008260008173ffffffffffffffffffffffffffffffffffffffff1614151515610a5957600080fd5b610aa2600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205484610c06565b600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610b2e600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205484610c1f565b600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef856040518082815260200191505060405180910390a3600191505092915050565b6002602052816000526040600020602052806000526040600020600091509150505481565b6000818310151515610c1457fe5b818303905092915050565b6000808284019050838110151515610c3357fe5b80915050929150505600a165627a7a723058207f2e5248b61b80365ea08a0f6d11ac0b47374c4dfd538de76bc2f19591bbbba40029"; +const QRC20_SWAP_CONTRACT_BYTES: &str = "608060405234801561001057600080fd5b50611437806100206000396000f3fe60806040526004361061004a5760003560e01c806302ed292b1461004f5780630716326d146100de578063152cf3af1461017b57806346fc0294146101f65780639b415b2a14610294575b600080fd5b34801561005b57600080fd5b506100dc600480360360a081101561007257600080fd5b81019080803590602001909291908035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610339565b005b3480156100ea57600080fd5b506101176004803603602081101561010157600080fd5b8101908080359060200190929190505050610867565b60405180846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526020018367ffffffffffffffff1667ffffffffffffffff16815260200182600381111561016557fe5b60ff168152602001935050505060405180910390f35b6101f46004803603608081101561019157600080fd5b8101908080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080356bffffffffffffffffffffffff19169060200190929190803567ffffffffffffffff1690602001909291905050506108bf565b005b34801561020257600080fd5b50610292600480360360a081101561021957600080fd5b81019080803590602001909291908035906020019092919080356bffffffffffffffffffffffff19169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610bd9565b005b610337600480360360c08110156102aa57600080fd5b810190808035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080356bffffffffffffffffffffffff19169060200190929190803567ffffffffffffffff169060200190929190505050610fe2565b005b6001600381111561034657fe5b600080878152602001908152602001600020600001601c9054906101000a900460ff16600381111561037457fe5b1461037e57600080fd5b6000600333836003600288604051602001808281526020019150506040516020818303038152906040526040518082805190602001908083835b602083106103db57805182526020820191506020810190506020830392506103b8565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa15801561041d573d6000803e3d6000fd5b5050506040513d602081101561043257600080fd5b8101908080519060200190929190505050604051602001808281526020019150506040516020818303038152906040526040518082805190602001908083835b602083106104955780518252602082019150602081019050602083039250610472565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa1580156104d7573d6000803e3d6000fd5b5050506040515160601b8689604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b602083106105fc57805182526020820191506020810190506020830392506105d9565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa15801561063e573d6000803e3d6000fd5b5050506040515160601b905060008087815260200190815260200160002060000160009054906101000a900460601b6bffffffffffffffffffffffff1916816bffffffffffffffffffffffff19161461069657600080fd5b6002600080888152602001908152602001600020600001601c6101000a81548160ff021916908360038111156106c857fe5b0217905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141561074e573373ffffffffffffffffffffffffffffffffffffffff166108fc869081150290604051600060405180830381858888f19350505050158015610748573d6000803e3d6000fd5b50610820565b60008390508073ffffffffffffffffffffffffffffffffffffffff1663a9059cbb33886040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156107da57600080fd5b505af11580156107ee573d6000803e3d6000fd5b505050506040513d602081101561080457600080fd5b810190808051906020019092919050505061081e57600080fd5b505b7f36c177bcb01c6d568244f05261e2946c8c977fa50822f3fa098c470770ee1f3e8685604051808381526020018281526020019250505060405180910390a1505050505050565b60006020528060005260406000206000915090508060000160009054906101000a900460601b908060000160149054906101000a900467ffffffffffffffff169080600001601c9054906101000a900460ff16905083565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141580156108fc5750600034115b801561094057506000600381111561091057fe5b600080868152602001908152602001600020600001601c9054906101000a900460ff16600381111561093e57fe5b145b61094957600080fd5b60006003843385600034604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b60208310610a6c5780518252602082019150602081019050602083039250610a49565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610aae573d6000803e3d6000fd5b5050506040515160601b90506040518060600160405280826bffffffffffffffffffffffff191681526020018367ffffffffffffffff16815260200160016003811115610af757fe5b81525060008087815260200190815260200160002060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908360601c021790555060208201518160000160146101000a81548167ffffffffffffffff021916908367ffffffffffffffff160217905550604082015181600001601c6101000a81548160ff02191690836003811115610b9357fe5b02179055509050507fccc9c05183599bd3135da606eaaf535daffe256e9de33c048014cffcccd4ad57856040518082815260200191505060405180910390a15050505050565b60016003811115610be657fe5b600080878152602001908152602001600020600001601c9054906101000a900460ff166003811115610c1457fe5b14610c1e57600080fd5b600060038233868689604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b60208310610d405780518252602082019150602081019050602083039250610d1d565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610d82573d6000803e3d6000fd5b5050506040515160601b905060008087815260200190815260200160002060000160009054906101000a900460601b6bffffffffffffffffffffffff1916816bffffffffffffffffffffffff1916148015610e10575060008087815260200190815260200160002060000160149054906101000a900467ffffffffffffffff1667ffffffffffffffff164210155b610e1957600080fd5b6003600080888152602001908152602001600020600001601c6101000a81548160ff02191690836003811115610e4b57fe5b0217905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415610ed1573373ffffffffffffffffffffffffffffffffffffffff166108fc869081150290604051600060405180830381858888f19350505050158015610ecb573d6000803e3d6000fd5b50610fa3565b60008390508073ffffffffffffffffffffffffffffffffffffffff1663a9059cbb33886040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b158015610f5d57600080fd5b505af1158015610f71573d6000803e3d6000fd5b505050506040513d6020811015610f8757600080fd5b8101908080519060200190929190505050610fa157600080fd5b505b7f1797d500133f8e427eb9da9523aa4a25cb40f50ebc7dbda3c7c81778973f35ba866040518082815260200191505060405180910390a1505050505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415801561101f5750600085115b801561106357506000600381111561103357fe5b600080888152602001908152602001600020600001601c9054906101000a900460ff16600381111561106157fe5b145b61106c57600080fd5b60006003843385888a604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b6020831061118e578051825260208201915060208101905060208303925061116b565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa1580156111d0573d6000803e3d6000fd5b5050506040515160601b90506040518060600160405280826bffffffffffffffffffffffff191681526020018367ffffffffffffffff1681526020016001600381111561121957fe5b81525060008089815260200190815260200160002060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908360601c021790555060208201518160000160146101000a81548167ffffffffffffffff021916908367ffffffffffffffff160217905550604082015181600001601c6101000a81548160ff021916908360038111156112b557fe5b021790555090505060008590508073ffffffffffffffffffffffffffffffffffffffff166323b872dd33308a6040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019350505050602060405180830381600087803b15801561137d57600080fd5b505af1158015611391573d6000803e3d6000fd5b505050506040513d60208110156113a757600080fd5b81019080805190602001909291905050506113c157600080fd5b7fccc9c05183599bd3135da606eaaf535daffe256e9de33c048014cffcccd4ad57886040518082815260200191505060405180910390a1505050505050505056fea265627a7a723158208c83db436905afce0b7be1012be64818c49323c12d451fe2ab6bce76ff6421c964736f6c63430005110032"; + +/// Docker ops for Qtum initialization. +/// Used to create contracts and configure the Qtum node. +pub struct QtumDockerOps { + #[allow(dead_code)] + ctx: MmArc, + coin: QtumCoin, +} + +impl CoinDockerOps for QtumDockerOps { + fn rpc_client(&self) -> &UtxoRpcClientEnum { + &self.coin.as_ref().rpc_client + } +} + +impl QtumDockerOps { + pub fn new() -> QtumDockerOps { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let confpath = qtum_conf_path(); + let conf = json!({"coin":"QTUM","decimals":8,"network":"regtest","confpath":confpath}); + let req = json!({ + "method": "enable", + }); + let priv_key = Secp256k1Secret::from("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f"); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); + QtumDockerOps { ctx, coin } + } + + pub fn initialize_contracts(&self) { + let sender = get_address_by_label(&self.coin, QTUM_ADDRESS_LABEL); + set_qick_token_address(self.create_contract(&sender, QRC20_TOKEN_BYTES)); + set_qorty_token_address(self.create_contract(&sender, QRC20_TOKEN_BYTES)); + set_qrc20_swap_contract_address(self.create_contract(&sender, QRC20_SWAP_CONTRACT_BYTES)); + } + + fn create_contract(&self, sender: &str, hexbytes: &str) -> H160Eth { + let bytecode = hex::decode(hexbytes).expect("Hex encoded bytes expected"); + let gas_limit = 2_500_000u64; + let gas_price = BigDecimal::from_str("0.0000004").unwrap(); + + match self.coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref native) => { + let result = block_on_f01(native.create_contract(&bytecode.into(), gas_limit, gas_price, sender)) + .expect("!createcontract"); + result.address.0.into() + }, + UtxoRpcClientEnum::Electrum(_) => panic!("Native client expected"), + } + } +} + +// ============================================================================= +// Docker node setup +// ============================================================================= + +/// Start a Qtum regtest docker node and initialize configuration. +pub fn qtum_docker_node(port: u16) -> DockerNode { + let image = GenericImage::new(QTUM_REGTEST_DOCKER_IMAGE, "latest") + .with_env_var("CLIENTS", "2") + .with_env_var("COIN_RPC_PORT", port.to_string()) + .with_env_var("ADDRESS_LABEL", QTUM_ADDRESS_LABEL) + .with_env_var("FILL_MEMPOOL", "true") + .with_wait_for(WaitFor::message_on_stdout("config is ready")); + let image = RunnableImage::from(image).with_mapped_port((port, port)); + let container = image.start().expect("Failed to start Qtum regtest docker container"); + + let name = "qtum"; + let mut conf_path = temp_dir().join("qtum-regtest"); + std::fs::create_dir_all(&conf_path).unwrap(); + conf_path.push(format!("{name}.conf")); + Command::new("docker") + .arg("cp") + .arg(format!("{}:/data/node_0/{}.conf", container.id(), name)) + .arg(&conf_path) + .status() + .expect("Failed to execute docker command"); + let timeout = wait_until_ms(3000); + loop { + if conf_path.exists() { + break; + }; + assert!(now_ms() < timeout, "Test timed out"); + } + + set_qtum_conf_path(conf_path); + DockerNode { + container, + ticker: name.to_owned(), + port, + } +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/sia.rs b/mm2src/mm2_main/tests/docker_tests/helpers/sia.rs new file mode 100644 index 0000000000..b4bfae4e10 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/sia.rs @@ -0,0 +1,68 @@ +//! Sia helpers for docker tests. +//! +//! This module provides: +//! - Sia docker node configuration and startup +//! - Sia RPC connection parameters + +use super::env::DockerNode; +use testcontainers::core::Mount; +use testcontainers::runners::SyncRunner; +use testcontainers::{core::WaitFor, GenericImage, RunnableImage}; + +// ============================================================================= +// Sia node configuration +// ============================================================================= + +/// SIA daemon RPC connection parameters: (host, port, password) +pub static SIA_RPC_PARAMS: (&str, u16, &str) = ("127.0.0.1", 9980, "password"); + +/// SIA docker image name +pub const SIA_DOCKER_IMAGE: &str = "ghcr.io/siafoundation/walletd"; +/// SIA docker image with tag +pub const SIA_DOCKER_IMAGE_WITH_TAG: &str = "ghcr.io/siafoundation/walletd:latest"; + +// ============================================================================= +// Docker node helpers +// ============================================================================= + +/// Start a Sia docker node for testing. +/// +/// This helper creates the necessary config files and starts the walletd container. +pub fn sia_docker_node(ticker: &'static str, port: u16) -> DockerNode { + use crate::sia_tests::utils::{WALLETD_CONFIG, WALLETD_NETWORK_CONFIG}; + + let config_dir = std::env::temp_dir() + .join(format!( + "sia-docker-tests-temp-{}", + chrono::Local::now().format("%Y-%m-%d_%H-%M-%S-%3f") + )) + .join("walletd_config"); + std::fs::create_dir_all(&config_dir).unwrap(); + + // Write walletd.yml + std::fs::write(config_dir.join("walletd.yml"), WALLETD_CONFIG).expect("failed to write walletd.yml"); + + // Write ci_network.json + std::fs::write(config_dir.join("ci_network.json"), WALLETD_NETWORK_CONFIG) + .expect("failed to write ci_network.json"); + + let image = GenericImage::new(SIA_DOCKER_IMAGE, "latest") + .with_env_var("WALLETD_CONFIG_FILE", "/config/walletd.yml") + .with_wait_for(WaitFor::message_on_stdout("node started")) + .with_mount(Mount::bind_mount( + config_dir.to_str().expect("config path is invalid"), + "/config", + )); + + let args = vec!["-network=/config/ci_network.json".to_string(), "-debug".to_string()]; + let image = RunnableImage::from(image) + .with_mapped_port((port, port)) + .with_args(args); + + let container = image.start().expect("Failed to start Sia docker node"); + DockerNode { + container, + ticker: ticker.into(), + port, + } +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/slp.rs b/mm2src/mm2_main/tests/docker_tests/helpers/slp.rs new file mode 100644 index 0000000000..9b79d25092 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/slp.rs @@ -0,0 +1,310 @@ +//! BCH/SLP helpers for docker tests. +//! +//! This module was extracted from `helpers::utxo`. +//! It provides: +//! - `BchDockerOps` wrapper for the FORSLP node (BCH-like UTXO chain with SLP enabled) +//! - `forslp_docker_node()` to start the FORSLP docker container +//! - `initialize_slp()` to mint/distribute test SLP tokens +//! - Accessors to retrieve a prefilled SLP private key and the token id + +use super::docker_ops::CoinDockerOps; +use super::env::DockerNode; +use chain::TransactionOutput; +use coins::utxo::bch::{bch_coin_with_priv_key, BchActivationRequest, BchCoin}; +use coins::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcClientOps}; +use coins::utxo::slp::{slp_genesis_output, SlpOutput, SlpToken}; +use coins::utxo::utxo_common::send_outputs_from_my_address; +use coins::utxo::{coin_daemon_data_dir, zcash_params_path, UtxoCoinFields, UtxoCommonOps}; +use coins::Transaction; +use coins::{ConfirmPaymentInput, MarketCoinOps}; +use common::executor::Timer; +use common::Future01CompatExt; +use common::{block_on, block_on_f01, now_ms, now_sec, wait_until_ms, wait_until_sec}; +use crypto::Secp256k1Secret; +use keys::{AddressBuilder, KeyPair, NetworkPrefix as CashAddrPrefix}; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_number::BigDecimal; +use primitives::hash::H256; +use script::Builder; +use serde_json::json; +use std::convert::TryFrom; +use std::process::Command; +use std::sync::Mutex; +use testcontainers::core::Mount; +use testcontainers::runners::SyncRunner; +use testcontainers::GenericImage; +use testcontainers::{core::WaitFor, RunnableImage}; +use tokio::sync::Mutex as AsyncMutex; + +// ============================================================================= +// SLP token metadata +// ============================================================================= + +lazy_static! { + /// SLP token ID (genesis tx hash). + pub static ref SLP_TOKEN_ID: Mutex = Mutex::new(H256::default()); + + /// Private keys supplied with 1000 SLP tokens on tests initialization. + /// + /// Due to the SLP protocol limitations only 19 outputs (18 + change) can be sent in one transaction. + pub static ref SLP_TOKEN_OWNERS: Mutex> = Mutex::new(Vec::with_capacity(18)); + + /// Lock for FORSLP funding operations. + static ref FORSLP_LOCK: AsyncMutex<()> = AsyncMutex::new(()); +} + +// ============================================================================= +// Docker image constants +// ============================================================================= + +/// FORSLP docker image (same as UTXO testblockchain). +const FORSLP_DOCKER_IMAGE: &str = "docker.io/gleec/testblockchain"; + +/// FORSLP docker image with tag (used by runner::required_images). +pub const FORSLP_IMAGE_WITH_TAG: &str = "docker.io/gleec/testblockchain:multiarch"; + +// ============================================================================= +// Docker node helpers +// ============================================================================= + +/// Start the FORSLP dockerized BCH/SLP node. +pub fn forslp_docker_node(port: u16) -> DockerNode { + let ticker = "FORSLP"; + let image = GenericImage::new(FORSLP_DOCKER_IMAGE, "multiarch") + .with_mount(Mount::bind_mount( + zcash_params_path().display().to_string(), + "/root/.zcash-params", + )) + .with_env_var("CLIENTS", "2") + .with_env_var("CHAIN", ticker) + .with_env_var("TEST_ADDY", "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF") + .with_env_var("TEST_WIF", "UqqW7f766rADem9heD8vSBvvrdfJb3zg5r8du9rJxPtccjWf7RG9") + .with_env_var( + "TEST_PUBKEY", + "021607076d7a2cb148d542fb9644c04ffc22d2cca752f80755a0402a24c567b17a", + ) + .with_env_var("DAEMON_URL", "http://test:test@127.0.0.1:7000") + .with_env_var("COIN", "Komodo") + .with_env_var("COIN_RPC_PORT", port.to_string()) + .with_wait_for(WaitFor::message_on_stdout("config is ready")); + let image = RunnableImage::from(image).with_mapped_port((port, port)); + let container = image.start().expect("Failed to start FORSLP docker node"); + + let mut conf_path = coin_daemon_data_dir(ticker, true); + std::fs::create_dir_all(&conf_path).unwrap(); + conf_path.push(format!("{ticker}.conf")); + + Command::new("docker") + .arg("cp") + .arg(format!("{}:/data/node_0/{}.conf", container.id(), ticker)) + .arg(&conf_path) + .status() + .expect("Failed to execute docker command"); + + let timeout = wait_until_ms(3000); + loop { + if conf_path.exists() { + break; + }; + assert!(now_ms() < timeout, "Test timed out waiting for config"); + } + + DockerNode { + container, + ticker: ticker.into(), + port, + } +} + +// ============================================================================= +// BCH/SLP funding utilities +// ============================================================================= + +/// Fill a BCH/SLP address with the specified amount. +fn fill_bch_address(coin: &T, address: &str, amount: BigDecimal, timeout: u64) +where + T: MarketCoinOps + AsRef, +{ + block_on(fill_bch_address_async(coin, address, amount, timeout)); +} + +/// Fill a BCH/SLP address with the specified amount (async version). +async fn fill_bch_address_async(coin: &T, address: &str, amount: BigDecimal, timeout: u64) +where + T: MarketCoinOps + AsRef, +{ + let _lock = FORSLP_LOCK.lock().await; + let timeout = wait_until_sec(timeout); + + if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { + client.import_address(address, address, false).compat().await.unwrap(); + let hash = client.send_to_address(address, &amount).compat().await.unwrap(); + let tx_bytes = client.get_transaction_bytes(&hash).compat().await.unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx_bytes.clone().0, + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input) + .compat() + .await + .unwrap(); + log!("{:02x}", tx_bytes); + loop { + let unspents = client + .list_unspent_impl(0, i32::MAX, vec![address.to_string()]) + .compat() + .await + .unwrap(); + if !unspents.is_empty() { + break; + } + assert!(now_sec() < timeout, "Test timed out"); + Timer::sleep(1.0).await; + } + }; +} + +// ============================================================================= +// BCH/SLP docker ops +// ============================================================================= + +/// Docker operations for BCH/SLP coins (FORSLP). +pub struct BchDockerOps { + #[allow(dead_code)] + ctx: MmArc, + coin: BchCoin, +} + +impl BchDockerOps { + /// Create BchDockerOps from ticker. + pub fn from_ticker(ticker: &str) -> BchDockerOps { + let conf = + json!({"coin": ticker,"asset": ticker,"txfee":1000,"network": "regtest","txversion":4,"overwintered":1}); + let req = json!({"method":"enable", "bchd_urls": [], "allow_slp_unsafe_conf": true}); + let priv_key = Secp256k1Secret::from("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f"); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = BchActivationRequest::from_legacy_req(&req).unwrap(); + + let coin = block_on(bch_coin_with_priv_key( + &ctx, + ticker, + &conf, + params, + CashAddrPrefix::SlpTest, + priv_key, + )) + .unwrap(); + BchDockerOps { ctx, coin } + } + + /// Initialize SLP tokens: + /// - Fund node wallet + /// - Create SLP genesis + /// - Distribute tokens to 18 new addresses + /// - Store their privkeys into `SLP_TOKEN_OWNERS` and token id into `SLP_TOKEN_ID` + pub fn initialize_slp(&self) { + fill_bch_address(&self.coin, &self.coin.my_address().unwrap(), 100000.into(), 30); + let mut slp_privkeys = vec![]; + + let slp_genesis_op_ret = slp_genesis_output("ADEXSLP", "ADEXSLP", None, None, 8, None, 1000000_00000000); + let slp_genesis = TransactionOutput { + value: self.coin.as_ref().dust_amount, + script_pubkey: Builder::build_p2pkh(&self.coin.my_public_key().unwrap().address_hash().into()).to_bytes(), + }; + + let mut bch_outputs = vec![slp_genesis_op_ret, slp_genesis]; + let mut slp_outputs = vec![]; + + for _ in 0..18 { + let key_pair = KeyPair::random_compressed(); + let address = AddressBuilder::new( + Default::default(), + Default::default(), + self.coin.as_ref().conf.address_prefixes.clone(), + None, + ) + .as_pkh_from_pk(*key_pair.public()) + .build() + .expect("valid address props"); + + block_on_f01( + self.native_client() + .import_address(&address.to_string(), &address.to_string(), false), + ) + .unwrap(); + + let script_pubkey = Builder::build_p2pkh(&key_pair.public().address_hash().into()); + + bch_outputs.push(TransactionOutput { + value: 1000_00000000, + script_pubkey: script_pubkey.to_bytes(), + }); + + slp_outputs.push(SlpOutput { + amount: 1000_00000000, + script_pubkey: script_pubkey.to_bytes(), + }); + slp_privkeys.push(*key_pair.private_ref()); + } + + let slp_genesis_tx = block_on_f01(send_outputs_from_my_address(self.coin.clone(), bch_outputs)).unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: slp_genesis_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: wait_until_sec(30), + check_every: 1, + }; + block_on_f01(self.coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let adex_slp = SlpToken::new( + 8, + "ADEXSLP".into(), + <&[u8; 32]>::try_from(slp_genesis_tx.tx_hash_as_bytes().as_slice()) + .unwrap() + .into(), + self.coin.clone(), + 1, + ) + .unwrap(); + + let tx = block_on(adex_slp.send_slp_outputs(slp_outputs)).unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: wait_until_sec(30), + check_every: 1, + }; + block_on_f01(self.coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + *SLP_TOKEN_OWNERS.lock().unwrap() = slp_privkeys; + *SLP_TOKEN_ID.lock().unwrap() = <[u8; 32]>::try_from(slp_genesis_tx.tx_hash_as_bytes().as_slice()) + .unwrap() + .into(); + } +} + +impl CoinDockerOps for BchDockerOps { + fn rpc_client(&self) -> &UtxoRpcClientEnum { + &self.coin.as_ref().rpc_client + } +} + +// ============================================================================= +// Public accessors used by tests +// ============================================================================= + +/// Get a prefilled SLP privkey from the pool. +/// +/// Panics if initialization didn't happen (runner must call `setup_slp()`). +pub fn get_prefilled_slp_privkey() -> [u8; 32] { + SLP_TOKEN_OWNERS.lock().unwrap().remove(0) +} + +/// Get the SLP token ID as hex string. +pub fn get_slp_token_id() -> String { + hex::encode(SLP_TOKEN_ID.lock().unwrap().as_slice()) +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/swap.rs b/mm2src/mm2_main/tests/docker_tests/helpers/swap.rs new file mode 100644 index 0000000000..1c6413891b --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/swap.rs @@ -0,0 +1,480 @@ +//! Swap orchestration helpers for docker tests. +//! +//! This module provides high-level cross-chain atomic swap test scenarios. +//! For chain-specific helpers, import directly from the other `helpers` submodules. +//! +//! ## Feature gating +//! +//! This module is compiled for all docker tests (gated by `run-docker-tests`), but +//! chain-specific imports and code blocks are gated by their respective feature flags: +//! +//! - ETH: `docker-tests-eth`, `docker-tests-ordermatch` +//! - QRC20: `docker-tests-qrc20` +//! - UTXO: `docker-tests-swaps`, `docker-tests-ordermatch`, `docker-tests-watchers`, `docker-tests-qrc20`, `docker-tests-sia`, `docker-tests-slp` +//! - SLP: `docker-tests-slp` + +use common::block_on; +use crypto::privkey::key_pair_from_secret; +use mm2_test_helpers::for_tests::{ + check_my_swap_status, check_recent_swaps, mm_dump, wait_check_stats_swap_status, MarketMakerIt, +}; +use serde_json::{json, Value as Json}; +use std::thread; +use std::time::Duration; + +use crypto::Secp256k1Secret; + +// random_secp256k1_secret - used by non-SLP swap paths +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-qrc20", + feature = "docker-tests-eth", + feature = "docker-tests-sia" +))] +use super::env::random_secp256k1_secret; + +// SET_BURN_PUBKEY_TO_ALICE - used by trade_base_rel +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-qrc20", + feature = "docker-tests-slp", + feature = "docker-tests-eth", + feature = "docker-tests-integration" +))] +use super::env::SET_BURN_PUBKEY_TO_ALICE; + +/// Timeout in seconds for wallet funding operations during test setup. +#[cfg(any( + feature = "docker-tests-qrc20", + feature = "docker-tests-swaps", + feature = "docker-tests-sia" +))] +const WALLET_FUNDING_TIMEOUT_SEC: u64 = 30; + +// ETH imports +#[cfg(feature = "docker-tests-eth")] +use super::eth::{erc20_contract_checksum, fill_eth_erc20_with_private_key, swap_contract_checksum, GETH_RPC_URL}; +#[cfg(feature = "docker-tests-eth")] +use mm2_test_helpers::for_tests::{enable_eth_coin, erc20_dev_conf, eth_dev_conf}; + +// QRC20 imports +#[cfg(feature = "docker-tests-qrc20")] +use super::qrc20::{ + enable_qrc20_native, fill_qrc20_address, generate_segwit_qtum_coin_with_random_privkey, qrc20_coin_conf_item, + qrc20_coin_from_privkey, qtum_conf_path, wait_for_estimate_smart_fee, +}; +#[cfg(feature = "docker-tests-qrc20")] +use super::utxo::fill_address as fill_utxo_address_qrc20; +#[cfg(feature = "docker-tests-qrc20")] +use coins::MarketCoinOps; +#[cfg(feature = "docker-tests-qrc20")] +use mm2_test_helpers::for_tests::enable_native as enable_native_qrc20; + +// UTXO imports (non-QRC20 paths) +#[cfg(all( + any(feature = "docker-tests-swaps", feature = "docker-tests-sia"), + not(feature = "docker-tests-qrc20") +))] +use super::utxo::{fill_address, utxo_coin_from_privkey}; +#[cfg(all( + any(feature = "docker-tests-swaps", feature = "docker-tests-sia"), + not(feature = "docker-tests-qrc20") +))] +use coins::MarketCoinOps; +#[cfg(all( + any(feature = "docker-tests-swaps", feature = "docker-tests-sia"), + not(feature = "docker-tests-qrc20") +))] +use mm2_test_helpers::for_tests::enable_native; + +// UTXO imports (QRC20 path - already imports fill_address as fill_utxo_address_qrc20) +#[cfg(feature = "docker-tests-qrc20")] +use super::utxo::utxo_coin_from_privkey as utxo_coin_from_privkey_qrc20; + +// SLP imports +#[cfg(feature = "docker-tests-slp")] +use super::slp::{get_prefilled_slp_privkey, get_slp_token_id}; +#[cfg(feature = "docker-tests-slp")] +use mm2_test_helpers::for_tests::{enable_native as enable_native_slp, enable_native_bch}; + +// ============================================================================= +// Cross-chain swap test scenarios +// ============================================================================= + +/// End-to-end atomic swap test between two coins. +/// +/// This function: +/// 1. Generates and funds wallets for both maker (base) and taker (rel) coins +/// 2. Starts two MarketMaker instances (Bob as maker, Alice as taker) +/// 3. Enables all required coins on both instances +/// 4. Places a setprice order and matches with a buy order +/// 5. Waits for swap completion and verifies both sides +/// +/// ## Feature requirements +/// +/// Different coin pairs require different feature flags: +/// - ETH/ERC20DEV: `docker-tests-eth` or `docker-tests-ordermatch` +/// - QTUM/QICK/QORTY: `docker-tests-qrc20` +/// - MYCOIN/MYCOIN1: `docker-tests-swaps`, `docker-tests-ordermatch`, `docker-tests-watchers`, `docker-tests-qrc20`, `docker-tests-sia` +/// - FORSLP/ADEXSLP: `docker-tests-slp` +pub fn trade_base_rel((base, rel): (&str, &str)) { + /// Generate a wallet with the random private key and fill the wallet with funds. + fn generate_and_fill_priv_key(ticker: &str) -> Secp256k1Secret { + match ticker { + #[cfg(feature = "docker-tests-qrc20")] + "QTUM" => { + wait_for_estimate_smart_fee(WALLET_FUNDING_TIMEOUT_SEC).expect("!wait_for_estimate_smart_fee"); + let (_ctx, _coin, priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", 10.into(), Some(0)); + priv_key + }, + #[cfg(feature = "docker-tests-qrc20")] + "QICK" | "QORTY" => { + let priv_key = random_secp256k1_secret(); + let (_ctx, coin) = qrc20_coin_from_privkey(ticker, priv_key); + let my_address = coin.my_address().expect("!my_address"); + fill_utxo_address_qrc20(&coin, &my_address, 10.into(), WALLET_FUNDING_TIMEOUT_SEC); + fill_qrc20_address(&coin, 10.into(), WALLET_FUNDING_TIMEOUT_SEC); + priv_key + }, + #[cfg(feature = "docker-tests-qrc20")] + "MYCOIN" | "MYCOIN1" => { + let priv_key = random_secp256k1_secret(); + let (_ctx, coin) = utxo_coin_from_privkey_qrc20(ticker, priv_key); + let my_address = coin.my_address().expect("!my_address"); + fill_utxo_address_qrc20(&coin, &my_address, 10.into(), WALLET_FUNDING_TIMEOUT_SEC); + priv_key + }, + #[cfg(all( + any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-sia" + ), + not(feature = "docker-tests-qrc20") + ))] + "MYCOIN" | "MYCOIN1" => { + let priv_key = random_secp256k1_secret(); + let (_ctx, coin) = utxo_coin_from_privkey(ticker, priv_key); + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, 10.into(), WALLET_FUNDING_TIMEOUT_SEC); + priv_key + }, + #[cfg(feature = "docker-tests-slp")] + "ADEXSLP" | "FORSLP" => Secp256k1Secret::from(get_prefilled_slp_privkey()), + #[cfg(feature = "docker-tests-eth")] + "ETH" | "ERC20DEV" => { + let priv_key = random_secp256k1_secret(); + fill_eth_erc20_with_private_key(priv_key); + priv_key + }, + _ => panic!( + "Unsupported ticker: {}. Check that the required feature flag is enabled. \ + ETH/ERC20DEV: docker-tests-eth or docker-tests-ordermatch. \ + QTUM/QICK/QORTY/MYCOIN: docker-tests-qrc20. \ + MYCOIN/MYCOIN1: docker-tests-swaps, docker-tests-ordermatch, docker-tests-watchers, docker-tests-sia. \ + FORSLP/ADEXSLP: docker-tests-slp.", + ticker + ), + } + } + + let bob_priv_key = generate_and_fill_priv_key(base); + let alice_priv_key = generate_and_fill_priv_key(rel); + let alice_pubkey_str = hex::encode( + key_pair_from_secret(&alice_priv_key) + .expect("valid test key pair") + .public() + .to_vec(), + ); + + let mut envs = vec![]; + if SET_BURN_PUBKEY_TO_ALICE.get() { + envs.push(("TEST_BURN_ADDR_RAW_PUBKEY", alice_pubkey_str.as_str())); + } + + // Build coins config based on enabled features + let mut coins_vec: Vec = Vec::new(); + + #[cfg(feature = "docker-tests-eth")] + { + coins_vec.push(eth_dev_conf()); + coins_vec.push(erc20_dev_conf(&erc20_contract_checksum())); + } + + #[cfg(feature = "docker-tests-qrc20")] + { + let confpath = qtum_conf_path(); + coins_vec.push(qrc20_coin_conf_item("QICK")); + coins_vec.push(qrc20_coin_conf_item("QORTY")); + coins_vec.push(json!({ + "coin": "QTUM", "asset": "QTUM", "required_confirmations": 0, "decimals": 8, + "pubtype": 120, "p2shtype": 110, "wiftype": 128, "segwit": true, "txfee": 0, + "txfee_volatility_percent": 0.1, "dust": 72800, "mm2": 1, "network": "regtest", + "confpath": confpath, "protocol": {"type": "UTXO"}, "bech32_hrp": "qcrt", + "address_format": {"format": "segwit"} + })); + coins_vec.push(json!({ + "coin": "MYCOIN", "asset": "MYCOIN", "required_confirmations": 0, + "txversion": 4, "overwintered": 1, "txfee": 1000, "protocol": {"type": "UTXO"} + })); + coins_vec.push(json!({ + "coin": "MYCOIN1", "asset": "MYCOIN1", "required_confirmations": 0, + "txversion": 4, "overwintered": 1, "txfee": 1000, "protocol": {"type": "UTXO"} + })); + } + + #[cfg(all( + any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-sia" + ), + not(feature = "docker-tests-qrc20") + ))] + { + coins_vec.push(json!({ + "coin": "MYCOIN", "asset": "MYCOIN", "required_confirmations": 0, + "txversion": 4, "overwintered": 1, "txfee": 1000, "protocol": {"type": "UTXO"} + })); + coins_vec.push(json!({ + "coin": "MYCOIN1", "asset": "MYCOIN1", "required_confirmations": 0, + "txversion": 4, "overwintered": 1, "txfee": 1000, "protocol": {"type": "UTXO"} + })); + } + + #[cfg(feature = "docker-tests-slp")] + { + coins_vec.push(json!({ + "coin": "FORSLP", "asset": "FORSLP", "required_confirmations": 0, + "txversion": 4, "overwintered": 1, "txfee": 1000, + "protocol": {"type": "BCH", "protocol_data": {"slp_prefix": "slptest"}} + })); + coins_vec.push(json!({ + "coin": "ADEXSLP", + "protocol": {"type": "SLPTOKEN", "protocol_data": {"decimals": 8, "token_id": get_slp_token_id(), "platform": "FORSLP"}} + })); + } + + let coins = Json::Array(coins_vec); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + json! ({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + envs.as_slice(), + )) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + json! ({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + envs.as_slice(), + )) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + // Enable coins for Bob based on enabled features + #[cfg(feature = "docker-tests-qrc20")] + { + log!("{:?}", block_on(enable_qrc20_native(&mm_bob, "QICK"))); + log!("{:?}", block_on(enable_qrc20_native(&mm_bob, "QORTY"))); + log!("{:?}", block_on(enable_native_qrc20(&mm_bob, "QTUM", &[], None))); + log!("{:?}", block_on(enable_native_qrc20(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native_qrc20(&mm_bob, "MYCOIN1", &[], None))); + } + + #[cfg(all( + any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-sia" + ), + not(feature = "docker-tests-qrc20") + ))] + { + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + } + + #[cfg(feature = "docker-tests-slp")] + { + log!("{:?}", block_on(enable_native_bch(&mm_bob, "FORSLP", &[]))); + log!("{:?}", block_on(enable_native_slp(&mm_bob, "ADEXSLP", &[], None))); + } + + #[cfg(feature = "docker-tests-eth")] + { + let swap_contract = swap_contract_checksum(); + log!( + "{:?}", + block_on(enable_eth_coin( + &mm_bob, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + )) + ); + log!( + "{:?}", + block_on(enable_eth_coin( + &mm_bob, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + )) + ); + } + + // Enable coins for Alice based on enabled features + #[cfg(feature = "docker-tests-qrc20")] + { + log!("{:?}", block_on(enable_qrc20_native(&mm_alice, "QICK"))); + log!("{:?}", block_on(enable_qrc20_native(&mm_alice, "QORTY"))); + log!("{:?}", block_on(enable_native_qrc20(&mm_alice, "QTUM", &[], None))); + log!("{:?}", block_on(enable_native_qrc20(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native_qrc20(&mm_alice, "MYCOIN1", &[], None))); + } + + #[cfg(all( + any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-sia" + ), + not(feature = "docker-tests-qrc20") + ))] + { + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + } + + #[cfg(feature = "docker-tests-slp")] + { + log!("{:?}", block_on(enable_native_bch(&mm_alice, "FORSLP", &[]))); + log!("{:?}", block_on(enable_native_slp(&mm_alice, "ADEXSLP", &[], None))); + } + + #[cfg(feature = "docker-tests-eth")] + { + let swap_contract = swap_contract_checksum(); + log!( + "{:?}", + block_on(enable_eth_coin( + &mm_alice, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + )) + ); + log!( + "{:?}", + block_on(enable_eth_coin( + &mm_alice, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false + )) + ); + } + + let rc = block_on(mm_bob.rpc(&json! ({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": 1, + "volume": "3", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + thread::sleep(Duration::from_secs(1)); + + log!("Issue alice {}/{} buy request", base, rel); + let rc = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": base, + "rel": rel, + "price": 1, + "volume": "2", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + let buy_json: Json = serde_json::from_str(&rc.1).unwrap(); + let uuid = buy_json["result"]["uuid"].as_str().unwrap().to_owned(); + + // ensure the swaps are started + block_on(mm_bob.wait_for_log(22., |log| { + log.contains(&format!("Entering the maker_swap_loop {base}/{rel}")) + })) + .unwrap(); + block_on(mm_alice.wait_for_log(22., |log| { + log.contains(&format!("Entering the taker_swap_loop {base}/{rel}")) + })) + .unwrap(); + + // ensure the swaps are finished + block_on(mm_bob.wait_for_log(600., |log| log.contains(&format!("[swap uuid={uuid}] Finished")))).unwrap(); + block_on(mm_alice.wait_for_log(600., |log| log.contains(&format!("[swap uuid={uuid}] Finished")))).unwrap(); + + log!("Checking alice/taker status.."); + block_on(check_my_swap_status( + &mm_alice, + &uuid, + "2".parse().unwrap(), + "2".parse().unwrap(), + )); + + log!("Checking bob/maker status.."); + block_on(check_my_swap_status( + &mm_bob, + &uuid, + "2".parse().unwrap(), + "2".parse().unwrap(), + )); + + log!("Checking alice status.."); + block_on(wait_check_stats_swap_status(&mm_alice, &uuid, 240)); + + log!("Checking bob status.."); + block_on(wait_check_stats_swap_status(&mm_bob, &uuid, 240)); + + log!("Checking alice recent swaps.."); + block_on(check_recent_swaps(&mm_alice, 1)); + log!("Checking bob recent swaps.."); + block_on(check_recent_swaps(&mm_bob, 1)); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/tendermint.rs b/mm2src/mm2_main/tests/docker_tests/helpers/tendermint.rs new file mode 100644 index 0000000000..2f21209130 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/tendermint.rs @@ -0,0 +1,161 @@ +//! Tendermint/Cosmos helpers for docker tests. +//! +//! This module provides: +//! - Docker node helpers for Nucleus, Atom, and IBC relayer +//! - IBC channel preparation utilities + +use crate::docker_tests::helpers::env::{resolve_compose_container_id, DockerNode, KDF_IBC_RELAYER_SERVICE}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::Duration; +use testcontainers::core::Mount; +use testcontainers::runners::SyncRunner; +use testcontainers::{GenericImage, RunnableImage}; + +// ============================================================================= +// Docker image constants +// ============================================================================= + +/// Nucleus docker image +pub const NUCLEUS_IMAGE: &str = "docker.io/gleec/nucleusd"; +/// Atom (Gaia) docker image with tag +pub const ATOM_IMAGE_WITH_TAG: &str = "docker.io/gleec/gaiad:kdf-ci"; +/// IBC relayer docker image with tag +pub const IBC_RELAYER_IMAGE_WITH_TAG: &str = "docker.io/gleec/ibc-relayer:kdf-ci"; + +// ============================================================================= +// Docker node helpers +// ============================================================================= + +/// Start a Nucleus docker node for testing. +pub fn nucleus_node(runtime_dir: PathBuf) -> DockerNode { + let nucleus_node_runtime_dir = runtime_dir.join("nucleus-testnet-data"); + assert!(nucleus_node_runtime_dir.exists()); + + let image = GenericImage::new(NUCLEUS_IMAGE, "latest").with_mount(Mount::bind_mount( + nucleus_node_runtime_dir.to_str().unwrap(), + "/root/.nucleus", + )); + let image = RunnableImage::from((image, vec![])).with_network("host"); + let container = image.start().expect("Failed to start Nucleus docker node"); + + DockerNode { + container, + ticker: "NUCLEUS-TEST".to_owned(), + port: Default::default(), // This doesn't need to be the correct value as we are using the host network. + } +} + +/// Start an Atom (Gaia) docker node for testing. +pub fn atom_node(runtime_dir: PathBuf) -> DockerNode { + let atom_node_runtime_dir = runtime_dir.join("atom-testnet-data"); + assert!(atom_node_runtime_dir.exists()); + + let (image, tag) = ATOM_IMAGE_WITH_TAG.rsplit_once(':').unwrap(); + let image = GenericImage::new(image, tag).with_mount(Mount::bind_mount( + atom_node_runtime_dir.to_str().unwrap(), + "/root/.gaia", + )); + let image = RunnableImage::from((image, vec![])).with_network("host"); + let container = image.start().expect("Failed to start Atom docker node"); + + DockerNode { + container, + ticker: "ATOM-TEST".to_owned(), + port: Default::default(), // This doesn't need to be the correct value as we are using the host network. + } +} + +/// Start an IBC relayer docker node for testing. +pub fn ibc_relayer_node(runtime_dir: PathBuf) -> DockerNode { + let relayer_node_runtime_dir = runtime_dir.join("ibc-relayer-data"); + assert!(relayer_node_runtime_dir.exists()); + + let (image, tag) = IBC_RELAYER_IMAGE_WITH_TAG.rsplit_once(':').unwrap(); + let image = GenericImage::new(image, tag).with_mount(Mount::bind_mount( + relayer_node_runtime_dir.to_str().unwrap(), + "/root/.relayer", + )); + let image = RunnableImage::from((image, vec![])).with_network("host"); + let container = image.start().expect("Failed to start IBC Relayer docker node"); + + DockerNode { + container, + ticker: Default::default(), // This isn't an asset node. + port: Default::default(), // This doesn't need to be the correct value as we are using the host network. + } +} + +// ============================================================================= +// IBC utilities +// ============================================================================= + +/// Prepare IBC channels between Nucleus and Atom. +pub fn prepare_ibc_channels(container_id: &str) { + let exec = |args: &[&str]| { + Command::new("docker") + .args(["exec", container_id]) + .args(args) + .output() + .unwrap(); + }; + + exec(&["rly", "transact", "clients", "nucleus-atom", "--override"]); + // It takes a couple of seconds for nodes to get into the right state after updating clients. + // Wait for 5 just to make sure. + thread::sleep(Duration::from_secs(5)); + + exec(&["rly", "transact", "link", "nucleus-atom"]); +} + +/// Wait until the IBC relayer container is ready. +pub fn wait_until_relayer_container_is_ready(container_id: &str) { + const Q_RESULT: &str = "0: nucleus-atom -> chns(✔) clnts(✔) conn(✔) (nucleus-testnet<>cosmoshub-testnet)"; + + let mut attempts = 0; + loop { + let mut docker = Command::new("docker"); + docker.arg("exec").arg(container_id).args(["rly", "paths", "list"]); + + log!("Running <<{docker:?}>>."); + + let output = docker.stderr(Stdio::inherit()).output().unwrap(); + let output = String::from_utf8(output.stdout).unwrap(); + let output = output.trim(); + + if output == Q_RESULT { + break; + } + attempts += 1; + + log!("Expected output {Q_RESULT}, received {output}."); + if attempts > 10 { + panic!("Reached max attempts for <<{:?}>>.", docker); + } else { + log!("Asking for relayer node status again.."); + } + + thread::sleep(Duration::from_secs(2)); + } +} + +// ============================================================================= +// Compose mode utilities +// ============================================================================= + +/// Prepare IBC channels for compose mode. +/// +/// Resolves the IBC relayer container ID from docker-compose and prepares channels. +pub fn prepare_ibc_channels_compose() { + let container_id = resolve_compose_container_id(KDF_IBC_RELAYER_SERVICE); + prepare_ibc_channels(&container_id); +} + +/// Wait for IBC relayer to be ready in compose mode. +/// +/// Resolves the IBC relayer container ID from docker-compose and waits for readiness. +pub fn wait_until_relayer_container_is_ready_compose() { + let container_id = resolve_compose_container_id(KDF_IBC_RELAYER_SERVICE); + wait_until_relayer_container_is_ready(&container_id); +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/utxo.rs b/mm2src/mm2_main/tests/docker_tests/helpers/utxo.rs new file mode 100644 index 0000000000..e6c69f109b --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/utxo.rs @@ -0,0 +1,335 @@ +//! UTXO coin helpers for docker tests. +//! +//! This module provides: +//! - UTXO asset docker node helpers (MYCOIN, MYCOIN1) +//! - Coin creation and funding utilities +//! +//! Note: BCH/SLP helpers are in the separate `slp` module. + +// ============================================================================= +// Imports +// ============================================================================= + +use crate::docker_tests::helpers::docker_ops::CoinDockerOps; +use crate::docker_tests::helpers::env::DockerNode; +use coins::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcClientOps}; +use coins::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; +use coins::utxo::{coin_daemon_data_dir, zcash_params_path, UtxoActivationParams, UtxoCoinFields}; +use coins::{ConfirmPaymentInput, MarketCoinOps}; +use common::executor::Timer; +use common::Future01CompatExt; +use common::{block_on, now_ms, now_sec, wait_until_ms, wait_until_sec}; +use crypto::Secp256k1Secret; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_number::BigDecimal; +use serde_json::json; +use std::process::Command; +use testcontainers::core::Mount; +use testcontainers::runners::SyncRunner; +use testcontainers::GenericImage; +use testcontainers::{core::WaitFor, RunnableImage}; +use tokio::sync::Mutex as AsyncMutex; + +// rmd160_from_priv imports (only for ordermatch/swaps-utxo) +#[cfg(any(feature = "docker-tests-ordermatch", feature = "docker-tests-swaps"))] +use bitcrypto::dhash160; +#[cfg(any(feature = "docker-tests-ordermatch", feature = "docker-tests-swaps"))] +use primitives::hash::H160; + +// random_secp256k1_secret import (only for features using generate_utxo_coin_with_random_privkey) +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers" +))] +use crate::docker_tests::helpers::env::random_secp256k1_secret; + +// ============================================================================= +// Funding Locks +// ============================================================================= + +lazy_static! { + /// Lock for MYCOIN funding operations + pub static ref MYCOIN_LOCK: AsyncMutex<()> = AsyncMutex::new(()); + + /// Lock for MYCOIN1 funding operations + pub static ref MYCOIN1_LOCK: AsyncMutex<()> = AsyncMutex::new(()); + + /// Lock for Qtum/QRC20 funding operations. + /// Shared by QTUM, QICK, and QORTY coins since they all run on the same Qtum node. + pub static ref QTUM_LOCK: AsyncMutex<()> = AsyncMutex::new(()); +} + +/// Get the appropriate funding lock for a given ticker. +fn get_funding_lock(ticker: &str) -> &'static AsyncMutex<()> { + match ticker { + "MYCOIN" => &MYCOIN_LOCK, + "MYCOIN1" => &MYCOIN1_LOCK, + "QTUM" | "QICK" | "QORTY" => &QTUM_LOCK, + _ => panic!("No funding lock defined for ticker: {}", ticker), + } +} + +// ============================================================================= +// Docker image constants +// ============================================================================= + +/// UTXO asset docker image +pub const UTXO_ASSET_DOCKER_IMAGE: &str = "docker.io/gleec/testblockchain"; +/// UTXO asset docker image with tag +pub const UTXO_ASSET_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/gleec/testblockchain:multiarch"; + +// ============================================================================= +// Ticker constants (UTXO asset features only) +// ============================================================================= + +/// Ticker of MYCOIN dockerized blockchain. +#[cfg(feature = "docker-tests-swaps")] +pub const MYCOIN: &str = "MYCOIN"; + +/// Ticker of MYCOIN1 dockerized blockchain. +#[cfg(feature = "docker-tests-swaps")] +pub const MYCOIN1: &str = "MYCOIN1"; + +// ============================================================================= +// UtxoAssetDockerOps +// ============================================================================= + +/// Docker operations for standard UTXO assets (MYCOIN, MYCOIN1). +pub struct UtxoAssetDockerOps { + #[allow(dead_code)] + ctx: MmArc, + coin: UtxoStandardCoin, +} + +impl CoinDockerOps for UtxoAssetDockerOps { + fn rpc_client(&self) -> &UtxoRpcClientEnum { + &self.coin.as_ref().rpc_client + } +} + +impl UtxoAssetDockerOps { + /// Create UtxoAssetDockerOps from ticker. + pub fn from_ticker(ticker: &str) -> UtxoAssetDockerOps { + let conf = json!({"coin": ticker, "asset": ticker, "txfee": 1000, "network": "regtest"}); + let req = json!({"method":"enable"}); + let priv_key = Secp256k1Secret::from("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f"); + let ctx = MmCtxBuilder::new().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); + UtxoAssetDockerOps { ctx, coin } + } +} + +// ============================================================================= +// Docker node helpers +// ============================================================================= + +/// Start a UTXO asset docker node. +pub fn utxo_asset_docker_node(ticker: &'static str, port: u16) -> DockerNode { + let image = GenericImage::new(UTXO_ASSET_DOCKER_IMAGE, "multiarch") + .with_mount(Mount::bind_mount( + zcash_params_path().display().to_string(), + "/root/.zcash-params", + )) + .with_env_var("CLIENTS", "2") + .with_env_var("CHAIN", ticker) + .with_env_var("TEST_ADDY", "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF") + .with_env_var("TEST_WIF", "UqqW7f766rADem9heD8vSBvvrdfJb3zg5r8du9rJxPtccjWf7RG9") + .with_env_var( + "TEST_PUBKEY", + "021607076d7a2cb148d542fb9644c04ffc22d2cca752f80755a0402a24c567b17a", + ) + .with_env_var("DAEMON_URL", "http://test:test@127.0.0.1:7000") + .with_env_var("COIN", "Komodo") + .with_env_var("COIN_RPC_PORT", port.to_string()) + .with_wait_for(WaitFor::message_on_stdout("config is ready")); + let image = RunnableImage::from(image).with_mapped_port((port, port)); + let container = image.start().expect("Failed to start UTXO asset docker node"); + let mut conf_path = coin_daemon_data_dir(ticker, true); + std::fs::create_dir_all(&conf_path).unwrap(); + conf_path.push(format!("{ticker}.conf")); + Command::new("docker") + .arg("cp") + .arg(format!("{}:/data/node_0/{}.conf", container.id(), ticker)) + .arg(&conf_path) + .status() + .expect("Failed to execute docker command"); + let timeout = wait_until_ms(3000); + loop { + if conf_path.exists() { + break; + }; + assert!(now_ms() < timeout, "Test timed out"); + } + DockerNode { + container, + ticker: ticker.into(), + port, + } +} + +// ============================================================================= +// Coin creation and funding utilities +// ============================================================================= + +/// Compute RIPEMD160(SHA256(pubkey)) from a private key. +#[cfg(any(feature = "docker-tests-ordermatch", feature = "docker-tests-swaps"))] +pub fn rmd160_from_priv(privkey: Secp256k1Secret) -> H160 { + use secp256k1::{PublicKey, Secp256k1, SecretKey}; + let secret = SecretKey::from_slice(privkey.as_slice()).unwrap(); + let public = PublicKey::from_secret_key(&Secp256k1::new(), &secret); + dhash160(&public.serialize()) +} + +/// Import an address to the coin's wallet. +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-integration" +))] +pub async fn import_address(coin: &T) +where + T: MarketCoinOps + AsRef, +{ + let mutex = get_funding_lock(coin.ticker()); + let _lock = mutex.lock().await; + + match coin.as_ref().rpc_client { + UtxoRpcClientEnum::Native(ref native) => { + let my_address = coin.my_address().unwrap(); + native + .import_address(&my_address, &my_address, false) + .compat() + .await + .unwrap(); + }, + UtxoRpcClientEnum::Electrum(_) => panic!("Expected NativeClient"), + } +} + +/// Build asset `UtxoStandardCoin` from ticker and privkey without filling the balance. +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-integration" +))] +pub fn utxo_coin_from_privkey(ticker: &str, priv_key: Secp256k1Secret) -> (MmArc, UtxoStandardCoin) { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let conf = json!({"coin":ticker,"asset":ticker,"txversion":4,"overwintered":1,"txfee":1000,"network":"regtest"}); + let req = json!({"method":"enable"}); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, priv_key)).unwrap(); + block_on(import_address(&coin)); + (ctx, coin) +} + +/// Create a UTXO coin for the given privkey and fill its address with the specified balance. +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-integration" +))] +pub fn generate_utxo_coin_with_privkey(ticker: &str, balance: BigDecimal, priv_key: Secp256k1Secret) { + let (_, coin) = utxo_coin_from_privkey(ticker, priv_key); + let timeout = 30; // timeout if test takes more than 30 seconds to run + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, balance, timeout); +} + +/// Fund a UTXO address with the specified balance (async version). +/// Only used by Sia tests which need async funding. +#[cfg(feature = "docker-tests-sia")] +pub async fn fund_privkey_utxo(ticker: &str, balance: BigDecimal, priv_key: &Secp256k1Secret) { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let conf = json!({"coin":ticker,"asset":ticker,"txversion":4,"overwintered":1,"txfee":1000,"network":"regtest"}); + let req = json!({"method":"enable"}); + let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); + let coin = utxo_standard_coin_with_priv_key(&ctx, ticker, &conf, ¶ms, *priv_key) + .await + .unwrap(); + let my_address = coin.my_address().expect("!my_address"); + fill_address_async(&coin, &my_address, balance, 30).await; +} + +/// Generate random privkey, create a UTXO coin and fill its address with the specified balance. +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers" +))] +pub fn generate_utxo_coin_with_random_privkey( + ticker: &str, + balance: BigDecimal, +) -> (MmArc, UtxoStandardCoin, Secp256k1Secret) { + let priv_key = random_secp256k1_secret(); + let (ctx, coin) = utxo_coin_from_privkey(ticker, priv_key); + let timeout = 30; // timeout if test takes more than 30 seconds to run + let my_address = coin.my_address().expect("!my_address"); + fill_address(&coin, &my_address, balance, timeout); + (ctx, coin, priv_key) +} + +/// Fill address with the specified amount (synchronous wrapper). +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-integration" +))] +pub fn fill_address(coin: &T, address: &str, amount: BigDecimal, timeout: u64) +where + T: MarketCoinOps + AsRef, +{ + block_on(fill_address_async(coin, address, amount, timeout)); +} + +/// Fill address with the specified amount (async version). +pub async fn fill_address_async(coin: &T, address: &str, amount: BigDecimal, timeout: u64) +where + T: MarketCoinOps + AsRef, +{ + // prevent concurrent fill since daemon RPC returns errors if send_to_address + // is called concurrently (insufficient funds) and it also may return other errors + // if previous transaction is not confirmed yet + let mutex = get_funding_lock(coin.ticker()); + let _lock = mutex.lock().await; + let timeout = wait_until_sec(timeout); + + if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { + client.import_address(address, address, false).compat().await.unwrap(); + let hash = client.send_to_address(address, &amount).compat().await.unwrap(); + let tx_bytes = client.get_transaction_bytes(&hash).compat().await.unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx_bytes.clone().0, + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input) + .compat() + .await + .unwrap(); + log!("{:02x}", tx_bytes); + loop { + let unspents = client + .list_unspent_impl(0, i32::MAX, vec![address.to_string()]) + .compat() + .await + .unwrap(); + if !unspents.is_empty() { + break; + } + assert!(now_sec() < timeout, "Test timed out"); + Timer::sleep(1.0).await; + } + }; +} diff --git a/mm2src/mm2_main/tests/docker_tests/helpers/zcoin.rs b/mm2src/mm2_main/tests/docker_tests/helpers/zcoin.rs new file mode 100644 index 0000000000..6aef52af4e --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/helpers/zcoin.rs @@ -0,0 +1,153 @@ +//! ZCoin helpers for docker tests. +//! +//! This module provides: +//! - ZCoin docker operations (ZCoinAssetDockerOps) +//! - Zombie asset docker node helpers +//! - ZCoin creation utilities + +use crate::docker_tests::helpers::docker_ops::CoinDockerOps; +use crate::docker_tests::helpers::env::DockerNode; +use coins::utxo::rpc_clients::UtxoRpcClientEnum; +use coins::utxo::{coin_daemon_data_dir, zcash_params_path}; +use coins::z_coin::ZCoin; +use common::{block_on, now_ms, wait_until_ms}; +use mm2_core::mm_ctx::MmArc; +use serde_json::json; +use std::process::Command; +use std::sync::Mutex; +use testcontainers::core::Mount; +use testcontainers::runners::SyncRunner; +use testcontainers::{core::WaitFor, GenericImage, RunnableImage}; + +// ============================================================================= +// Docker image constants +// ============================================================================= + +/// Zombie asset docker image +pub const ZOMBIE_ASSET_DOCKER_IMAGE: &str = "docker.io/gleec/zombietestrunner"; +/// Zombie asset docker image with tag +pub const ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/gleec/zombietestrunner:multiarch"; + +// ============================================================================= +// ZCoinAssetDockerOps +// ============================================================================= + +/// Docker operations for ZCoin/Zombie assets. +pub struct ZCoinAssetDockerOps { + #[allow(dead_code)] + ctx: MmArc, + coin: ZCoin, +} + +impl CoinDockerOps for ZCoinAssetDockerOps { + fn rpc_client(&self) -> &UtxoRpcClientEnum { + &self.coin.as_ref().rpc_client + } +} + +impl ZCoinAssetDockerOps { + /// Create ZCoinAssetDockerOps with default settings. + pub fn new() -> ZCoinAssetDockerOps { + let (ctx, coin) = block_on(z_coin_from_spending_key( + "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", + "fe", + )); + + ZCoinAssetDockerOps { ctx, coin } + } +} + +// ============================================================================= +// Statics for ZCoin tests +// ============================================================================= + +lazy_static! { + /// Temporary directory for ZCoin databases (created once, cleaned up on process exit) + pub static ref TEMP_DIR: Mutex = Mutex::new(tempfile::TempDir::new().unwrap()); +} + +// ============================================================================= +// Docker node helpers +// ============================================================================= + +/// Start a Zombie asset docker node for testing. +pub fn zombie_asset_docker_node(port: u16) -> DockerNode { + let image = GenericImage::new(ZOMBIE_ASSET_DOCKER_IMAGE, "multiarch") + .with_mount(Mount::bind_mount( + zcash_params_path().display().to_string(), + "/root/.zcash-params", + )) + .with_env_var("COIN_RPC_PORT", port.to_string()) + .with_wait_for(WaitFor::message_on_stdout("config is ready")); + + let image = RunnableImage::from(image).with_mapped_port((port, port)); + let container = image.start().expect("Failed to start Zombie asset docker node"); + let config_ticker = "ZOMBIE"; + let mut conf_path = coin_daemon_data_dir(config_ticker, true); + + std::fs::create_dir_all(&conf_path).unwrap(); + conf_path.push(format!("{config_ticker}.conf")); + Command::new("docker") + .arg("cp") + .arg(format!("{}:/data/node_0/{}.conf", container.id(), config_ticker)) + .arg(&conf_path) + .status() + .expect("Failed to execute docker command"); + + let timeout = wait_until_ms(3000); + while !conf_path.exists() { + assert!(now_ms() < timeout, "Test timed out"); + } + + DockerNode { + container, + ticker: config_ticker.into(), + port, + } +} + +// ============================================================================= +// ZCoin creation utilities +// ============================================================================= + +/// Build asset `ZCoin` from ticker and spending_key. +pub async fn z_coin_from_spending_key(spending_key: &str, path: &str) -> (MmArc, ZCoin) { + use coins::z_coin::{z_coin_from_conf_and_params_with_docker, ZcoinActivationParams, ZcoinRpcMode}; + use coins::{CoinProtocol, PrivKeyBuildPolicy}; + use mm2_core::mm_ctx::MmCtxBuilder; + use mm2_test_helpers::for_tests::zombie_conf_for_docker; + + let db_path = { + let tmp = TEMP_DIR.lock().unwrap(); + let path = tmp.path().join(format!("ZOMBIE_DB_{path}")); + std::fs::create_dir_all(&path).unwrap(); + path + }; + let ctx = MmCtxBuilder::new().with_conf(json!({ "dbdir": db_path})).into_mm_arc(); + + let mut conf = zombie_conf_for_docker(); + let params = ZcoinActivationParams { + mode: ZcoinRpcMode::Native, + ..Default::default() + }; + let pk_data = [1; 32]; + + let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; + + let coin = z_coin_from_conf_and_params_with_docker( + &ctx, + "ZOMBIE", + &conf, + ¶ms, + PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), + protocol_info, + spending_key, + ) + .await + .unwrap(); + + (ctx, coin) +} diff --git a/mm2src/mm2_main/tests/docker_tests/mod.rs b/mm2src/mm2_main/tests/docker_tests/mod.rs index e9b07c3c84..ecd830ef86 100644 --- a/mm2src/mm2_main/tests/docker_tests/mod.rs +++ b/mm2src/mm2_main/tests/docker_tests/mod.rs @@ -1,17 +1,145 @@ #![allow(static_mut_refs)] -pub mod docker_tests_common; +pub mod runner; + +// Helpers are used by all docker tests +#[cfg(feature = "run-docker-tests")] +pub mod helpers; + +// ============================================================================ +// ORDERMATCHING TESTS +// Tests for the orderbook and order matching engine (lp_ordermatch) +// Future destination: mm2_main::lp_ordermatch/tests +// ============================================================================ + +// Ordermatching tests - UTXO-only orderbook +// Tests: best_orders, orderbook depth, price aggregation, custom orderbook tickers +// Chains: UTXO-MYCOIN, UTXO-MYCOIN1 +#[cfg(feature = "docker-tests-ordermatch")] mod docker_ordermatch_tests; + +// UTXO Ordermatching V1 tests - UTXO-only orderbook mechanics (extracted from docker_tests_inner) +// Tests: order lifecycle, balance-driven cancellations/updates, restart kickstart, best-price matching, +// RPC response formats, min_volume/dust validation, P2P time sync validation +// Chains: UTXO-MYCOIN, UTXO-MYCOIN1 +#[cfg(feature = "docker-tests-ordermatch")] +mod utxo_ordermatch_v1_tests; + +// ============================================================================ +// SWAP TESTS +// Tests for atomic swap execution (lp_swap) +// Future destination: mm2_main::lp_swap/tests or coins::*/tests +// ============================================================================ + +// Cross-chain tests - UTXO + ETH cross-chain order matching and validation +// Tests: cross-chain order matching, volume validation, orderbook depth, best_orders +// Chains: UTXO-MYCOIN, UTXO-MYCOIN1, ETH, ERC20 +// Note: Contains tests that require BOTH ETH and UTXO chains simultaneously +#[cfg(feature = "docker-tests-integration")] mod docker_tests_inner; + +// ETH Inner tests - ETH-only tests (extracted from docker_tests_inner) +// Tests: ETH/ERC20 activation, disable, withdraw, swap contract negotiation, order management, ERC20 approval +// Chains: ETH, ERC20 +// Future: Consider separate feature flag (docker-tests-eth-only) for tests that don't need UTXO +#[cfg(feature = "docker-tests-eth")] +mod eth_inner_tests; + +// Swap protocol v2 tests - UTXO-only TPU protocol +// Tests: MakerSwapStateMachine, TakerSwapStateMachine, trading protocol upgrade +// Chains: UTXO-MYCOIN, UTXO-MYCOIN1 +#[cfg(feature = "docker-tests-swaps")] +mod swap_proto_v2_tests; + +// UTXO Swaps V1 tests - UTXO-only swap mechanics (extracted from docker_tests_inner) +// Tests: swap spend/refund, trade preimage, max taker/maker vol, locked amounts, UTXO merge +// Chains: UTXO-MYCOIN, UTXO-MYCOIN1 +#[cfg(feature = "docker-tests-swaps")] +mod utxo_swaps_v1_tests; + +// Swap confirmation settings sync tests - UTXO-only +// Tests: confirmation requirements, settings synchronization between maker/taker +// Chains: UTXO-MYCOIN, UTXO-MYCOIN1 +#[cfg(feature = "docker-tests-swaps")] +mod swaps_confs_settings_sync_tests; + +// Swap file lock tests - UTXO-only infrastructure +// Tests: concurrent swap file locking, race condition prevention +// Chains: UTXO-MYCOIN, UTXO-MYCOIN1 +#[cfg(feature = "docker-tests-swaps")] +mod swaps_file_lock_tests; + +// ============================================================================ +// CROSS-CHAIN INTEGRATION TESTS +// Tests for atomic swaps between different chain families (requires all containers) +// Future destination: Integration test suite +// ============================================================================ + +// BCH-SLP swap tests +// Tests: BCH/SLP atomic swaps (FORSLP, ADEXSLP pairs) +// Chains: BCH-SLP (FORSLP node only) +#[cfg(feature = "docker-tests-slp")] +mod swap_tests; + +// ============================================================================ +// WATCHER TESTS +// Tests for swap watcher nodes (lp_swap::watchers) +// Future destination: mm2_main::lp_swap::watchers/tests +// ============================================================================ + +// Swap watcher tests. +// UTXO watcher tests are enabled with `docker-tests-watchers`. +// ETH/ERC20 watcher tests are behind `docker-tests-watchers-eth` (disabled by default). +#[cfg(feature = "docker-tests-watchers")] +mod swap_watcher_tests; + +// ============================================================================ +// COIN-SPECIFIC TESTS +// Tests for individual coin implementations (coins crate) +// Future destination: coins::*/tests +// ============================================================================ + +// ETH/ERC20 coin tests +// Tests: gas estimation, nonce management, ERC20 activation, NFT swaps +// Chains: ETH, ERC20, ERC721, ERC1155 +#[cfg(feature = "docker-tests-eth")] mod eth_docker_tests; + +// QRC20 coin and swap tests +// Tests: QRC20 activation, QTUM gas, QRC20<->UTXO swaps +// Chains: QRC20, UTXO-MYCOIN +#[cfg(feature = "docker-tests-qrc20")] pub mod qrc20_tests; + +// SIA coin tests +// Tests: Sia activation, balance, withdraw +// Chains: Sia +#[cfg(feature = "docker-tests-sia")] mod sia_docker_tests; + +// SLP/BCH coin tests +// Tests: SLP token activation, BCH-SLP balance +// Chains: BCH-SLP (FORSLP, ADEXSLP) +#[cfg(feature = "docker-tests-slp")] mod slp_tests; -mod swap_proto_v2_tests; -mod swap_watcher_tests; -mod swaps_confs_settings_sync_tests; -mod swaps_file_lock_tests; + +// Tendermint coin and IBC tests (Cosmos-only) +// Tests: ATOM/Nucleus/IRIS activation, staking, IBC transfers, withdraw, delegation +// Chains: Tendermint (ATOM, Nucleus, IRIS) +#[cfg(feature = "docker-tests-tendermint")] mod tendermint_tests; + +// Tendermint cross-chain swap tests +// Tests: NUCLEUS<->DOC, NUCLEUS<->ETH, DOC<->IRIS-IBC-NUCLEUS swaps +// Chains: Tendermint (NUCLEUS, IRIS) + ETH/Electrum +// Note: Requires multiple chain families (Tendermint + ETH) - part of integration test suite +#[cfg(feature = "docker-tests-integration")] +mod tendermint_swap_tests; + +// ZCoin/Zombie coin tests +// Tests: ZCoin activation, shielded transactions, DEX fee collection +// Chains: ZCoin/Zombie +#[cfg(feature = "docker-tests-zcoin")] mod z_coin_docker_tests; // dummy test helping IDE to recognize this as test module diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index b39aeb1f8d..7f6dc14c9c 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -1,131 +1,42 @@ -use crate::docker_tests::docker_tests_common::*; +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::qrc20::{ + enable_qrc20_native, fill_qrc20_address, generate_qrc20_coin_with_random_privkey, + generate_qtum_coin_with_random_privkey, generate_segwit_qtum_coin_with_random_privkey, qick_token_address, + qrc20_coin_from_privkey, qtum_conf_path, wait_for_estimate_smart_fee, +}; +use crate::docker_tests::helpers::swap::trade_base_rel; +use crate::docker_tests::helpers::utxo::{fill_address, utxo_coin_from_privkey}; use crate::integration_tests_common::enable_native; use bitcrypto::dhash160; -use coins::qrc20::rpc_clients::for_tests::Qrc20NativeWalletOps; -use coins::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin}; -use coins::utxo::rpc_clients::UtxoRpcClientEnum; +use coins::utxo::qtum::QtumCoin; use coins::utxo::utxo_common::big_decimal_from_sat; -use coins::utxo::{UtxoActivationParams, UtxoCommonOps}; +use coins::utxo::UtxoCommonOps; use coins::{ CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, DexFeeBurnDestination, FeeApproxStage, FoundSwapTxSpend, MarketCoinOps, MmCoin, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput, WaitForHTLCTxSpendArgs, }; -use common::{block_on_f01, temp_dir, DEX_FEE_ADDR_RAW_PUBKEY}; -use crypto::Secp256k1Secret; -use ethereum_types::H160; +use common::{block_on, now_sec, wait_until_sec}; +use common::{block_on_f01, DEX_FEE_ADDR_RAW_PUBKEY}; use http::StatusCode; -use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_main::lp_swap::max_taker_vol_from_available; use mm2_number::BigDecimal; +use mm2_number::MmNumber; use mm2_rpc::data::legacy::{CoinInitResponse, OrderbookResponse}; +use mm2_test_helpers::for_tests::{mm_dump, MarketMakerIt}; use mm2_test_helpers::structs::{trade_preimage_error, RpcErrorResponse, RpcSuccessResponse, TransactionDetails}; use rand6::Rng; -use serde_json::{self as json, Value as Json}; +use serde_json::{self as json, json, Value as Json}; use std::convert::TryFrom; -use std::process::Command; +use std::env; use std::str::FromStr; use std::sync::Mutex; +use std::thread; use std::time::Duration; -use testcontainers::core::WaitFor; -use testcontainers::runners::SyncRunner; -use testcontainers::{GenericImage, RunnableImage}; - -pub const QTUM_REGTEST_DOCKER_IMAGE: &str = "docker.io/sergeyboyko/qtumregtest"; -pub const QTUM_REGTEST_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/sergeyboyko/qtumregtest:latest"; -const QRC20_TOKEN_BYTES: &str = "6080604052600860ff16600a0a633b9aca000260005534801561002157600080fd5b50600054600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610c69806100776000396000f3006080604052600436106100a4576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100a9578063095ea7b31461013957806318160ddd1461019e57806323b872dd146101c9578063313ce5671461024e5780635a3b7e421461027f57806370a082311461030f57806395d89b4114610366578063a9059cbb146103f6578063dd62ed3e1461045b575b600080fd5b3480156100b557600080fd5b506100be6104d2565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100fe5780820151818401526020810190506100e3565b50505050905090810190601f16801561012b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561014557600080fd5b50610184600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061050b565b604051808215151515815260200191505060405180910390f35b3480156101aa57600080fd5b506101b36106bb565b6040518082815260200191505060405180910390f35b3480156101d557600080fd5b50610234600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506106c1565b604051808215151515815260200191505060405180910390f35b34801561025a57600080fd5b506102636109a1565b604051808260ff1660ff16815260200191505060405180910390f35b34801561028b57600080fd5b506102946109a6565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156102d45780820151818401526020810190506102b9565b50505050905090810190601f1680156103015780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561031b57600080fd5b50610350600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506109df565b6040518082815260200191505060405180910390f35b34801561037257600080fd5b5061037b6109f7565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103bb5780820151818401526020810190506103a0565b50505050905090810190601f1680156103e85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561040257600080fd5b50610441600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610a30565b604051808215151515815260200191505060405180910390f35b34801561046757600080fd5b506104bc600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610be1565b6040518082815260200191505060405180910390f35b6040805190810160405280600881526020017f515243205445535400000000000000000000000000000000000000000000000081525081565b60008260008173ffffffffffffffffffffffffffffffffffffffff161415151561053457600080fd5b60008314806105bf57506000600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054145b15156105ca57600080fd5b82600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925856040518082815260200191505060405180910390a3600191505092915050565b60005481565b60008360008173ffffffffffffffffffffffffffffffffffffffff16141515156106ea57600080fd5b8360008173ffffffffffffffffffffffffffffffffffffffff161415151561071157600080fd5b610797600260008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c06565b600260008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610860600160008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c06565b600160008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506108ec600160008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205485610c1f565b600160008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508473ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef866040518082815260200191505060405180910390a36001925050509392505050565b600881565b6040805190810160405280600981526020017f546f6b656e20302e31000000000000000000000000000000000000000000000081525081565b60016020528060005260406000206000915090505481565b6040805190810160405280600381526020017f515443000000000000000000000000000000000000000000000000000000000081525081565b60008260008173ffffffffffffffffffffffffffffffffffffffff1614151515610a5957600080fd5b610aa2600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205484610c06565b600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610b2e600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205484610c1f565b600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef856040518082815260200191505060405180910390a3600191505092915050565b6002602052816000526040600020602052806000526040600020600091509150505481565b6000818310151515610c1457fe5b818303905092915050565b6000808284019050838110151515610c3357fe5b80915050929150505600a165627a7a723058207f2e5248b61b80365ea08a0f6d11ac0b47374c4dfd538de76bc2f19591bbbba40029"; -const QRC20_SWAP_CONTRACT_BYTES: &str = "608060405234801561001057600080fd5b50611437806100206000396000f3fe60806040526004361061004a5760003560e01c806302ed292b1461004f5780630716326d146100de578063152cf3af1461017b57806346fc0294146101f65780639b415b2a14610294575b600080fd5b34801561005b57600080fd5b506100dc600480360360a081101561007257600080fd5b81019080803590602001909291908035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610339565b005b3480156100ea57600080fd5b506101176004803603602081101561010157600080fd5b8101908080359060200190929190505050610867565b60405180846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526020018367ffffffffffffffff1667ffffffffffffffff16815260200182600381111561016557fe5b60ff168152602001935050505060405180910390f35b6101f46004803603608081101561019157600080fd5b8101908080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080356bffffffffffffffffffffffff19169060200190929190803567ffffffffffffffff1690602001909291905050506108bf565b005b34801561020257600080fd5b50610292600480360360a081101561021957600080fd5b81019080803590602001909291908035906020019092919080356bffffffffffffffffffffffff19169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610bd9565b005b610337600480360360c08110156102aa57600080fd5b810190808035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080356bffffffffffffffffffffffff19169060200190929190803567ffffffffffffffff169060200190929190505050610fe2565b005b6001600381111561034657fe5b600080878152602001908152602001600020600001601c9054906101000a900460ff16600381111561037457fe5b1461037e57600080fd5b6000600333836003600288604051602001808281526020019150506040516020818303038152906040526040518082805190602001908083835b602083106103db57805182526020820191506020810190506020830392506103b8565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa15801561041d573d6000803e3d6000fd5b5050506040513d602081101561043257600080fd5b8101908080519060200190929190505050604051602001808281526020019150506040516020818303038152906040526040518082805190602001908083835b602083106104955780518252602082019150602081019050602083039250610472565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa1580156104d7573d6000803e3d6000fd5b5050506040515160601b8689604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b602083106105fc57805182526020820191506020810190506020830392506105d9565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa15801561063e573d6000803e3d6000fd5b5050506040515160601b905060008087815260200190815260200160002060000160009054906101000a900460601b6bffffffffffffffffffffffff1916816bffffffffffffffffffffffff19161461069657600080fd5b6002600080888152602001908152602001600020600001601c6101000a81548160ff021916908360038111156106c857fe5b0217905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141561074e573373ffffffffffffffffffffffffffffffffffffffff166108fc869081150290604051600060405180830381858888f19350505050158015610748573d6000803e3d6000fd5b50610820565b60008390508073ffffffffffffffffffffffffffffffffffffffff1663a9059cbb33886040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b1580156107da57600080fd5b505af11580156107ee573d6000803e3d6000fd5b505050506040513d602081101561080457600080fd5b810190808051906020019092919050505061081e57600080fd5b505b7f36c177bcb01c6d568244f05261e2946c8c977fa50822f3fa098c470770ee1f3e8685604051808381526020018281526020019250505060405180910390a1505050505050565b60006020528060005260406000206000915090508060000160009054906101000a900460601b908060000160149054906101000a900467ffffffffffffffff169080600001601c9054906101000a900460ff16905083565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141580156108fc5750600034115b801561094057506000600381111561091057fe5b600080868152602001908152602001600020600001601c9054906101000a900460ff16600381111561093e57fe5b145b61094957600080fd5b60006003843385600034604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b60208310610a6c5780518252602082019150602081019050602083039250610a49565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610aae573d6000803e3d6000fd5b5050506040515160601b90506040518060600160405280826bffffffffffffffffffffffff191681526020018367ffffffffffffffff16815260200160016003811115610af757fe5b81525060008087815260200190815260200160002060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908360601c021790555060208201518160000160146101000a81548167ffffffffffffffff021916908367ffffffffffffffff160217905550604082015181600001601c6101000a81548160ff02191690836003811115610b9357fe5b02179055509050507fccc9c05183599bd3135da606eaaf535daffe256e9de33c048014cffcccd4ad57856040518082815260200191505060405180910390a15050505050565b60016003811115610be657fe5b600080878152602001908152602001600020600001601c9054906101000a900460ff166003811115610c1457fe5b14610c1e57600080fd5b600060038233868689604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b60208310610d405780518252602082019150602081019050602083039250610d1d565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa158015610d82573d6000803e3d6000fd5b5050506040515160601b905060008087815260200190815260200160002060000160009054906101000a900460601b6bffffffffffffffffffffffff1916816bffffffffffffffffffffffff1916148015610e10575060008087815260200190815260200160002060000160149054906101000a900467ffffffffffffffff1667ffffffffffffffff164210155b610e1957600080fd5b6003600080888152602001908152602001600020600001601c6101000a81548160ff02191690836003811115610e4b57fe5b0217905550600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415610ed1573373ffffffffffffffffffffffffffffffffffffffff166108fc869081150290604051600060405180830381858888f19350505050158015610ecb573d6000803e3d6000fd5b50610fa3565b60008390508073ffffffffffffffffffffffffffffffffffffffff1663a9059cbb33886040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050602060405180830381600087803b158015610f5d57600080fd5b505af1158015610f71573d6000803e3d6000fd5b505050506040513d6020811015610f8757600080fd5b8101908080519060200190929190505050610fa157600080fd5b505b7f1797d500133f8e427eb9da9523aa4a25cb40f50ebc7dbda3c7c81778973f35ba866040518082815260200191505060405180910390a1505050505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415801561101f5750600085115b801561106357506000600381111561103357fe5b600080888152602001908152602001600020600001601c9054906101000a900460ff16600381111561106157fe5b145b61106c57600080fd5b60006003843385888a604051602001808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b81526014018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401846bffffffffffffffffffffffff19166bffffffffffffffffffffffff191681526014018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1660601b8152601401828152602001955050505050506040516020818303038152906040526040518082805190602001908083835b6020831061118e578051825260208201915060208101905060208303925061116b565b6001836020036101000a038019825116818451168082178552505050505050905001915050602060405180830381855afa1580156111d0573d6000803e3d6000fd5b5050506040515160601b90506040518060600160405280826bffffffffffffffffffffffff191681526020018367ffffffffffffffff1681526020016001600381111561121957fe5b81525060008089815260200190815260200160002060008201518160000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908360601c021790555060208201518160000160146101000a81548167ffffffffffffffff021916908367ffffffffffffffff160217905550604082015181600001601c6101000a81548160ff021916908360038111156112b557fe5b021790555090505060008590508073ffffffffffffffffffffffffffffffffffffffff166323b872dd33308a6040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019350505050602060405180830381600087803b15801561137d57600080fd5b505af1158015611391573d6000803e3d6000fd5b505050506040513d60208110156113a757600080fd5b81019080805190602001909291905050506113c157600080fd5b7fccc9c05183599bd3135da606eaaf535daffe256e9de33c048014cffcccd4ad57886040518082815260200191505060405180910390a1505050505050505056fea265627a7a723158208c83db436905afce0b7be1012be64818c49323c12d451fe2ab6bce76ff6421c964736f6c63430005110032"; const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; -pub struct QtumDockerOps { - #[allow(dead_code)] - ctx: MmArc, - coin: QtumCoin, -} - -impl CoinDockerOps for QtumDockerOps { - fn rpc_client(&self) -> &UtxoRpcClientEnum { - &self.coin.as_ref().rpc_client - } -} - -impl QtumDockerOps { - pub fn new() -> QtumDockerOps { - let ctx = MmCtxBuilder::new().into_mm_arc(); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; - let conf = json!({"coin":"QTUM","decimals":8,"network":"regtest","confpath":confpath}); - let req = json!({ - "method": "enable", - }); - let priv_key = Secp256k1Secret::from("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f"); - let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); - let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); - QtumDockerOps { ctx, coin } - } - - pub fn initialize_contracts(&self) { - let sender = get_address_by_label(&self.coin, QTUM_ADDRESS_LABEL); - unsafe { - QICK_TOKEN_ADDRESS = Some(self.create_contract(&sender, QRC20_TOKEN_BYTES)); - QORTY_TOKEN_ADDRESS = Some(self.create_contract(&sender, QRC20_TOKEN_BYTES)); - QRC20_SWAP_CONTRACT_ADDRESS = Some(self.create_contract(&sender, QRC20_SWAP_CONTRACT_BYTES)); - } - } - - fn create_contract(&self, sender: &str, hexbytes: &str) -> H160 { - let bytecode = hex::decode(hexbytes).expect("Hex encoded bytes expected"); - let gas_limit = 2_500_000u64; - let gas_price = BigDecimal::from_str("0.0000004").unwrap(); - - match self.coin.as_ref().rpc_client { - UtxoRpcClientEnum::Native(ref native) => { - let result = block_on_f01(native.create_contract(&bytecode.into(), gas_limit, gas_price, sender)) - .expect("!createcontract"); - result.address.0.into() - }, - UtxoRpcClientEnum::Electrum(_) => panic!("Native client expected"), - } - } -} - -pub fn qtum_docker_node(port: u16) -> DockerNode { - let image = GenericImage::new(QTUM_REGTEST_DOCKER_IMAGE, "latest") - .with_env_var("CLIENTS", "2") - .with_env_var("COIN_RPC_PORT", port.to_string()) - .with_env_var("ADDRESS_LABEL", QTUM_ADDRESS_LABEL) - .with_env_var("FILL_MEMPOOL", "true") - .with_wait_for(WaitFor::message_on_stdout("config is ready")); - let image = RunnableImage::from(image).with_mapped_port((port, port)); - let container = image.start().expect("Failed to start Qtum regtest docker container"); - - let name = "qtum"; - let mut conf_path = temp_dir().join("qtum-regtest"); - std::fs::create_dir_all(&conf_path).unwrap(); - conf_path.push(format!("{name}.conf")); - Command::new("docker") - .arg("cp") - .arg(format!("{}:/data/node_0/{}.conf", container.id(), name)) - .arg(&conf_path) - .status() - .expect("Failed to execute docker command"); - let timeout = wait_until_ms(3000); - loop { - if conf_path.exists() { - break; - }; - assert!(now_ms() < timeout, "Test timed out"); - } - - unsafe { QTUM_CONF_PATH = Some(conf_path) }; - DockerNode { - container, - ticker: name.to_owned(), - port, - } -} - fn withdraw_and_send(mm: &MarketMakerIt, coin: &str, to: &str, amount: f64) { let withdraw = block_on(mm.rpc(&json! ({ "mmrpc": "2.0", @@ -612,7 +523,6 @@ fn test_search_for_swap_tx_spend_taker_spent() { search_from_block, swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], - watcher_reward: false, }; let actual = block_on(maker_coin.search_for_swap_tx_spend_my(search_input)); let expected = Ok(Some(FoundSwapTxSpend::Spent(spend))); @@ -692,7 +602,6 @@ fn test_search_for_swap_tx_spend_maker_refunded() { search_from_block, swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], - watcher_reward: false, }; let actual = block_on(maker_coin.search_for_swap_tx_spend_my(search_input)); let expected = Ok(Some(FoundSwapTxSpend::Refunded(refund))); @@ -747,7 +656,6 @@ fn test_search_for_swap_tx_spend_not_spent() { search_from_block, swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], - watcher_reward: false, }; let actual = block_on(maker_coin.search_for_swap_tx_spend_my(search_input)); // maker payment hasn't been spent or refunded yet @@ -874,8 +782,8 @@ fn test_check_balance_on_order_post_base_coin_locked() { let my_address = coin.my_address().expect("!my_address"); fill_address(&coin, &my_address, 10.into(), timeout); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; - let qick_contract_address = format!("{:#02x}", unsafe { QICK_TOKEN_ADDRESS.expect("!QICK_TOKEN_ADDRESS") }); + let confpath = qtum_conf_path(); + let qick_contract_address = format!("{:#02x}", qick_token_address()); let coins = json!([ {"coin":"MYCOIN","asset":"MYCOIN","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, {"coin":"QICK","required_confirmations":1,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"mm2": 1,"mature_confirmations": 500,"confpath": confpath,"network":"regtest", @@ -978,7 +886,7 @@ fn test_check_balance_on_order_post_base_coin_locked() { /// /// Please note this function should be called before the Qtum balance is filled. fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_key: &[u8]) { - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let confpath = qtum_conf_path(); let coins = json! ([ {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"txfee":0,"txfee_volatility_percent":0.1, @@ -1092,10 +1000,12 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ let _taker_payment_tx = block_on(coin.send_taker_payment(taker_payment_args)).expect("!send_taker_payment"); let my_balance = block_on_f01(coin.my_spendable_balance()).expect("!my_balance"); - assert_eq!( - my_balance, - BigDecimal::from(0u32), - "NOT AN ERROR, but it would be better if the balance remained zero" + let tolerance = BigDecimal::from_str("0.001").unwrap(); + assert!( + my_balance < tolerance, + "NOT AN ERROR, but it would be better if the balance remained near zero. \ + Due to dynamic fee calculation precision, a small dust amount ({}) may remain.", + my_balance ); } @@ -1152,8 +1062,8 @@ fn test_trade_preimage_not_sufficient_base_coin_balance_for_ticker() { let qtum_balance = MmNumber::from("0.005").to_decimal(); let (_, _, priv_key) = generate_qrc20_coin_with_random_privkey("QICK", qtum_balance.clone(), qick_balance); - let qick_contract_address = format!("{:#02x}", unsafe { QICK_TOKEN_ADDRESS.expect("!QICK_TOKEN_ADDRESS") }); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let qick_contract_address = format!("{:#02x}", qick_token_address()); + let confpath = qtum_conf_path(); let coins = json! ([ {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, {"coin":"QICK","required_confirmations":1,"pubtype": 120,"p2shtype": 50,"wiftype": 128,"mm2": 1,"mature_confirmations": 500,"confpath": confpath,"network":"regtest", @@ -1213,7 +1123,7 @@ fn test_trade_preimage_dynamic_fee_not_sufficient_balance() { let qtum_balance = MmNumber::from("0.5").to_decimal(); let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", qtum_balance.clone(), Some(0)); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let confpath = qtum_conf_path(); let coins = json! ([ {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"txfee":0,"txfee_volatility_percent":0.1, @@ -1275,7 +1185,7 @@ fn test_trade_preimage_deduct_fee_from_output_failed() { let qtum_balance = MmNumber::from("0.00073").to_decimal(); let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", qtum_balance.clone(), Some(0)); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let confpath = qtum_conf_path(); let coins = json! ([ {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"txfee":0,"txfee_volatility_percent":0.1, @@ -1336,7 +1246,7 @@ fn test_segwit_native_balance() { let (_ctx, _coin, priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let confpath = qtum_conf_path(); let coins = json! ([ {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, "mm2":1,"mature_confirmations":500,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"},"bech32_hrp":"qcrt","address_format":{"format":"segwit"}}, @@ -1383,7 +1293,7 @@ fn test_withdraw_and_send_from_segwit() { let (_ctx, _coin, priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.7).unwrap(), Some(0)); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let confpath = qtum_conf_path(); let coins = json! ([ {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, "mm2":1,"mature_confirmations":500,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"},"bech32_hrp":"qcrt","address_format":{"format":"segwit"}}, @@ -1432,7 +1342,7 @@ fn test_withdraw_and_send_legacy_to_segwit() { let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.7).unwrap(), Some(0)); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let confpath = qtum_conf_path(); let coins = json! ([ {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, "mm2":1,"mature_confirmations":500,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"},"bech32_hrp":"qcrt"}, @@ -1531,7 +1441,6 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], - watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -1599,7 +1508,6 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], - watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -1632,7 +1540,7 @@ fn segwit_address_in_the_orderbook() { let (_ctx, coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); - let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; + let confpath = qtum_conf_path(); let coins = json! ([ {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, "mm2":1,"mature_confirmations":500,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"},"bech32_hrp":"qcrt"}, diff --git a/mm2src/mm2_main/tests/docker_tests/runner/geth.rs b/mm2src/mm2_main/tests/docker_tests/runner/geth.rs new file mode 100644 index 0000000000..0232c65a6c --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/runner/geth.rs @@ -0,0 +1,35 @@ +//! Geth/ETH setup for docker tests. + +use super::{DockerTestMode, DockerTestRunner}; + +use crate::docker_tests::helpers::eth::{ + erc20_contract, geth_account, geth_docker_node, geth_erc1155_contract, geth_erc721_contract, geth_maker_swap_v2, + geth_nft_maker_swap_v2, geth_taker_swap_v2, init_geth_node, swap_contract, wait_for_geth_node_ready, + watchers_swap_contract, +}; + +pub(super) fn setup(runner: &mut DockerTestRunner) { + match runner.config.mode { + DockerTestMode::Testcontainers => { + let node = geth_docker_node("ETH", 8545); + wait_for_geth_node_ready(); + init_geth_node(); + runner.hold(node); + }, + DockerTestMode::ComposeInit => { + wait_for_geth_node_ready(); + init_geth_node(); + }, + } + + // Ensure globals are initialized for test helpers. + let _ = geth_account(); + let _ = erc20_contract(); + let _ = swap_contract(); + let _ = geth_maker_swap_v2(); + let _ = geth_taker_swap_v2(); + let _ = watchers_swap_contract(); + let _ = geth_erc721_contract(); + let _ = geth_erc1155_contract(); + let _ = geth_nft_maker_swap_v2(); +} diff --git a/mm2src/mm2_main/tests/docker_tests/runner/mod.rs b/mm2src/mm2_main/tests/docker_tests/runner/mod.rs new file mode 100644 index 0000000000..965d75249a --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/runner/mod.rs @@ -0,0 +1,288 @@ +//! Docker tests custom runner (split from the old monolithic `runner.rs`). +//! +//! Public API is preserved: +//! - `docker_tests::runner::docker_tests_runner_impl()` +//! +//! Internals are split into per-chain setup submodules to push cfg gates to module boundaries. + +use std::any::Any; +use std::env; +use std::io::{BufRead, BufReader}; +use std::process::Command; +use test::{test_main, StaticBenchFn, StaticTestFn, TestDescAndFn}; + +// ============================================================================= +// Per-chain setup submodules +// ============================================================================= + +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-sia", + feature = "docker-tests-integration" +))] +mod utxo; + +#[cfg(any(feature = "docker-tests-slp", feature = "docker-tests-integration"))] +mod slp; + +#[cfg(feature = "docker-tests-qrc20")] +mod qtum; + +#[cfg(any( + feature = "docker-tests-eth", + feature = "docker-tests-watchers-eth", + feature = "docker-tests-integration" +))] +mod geth; + +#[cfg(feature = "docker-tests-zcoin")] +mod zcoin; + +#[cfg(any(feature = "docker-tests-tendermint", feature = "docker-tests-integration"))] +mod tendermint; + +#[cfg(feature = "docker-tests-sia")] +mod sia; + +// ============================================================================= +// Core runner types +// ============================================================================= + +/// Execution mode for docker tests. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum DockerTestMode { + /// Default: Start containers via testcontainers, run initialization. + Testcontainers, + /// Docker-compose mode: Containers already running, run initialization. + ComposeInit, +} + +/// Environment variable to indicate docker-compose mode (containers already running). +const ENV_DOCKER_COMPOSE_MODE: &str = "KDF_DOCKER_COMPOSE_ENV"; + +/// Determine which execution mode to use based on environment variables. +fn determine_test_mode() -> DockerTestMode { + if env::var(ENV_DOCKER_COMPOSE_MODE).is_ok() { + DockerTestMode::ComposeInit + } else { + DockerTestMode::Testcontainers + } +} + +/// Parses runner config from env once. +pub(crate) struct DockerTestConfig { + pub(crate) mode: DockerTestMode, + /// When `_MM2_TEST_CONF` is set, the runner must skip docker setup entirely. + pub(crate) skip_setup: bool, +} + +impl DockerTestConfig { + fn from_env() -> Self { + DockerTestConfig { + mode: determine_test_mode(), + skip_setup: env::var("_MM2_TEST_CONF").is_ok(), + } + } +} + +/// Stateful docker test runner holding container keep-alives. +/// +/// Keep-alives are stored as `Box` to ensure RAII drop only happens +/// after `test_main` returns. +pub(crate) struct DockerTestRunner { + pub(crate) config: DockerTestConfig, + pub(crate) keep_alive: Vec>, +} + +impl DockerTestRunner { + fn new(config: DockerTestConfig) -> Self { + DockerTestRunner { + config, + keep_alive: Vec::new(), + } + } + + pub(crate) fn hold(&mut self, container: T) { + self.keep_alive.push(Box::new(container)); + } + + pub(crate) fn is_testcontainers(&self) -> bool { + self.config.mode == DockerTestMode::Testcontainers + } + + fn setup_or_reuse_nodes(&mut self) { + if self.is_testcontainers() { + for image in required_images() { + pull_docker_image(image); + remove_docker_containers(image); + } + } + + #[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-sia", + feature = "docker-tests-integration" + ))] + utxo::setup(self); + + #[cfg(feature = "docker-tests-qrc20")] + qtum::setup(self); + + #[cfg(any(feature = "docker-tests-slp", feature = "docker-tests-integration"))] + slp::setup(self); + + #[cfg(any( + feature = "docker-tests-eth", + feature = "docker-tests-watchers-eth", + feature = "docker-tests-integration" + ))] + geth::setup(self); + + #[cfg(feature = "docker-tests-zcoin")] + zcoin::setup(self); + + #[cfg(any(feature = "docker-tests-tendermint", feature = "docker-tests-integration"))] + tendermint::setup(self); + + #[cfg(feature = "docker-tests-sia")] + sia::setup(self); + } + + fn run_tests(&mut self, tests: &[&TestDescAndFn]) { + let owned_tests: Vec<_> = tests + .iter() + .map(|t| match t.testfn { + StaticTestFn(f) => TestDescAndFn { + testfn: StaticTestFn(f), + desc: t.desc.clone(), + }, + StaticBenchFn(f) => TestDescAndFn { + testfn: StaticBenchFn(f), + desc: t.desc.clone(), + }, + _ => panic!("non-static tests passed to lp_coins test runner"), + }) + .collect(); + + let args: Vec = env::args().collect(); + test_main(&args, owned_tests, None); + } +} + +/// Public API: custom test runner implementation called by `docker_tests_main.rs`. +pub fn docker_tests_runner_impl(tests: &[&TestDescAndFn]) { + let config = DockerTestConfig::from_env(); + log!("Docker test mode: {:?}", config.mode); + + let mut runner = DockerTestRunner::new(config); + + if !runner.config.skip_setup { + runner.setup_or_reuse_nodes(); + } + + runner.run_tests(tests); +} + +// ============================================================================= +// Images + docker utility functions +// ============================================================================= + +fn required_images() -> Vec<&'static str> { + let mut images = Vec::new(); + + #[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-sia", + feature = "docker-tests-integration" + ))] + { + use crate::docker_tests::helpers::utxo::UTXO_ASSET_DOCKER_IMAGE_WITH_TAG; + images.push(UTXO_ASSET_DOCKER_IMAGE_WITH_TAG); + } + + #[cfg(any(feature = "docker-tests-slp", feature = "docker-tests-integration"))] + { + use crate::docker_tests::helpers::slp::FORSLP_IMAGE_WITH_TAG; + images.push(FORSLP_IMAGE_WITH_TAG); + } + + #[cfg(feature = "docker-tests-qrc20")] + { + use crate::docker_tests::helpers::qrc20::QTUM_REGTEST_DOCKER_IMAGE_WITH_TAG; + images.push(QTUM_REGTEST_DOCKER_IMAGE_WITH_TAG); + } + + #[cfg(any( + feature = "docker-tests-eth", + feature = "docker-tests-watchers-eth", + feature = "docker-tests-integration" + ))] + { + use crate::docker_tests::helpers::eth::GETH_DOCKER_IMAGE_WITH_TAG; + images.push(GETH_DOCKER_IMAGE_WITH_TAG); + } + + #[cfg(any(feature = "docker-tests-tendermint", feature = "docker-tests-integration"))] + { + use crate::docker_tests::helpers::tendermint::{ + ATOM_IMAGE_WITH_TAG, IBC_RELAYER_IMAGE_WITH_TAG, NUCLEUS_IMAGE, + }; + images.push(NUCLEUS_IMAGE); + images.push(ATOM_IMAGE_WITH_TAG); + images.push(IBC_RELAYER_IMAGE_WITH_TAG); + } + + #[cfg(feature = "docker-tests-zcoin")] + { + use crate::docker_tests::helpers::zcoin::ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG; + images.push(ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG); + } + + #[cfg(feature = "docker-tests-sia")] + { + use crate::docker_tests::helpers::sia::SIA_DOCKER_IMAGE_WITH_TAG; + images.push(SIA_DOCKER_IMAGE_WITH_TAG); + } + + images.sort_unstable(); + images.dedup(); + images +} + +fn pull_docker_image(name: &str) { + Command::new("docker") + .arg("pull") + .arg(name) + .status() + .expect("Failed to execute docker command"); +} + +fn remove_docker_containers(name: &str) { + let stdout = Command::new("docker") + .arg("ps") + .arg("-f") + .arg(format!("ancestor={name}")) + .arg("-q") + .output() + .expect("Failed to execute docker command"); + + let reader = BufReader::new(stdout.stdout.as_slice()); + let ids: Vec<_> = reader.lines().map(|line| line.unwrap()).collect(); + if !ids.is_empty() { + Command::new("docker") + .arg("rm") + .arg("-f") + .args(ids) + .status() + .expect("Failed to execute docker command"); + } +} diff --git a/mm2src/mm2_main/tests/docker_tests/runner/qtum.rs b/mm2src/mm2_main/tests/docker_tests/runner/qtum.rs new file mode 100644 index 0000000000..5bc70cf131 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/runner/qtum.rs @@ -0,0 +1,32 @@ +//! Qtum/QRC20 setup for docker tests. + +use super::{DockerTestMode, DockerTestRunner}; + +use crate::docker_tests::helpers::qrc20::{ + qick_token_address, qorty_token_address, qrc20_swap_contract_address, qtum_conf_path, qtum_docker_node, + setup_qtum_conf_for_compose, QtumDockerOps, +}; + +pub(super) fn setup(runner: &mut DockerTestRunner) { + match runner.config.mode { + DockerTestMode::Testcontainers => { + let node = qtum_docker_node(9000); + let qtum_ops = QtumDockerOps::new(); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&qtum_ops, 2); + qtum_ops.initialize_contracts(); + runner.hold(node); + }, + DockerTestMode::ComposeInit => { + setup_qtum_conf_for_compose(); + let qtum_ops = QtumDockerOps::new(); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&qtum_ops, 2); + qtum_ops.initialize_contracts(); + }, + } + + // Ensure globals are initialized for test helpers. + let _ = qtum_conf_path().clone(); + let _ = qick_token_address(); + let _ = qorty_token_address(); + let _ = qrc20_swap_contract_address(); +} diff --git a/mm2src/mm2_main/tests/docker_tests/runner/sia.rs b/mm2src/mm2_main/tests/docker_tests/runner/sia.rs new file mode 100644 index 0000000000..e6994e1ed3 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/runner/sia.rs @@ -0,0 +1,21 @@ +//! Sia setup for docker tests. + +use super::{DockerTestMode, DockerTestRunner}; + +use common::block_on; + +use crate::docker_tests::helpers::sia::sia_docker_node; +use crate::sia_tests::utils::wait_for_dsia_node_ready; + +pub(super) fn setup(runner: &mut DockerTestRunner) { + match runner.config.mode { + DockerTestMode::Testcontainers => { + let node = sia_docker_node("SIA", 9980); + block_on(wait_for_dsia_node_ready()); + runner.hold(node); + }, + DockerTestMode::ComposeInit => { + block_on(wait_for_dsia_node_ready()); + }, + } +} diff --git a/mm2src/mm2_main/tests/docker_tests/runner/slp.rs b/mm2src/mm2_main/tests/docker_tests/runner/slp.rs new file mode 100644 index 0000000000..3d0c9a4cce --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/runner/slp.rs @@ -0,0 +1,25 @@ +//! SLP/BCH (FORSLP) setup for docker tests. + +use super::{DockerTestMode, DockerTestRunner}; + +use crate::docker_tests::helpers::docker_ops::setup_utxo_conf_for_compose; +use crate::docker_tests::helpers::env::KDF_FORSLP_SERVICE; +use crate::docker_tests::helpers::slp::{forslp_docker_node, BchDockerOps}; + +pub(super) fn setup(runner: &mut DockerTestRunner) { + match runner.config.mode { + DockerTestMode::Testcontainers => { + let node = forslp_docker_node(10000); + let for_slp_ops = BchDockerOps::from_ticker("FORSLP"); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&for_slp_ops, 4); + for_slp_ops.initialize_slp(); + runner.hold(node); + }, + DockerTestMode::ComposeInit => { + setup_utxo_conf_for_compose("FORSLP", KDF_FORSLP_SERVICE); + let for_slp_ops = BchDockerOps::from_ticker("FORSLP"); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&for_slp_ops, 4); + for_slp_ops.initialize_slp(); + }, + } +} diff --git a/mm2src/mm2_main/tests/docker_tests/runner/tendermint.rs b/mm2src/mm2_main/tests/docker_tests/runner/tendermint.rs new file mode 100644 index 0000000000..e7925eab9f --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/runner/tendermint.rs @@ -0,0 +1,71 @@ +//! Tendermint/Cosmos/IBC setup for docker tests. + +use super::{DockerTestMode, DockerTestRunner}; + +use crate::docker_tests::helpers::tendermint::{ + atom_node, ibc_relayer_node, nucleus_node, prepare_ibc_channels, prepare_ibc_channels_compose, + wait_until_relayer_container_is_ready, wait_until_relayer_container_is_ready_compose, +}; + +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +pub(super) fn setup(runner: &mut DockerTestRunner) { + match runner.config.mode { + DockerTestMode::Testcontainers => { + let runtime_dir = prepare_runtime_dir().unwrap(); + + let nucleus_node_instance = nucleus_node(runtime_dir.clone()); + let atom_node_instance = atom_node(runtime_dir.clone()); + let ibc_relayer_node_instance = ibc_relayer_node(runtime_dir.clone()); + + prepare_ibc_channels(ibc_relayer_node_instance.container.id()); + thread::sleep(Duration::from_secs(10)); + wait_until_relayer_container_is_ready(ibc_relayer_node_instance.container.id()); + + runner.hold(nucleus_node_instance); + runner.hold(atom_node_instance); + runner.hold(ibc_relayer_node_instance); + }, + DockerTestMode::ComposeInit => { + let _runtime_dir = get_runtime_dir(); + + prepare_ibc_channels_compose(); + thread::sleep(Duration::from_secs(10)); + wait_until_relayer_container_is_ready_compose(); + }, + } +} + +/// Get the runtime directory path. +fn get_runtime_dir() -> PathBuf { + let project_root = { + let mut current_dir = std::env::current_dir().unwrap(); + current_dir.pop(); + current_dir.pop(); + current_dir + }; + project_root.join(".docker/container-runtime") +} + +fn prepare_runtime_dir() -> std::io::Result { + let project_root = { + let mut current_dir = std::env::current_dir().unwrap(); + current_dir.pop(); + current_dir.pop(); + current_dir + }; + + let containers_state_dir = project_root.join(".docker/container-state"); + assert!(containers_state_dir.exists()); + let containers_runtime_dir = project_root.join(".docker/container-runtime"); + + if containers_runtime_dir.exists() { + std::fs::remove_dir_all(&containers_runtime_dir).unwrap(); + } + + mm2_io::fs::copy_dir_all(&containers_state_dir, &containers_runtime_dir).unwrap(); + + Ok(containers_runtime_dir) +} diff --git a/mm2src/mm2_main/tests/docker_tests/runner/utxo.rs b/mm2src/mm2_main/tests/docker_tests/runner/utxo.rs new file mode 100644 index 0000000000..fb1742b501 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/runner/utxo.rs @@ -0,0 +1,57 @@ +//! UTXO (MYCOIN, MYCOIN1) setup for docker tests. + +use super::{DockerTestMode, DockerTestRunner}; + +use crate::docker_tests::helpers::docker_ops::setup_utxo_conf_for_compose; +use crate::docker_tests::helpers::env::KDF_MYCOIN_SERVICE; +use crate::docker_tests::helpers::utxo::{utxo_asset_docker_node, UtxoAssetDockerOps}; + +#[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-integration" +))] +use crate::docker_tests::helpers::env::KDF_MYCOIN1_SERVICE; + +pub(super) fn setup(runner: &mut DockerTestRunner) { + // MYCOIN + match runner.config.mode { + DockerTestMode::Testcontainers => { + let node = utxo_asset_docker_node("MYCOIN", 8000); + let utxo_ops = UtxoAssetDockerOps::from_ticker("MYCOIN"); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&utxo_ops, 4); + runner.hold(node); + }, + DockerTestMode::ComposeInit => { + setup_utxo_conf_for_compose("MYCOIN", KDF_MYCOIN_SERVICE); + let utxo_ops = UtxoAssetDockerOps::from_ticker("MYCOIN"); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&utxo_ops, 4); + }, + } + + // MYCOIN1 (only for utxo pair tests - not needed by Sia) + #[cfg(any( + feature = "docker-tests-swaps", + feature = "docker-tests-ordermatch", + feature = "docker-tests-watchers", + feature = "docker-tests-qrc20", + feature = "docker-tests-integration" + ))] + { + match runner.config.mode { + DockerTestMode::Testcontainers => { + let node = utxo_asset_docker_node("MYCOIN1", 8001); + let utxo_ops1 = UtxoAssetDockerOps::from_ticker("MYCOIN1"); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&utxo_ops1, 4); + runner.hold(node); + }, + DockerTestMode::ComposeInit => { + setup_utxo_conf_for_compose("MYCOIN1", KDF_MYCOIN1_SERVICE); + let utxo_ops1 = UtxoAssetDockerOps::from_ticker("MYCOIN1"); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&utxo_ops1, 4); + }, + } + } +} diff --git a/mm2src/mm2_main/tests/docker_tests/runner/zcoin.rs b/mm2src/mm2_main/tests/docker_tests/runner/zcoin.rs new file mode 100644 index 0000000000..e5ba30ae33 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/runner/zcoin.rs @@ -0,0 +1,23 @@ +//! ZCoin/Zombie setup for docker tests. + +use super::{DockerTestMode, DockerTestRunner}; + +use crate::docker_tests::helpers::docker_ops::setup_utxo_conf_for_compose; +use crate::docker_tests::helpers::env::KDF_ZOMBIE_SERVICE; +use crate::docker_tests::helpers::zcoin::{zombie_asset_docker_node, ZCoinAssetDockerOps}; + +pub(super) fn setup(runner: &mut DockerTestRunner) { + match runner.config.mode { + DockerTestMode::Testcontainers => { + let node = zombie_asset_docker_node(7090); + let zombie_ops = ZCoinAssetDockerOps::new(); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&zombie_ops, 4); + runner.hold(node); + }, + DockerTestMode::ComposeInit => { + setup_utxo_conf_for_compose("ZOMBIE", KDF_ZOMBIE_SERVICE); + let zombie_ops = ZCoinAssetDockerOps::new(); + crate::docker_tests::helpers::docker_ops::CoinDockerOps::wait_ready(&zombie_ops, 4); + }, + } +} diff --git a/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs index 2770e58a92..fb09ba3add 100644 --- a/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/sia_docker_tests.rs @@ -59,8 +59,7 @@ fn test_sia_client_consensus_tip() { let _response = block_on(api_client.dispatcher(ConsensusTipRequest)).unwrap(); } -// This test likely needs to be removed because mine_blocks has possibility of interfering with other async tests -// related to block height +// Test that mining to an address results in visible balance. #[test] fn test_sia_client_address_balance() { let conf = SiaHttpConf { @@ -74,12 +73,15 @@ fn test_sia_client_address_balance() { Address::from_str("591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); block_on(api_client.mine_blocks(10, &address)).unwrap(); + // Wait briefly for the address indexer to process the new blocks. + // This is needed because the indexer may have lag, especially when + // tests run in parallel and the indexer is busy with other operations. + std::thread::sleep(std::time::Duration::from_millis(100)); + let request = AddressBalanceRequest { address }; let response = block_on(api_client.dispatcher(request)).unwrap(); - // It's hard to predict how much was mined to this address while other tests are also mining in the same network. - // Looks like the halving happens so quickly and the sum of mined coins change between different test runs. - // Just make sure we at least mined something. + // Check that we have some coins (either mature or immature) assert!(response.immature_siacoins + response.siacoins > Currency(0)); } diff --git a/mm2src/mm2_main/tests/docker_tests/slp_tests.rs b/mm2src/mm2_main/tests/docker_tests/slp_tests.rs index 4bc7223bae..7fd9c7930f 100644 --- a/mm2src/mm2_main/tests/docker_tests/slp_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/slp_tests.rs @@ -1,16 +1,100 @@ -use crate::docker_tests::docker_tests_common::*; +use crate::docker_tests::helpers::slp::{get_prefilled_slp_privkey, get_slp_token_id}; use crate::integration_tests_common::enable_native; +use bitcrypto::ChecksumType; +use coins::utxo::UtxoAddressFormat; +use common::block_on; use http::StatusCode; +use keys::{Address, AddressBuilder, AddressHashEnum, AddressPrefix, NetworkAddressPrefixes}; use mm2_number::BigDecimal; -use mm2_rpc::data::legacy::CoinInitResponse; +use mm2_rpc::data::legacy::{BalanceResponse, CoinInitResponse}; use mm2_test_helpers::for_tests::{ - assert_coin_not_found_on_balance, disable_coin, enable_bch_with_tokens, enable_slp, my_balance, UtxoRpcMode, + assert_coin_not_found_on_balance, disable_coin, enable_bch_with_tokens, enable_native_bch, enable_slp, my_balance, + MarketMakerIt, UtxoRpcMode, }; -use mm2_test_helpers::structs::{EnableBchWithTokensResponse, EnableSlpResponse, RpcV2Response}; +use mm2_test_helpers::structs::{EnableBchWithTokensResponse, EnableSlpResponse, RpcV2Response, TransactionDetails}; use serde_json::{self as json, json, Value as Json}; use std::collections::HashSet; +use std::thread; use std::time::Duration; +// ============================================================================ +// SLP-specific helper functions +// ============================================================================ + +fn slp_supplied_node() -> MarketMakerIt { + let coins = json! ([ + {"coin":"FORSLP","asset":"FORSLP","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"BCH","protocol_data":{"slp_prefix":"slptest"}}}, + {"coin":"ADEXSLP","protocol":{"type":"SLPTOKEN","protocol_data":{"decimals":8,"token_id":get_slp_token_id(),"platform":"FORSLP"}}} + ]); + + let priv_key = get_prefilled_slp_privkey(); + MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap() +} + +fn get_balance(mm: &MarketMakerIt, coin: &str) -> BalanceResponse { + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "my_balance", + "coin": coin, + }))) + .unwrap(); + assert_eq!(rc.0, StatusCode::OK, "my_balance request failed {}", rc.1); + json::from_str(&rc.1).unwrap() +} + +fn utxo_burn_address() -> Address { + AddressBuilder::new( + UtxoAddressFormat::Standard, + ChecksumType::DSHA256, + NetworkAddressPrefixes { + p2pkh: [60].into(), + p2sh: AddressPrefix::default(), + }, + None, + ) + .as_pkh(AddressHashEnum::default_address_hash()) + .build() + .expect("valid address props") +} + +fn withdraw_max_and_send_v1(mm: &MarketMakerIt, coin: &str, to: &str) -> TransactionDetails { + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "withdraw", + "coin": coin, + "max": true, + "to": to, + }))) + .unwrap(); + assert_eq!(rc.0, StatusCode::OK, "withdraw request failed {}", rc.1); + let tx_details: TransactionDetails = json::from_str(&rc.1).unwrap(); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "send_raw_transaction", + "tx_hex": tx_details.tx_hex, + "coin": coin, + }))) + .unwrap(); + assert_eq!(rc.0, StatusCode::OK, "send_raw_transaction request failed {}", rc.1); + + tx_details +} + async fn enable_bch_with_tokens_without_balance( mm: &MarketMakerIt, platform_coin: &str, @@ -46,16 +130,6 @@ async fn enable_bch_with_tokens_without_balance( json::from_str(&enable.1).unwrap() } -#[test] -fn trade_test_with_maker_slp() { - trade_base_rel(("ADEXSLP", "FORSLP")); -} - -#[test] -fn trade_test_with_taker_slp() { - trade_base_rel(("FORSLP", "ADEXSLP")); -} - #[test] fn test_bch_and_slp_balance() { // MM2 should mark the SLP-related and other UTXOs as unspendable BCH balance diff --git a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs index 0ced4f84d4..0069ce9c72 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs @@ -1,4 +1,5 @@ -use crate::{generate_utxo_coin_with_random_privkey, MYCOIN, MYCOIN1, SET_BURN_PUBKEY_TO_ALICE}; +use crate::docker_tests::helpers::env::SET_BURN_PUBKEY_TO_ALICE; +use crate::docker_tests::helpers::utxo::{generate_utxo_coin_with_random_privkey, MYCOIN, MYCOIN1}; use bitcrypto::dhash160; use coins::utxo::UtxoCommonOps; use coins::{ @@ -18,6 +19,7 @@ use mm2_test_helpers::for_tests::{ }; use mm2_test_helpers::structs::MmNumberMultiRepr; use script::{Builder, Opcode}; +use serde_json::json; use serialization::serialize; use std::time::Duration; use uuid::Uuid; diff --git a/mm2src/mm2_main/tests/docker_tests/swap_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_tests.rs new file mode 100644 index 0000000000..e50596fecf --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/swap_tests.rs @@ -0,0 +1,25 @@ +//! SLP platform swap tests. +//! +//! These tests exercise swaps where a platform coin and its SLP token are traded +//! against each other on the same underlying chain: +//! - `FORSLP` (BCH-like UTXO chain with SLP support) +//! - `ADEXSLP` (SLP token on `FORSLP`) +//! +//! This is not a multi-chain integration scenario; it only requires the `FORSLP` +//! docker node. + +use crate::docker_tests::helpers::swap::trade_base_rel; + +/// Test atomic swap with SLP token as maker coin. +/// Requires: FORSLP node only (both coins are on the same platform) +#[test] +fn trade_test_with_maker_slp() { + trade_base_rel(("ADEXSLP", "FORSLP")); +} + +/// Test atomic swap with SLP token as taker coin. +/// Requires: FORSLP node only (both coins are on the same platform) +#[test] +fn trade_test_with_taker_slp() { + trade_base_rel(("FORSLP", "ADEXSLP")); +} diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/eth.rs similarity index 59% rename from mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs rename to mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/eth.rs index 67799a3f30..4bc5989bee 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/eth.rs @@ -1,782 +1,9 @@ -use crate::docker_tests::docker_tests_common::GETH_RPC_URL; -use crate::docker_tests::eth_docker_tests::{ - erc20_coin_with_random_privkey, erc20_contract_checksum, eth_coin_with_random_privkey, watchers_swap_contract, -}; -use crate::integration_tests_common::*; -use crate::{generate_utxo_coin_with_privkey, generate_utxo_coin_with_random_privkey, random_secp256k1_secret}; -use coins::coin_errors::ValidatePaymentError; -use coins::eth::{checksum_address, EthCoin}; -use coins::utxo::utxo_standard::UtxoStandardCoin; -use coins::utxo::{dhash160, UtxoCommonOps}; -use coins::{ - ConfirmPaymentInput, DexFee, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, RewardTarget, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, - TestCoin, ValidateWatcherSpendInput, WatcherOps, WatcherSpendType, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, - INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, - INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG, OLD_TRANSACTION_ERR_LOG, -}; -use common::{block_on, block_on_f01, now_sec, wait_until_sec}; -use crypto::privkey::{key_pair_from_secret, key_pair_from_seed}; -use mm2_main::lp_swap::{ - generate_secret, get_payment_locktime, MAKER_PAYMENT_SENT_LOG, MAKER_PAYMENT_SPEND_FOUND_LOG, - MAKER_PAYMENT_SPEND_SENT_LOG, REFUND_TEST_FAILURE_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, WATCHER_MESSAGE_SENT_LOG, -}; -use mm2_number::BigDecimal; -use mm2_number::MmNumber; -use mm2_test_helpers::for_tests::{ - enable_eth_coin, erc20_dev_conf, eth_dev_conf, eth_jst_testnet_conf, mm_dump, my_balance, my_swap_status, - mycoin1_conf, mycoin_conf, start_swaps, wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2TestConf, - DEFAULT_RPC_PASSWORD, -}; -use mm2_test_helpers::get_passphrase; -use mm2_test_helpers::structs::WatcherConf; -use mocktopus::mocking::*; -use num_traits::{One, Zero}; -use primitives::hash::H256; -use serde_json::Value; -use std::str::FromStr; -use std::thread; -use std::time::Duration; -use uuid::Uuid; - -#[derive(Debug, Clone)] -struct BalanceResult { - alice_acoin_balance_before: BigDecimal, - alice_acoin_balance_middle: BigDecimal, - alice_acoin_balance_after: BigDecimal, - alice_bcoin_balance_before: BigDecimal, - alice_bcoin_balance_middle: BigDecimal, - alice_bcoin_balance_after: BigDecimal, - alice_eth_balance_middle: BigDecimal, - alice_eth_balance_after: BigDecimal, - bob_acoin_balance_before: BigDecimal, - bob_acoin_balance_after: BigDecimal, - bob_bcoin_balance_before: BigDecimal, - bob_bcoin_balance_after: BigDecimal, - watcher_acoin_balance_before: BigDecimal, - watcher_acoin_balance_after: BigDecimal, - watcher_bcoin_balance_before: BigDecimal, - watcher_bcoin_balance_after: BigDecimal, -} - -fn enable_coin(mm_node: &MarketMakerIt, coin: &str) { - if coin == "MYCOIN" { - log!("{:?}", block_on(enable_native(mm_node, coin, &[], None))); - } else { - enable_eth(mm_node, coin); - } -} - -fn enable_eth(mm_node: &MarketMakerIt, coin: &str) { - dbg!(block_on(enable_eth_coin( - mm_node, - coin, - &[GETH_RPC_URL], - &checksum_address(&format!("{:02x}", watchers_swap_contract())), - Some(&checksum_address(&format!("{:02x}", watchers_swap_contract()))), - true - ))); -} - -#[allow(clippy::enum_variant_names)] -enum SwapFlow { - WatcherSpendsMakerPayment, - WatcherRefundsTakerPayment, - TakerSpendsMakerPayment, -} - -#[allow(clippy::too_many_arguments)] -fn start_swaps_and_get_balances( - a_coin: &'static str, - b_coin: &'static str, - maker_price: f64, - taker_price: f64, - volume: f64, - envs: &[(&str, &str)], - swap_flow: SwapFlow, - alice_privkey: &str, - bob_privkey: &str, - watcher_privkey: &str, - custom_locktime: Option, -) -> BalanceResult { - let coins = json!([ - eth_dev_conf(), - erc20_dev_conf(&erc20_contract_checksum()), - mycoin_conf(1000), - mycoin1_conf(1000) - ]); - - let mut alice_conf = Mm2TestConf::seednode(&format!("0x{alice_privkey}"), &coins); - if let Some(locktime) = custom_locktime { - alice_conf.conf["payment_locktime"] = locktime.into(); - } - let mut mm_alice = block_on(MarketMakerIt::start_with_envs( - alice_conf.conf.clone(), - alice_conf.rpc_password.clone(), - None, - envs, - )) - .unwrap(); - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - - let mut bob_conf = Mm2TestConf::light_node(&format!("0x{bob_privkey}"), &coins, &[&mm_alice.ip.to_string()]); - if let Some(locktime) = custom_locktime { - bob_conf.conf["payment_locktime"] = locktime.into(); - } - let mut mm_bob = block_on(MarketMakerIt::start_with_envs( - bob_conf.conf.clone(), - bob_conf.rpc_password, - None, - envs, - )) - .unwrap(); - let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); - log!("Bob log path: {}", mm_bob.log_path.display()); - - generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(bob_privkey).unwrap()); - generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(alice_privkey).unwrap()); - generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(bob_privkey).unwrap()); - generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(alice_privkey).unwrap()); - - let (watcher_conf, watcher_log_to_wait) = match swap_flow { - SwapFlow::WatcherSpendsMakerPayment => ( - WatcherConf { - wait_taker_payment: 0., - wait_maker_payment_spend_factor: 0., - refund_start_factor: 1.5, - search_interval: 1.0, - }, - MAKER_PAYMENT_SPEND_SENT_LOG, - ), - SwapFlow::WatcherRefundsTakerPayment => ( - WatcherConf { - wait_taker_payment: 0., - wait_maker_payment_spend_factor: 1., - refund_start_factor: 0., - search_interval: 1., - }, - TAKER_PAYMENT_REFUND_SENT_LOG, - ), - SwapFlow::TakerSpendsMakerPayment => ( - WatcherConf { - wait_taker_payment: 0., - wait_maker_payment_spend_factor: 1., - refund_start_factor: 1.5, - search_interval: 1.0, - }, - MAKER_PAYMENT_SPEND_FOUND_LOG, - ), - }; - - let mut watcher_conf = Mm2TestConf::watcher_light_node( - &format!("0x{watcher_privkey}"), - &coins, - &[&mm_alice.ip.to_string()], - watcher_conf, - ) - .conf; - if let Some(locktime) = custom_locktime { - watcher_conf["payment_locktime"] = locktime.into(); - } - - let mut mm_watcher = block_on(MarketMakerIt::start_with_envs( - watcher_conf, - DEFAULT_RPC_PASSWORD.to_string(), - None, - envs, - )) - .unwrap(); - let (_watcher_dump_log, _watcher_dump_dashboard) = mm_dump(&mm_watcher.log_path); - log!("Watcher log path: {}", mm_watcher.log_path.display()); - - enable_coin(&mm_alice, a_coin); - enable_coin(&mm_alice, b_coin); - enable_coin(&mm_bob, a_coin); - enable_coin(&mm_bob, b_coin); - enable_coin(&mm_watcher, a_coin); - enable_coin(&mm_watcher, b_coin); - - if a_coin != "ETH" && b_coin != "ETH" { - enable_coin(&mm_alice, "ETH"); - } - - let alice_acoin_balance_before = block_on(my_balance(&mm_alice, a_coin)).balance; - let alice_bcoin_balance_before = block_on(my_balance(&mm_alice, b_coin)).balance; - let bob_acoin_balance_before = block_on(my_balance(&mm_bob, a_coin)).balance; - let bob_bcoin_balance_before = block_on(my_balance(&mm_bob, b_coin)).balance; - let watcher_acoin_balance_before = block_on(my_balance(&mm_watcher, a_coin)).balance; - let watcher_bcoin_balance_before = block_on(my_balance(&mm_watcher, b_coin)).balance; - - let mut alice_acoin_balance_middle = BigDecimal::zero(); - let mut alice_bcoin_balance_middle = BigDecimal::zero(); - let mut alice_eth_balance_middle = BigDecimal::zero(); - let mut bob_acoin_balance_after = BigDecimal::zero(); - let mut bob_bcoin_balance_after = BigDecimal::zero(); - - block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[(b_coin, a_coin)], - maker_price, - taker_price, - volume, - )); - - if matches!(swap_flow, SwapFlow::WatcherRefundsTakerPayment) { - block_on(mm_bob.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SENT_LOG))).unwrap(); - block_on(mm_bob.stop()).unwrap(); - } - if !matches!(swap_flow, SwapFlow::TakerSpendsMakerPayment) { - block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); - alice_acoin_balance_middle = block_on(my_balance(&mm_alice, a_coin)).balance; - alice_bcoin_balance_middle = block_on(my_balance(&mm_alice, b_coin)).balance; - alice_eth_balance_middle = block_on(my_balance(&mm_alice, "ETH")).balance; - block_on(mm_alice.stop()).unwrap(); - } - - block_on(mm_watcher.wait_for_log(120., |log| log.contains(watcher_log_to_wait))).unwrap(); - thread::sleep(Duration::from_secs(20)); - - let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); - enable_coin(&mm_alice, a_coin); - enable_coin(&mm_alice, b_coin); - - if a_coin != "ETH" && b_coin != "ETH" { - enable_coin(&mm_alice, "ETH"); - } - - let alice_acoin_balance_after = block_on(my_balance(&mm_alice, a_coin)).balance; - let alice_bcoin_balance_after = block_on(my_balance(&mm_alice, b_coin)).balance; - let alice_eth_balance_after = block_on(my_balance(&mm_alice, "ETH")).balance; - if !matches!(swap_flow, SwapFlow::WatcherRefundsTakerPayment) { - bob_acoin_balance_after = block_on(my_balance(&mm_bob, a_coin)).balance; - bob_bcoin_balance_after = block_on(my_balance(&mm_bob, b_coin)).balance; - } - let watcher_acoin_balance_after = block_on(my_balance(&mm_watcher, a_coin)).balance; - let watcher_bcoin_balance_after = block_on(my_balance(&mm_watcher, b_coin)).balance; - - BalanceResult { - alice_acoin_balance_before, - alice_acoin_balance_middle, - alice_acoin_balance_after, - alice_bcoin_balance_before, - alice_bcoin_balance_middle, - alice_bcoin_balance_after, - alice_eth_balance_middle, - alice_eth_balance_after, - bob_acoin_balance_before, - bob_acoin_balance_after, - bob_bcoin_balance_before, - bob_bcoin_balance_after, - watcher_acoin_balance_before, - watcher_acoin_balance_after, - watcher_bcoin_balance_before, - watcher_bcoin_balance_after, - } -} - -fn check_actual_events(mm_alice: &MarketMakerIt, uuid: &str, expected_events: &[&'static str]) -> Value { - let status_response = block_on(my_swap_status(mm_alice, uuid)).unwrap(); - let events_array = status_response["result"]["events"].as_array().unwrap(); - let actual_events = events_array.iter().map(|item| item["event"]["type"].as_str().unwrap()); - let actual_events: Vec<&str> = actual_events.collect(); - assert_eq!(expected_events, actual_events.as_slice()); - status_response -} - -fn run_taker_node( - coins: &Value, - envs: &[(&str, &str)], - seednodes: &[&str], - custom_locktime: Option, -) -> (MarketMakerIt, Mm2TestConf) { - let privkey = hex::encode(random_secp256k1_secret()); - let mut conf = Mm2TestConf::light_node(&format!("0x{privkey}"), coins, seednodes); - if let Some(locktime) = custom_locktime { - conf.conf["payment_locktime"] = locktime.into(); - } - let mm = block_on(MarketMakerIt::start_with_envs( - conf.conf.clone(), - conf.rpc_password.clone(), - None, - envs, - )) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("Log path: {}", mm.log_path.display()); - - generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(&privkey).unwrap()); - generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(&privkey).unwrap()); - enable_coin(&mm, "MYCOIN"); - enable_coin(&mm, "MYCOIN1"); - - (mm, conf) -} - -fn restart_taker_and_wait_until(conf: &Mm2TestConf, envs: &[(&str, &str)], wait_until: &str) -> MarketMakerIt { - let mut mm_alice = block_on(MarketMakerIt::start_with_envs( - conf.conf.clone(), - conf.rpc_password.clone(), - None, - envs, - )) - .unwrap(); - - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - enable_coin(&mm_alice, "MYCOIN"); - enable_coin(&mm_alice, "MYCOIN1"); - - block_on(mm_alice.wait_for_log(120., |log| log.contains(wait_until))).unwrap(); - mm_alice -} - -fn run_maker_node( - coins: &Value, - envs: &[(&str, &str)], - seednodes: &[&str], - custom_locktime: Option, -) -> MarketMakerIt { - let privkey = hex::encode(random_secp256k1_secret()); - let mut conf = if seednodes.is_empty() { - Mm2TestConf::seednode(&format!("0x{privkey}"), coins) - } else { - Mm2TestConf::light_node(&format!("0x{privkey}"), coins, seednodes) - }; - if let Some(locktime) = custom_locktime { - conf.conf["payment_locktime"] = locktime.into(); - } - let mm = block_on(MarketMakerIt::start_with_envs( - conf.conf.clone(), - conf.rpc_password, - None, - envs, - )) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("Log path: {}", mm.log_path.display()); - - generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(&privkey).unwrap()); - generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(&privkey).unwrap()); - enable_coin(&mm, "MYCOIN"); - enable_coin(&mm, "MYCOIN1"); - - mm -} - -fn run_watcher_node( - coins: &Value, - envs: &[(&str, &str)], - seednodes: &[&str], - watcher_conf: WatcherConf, - custom_locktime: Option, -) -> MarketMakerIt { - let privkey = hex::encode(random_secp256k1_secret()); - let mut conf = Mm2TestConf::watcher_light_node(&format!("0x{privkey}"), coins, seednodes, watcher_conf).conf; - if let Some(locktime) = custom_locktime { - conf["payment_locktime"] = locktime.into(); - } - let mm = block_on(MarketMakerIt::start_with_envs( - conf, - DEFAULT_RPC_PASSWORD.to_string(), - None, - envs, - )) - .unwrap(); - let (_dump_log, _dump_dashboard) = mm.mm_dump(); - log!("Log path: {}", mm.log_path.display()); - - generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(&privkey).unwrap()); - generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(&privkey).unwrap()); - enable_coin(&mm, "MYCOIN"); - enable_coin(&mm, "MYCOIN1"); - - mm -} - -#[test] -fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker_payment_spend() { - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[], None); - let (mut mm_alice, mut alice_conf) = run_taker_node( - &coins, - &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], - &[&mm_bob.ip.to_string()], - None, - ); - - let watcher_conf = WatcherConf { - wait_taker_payment: 0., - wait_maker_payment_spend_factor: 0., - refund_start_factor: 1.5, - search_interval: 1.0, - }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); - - let uuids = block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("MYCOIN1", "MYCOIN")], - 25., - 25., - 2., - )); - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); - - block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); - block_on(mm_bob.wait_for_log(120., |log| log.contains(&format!("[swap uuid={}] Finished", &uuids[0])))).unwrap(); - block_on(mm_watcher.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SPEND_SENT_LOG))).unwrap(); - - block_on(mm_alice.stop()).unwrap(); - - let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); - - let expected_events = [ - "Started", - "Negotiated", - "TakerFeeSent", - "TakerPaymentInstructionsReceived", - "MakerPaymentReceived", - "MakerPaymentWaitConfirmStarted", - "MakerPaymentValidatedAndConfirmed", - "TakerPaymentSent", - "WatcherMessageSent", - "TakerPaymentSpent", - "MakerPaymentSpentByWatcher", - "MakerPaymentSpendConfirmed", - "Finished", - ]; - check_actual_events(&mm_alice, &uuids[0], &expected_events); - - block_on(mm_alice.stop()).unwrap(); - block_on(mm_watcher.stop()).unwrap(); - block_on(mm_bob.stop()).unwrap(); -} - -#[test] -fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_spend() { - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[], None); - let (mut mm_alice, mut alice_conf) = run_taker_node( - &coins, - &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], - &[&mm_bob.ip.to_string()], - None, - ); - - let watcher_conf = WatcherConf { - wait_taker_payment: 0., - wait_maker_payment_spend_factor: 0., - refund_start_factor: 1.5, - search_interval: 1.0, - }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); - - let uuids = block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("MYCOIN1", "MYCOIN")], - 25., - 25., - 2., - )); - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); - - block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); - block_on(mm_bob.wait_for_log(120., |log| log.contains(&format!("[swap uuid={}] Finished", &uuids[0])))).unwrap(); - block_on(mm_watcher.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SPEND_SENT_LOG))).unwrap(); - - block_on(mm_alice.stop()).unwrap(); - - let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); - - let expected_events = [ - "Started", - "Negotiated", - "TakerFeeSent", - "TakerPaymentInstructionsReceived", - "MakerPaymentReceived", - "MakerPaymentWaitConfirmStarted", - "MakerPaymentValidatedAndConfirmed", - "TakerPaymentSent", - "WatcherMessageSent", - "TakerPaymentSpent", - "MakerPaymentSpentByWatcher", - "MakerPaymentSpendConfirmed", - "Finished", - ]; - check_actual_events(&mm_alice, &uuids[0], &expected_events); -} - -#[test] -fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_wait_for_taker_payment_spend() { - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); - let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); - let (mut mm_alice, mut alice_conf) = run_taker_node( - &coins, - &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], - &[&mm_seednode.ip.to_string()], - Some(60), - ); - - let watcher_conf = WatcherConf { - wait_taker_payment: 0., - wait_maker_payment_spend_factor: 1., - refund_start_factor: 0., - search_interval: 1., - }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); - - let uuids = block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("MYCOIN1", "MYCOIN")], - 25., - 25., - 2., - )); - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); - - block_on(mm_bob.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SENT_LOG))).unwrap(); - block_on(mm_bob.stop()).unwrap(); - - block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); - block_on(mm_watcher.wait_for_log(120., |log| log.contains(TAKER_PAYMENT_REFUND_SENT_LOG))).unwrap(); - - block_on(mm_alice.stop()).unwrap(); - - let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); - - let expected_events = [ - "Started", - "Negotiated", - "TakerFeeSent", - "TakerPaymentInstructionsReceived", - "MakerPaymentReceived", - "MakerPaymentWaitConfirmStarted", - "MakerPaymentValidatedAndConfirmed", - "TakerPaymentSent", - "WatcherMessageSent", - "TakerPaymentRefundedByWatcher", - "Finished", - ]; - check_actual_events(&mm_alice, &uuids[0], &expected_events); - - block_on(mm_alice.stop()).unwrap(); - block_on(mm_watcher.stop()).unwrap(); - block_on(mm_seednode.stop()).unwrap(); -} - -#[test] -fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_taker_payment_refund() { - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); - let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); - let (mut mm_alice, mut alice_conf) = run_taker_node( - &coins, - &[("TAKER_FAIL_AT", "taker_payment_refund_panic")], - &[&mm_seednode.ip.to_string()], - Some(60), - ); - - let watcher_conf = WatcherConf { - wait_taker_payment: 0., - wait_maker_payment_spend_factor: 1., - refund_start_factor: 0., - search_interval: 1., - }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); - - let uuids = block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("MYCOIN1", "MYCOIN")], - 25., - 25., - 2., - )); - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); - - block_on(mm_bob.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SENT_LOG))).unwrap(); - block_on(mm_bob.stop()).unwrap(); - - block_on(mm_alice.wait_for_log(120., |log| log.contains(REFUND_TEST_FAILURE_LOG))).unwrap(); - block_on(mm_watcher.wait_for_log(120., |log| log.contains(TAKER_PAYMENT_REFUND_SENT_LOG))).unwrap(); - - block_on(mm_alice.stop()).unwrap(); - - let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); - - let expected_events = [ - "Started", - "Negotiated", - "TakerFeeSent", - "TakerPaymentInstructionsReceived", - "MakerPaymentReceived", - "MakerPaymentWaitConfirmStarted", - "MakerPaymentValidatedAndConfirmed", - "TakerPaymentSent", - "WatcherMessageSent", - "TakerPaymentWaitForSpendFailed", - "TakerPaymentWaitRefundStarted", - "TakerPaymentRefundStarted", - "TakerPaymentRefundedByWatcher", - "Finished", - ]; - check_actual_events(&mm_alice, &uuids[0], &expected_events); - - block_on(mm_alice.stop()).unwrap(); - block_on(mm_watcher.stop()).unwrap(); - block_on(mm_seednode.stop()).unwrap(); -} - -#[test] -fn test_taker_completes_swap_after_restart() { - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[], None); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); - - let uuids = block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("MYCOIN1", "MYCOIN")], - 25., - 25., - 2., - )); - - block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); - alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); - block_on(mm_alice.stop()).unwrap(); - - let mut mm_alice = block_on(MarketMakerIt::start_with_envs( - alice_conf.conf, - alice_conf.rpc_password.clone(), - None, - &[], - )) - .unwrap(); - - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - enable_coin(&mm_alice, "MYCOIN"); - enable_coin(&mm_alice, "MYCOIN1"); - - block_on(wait_for_swaps_finish_and_check_status( - &mut mm_bob, - &mut mm_alice, - &uuids, - 2., - 25., - )); - - block_on(mm_alice.stop()).unwrap(); - block_on(mm_bob.stop()).unwrap(); -} - -// Verifies https://github.com/KomodoPlatform/komodo-defi-framework/issues/2111 -#[test] -fn test_taker_completes_swap_after_taker_payment_spent_while_offline() { - let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[], None); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); - - let uuids = block_on(start_swaps( - &mut mm_bob, - &mut mm_alice, - &[("MYCOIN1", "MYCOIN")], - 25., - 25., - 2., - )); - - // stop taker after taker payment sent - let taker_payment_msg = "Taker payment tx hash "; - block_on(mm_alice.wait_for_log(120., |log| log.contains(taker_payment_msg))).unwrap(); - // ensure p2p message is sent to the maker, this happens before this message: - block_on(mm_alice.wait_for_log(120., |log| log.contains("Waiting for maker to spend taker payment!"))).unwrap(); - alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); - block_on(mm_alice.stop()).unwrap(); - - // wait for taker payment spent by maker - block_on(mm_bob.wait_for_log(120., |log| log.contains("Taker payment spend tx"))).unwrap(); - // and restart taker - let mut mm_alice = block_on(MarketMakerIt::start_with_envs( - alice_conf.conf, - alice_conf.rpc_password.clone(), - None, - &[], - )) - .unwrap(); - - let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); - log!("Alice log path: {}", mm_alice.log_path.display()); - enable_coin(&mm_alice, "MYCOIN"); - enable_coin(&mm_alice, "MYCOIN1"); - - block_on(wait_for_swaps_finish_and_check_status( - &mut mm_bob, - &mut mm_alice, - &uuids, - 2., - 25., - )); - - block_on(mm_alice.stop()).unwrap(); - block_on(mm_bob.stop()).unwrap(); -} - -#[test] -fn test_watcher_spends_maker_payment_utxo_utxo() { - let alice_privkey = hex::encode(random_secp256k1_secret()); - let bob_privkey = hex::encode(random_secp256k1_secret()); - let watcher_privkey = hex::encode(random_secp256k1_secret()); - - let balances = start_swaps_and_get_balances( - "MYCOIN", - "MYCOIN1", - 25., - 25., - 2., - &[], - SwapFlow::WatcherSpendsMakerPayment, - &alice_privkey, - &bob_privkey, - &watcher_privkey, - None, - ); +//! ETH/ERC20 Watcher Tests +//! +//! These tests are disabled by default because ETH watchers are unstable +//! and not completed yet. Enable with feature `docker-tests-watchers-eth`. - let acoin_volume = BigDecimal::from_str("50").unwrap(); - let bcoin_volume = BigDecimal::from_str("2").unwrap(); - - assert_eq!( - balances.alice_acoin_balance_after.round(0), - balances.alice_acoin_balance_before - acoin_volume.clone() - ); - assert_eq!( - balances.alice_bcoin_balance_after.round(0), - balances.alice_bcoin_balance_before + bcoin_volume.clone() - ); - assert_eq!( - balances.bob_acoin_balance_after.round(0), - balances.bob_acoin_balance_before + acoin_volume - ); - assert_eq!( - balances.bob_bcoin_balance_after.round(0), - balances.bob_bcoin_balance_before - bcoin_volume - ); -} +use super::*; #[test] fn test_watcher_spends_maker_payment_utxo_eth() { @@ -1003,33 +230,6 @@ fn test_watcher_spends_maker_payment_erc20_utxo() { ); } -#[test] -fn test_watcher_refunds_taker_payment_utxo() { - let alice_privkey = &hex::encode(random_secp256k1_secret()); - let bob_privkey = &hex::encode(random_secp256k1_secret()); - let watcher_privkey = &hex::encode(random_secp256k1_secret()); - - let balances = start_swaps_and_get_balances( - "MYCOIN1", - "MYCOIN", - 25., - 25., - 2., - &[], - SwapFlow::WatcherRefundsTakerPayment, - alice_privkey, - bob_privkey, - watcher_privkey, - Some(60), - ); - - assert_eq!( - balances.alice_acoin_balance_after.round(0), - balances.alice_acoin_balance_before - ); - assert_eq!(balances.alice_bcoin_balance_after, balances.alice_bcoin_balance_before); -} - #[test] fn test_watcher_refunds_taker_payment_eth() { let alice_coin = eth_coin_with_random_privkey(watchers_swap_contract()); @@ -1086,27 +286,6 @@ fn test_watcher_refunds_taker_payment_erc20() { assert!(balances.watcher_bcoin_balance_after > balances.watcher_bcoin_balance_before); } -#[test] -fn test_watcher_waits_for_taker_utxo() { - let alice_privkey = &hex::encode(random_secp256k1_secret()); - let bob_privkey = &hex::encode(random_secp256k1_secret()); - let watcher_privkey = &hex::encode(random_secp256k1_secret()); - - start_swaps_and_get_balances( - "MYCOIN1", - "MYCOIN", - 25., - 25., - 2., - &[], - SwapFlow::TakerSpendsMakerPayment, - alice_privkey, - bob_privkey, - watcher_privkey, - None, - ); -} - #[test] fn test_watcher_waits_for_taker_eth() { let alice_coin = erc20_coin_with_random_privkey(watchers_swap_contract()); @@ -1220,25 +399,22 @@ fn test_two_watchers_spend_maker_payment_eth_erc20() { assert_eq!(bob_jst_balance_before + volume.clone(), bob_jst_balance_after); assert_eq!(alice_eth_balance_before + volume.clone(), alice_eth_balance_after); assert_eq!(bob_eth_balance_before - volume, bob_eth_balance_after); - if watcher1_eth_balance_after > watcher1_eth_balance_before { - assert_eq!(watcher2_eth_balance_after, watcher2_eth_balance_after); - } - if watcher2_eth_balance_after > watcher2_eth_balance_before { - assert_eq!(watcher1_eth_balance_after, watcher1_eth_balance_after); - } + let w1_gain = watcher1_eth_balance_after > watcher1_eth_balance_before; + let w2_gain = watcher2_eth_balance_after > watcher2_eth_balance_before; + assert_ne!(w1_gain, w2_gain, "exactly one watcher must receive the reward"); } #[test] -fn test_watcher_validate_taker_fee_utxo() { +fn test_watcher_validate_taker_fee_eth() { let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run let lock_duration = get_payment_locktime(); - let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let taker_pubkey = taker_coin.my_public_key().unwrap(); - let taker_amount = MmNumber::from((10, 1)); - let dex_fee = DexFee::new_from_taker_coin(&taker_coin, maker_coin.ticker(), &taker_amount); + let taker_coin = eth_coin_with_random_privkey(watchers_swap_contract()); + let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); + let taker_pubkey = taker_keypair.public(); + let taker_amount = MmNumber::from((1, 1)); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, "ETH", &taker_amount); let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { @@ -1248,7 +424,6 @@ fn test_watcher_validate_taker_fee_utxo() { wait_until: timeout, check_every: 1, }; - block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); let validate_taker_fee_res = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { @@ -1259,9 +434,10 @@ fn test_watcher_validate_taker_fee_utxo() { })); assert!(validate_taker_fee_res.is_ok()); + let wrong_keypair = key_pair_from_secret(&random_secp256k1_secret().take()).unwrap(); let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: maker_coin.my_public_key().unwrap().to_vec(), + sender_pubkey: wrong_keypair.public().to_vec(), min_block_number: 0, lock_duration, })) @@ -1292,357 +468,48 @@ fn test_watcher_validate_taker_fee_utxo() { _ => panic!( "Expected `WrongPaymentTx` confirmed before min_block, found {:?}", error - ), - } - - let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: taker_pubkey.to_vec(), - min_block_number: 0, - lock_duration: 0, - })) - .unwrap_err() - .into_inner(); - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(OLD_TRANSACTION_ERR_LOG)) - }, - _ => panic!("Expected `WrongPaymentTx` transaction too old, found {:?}", error), - } - - let mock_pubkey = taker_pubkey.to_vec(); - ::dex_pubkey - .mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); - - let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: taker_pubkey.to_vec(), - min_block_number: 0, - lock_duration, - })) - .unwrap_err() - .into_inner(); - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_RECEIVER_ERR_LOG)) - }, - _ => panic!( - "Expected `WrongPaymentTx` tx output script_pubkey doesn't match expected, found {:?}", - error - ), - } -} - -#[test] -fn test_watcher_validate_taker_fee_eth() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let lock_duration = get_payment_locktime(); - - let taker_coin = eth_coin_with_random_privkey(watchers_swap_contract()); - let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); - let taker_pubkey = taker_keypair.public(); - - let taker_amount = MmNumber::from((1, 1)); - let dex_fee = DexFee::new_from_taker_coin(&taker_coin, "ETH", &taker_amount); - let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: taker_fee.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let validate_taker_fee_res = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: taker_pubkey.to_vec(), - min_block_number: 0, - lock_duration, - })); - assert!(validate_taker_fee_res.is_ok()); - - let wrong_keypair = key_pair_from_secret(&random_secp256k1_secret().take()).unwrap(); - let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: wrong_keypair.public().to_vec(), - min_block_number: 0, - lock_duration, - })) - .unwrap_err() - .into_inner(); - - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_SENDER_ERR_LOG)) - }, - _ => panic!("Expected `WrongPaymentTx` invalid public key, found {:?}", error), - } - - let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: taker_pubkey.to_vec(), - min_block_number: u64::MAX, - lock_duration, - })) - .unwrap_err() - .into_inner(); - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(EARLY_CONFIRMATION_ERR_LOG)) - }, - _ => panic!( - "Expected `WrongPaymentTx` confirmed before min_block, found {:?}", - error - ), - } - - let mock_pubkey = taker_pubkey.to_vec(); - ::dex_pubkey.mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); - - let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: taker_pubkey.to_vec(), - min_block_number: 0, - lock_duration, - })) - .unwrap_err() - .into_inner(); - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_RECEIVER_ERR_LOG)) - }, - _ => panic!( - "Expected `WrongPaymentTx` tx output script_pubkey doesn't match expected, found {:?}", - error - ), - } - ::dex_pubkey.clear_mock(); -} - -#[test] -fn test_watcher_validate_taker_fee_erc20() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let lock_duration = get_payment_locktime(); - - let taker_coin = erc20_coin_with_random_privkey(watchers_swap_contract()); - let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); - let taker_pubkey = taker_keypair.public(); - - let taker_amount = MmNumber::from((1, 1)); - let dex_fee = DexFee::new_from_taker_coin(&taker_coin, "ETH", &taker_amount); - let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: taker_fee.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let validate_taker_fee_res = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: taker_pubkey.to_vec(), - min_block_number: 0, - lock_duration, - })); - assert!(validate_taker_fee_res.is_ok()); - - let wrong_keypair = key_pair_from_secret(&random_secp256k1_secret().take()).unwrap(); - let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: wrong_keypair.public().to_vec(), - min_block_number: 0, - lock_duration, - })) - .unwrap_err() - .into_inner(); - - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_SENDER_ERR_LOG)) - }, - _ => panic!("Expected `WrongPaymentTx` invalid public key, found {:?}", error), - } - - let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: taker_pubkey.to_vec(), - min_block_number: u64::MAX, - lock_duration, - })) - .unwrap_err() - .into_inner(); - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(EARLY_CONFIRMATION_ERR_LOG)) - }, - _ => panic!( - "Expected `WrongPaymentTx` confirmed before min_block, found {:?}", - error - ), - } - - let mock_pubkey = taker_pubkey.to_vec(); - ::dex_pubkey.mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); - - let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), - sender_pubkey: taker_pubkey.to_vec(), - min_block_number: 0, - lock_duration, - })) - .unwrap_err() - .into_inner(); - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_RECEIVER_ERR_LOG)) - }, - _ => panic!( - "Expected `WrongPaymentTx` tx output script_pubkey doesn't match expected, found {:?}", - error - ), - } - ::dex_pubkey.clear_mock(); -} - -#[test] -fn test_watcher_validate_taker_payment_utxo() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let time_lock_duration = get_payment_locktime(); - let wait_for_confirmation_until = wait_until_sec(time_lock_duration); - let time_lock = wait_for_confirmation_until; - - let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let taker_pubkey = taker_coin.my_public_key().unwrap(); - - let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let maker_pubkey = maker_coin.my_public_key().unwrap(); - - let secret_hash = dhash160(&generate_secret().unwrap()); - - let taker_payment = block_on(taker_coin.send_taker_payment(SendPaymentArgs { - time_lock_duration, - time_lock, - other_pubkey: maker_pubkey, - secret_hash: secret_hash.as_slice(), - amount: BigDecimal::from(10), - swap_contract_address: &None, - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until, - })) - .unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: taker_payment.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let taker_payment_refund_preimage = block_on_f01(taker_coin.create_taker_payment_refund_preimage( - &taker_payment.tx_hex(), - time_lock, - maker_pubkey, - secret_hash.as_slice(), - &None, - &[], - )) - .unwrap(); - let validate_taker_payment_res = - block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { - payment_tx: taker_payment.tx_hex(), - taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), - time_lock, - taker_pub: taker_pubkey.to_vec(), - maker_pub: maker_pubkey.to_vec(), - secret_hash: secret_hash.to_vec(), - wait_until: timeout, - confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), - })); - assert!(validate_taker_payment_res.is_ok()); - - let error = block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { - payment_tx: taker_payment.tx_hex(), - taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), - time_lock, - taker_pub: maker_pubkey.to_vec(), - maker_pub: maker_pubkey.to_vec(), - secret_hash: secret_hash.to_vec(), - wait_until: timeout, - confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), - })) - .unwrap_err() - .into_inner(); - - log!("error: {:?}", error); - match error { - ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_SENDER_ERR_LOG)) - }, - _ => panic!("Expected `WrongPaymentTx` {INVALID_SENDER_ERR_LOG}, found {:?}", error), - } - - // Used to get wrong swap id - let wrong_secret_hash = dhash160(&generate_secret().unwrap()); - let error = block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { - payment_tx: taker_payment.tx_hex(), - taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), - time_lock, - taker_pub: taker_pubkey.to_vec(), - maker_pub: maker_pubkey.to_vec(), - secret_hash: wrong_secret_hash.to_vec(), - wait_until: timeout, - confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), + ), + } + + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey.mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); + + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: taker_pubkey.to_vec(), + min_block_number: 0, + lock_duration, })) .unwrap_err() .into_inner(); - log!("error: {:?}", error); match error { ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_SCRIPT_ERR_LOG)) + assert!(err.contains(INVALID_RECEIVER_ERR_LOG)) }, _ => panic!( - "Expected `WrongPaymentTx` {}, found {:?}", - INVALID_SCRIPT_ERR_LOG, error + "Expected `WrongPaymentTx` tx output script_pubkey doesn't match expected, found {:?}", + error ), } + ::dex_pubkey.clear_mock(); +} - let taker_payment_wrong_secret = block_on(taker_coin.send_taker_payment(SendPaymentArgs { - time_lock_duration, - time_lock, - other_pubkey: maker_pubkey, - secret_hash: wrong_secret_hash.as_slice(), - amount: BigDecimal::from(10), - swap_contract_address: &taker_coin.swap_contract_address(), - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until, - })) - .unwrap(); +#[test] +fn test_watcher_validate_taker_fee_erc20() { + let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run + let lock_duration = get_payment_locktime(); + + let taker_coin = erc20_coin_with_random_privkey(watchers_swap_contract()); + let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); + let taker_pubkey = taker_keypair.public(); + + let taker_amount = MmNumber::from((1, 1)); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, "ETH", &taker_amount); + let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { - payment_tx: taker_payment_wrong_secret.tx_hex(), + payment_tx: taker_fee.tx_hex(), confirmations: 1, requires_nota: false, wait_until: timeout, @@ -1650,16 +517,20 @@ fn test_watcher_validate_taker_payment_utxo() { }; block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - let error = block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { - payment_tx: taker_payment.tx_hex(), - taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), - time_lock: 500, - taker_pub: taker_pubkey.to_vec(), - maker_pub: maker_pubkey.to_vec(), - secret_hash: wrong_secret_hash.to_vec(), - wait_until: timeout, - confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), + let validate_taker_fee_res = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: taker_pubkey.to_vec(), + min_block_number: 0, + lock_duration, + })); + assert!(validate_taker_fee_res.is_ok()); + + let wrong_keypair = key_pair_from_secret(&random_secp256k1_secret().take()).unwrap(); + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: wrong_keypair.public().to_vec(), + min_block_number: 0, + lock_duration, })) .unwrap_err() .into_inner(); @@ -1667,48 +538,52 @@ fn test_watcher_validate_taker_payment_utxo() { log!("error: {:?}", error); match error { ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_SCRIPT_ERR_LOG)) + assert!(err.contains(INVALID_SENDER_ERR_LOG)) + }, + _ => panic!("Expected `WrongPaymentTx` invalid public key, found {:?}", error), + } + + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: taker_pubkey.to_vec(), + min_block_number: u64::MAX, + lock_duration, + })) + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(EARLY_CONFIRMATION_ERR_LOG)) }, _ => panic!( - "Expected `WrongPaymentTx` {}, found {:?}", - INVALID_SCRIPT_ERR_LOG, error + "Expected `WrongPaymentTx` confirmed before min_block, found {:?}", + error ), } - let wrong_taker_payment_refund_preimage = block_on_f01(taker_coin.create_taker_payment_refund_preimage( - &taker_payment.tx_hex(), - time_lock, - maker_pubkey, - wrong_secret_hash.as_slice(), - &None, - &[], - )) - .unwrap(); + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey.mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); - let error = block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { - payment_tx: taker_payment.tx_hex(), - taker_payment_refund_preimage: wrong_taker_payment_refund_preimage.tx_hex(), - time_lock, - taker_pub: taker_pubkey.to_vec(), - maker_pub: maker_pubkey.to_vec(), - secret_hash: secret_hash.to_vec(), - wait_until: timeout, - confirmations: 1, - maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: taker_pubkey.to_vec(), + min_block_number: 0, + lock_duration, })) .unwrap_err() .into_inner(); - log!("error: {:?}", error); match error { ValidatePaymentError::WrongPaymentTx(err) => { - assert!(err.contains(INVALID_REFUND_TX_ERR_LOG)) + assert!(err.contains(INVALID_RECEIVER_ERR_LOG)) }, _ => panic!( - "Expected `WrongPaymentTx` {}, found {:?}", - INVALID_REFUND_TX_ERR_LOG, error + "Expected `WrongPaymentTx` tx output script_pubkey doesn't match expected, found {:?}", + error ), } + ::dex_pubkey.clear_mock(); } #[test] @@ -2173,80 +1048,6 @@ fn test_watcher_validate_taker_payment_erc20() { } } -#[test] -fn test_taker_validates_taker_payment_refund_utxo() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let time_lock_duration = get_payment_locktime(); - let wait_for_confirmation_until = wait_until_sec(time_lock_duration); - let time_lock = now_sec() - 10; - - let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let maker_pubkey = maker_coin.my_public_key().unwrap(); - - let secret_hash = dhash160(&generate_secret().unwrap()); - - let taker_payment = block_on(taker_coin.send_taker_payment(SendPaymentArgs { - time_lock_duration, - time_lock, - other_pubkey: maker_pubkey, - secret_hash: secret_hash.as_slice(), - amount: BigDecimal::from(10), - swap_contract_address: &None, - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until, - })) - .unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: taker_payment.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let taker_payment_refund_preimage = block_on_f01(taker_coin.create_taker_payment_refund_preimage( - &taker_payment.tx_hex(), - time_lock, - maker_pubkey, - secret_hash.as_slice(), - &None, - &[], - )) - .unwrap(); - - let taker_payment_refund = block_on_f01(taker_coin.send_taker_payment_refund_preimage(RefundPaymentArgs { - payment_tx: &taker_payment_refund_preimage.tx_hex(), - other_pubkey: maker_pubkey, - tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { - maker_secret_hash: secret_hash.as_slice(), - }, - time_lock, - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - })) - .unwrap(); - - let validate_input = ValidateWatcherSpendInput { - payment_tx: taker_payment_refund.tx_hex(), - maker_pub: maker_pubkey.to_vec(), - swap_contract_address: None, - time_lock, - secret_hash: secret_hash.to_vec(), - amount: BigDecimal::from(10), - watcher_reward: None, - spend_type: WatcherSpendType::TakerPaymentRefund, - }; - - let validate_watcher_refund = block_on_f01(taker_coin.taker_validates_payment_spend_or_refund(validate_input)); - assert!(validate_watcher_refund.is_ok()); -} - #[test] fn test_taker_validates_taker_payment_refund_eth() { let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run @@ -2662,79 +1463,6 @@ fn test_taker_validates_taker_payment_refund_erc20() { } } -#[test] -fn test_taker_validates_maker_payment_spend_utxo() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let time_lock_duration = get_payment_locktime(); - let wait_for_confirmation_until = wait_until_sec(time_lock_duration); - let time_lock = wait_for_confirmation_until; - - let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let taker_pubkey = taker_coin.my_public_key().unwrap(); - let maker_pubkey = maker_coin.my_public_key().unwrap(); - - let secret = generate_secret().unwrap(); - let secret_hash = dhash160(&secret); - - let maker_payment = block_on(maker_coin.send_maker_payment(SendPaymentArgs { - time_lock_duration, - time_lock, - other_pubkey: taker_pubkey, - secret_hash: secret_hash.as_slice(), - amount: BigDecimal::from(10), - swap_contract_address: &None, - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until, - })) - .unwrap(); - - block_on_f01(maker_coin.wait_for_confirmations(ConfirmPaymentInput { - payment_tx: maker_payment.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - })) - .unwrap(); - - let maker_payment_spend_preimage = block_on_f01(taker_coin.create_maker_payment_spend_preimage( - &maker_payment.tx_hex(), - time_lock, - maker_pubkey, - secret_hash.as_slice(), - &[], - )) - .unwrap(); - - let maker_payment_spend = block_on_f01(taker_coin.send_maker_payment_spend_preimage( - SendMakerPaymentSpendPreimageInput { - preimage: &maker_payment_spend_preimage.tx_hex(), - secret_hash: secret_hash.as_slice(), - secret: secret.as_slice(), - taker_pub: taker_pubkey, - watcher_reward: false, - }, - )) - .unwrap(); - - let validate_input = ValidateWatcherSpendInput { - payment_tx: maker_payment_spend.tx_hex(), - maker_pub: maker_pubkey.to_vec(), - swap_contract_address: None, - time_lock, - secret_hash: secret_hash.to_vec(), - amount: BigDecimal::from(10), - watcher_reward: None, - spend_type: WatcherSpendType::TakerPaymentRefund, - }; - - let validate_watcher_spend = block_on_f01(taker_coin.taker_validates_payment_spend_or_refund(validate_input)); - assert!(validate_watcher_spend.is_ok()); -} - #[test] fn test_taker_validates_maker_payment_spend_eth() { let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run @@ -3156,84 +1884,6 @@ fn test_taker_validates_maker_payment_spend_erc20() { }; } -#[test] -fn test_send_taker_payment_refund_preimage_utxo() { - let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); - let my_public_key = coin.my_public_key().unwrap(); - - let time_lock = now_sec() - 3600; - let taker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: my_public_key, - secret_hash: &[0; 20], - amount: 1u64.into(), - swap_contract_address: &None, - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = block_on(coin.send_taker_payment(taker_payment_args)).unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let refund_tx = block_on_f01(coin.create_taker_payment_refund_preimage( - &tx.tx_hex(), - time_lock, - my_public_key, - &[0; 20], - &None, - &[], - )) - .unwrap(); - - let refund_tx = block_on_f01(coin.send_taker_payment_refund_preimage(RefundPaymentArgs { - payment_tx: &refund_tx.tx_hex(), - swap_contract_address: &None, - tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { - maker_secret_hash: &[0; 20], - }, - other_pubkey: my_public_key, - time_lock, - swap_unique_data: &[], - watcher_reward: false, - })) - .unwrap(); - - let confirm_payment_input = ConfirmPaymentInput { - payment_tx: refund_tx.tx_hex(), - confirmations: 1, - requires_nota: false, - wait_until: timeout, - check_every: 1, - }; - block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); - - let search_input = SearchForSwapTxSpendInput { - time_lock, - other_pub: coin.my_public_key().unwrap(), - secret_hash: &[0; 20], - tx: &tx.tx_hex(), - search_from_block: 0, - swap_contract_address: &None, - swap_unique_data: &[], - watcher_reward: false, - }; - let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) - .unwrap() - .unwrap(); - assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); -} - #[test] fn test_watcher_reward() { let timeout = wait_until_sec(300); // timeout if test takes more than 300 seconds to run diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/mod.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/mod.rs new file mode 100644 index 0000000000..f18f401f48 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/mod.rs @@ -0,0 +1,460 @@ +//! Swap Watcher Tests +//! +//! Shared helpers for watcher tests. UTXO tests are always enabled, +//! ETH/ERC20 tests require the `docker-tests-watchers-eth` feature. + +// UTXO watcher tests - always enabled with docker-tests-watchers +mod utxo; + +// ETH/ERC20 watcher tests - disabled by default (unstable, not completed yet) +#[cfg(feature = "docker-tests-watchers-eth")] +mod eth; + +// Common imports (used by UTXO watcher tests) +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::utxo::{generate_utxo_coin_with_privkey, generate_utxo_coin_with_random_privkey}; +use crate::integration_tests_common::*; +use coins::coin_errors::ValidatePaymentError; +use coins::utxo::utxo_standard::UtxoStandardCoin; +use coins::utxo::{dhash160, UtxoCommonOps}; +use coins::{ + ConfirmPaymentInput, DexFee, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, + ValidateWatcherSpendInput, WatcherOps, WatcherSpendType, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, + INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG, +}; +use common::{block_on, block_on_f01, now_sec, wait_until_sec}; +use mm2_main::lp_swap::{ + generate_secret, get_payment_locktime, MAKER_PAYMENT_SENT_LOG, MAKER_PAYMENT_SPEND_FOUND_LOG, + MAKER_PAYMENT_SPEND_SENT_LOG, REFUND_TEST_FAILURE_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, WATCHER_MESSAGE_SENT_LOG, +}; +use mm2_number::BigDecimal; +use mm2_number::MmNumber; +use mm2_test_helpers::for_tests::{ + mm_dump, my_balance, my_swap_status, mycoin1_conf, mycoin_conf, start_swaps, + wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2TestConf, DEFAULT_RPC_PASSWORD, +}; +use mm2_test_helpers::structs::WatcherConf; +use mocktopus::mocking::*; +use num_traits::Zero; +use serde_json::json; + +// ETH-only imports (used only by ETH watcher tests) +#[cfg(feature = "docker-tests-watchers-eth")] +use crate::docker_tests::helpers::eth::{ + erc20_coin_with_random_privkey, erc20_contract_checksum, eth_coin_with_random_privkey, watchers_swap_contract, + watchers_swap_contract_checksum, GETH_RPC_URL, +}; +#[cfg(feature = "docker-tests-watchers-eth")] +use coins::eth::EthCoin; +#[cfg(feature = "docker-tests-watchers-eth")] +use coins::{ + RewardTarget, TestCoin, INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, INVALID_SWAP_ID_ERR_LOG, +}; +#[cfg(feature = "docker-tests-watchers-eth")] +use crypto::privkey::{key_pair_from_secret, key_pair_from_seed}; +#[cfg(feature = "docker-tests-watchers-eth")] +use mm2_test_helpers::for_tests::{enable_eth_coin, erc20_dev_conf, eth_dev_conf, eth_jst_testnet_conf}; +#[cfg(feature = "docker-tests-watchers-eth")] +use mm2_test_helpers::get_passphrase; +#[cfg(feature = "docker-tests-watchers-eth")] +use num_traits::One; +use primitives::hash::H256; +use serde_json::Value; +use std::str::FromStr; +use std::thread; +use std::time::Duration; +use uuid::Uuid; + +#[derive(Debug, Clone)] +struct BalanceResult { + alice_acoin_balance_before: BigDecimal, + #[cfg(feature = "docker-tests-watchers-eth")] + alice_acoin_balance_middle: BigDecimal, + alice_acoin_balance_after: BigDecimal, + alice_bcoin_balance_before: BigDecimal, + #[cfg(feature = "docker-tests-watchers-eth")] + alice_bcoin_balance_middle: BigDecimal, + alice_bcoin_balance_after: BigDecimal, + #[cfg(feature = "docker-tests-watchers-eth")] + alice_eth_balance_middle: BigDecimal, + #[cfg(feature = "docker-tests-watchers-eth")] + alice_eth_balance_after: BigDecimal, + bob_acoin_balance_before: BigDecimal, + bob_acoin_balance_after: BigDecimal, + bob_bcoin_balance_before: BigDecimal, + bob_bcoin_balance_after: BigDecimal, + #[cfg(feature = "docker-tests-watchers-eth")] + watcher_acoin_balance_before: BigDecimal, + #[cfg(feature = "docker-tests-watchers-eth")] + watcher_acoin_balance_after: BigDecimal, + #[cfg(feature = "docker-tests-watchers-eth")] + watcher_bcoin_balance_before: BigDecimal, + #[cfg(feature = "docker-tests-watchers-eth")] + watcher_bcoin_balance_after: BigDecimal, +} + +fn enable_coin(mm_node: &MarketMakerIt, coin: &str) { + if coin == "MYCOIN" || coin == "MYCOIN1" { + log!("{:?}", block_on(enable_native(mm_node, coin, &[], None))); + } else { + #[cfg(feature = "docker-tests-watchers-eth")] + enable_eth(mm_node, coin); + #[cfg(not(feature = "docker-tests-watchers-eth"))] + panic!("ETH coin {} requires docker-tests-watchers-eth feature", coin); + } +} + +#[cfg(feature = "docker-tests-watchers-eth")] +fn enable_eth(mm_node: &MarketMakerIt, coin: &str) { + dbg!(block_on(enable_eth_coin( + mm_node, + coin, + &[GETH_RPC_URL], + &watchers_swap_contract_checksum(), + Some(&watchers_swap_contract_checksum()), + true + ))); +} + +#[allow(clippy::enum_variant_names)] +enum SwapFlow { + WatcherSpendsMakerPayment, + WatcherRefundsTakerPayment, + TakerSpendsMakerPayment, +} + +#[allow(clippy::too_many_arguments)] +fn start_swaps_and_get_balances( + a_coin: &'static str, + b_coin: &'static str, + maker_price: f64, + taker_price: f64, + volume: f64, + envs: &[(&str, &str)], + swap_flow: SwapFlow, + alice_privkey: &str, + bob_privkey: &str, + watcher_privkey: &str, + custom_locktime: Option, +) -> BalanceResult { + #[cfg(feature = "docker-tests-watchers-eth")] + let coins = json!([ + eth_dev_conf(), + erc20_dev_conf(&erc20_contract_checksum()), + mycoin_conf(1000), + mycoin1_conf(1000) + ]); + #[cfg(not(feature = "docker-tests-watchers-eth"))] + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + + let mut alice_conf = Mm2TestConf::seednode(&format!("0x{alice_privkey}"), &coins); + if let Some(locktime) = custom_locktime { + alice_conf.conf["payment_locktime"] = locktime.into(); + } + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf.clone(), + alice_conf.rpc_password.clone(), + None, + envs, + )) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + + let mut bob_conf = Mm2TestConf::light_node(&format!("0x{bob_privkey}"), &coins, &[&mm_alice.ip.to_string()]); + if let Some(locktime) = custom_locktime { + bob_conf.conf["payment_locktime"] = locktime.into(); + } + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + bob_conf.conf.clone(), + bob_conf.rpc_password, + None, + envs, + )) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(bob_privkey).unwrap()); + generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(alice_privkey).unwrap()); + generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(bob_privkey).unwrap()); + generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(alice_privkey).unwrap()); + + let (watcher_conf, watcher_log_to_wait) = match swap_flow { + SwapFlow::WatcherSpendsMakerPayment => ( + WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 0., + refund_start_factor: 1.5, + search_interval: 1.0, + }, + MAKER_PAYMENT_SPEND_SENT_LOG, + ), + SwapFlow::WatcherRefundsTakerPayment => ( + WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 1., + refund_start_factor: 0., + search_interval: 1., + }, + TAKER_PAYMENT_REFUND_SENT_LOG, + ), + SwapFlow::TakerSpendsMakerPayment => ( + WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 1., + refund_start_factor: 1.5, + search_interval: 1.0, + }, + MAKER_PAYMENT_SPEND_FOUND_LOG, + ), + }; + + let mut watcher_conf = Mm2TestConf::watcher_light_node( + &format!("0x{watcher_privkey}"), + &coins, + &[&mm_alice.ip.to_string()], + watcher_conf, + ) + .conf; + if let Some(locktime) = custom_locktime { + watcher_conf["payment_locktime"] = locktime.into(); + } + + let mut mm_watcher = block_on(MarketMakerIt::start_with_envs( + watcher_conf, + DEFAULT_RPC_PASSWORD.to_string(), + None, + envs, + )) + .unwrap(); + let (_watcher_dump_log, _watcher_dump_dashboard) = mm_dump(&mm_watcher.log_path); + log!("Watcher log path: {}", mm_watcher.log_path.display()); + + enable_coin(&mm_alice, a_coin); + enable_coin(&mm_alice, b_coin); + enable_coin(&mm_bob, a_coin); + enable_coin(&mm_bob, b_coin); + enable_coin(&mm_watcher, a_coin); + enable_coin(&mm_watcher, b_coin); + + #[cfg(feature = "docker-tests-watchers-eth")] + if a_coin != "ETH" && b_coin != "ETH" { + enable_coin(&mm_alice, "ETH"); + } + + let alice_acoin_balance_before = block_on(my_balance(&mm_alice, a_coin)).balance; + let alice_bcoin_balance_before = block_on(my_balance(&mm_alice, b_coin)).balance; + let bob_acoin_balance_before = block_on(my_balance(&mm_bob, a_coin)).balance; + let bob_bcoin_balance_before = block_on(my_balance(&mm_bob, b_coin)).balance; + #[cfg(feature = "docker-tests-watchers-eth")] + let watcher_acoin_balance_before = block_on(my_balance(&mm_watcher, a_coin)).balance; + #[cfg(feature = "docker-tests-watchers-eth")] + let watcher_bcoin_balance_before = block_on(my_balance(&mm_watcher, b_coin)).balance; + + #[cfg(feature = "docker-tests-watchers-eth")] + let mut alice_acoin_balance_middle = BigDecimal::zero(); + #[cfg(feature = "docker-tests-watchers-eth")] + let mut alice_bcoin_balance_middle = BigDecimal::zero(); + #[cfg(feature = "docker-tests-watchers-eth")] + let mut alice_eth_balance_middle = BigDecimal::zero(); + let mut bob_acoin_balance_after = BigDecimal::zero(); + let mut bob_bcoin_balance_after = BigDecimal::zero(); + + block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[(b_coin, a_coin)], + maker_price, + taker_price, + volume, + )); + + if matches!(swap_flow, SwapFlow::WatcherRefundsTakerPayment) { + block_on(mm_bob.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SENT_LOG))).unwrap(); + block_on(mm_bob.stop()).unwrap(); + } + if !matches!(swap_flow, SwapFlow::TakerSpendsMakerPayment) { + block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); + #[cfg(feature = "docker-tests-watchers-eth")] + { + alice_acoin_balance_middle = block_on(my_balance(&mm_alice, a_coin)).balance; + alice_bcoin_balance_middle = block_on(my_balance(&mm_alice, b_coin)).balance; + alice_eth_balance_middle = block_on(my_balance(&mm_alice, "ETH")).balance; + } + block_on(mm_alice.stop()).unwrap(); + } + + block_on(mm_watcher.wait_for_log(120., |log| log.contains(watcher_log_to_wait))).unwrap(); + thread::sleep(Duration::from_secs(20)); + + let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + enable_coin(&mm_alice, a_coin); + enable_coin(&mm_alice, b_coin); + + #[cfg(feature = "docker-tests-watchers-eth")] + if a_coin != "ETH" && b_coin != "ETH" { + enable_coin(&mm_alice, "ETH"); + } + + let alice_acoin_balance_after = block_on(my_balance(&mm_alice, a_coin)).balance; + let alice_bcoin_balance_after = block_on(my_balance(&mm_alice, b_coin)).balance; + #[cfg(feature = "docker-tests-watchers-eth")] + let alice_eth_balance_after = block_on(my_balance(&mm_alice, "ETH")).balance; + if !matches!(swap_flow, SwapFlow::WatcherRefundsTakerPayment) { + bob_acoin_balance_after = block_on(my_balance(&mm_bob, a_coin)).balance; + bob_bcoin_balance_after = block_on(my_balance(&mm_bob, b_coin)).balance; + } + #[cfg(feature = "docker-tests-watchers-eth")] + let watcher_acoin_balance_after = block_on(my_balance(&mm_watcher, a_coin)).balance; + #[cfg(feature = "docker-tests-watchers-eth")] + let watcher_bcoin_balance_after = block_on(my_balance(&mm_watcher, b_coin)).balance; + + BalanceResult { + alice_acoin_balance_before, + #[cfg(feature = "docker-tests-watchers-eth")] + alice_acoin_balance_middle, + alice_acoin_balance_after, + alice_bcoin_balance_before, + #[cfg(feature = "docker-tests-watchers-eth")] + alice_bcoin_balance_middle, + alice_bcoin_balance_after, + #[cfg(feature = "docker-tests-watchers-eth")] + alice_eth_balance_middle, + #[cfg(feature = "docker-tests-watchers-eth")] + alice_eth_balance_after, + bob_acoin_balance_before, + bob_acoin_balance_after, + bob_bcoin_balance_before, + bob_bcoin_balance_after, + #[cfg(feature = "docker-tests-watchers-eth")] + watcher_acoin_balance_before, + #[cfg(feature = "docker-tests-watchers-eth")] + watcher_acoin_balance_after, + #[cfg(feature = "docker-tests-watchers-eth")] + watcher_bcoin_balance_before, + #[cfg(feature = "docker-tests-watchers-eth")] + watcher_bcoin_balance_after, + } +} + +fn check_actual_events(mm_alice: &MarketMakerIt, uuid: &str, expected_events: &[&'static str]) -> Value { + let status_response = block_on(my_swap_status(mm_alice, uuid)).unwrap(); + let events_array = status_response["result"]["events"].as_array().unwrap(); + let actual_events = events_array.iter().map(|item| item["event"]["type"].as_str().unwrap()); + let actual_events: Vec<&str> = actual_events.collect(); + assert_eq!(expected_events, actual_events.as_slice()); + status_response +} + +fn run_taker_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + custom_locktime: Option, +) -> (MarketMakerIt, Mm2TestConf) { + let privkey = hex::encode(random_secp256k1_secret()); + let mut conf = Mm2TestConf::light_node(&format!("0x{privkey}"), coins, seednodes); + if let Some(locktime) = custom_locktime { + conf.conf["payment_locktime"] = locktime.into(); + } + let mm = block_on(MarketMakerIt::start_with_envs( + conf.conf.clone(), + conf.rpc_password.clone(), + None, + envs, + )) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("Log path: {}", mm.log_path.display()); + + generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(&privkey).unwrap()); + generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(&privkey).unwrap()); + enable_coin(&mm, "MYCOIN"); + enable_coin(&mm, "MYCOIN1"); + + (mm, conf) +} + +fn restart_taker_and_wait_until(conf: &Mm2TestConf, envs: &[(&str, &str)], wait_until: &str) -> MarketMakerIt { + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + conf.conf.clone(), + conf.rpc_password.clone(), + None, + envs, + )) + .unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + enable_coin(&mm_alice, "MYCOIN"); + enable_coin(&mm_alice, "MYCOIN1"); + + block_on(mm_alice.wait_for_log(120., |log| log.contains(wait_until))).unwrap(); + mm_alice +} + +fn run_maker_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + custom_locktime: Option, +) -> MarketMakerIt { + let privkey = hex::encode(random_secp256k1_secret()); + let mut conf = if seednodes.is_empty() { + Mm2TestConf::seednode(&format!("0x{privkey}"), coins) + } else { + Mm2TestConf::light_node(&format!("0x{privkey}"), coins, seednodes) + }; + if let Some(locktime) = custom_locktime { + conf.conf["payment_locktime"] = locktime.into(); + } + let mm = block_on(MarketMakerIt::start_with_envs( + conf.conf.clone(), + conf.rpc_password, + None, + envs, + )) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("Log path: {}", mm.log_path.display()); + + generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(&privkey).unwrap()); + generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(&privkey).unwrap()); + enable_coin(&mm, "MYCOIN"); + enable_coin(&mm, "MYCOIN1"); + + mm +} + +fn run_watcher_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + watcher_conf: WatcherConf, + custom_locktime: Option, +) -> MarketMakerIt { + let privkey = hex::encode(random_secp256k1_secret()); + let mut conf = Mm2TestConf::watcher_light_node(&format!("0x{privkey}"), coins, seednodes, watcher_conf).conf; + if let Some(locktime) = custom_locktime { + conf["payment_locktime"] = locktime.into(); + } + let mm = block_on(MarketMakerIt::start_with_envs( + conf, + DEFAULT_RPC_PASSWORD.to_string(), + None, + envs, + )) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm.mm_dump(); + log!("Log path: {}", mm.log_path.display()); + + generate_utxo_coin_with_privkey("MYCOIN", 100.into(), H256::from_str(&privkey).unwrap()); + generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), H256::from_str(&privkey).unwrap()); + enable_coin(&mm, "MYCOIN"); + enable_coin(&mm, "MYCOIN1"); + + mm +} diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/utxo.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/utxo.rs new file mode 100644 index 0000000000..f86fb84ddb --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests/utxo.rs @@ -0,0 +1,960 @@ +//! UTXO-only Watcher Tests +//! +//! Tests for watcher node functionality with UTXO coins only. + +use super::*; + +#[test] +fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker_payment_spend() { + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], + &[&mm_bob.ip.to_string()], + None, + ); + + let watcher_conf = WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 0., + refund_start_factor: 1.5, + search_interval: 1.0, + }; + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN1", "MYCOIN")], + 25., + 25., + 2., + )); + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + + block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); + block_on(mm_bob.wait_for_log(120., |log| log.contains(&format!("[swap uuid={}] Finished", &uuids[0])))).unwrap(); + block_on(mm_watcher.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SPEND_SENT_LOG))).unwrap(); + + block_on(mm_alice.stop()).unwrap(); + + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); + + let expected_events = [ + "Started", + "Negotiated", + "TakerFeeSent", + "TakerPaymentInstructionsReceived", + "MakerPaymentReceived", + "MakerPaymentWaitConfirmStarted", + "MakerPaymentValidatedAndConfirmed", + "TakerPaymentSent", + "WatcherMessageSent", + "TakerPaymentSpent", + "MakerPaymentSpentByWatcher", + "MakerPaymentSpendConfirmed", + "Finished", + ]; + check_actual_events(&mm_alice, &uuids[0], &expected_events); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_watcher.stop()).unwrap(); + block_on(mm_bob.stop()).unwrap(); +} + +#[test] +fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_spend() { + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], + &[&mm_bob.ip.to_string()], + None, + ); + + let watcher_conf = WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 0., + refund_start_factor: 1.5, + search_interval: 1.0, + }; + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN1", "MYCOIN")], + 25., + 25., + 2., + )); + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + + block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); + block_on(mm_bob.wait_for_log(120., |log| log.contains(&format!("[swap uuid={}] Finished", &uuids[0])))).unwrap(); + block_on(mm_watcher.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SPEND_SENT_LOG))).unwrap(); + + block_on(mm_alice.stop()).unwrap(); + + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); + + let expected_events = [ + "Started", + "Negotiated", + "TakerFeeSent", + "TakerPaymentInstructionsReceived", + "MakerPaymentReceived", + "MakerPaymentWaitConfirmStarted", + "MakerPaymentValidatedAndConfirmed", + "TakerPaymentSent", + "WatcherMessageSent", + "TakerPaymentSpent", + "MakerPaymentSpentByWatcher", + "MakerPaymentSpendConfirmed", + "Finished", + ]; + check_actual_events(&mm_alice, &uuids[0], &expected_events); +} + +#[test] +fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_wait_for_taker_payment_spend() { + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); + let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], + &[&mm_seednode.ip.to_string()], + Some(60), + ); + + let watcher_conf = WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 1., + refund_start_factor: 0., + search_interval: 1., + }; + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN1", "MYCOIN")], + 25., + 25., + 2., + )); + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + + block_on(mm_bob.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SENT_LOG))).unwrap(); + block_on(mm_bob.stop()).unwrap(); + + block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); + block_on(mm_watcher.wait_for_log(120., |log| log.contains(TAKER_PAYMENT_REFUND_SENT_LOG))).unwrap(); + + block_on(mm_alice.stop()).unwrap(); + + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); + + let expected_events = [ + "Started", + "Negotiated", + "TakerFeeSent", + "TakerPaymentInstructionsReceived", + "MakerPaymentReceived", + "MakerPaymentWaitConfirmStarted", + "MakerPaymentValidatedAndConfirmed", + "TakerPaymentSent", + "WatcherMessageSent", + "TakerPaymentRefundedByWatcher", + "Finished", + ]; + check_actual_events(&mm_alice, &uuids[0], &expected_events); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_watcher.stop()).unwrap(); + block_on(mm_seednode.stop()).unwrap(); +} + +#[test] +fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_taker_payment_refund() { + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); + let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "taker_payment_refund_panic")], + &[&mm_seednode.ip.to_string()], + Some(60), + ); + + let watcher_conf = WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 1., + refund_start_factor: 0., + search_interval: 1., + }; + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN1", "MYCOIN")], + 25., + 25., + 2., + )); + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + + block_on(mm_bob.wait_for_log(120., |log| log.contains(MAKER_PAYMENT_SENT_LOG))).unwrap(); + block_on(mm_bob.stop()).unwrap(); + + block_on(mm_alice.wait_for_log(120., |log| log.contains(REFUND_TEST_FAILURE_LOG))).unwrap(); + block_on(mm_watcher.wait_for_log(120., |log| log.contains(TAKER_PAYMENT_REFUND_SENT_LOG))).unwrap(); + + block_on(mm_alice.stop()).unwrap(); + + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); + + let expected_events = [ + "Started", + "Negotiated", + "TakerFeeSent", + "TakerPaymentInstructionsReceived", + "MakerPaymentReceived", + "MakerPaymentWaitConfirmStarted", + "MakerPaymentValidatedAndConfirmed", + "TakerPaymentSent", + "WatcherMessageSent", + "TakerPaymentWaitForSpendFailed", + "TakerPaymentWaitRefundStarted", + "TakerPaymentRefundStarted", + "TakerPaymentRefundedByWatcher", + "Finished", + ]; + check_actual_events(&mm_alice, &uuids[0], &expected_events); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_watcher.stop()).unwrap(); + block_on(mm_seednode.stop()).unwrap(); +} + +#[test] +fn test_taker_completes_swap_after_restart() { + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN1", "MYCOIN")], + 25., + 25., + 2., + )); + + block_on(mm_alice.wait_for_log(120., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + block_on(mm_alice.stop()).unwrap(); + + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password.clone(), + None, + &[], + )) + .unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + enable_coin(&mm_alice, "MYCOIN"); + enable_coin(&mm_alice, "MYCOIN1"); + + block_on(wait_for_swaps_finish_and_check_status( + &mut mm_bob, + &mut mm_alice, + &uuids, + 2., + 25., + )); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_bob.stop()).unwrap(); +} + +// Verifies https://github.com/KomodoPlatform/komodo-defi-framework/issues/2111 +#[test] +fn test_taker_completes_swap_after_taker_payment_spent_while_offline() { + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); + + let uuids = block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN1", "MYCOIN")], + 25., + 25., + 2., + )); + + // stop taker after taker payment sent + let taker_payment_msg = "Taker payment tx hash "; + block_on(mm_alice.wait_for_log(120., |log| log.contains(taker_payment_msg))).unwrap(); + // ensure p2p message is sent to the maker, this happens before this message: + block_on(mm_alice.wait_for_log(120., |log| log.contains("Waiting for maker to spend taker payment!"))).unwrap(); + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + block_on(mm_alice.stop()).unwrap(); + + // wait for taker payment spent by maker + block_on(mm_bob.wait_for_log(120., |log| log.contains("Taker payment spend tx"))).unwrap(); + // and restart taker + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password.clone(), + None, + &[], + )) + .unwrap(); + + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + enable_coin(&mm_alice, "MYCOIN"); + enable_coin(&mm_alice, "MYCOIN1"); + + block_on(wait_for_swaps_finish_and_check_status( + &mut mm_bob, + &mut mm_alice, + &uuids, + 2., + 25., + )); + + block_on(mm_alice.stop()).unwrap(); + block_on(mm_bob.stop()).unwrap(); +} + +#[test] +fn test_watcher_spends_maker_payment_utxo_utxo() { + let alice_privkey = hex::encode(random_secp256k1_secret()); + let bob_privkey = hex::encode(random_secp256k1_secret()); + let watcher_privkey = hex::encode(random_secp256k1_secret()); + + let balances = start_swaps_and_get_balances( + "MYCOIN", + "MYCOIN1", + 25., + 25., + 2., + &[], + SwapFlow::WatcherSpendsMakerPayment, + &alice_privkey, + &bob_privkey, + &watcher_privkey, + None, + ); + + let acoin_volume = BigDecimal::from_str("50").unwrap(); + let bcoin_volume = BigDecimal::from_str("2").unwrap(); + + assert_eq!( + balances.alice_acoin_balance_after.round(0), + balances.alice_acoin_balance_before - acoin_volume.clone() + ); + assert_eq!( + balances.alice_bcoin_balance_after.round(0), + balances.alice_bcoin_balance_before + bcoin_volume.clone() + ); + assert_eq!( + balances.bob_acoin_balance_after.round(0), + balances.bob_acoin_balance_before + acoin_volume + ); + assert_eq!( + balances.bob_bcoin_balance_after.round(0), + balances.bob_bcoin_balance_before - bcoin_volume + ); +} + +#[test] +fn test_watcher_refunds_taker_payment_utxo() { + let alice_privkey = &hex::encode(random_secp256k1_secret()); + let bob_privkey = &hex::encode(random_secp256k1_secret()); + let watcher_privkey = &hex::encode(random_secp256k1_secret()); + + let balances = start_swaps_and_get_balances( + "MYCOIN1", + "MYCOIN", + 25., + 25., + 2., + &[], + SwapFlow::WatcherRefundsTakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, + Some(60), + ); + + assert_eq!( + balances.alice_acoin_balance_after.round(0), + balances.alice_acoin_balance_before + ); + assert_eq!(balances.alice_bcoin_balance_after, balances.alice_bcoin_balance_before); +} + +#[test] +fn test_watcher_waits_for_taker_utxo() { + let alice_privkey = &hex::encode(random_secp256k1_secret()); + let bob_privkey = &hex::encode(random_secp256k1_secret()); + let watcher_privkey = &hex::encode(random_secp256k1_secret()); + + start_swaps_and_get_balances( + "MYCOIN1", + "MYCOIN", + 25., + 25., + 2., + &[], + SwapFlow::TakerSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, + None, + ); +} + +#[test] +fn test_watcher_validate_taker_fee_utxo() { + let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run + let lock_duration = get_payment_locktime(); + let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let taker_pubkey = taker_coin.my_public_key().unwrap(); + + let taker_amount = MmNumber::from((10, 1)); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, maker_coin.ticker(), &taker_amount); + + let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_fee.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + + block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let validate_taker_fee_res = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: taker_pubkey.to_vec(), + min_block_number: 0, + lock_duration, + })); + assert!(validate_taker_fee_res.is_ok()); + + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: maker_coin.my_public_key().unwrap().to_vec(), + min_block_number: 0, + lock_duration, + })) + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SENDER_ERR_LOG)) + }, + _ => panic!("Expected `WrongPaymentTx` invalid public key, found {:?}", error), + } + + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: taker_pubkey.to_vec(), + min_block_number: u64::MAX, + lock_duration, + })) + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(EARLY_CONFIRMATION_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` confirmed before min_block, found {:?}", + error + ), + } + + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: taker_pubkey.to_vec(), + min_block_number: 0, + lock_duration: 0, + })) + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(OLD_TRANSACTION_ERR_LOG)) + }, + _ => panic!("Expected `WrongPaymentTx` transaction too old, found {:?}", error), + } + + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey + .mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); + + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), + sender_pubkey: taker_pubkey.to_vec(), + min_block_number: 0, + lock_duration, + })) + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_RECEIVER_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` tx output script_pubkey doesn't match expected, found {:?}", + error + ), + } +} + +#[test] +fn test_watcher_validate_taker_payment_utxo() { + let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run + let time_lock_duration = get_payment_locktime(); + let wait_for_confirmation_until = wait_until_sec(time_lock_duration); + let time_lock = wait_for_confirmation_until; + + let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let taker_pubkey = taker_coin.my_public_key().unwrap(); + + let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let maker_pubkey = maker_coin.my_public_key().unwrap(); + + let secret_hash = dhash160(&generate_secret().unwrap()); + + let taker_payment = block_on(taker_coin.send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pubkey, + secret_hash: secret_hash.as_slice(), + amount: BigDecimal::from(10), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until, + })) + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let taker_payment_refund_preimage = block_on_f01(taker_coin.create_taker_payment_refund_preimage( + &taker_payment.tx_hex(), + time_lock, + maker_pubkey, + secret_hash.as_slice(), + &None, + &[], + )) + .unwrap(); + let validate_taker_payment_res = + block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), + time_lock, + taker_pub: taker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: secret_hash.to_vec(), + wait_until: timeout, + confirmations: 1, + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), + })); + assert!(validate_taker_payment_res.is_ok()); + + let error = block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), + time_lock, + taker_pub: maker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: secret_hash.to_vec(), + wait_until: timeout, + confirmations: 1, + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), + })) + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SENDER_ERR_LOG)) + }, + _ => panic!("Expected `WrongPaymentTx` {INVALID_SENDER_ERR_LOG}, found {:?}", error), + } + + // Used to get wrong swap id + let wrong_secret_hash = dhash160(&generate_secret().unwrap()); + let error = block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), + time_lock, + taker_pub: taker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: wrong_secret_hash.to_vec(), + wait_until: timeout, + confirmations: 1, + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), + })) + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SCRIPT_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_SCRIPT_ERR_LOG, error + ), + } + + let taker_payment_wrong_secret = block_on(taker_coin.send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pubkey, + secret_hash: wrong_secret_hash.as_slice(), + amount: BigDecimal::from(10), + swap_contract_address: &taker_coin.swap_contract_address(), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until, + })) + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment_wrong_secret.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let error = block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), + time_lock: 500, + taker_pub: taker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: wrong_secret_hash.to_vec(), + wait_until: timeout, + confirmations: 1, + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), + })) + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SCRIPT_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_SCRIPT_ERR_LOG, error + ), + } + + let wrong_taker_payment_refund_preimage = block_on_f01(taker_coin.create_taker_payment_refund_preimage( + &taker_payment.tx_hex(), + time_lock, + maker_pubkey, + wrong_secret_hash.as_slice(), + &None, + &[], + )) + .unwrap(); + + let error = block_on_f01(taker_coin.watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: wrong_taker_payment_refund_preimage.tx_hex(), + time_lock, + taker_pub: taker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: secret_hash.to_vec(), + wait_until: timeout, + confirmations: 1, + maker_coin: MmCoinEnum::UtxoCoinVariant(maker_coin.clone()), + })) + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_REFUND_TX_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_REFUND_TX_ERR_LOG, error + ), + } +} + +#[test] +fn test_taker_validates_taker_payment_refund_utxo() { + let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run + let time_lock_duration = get_payment_locktime(); + let wait_for_confirmation_until = wait_until_sec(time_lock_duration); + let time_lock = now_sec() - 10; + + let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let maker_pubkey = maker_coin.my_public_key().unwrap(); + + let secret_hash = dhash160(&generate_secret().unwrap()); + + let taker_payment = block_on(taker_coin.send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pubkey, + secret_hash: secret_hash.as_slice(), + amount: BigDecimal::from(10), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until, + })) + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(taker_coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let taker_payment_refund_preimage = block_on_f01(taker_coin.create_taker_payment_refund_preimage( + &taker_payment.tx_hex(), + time_lock, + maker_pubkey, + secret_hash.as_slice(), + &None, + &[], + )) + .unwrap(); + + let taker_payment_refund = block_on_f01(taker_coin.send_taker_payment_refund_preimage(RefundPaymentArgs { + payment_tx: &taker_payment_refund_preimage.tx_hex(), + other_pubkey: maker_pubkey, + tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: secret_hash.as_slice(), + }, + time_lock, + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + })) + .unwrap(); + + let validate_input = ValidateWatcherSpendInput { + payment_tx: taker_payment_refund.tx_hex(), + maker_pub: maker_pubkey.to_vec(), + swap_contract_address: None, + time_lock, + secret_hash: secret_hash.to_vec(), + amount: BigDecimal::from(10), + watcher_reward: None, + spend_type: WatcherSpendType::TakerPaymentRefund, + }; + + let validate_watcher_refund = block_on_f01(taker_coin.taker_validates_payment_spend_or_refund(validate_input)); + assert!(validate_watcher_refund.is_ok()); +} + +#[test] +fn test_taker_validates_maker_payment_spend_utxo() { + let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run + let time_lock_duration = get_payment_locktime(); + let wait_for_confirmation_until = wait_until_sec(time_lock_duration); + let time_lock = wait_for_confirmation_until; + + let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let taker_pubkey = taker_coin.my_public_key().unwrap(); + let maker_pubkey = maker_coin.my_public_key().unwrap(); + + let secret = generate_secret().unwrap(); + let secret_hash = dhash160(&secret); + + let maker_payment = block_on(maker_coin.send_maker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: taker_pubkey, + secret_hash: secret_hash.as_slice(), + amount: BigDecimal::from(10), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until, + })) + .unwrap(); + + block_on_f01(maker_coin.wait_for_confirmations(ConfirmPaymentInput { + payment_tx: maker_payment.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + })) + .unwrap(); + + let maker_payment_spend_preimage = block_on_f01(taker_coin.create_maker_payment_spend_preimage( + &maker_payment.tx_hex(), + time_lock, + maker_pubkey, + secret_hash.as_slice(), + &[], + )) + .unwrap(); + + let maker_payment_spend = block_on_f01(taker_coin.send_maker_payment_spend_preimage( + SendMakerPaymentSpendPreimageInput { + preimage: &maker_payment_spend_preimage.tx_hex(), + secret_hash: secret_hash.as_slice(), + secret: secret.as_slice(), + taker_pub: taker_pubkey, + watcher_reward: false, + }, + )) + .unwrap(); + + let validate_input = ValidateWatcherSpendInput { + payment_tx: maker_payment_spend.tx_hex(), + maker_pub: maker_pubkey.to_vec(), + swap_contract_address: None, + time_lock, + secret_hash: secret_hash.to_vec(), + amount: BigDecimal::from(10), + watcher_reward: None, + spend_type: WatcherSpendType::TakerPaymentRefund, + }; + + let validate_watcher_spend = block_on_f01(taker_coin.taker_validates_payment_spend_or_refund(validate_input)); + assert!(validate_watcher_spend.is_ok()); +} + +#[test] +fn test_send_taker_payment_refund_preimage_utxo() { + let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run + let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let my_public_key = coin.my_public_key().unwrap(); + + let time_lock = now_sec() - 3600; + let taker_payment_args = SendPaymentArgs { + time_lock_duration: 0, + time_lock, + other_pubkey: my_public_key, + secret_hash: &[0; 20], + amount: 1u64.into(), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let tx = block_on(coin.send_taker_payment(taker_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let refund_tx = block_on_f01(coin.create_taker_payment_refund_preimage( + &tx.tx_hex(), + time_lock, + my_public_key, + &[0; 20], + &None, + &[], + )) + .unwrap(); + + let refund_tx = block_on_f01(coin.send_taker_payment_refund_preimage(RefundPaymentArgs { + payment_tx: &refund_tx.tx_hex(), + swap_contract_address: &None, + tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &[0; 20], + }, + other_pubkey: my_public_key, + time_lock, + swap_unique_data: &[], + watcher_reward: false, + })) + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let search_input = SearchForSwapTxSpendInput { + time_lock, + other_pub: coin.my_public_key().unwrap(), + secret_hash: &[0; 20], + tx: &tx.tx_hex(), + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) + .unwrap() + .unwrap(); + assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); +} diff --git a/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs b/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs index bda9b8fdfd..1b5996c2a3 100644 --- a/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs @@ -1,10 +1,10 @@ -use crate::generate_utxo_coin_with_random_privkey; +use crate::docker_tests::helpers::utxo::generate_utxo_coin_with_random_privkey; use crate::integration_tests_common::enable_native; use common::block_on; use mm2_main::lp_swap::get_payment_locktime; use mm2_rpc::data::legacy::OrderConfirmationsSettings; use mm2_test_helpers::for_tests::{mm_dump, MarketMakerIt}; -use serde_json::Value as Json; +use serde_json::{json, Value as Json}; use std::thread; use std::time::Duration; diff --git a/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs b/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs index fdabe53544..160d2d1434 100644 --- a/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs @@ -1,4 +1,4 @@ -use crate::generate_utxo_coin_with_random_privkey; +use crate::docker_tests::helpers::utxo::generate_utxo_coin_with_random_privkey; use crate::integration_tests_common::enable_native; use bitcrypto::ChecksumType; use common::block_on; @@ -6,7 +6,7 @@ use crypto::Secp256k1Secret; use keys::{KeyPair, Private}; use mm2_io::file_lock::FileLock; use mm2_test_helpers::for_tests::{mm_dump, new_mm2_temp_folder_path, MarketMakerIt}; -use serde_json::Value as Json; +use serde_json::{json, Value as Json}; use std::thread; use std::time::Duration; diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_swap_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_swap_tests.rs new file mode 100644 index 0000000000..c9015e8875 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_swap_tests.rs @@ -0,0 +1,475 @@ +// Tendermint Cross-Chain Swap Tests +// +// This module contains tests that require Tendermint AND other chain types (ETH, Electrum). +// These tests cannot be placed in tendermint_tests.rs because they require additional +// infrastructure beyond Tendermint nodes. +// +// Tests: +// - swap_nucleus_with_doc: NUCLEUS <-> DOC (Tendermint + Electrum) +// - swap_nucleus_with_eth: NUCLEUS <-> ETH (Tendermint + Geth) +// - swap_doc_with_iris_ibc_nucleus: DOC <-> IRIS-IBC-NUCLEUS (Tendermint + Electrum) +// +// Gated by: docker-tests-tendermint + docker-tests-eth (cross-chain Tendermint swaps) + +use crate::docker_tests::helpers::eth::{fill_eth, swap_contract_checksum, GETH_RPC_URL}; +use crate::integration_tests_common::enable_electrum; +use common::executor::Timer; +use common::{block_on, log}; +use compatible_time::Duration; +use ethereum_types::{Address, U256}; +use mm2_number::BigDecimal; +use mm2_rpc::data::legacy::OrderbookResponse; +use mm2_test_helpers::for_tests::{ + check_my_swap_status, check_recent_swaps, doc_conf, enable_eth_coin, enable_tendermint, eth_dev_conf, + iris_ibc_nucleus_testnet_conf, nucleus_testnet_conf, wait_check_stats_swap_status, MarketMakerIt, + DOC_ELECTRUM_ADDRS, +}; +use serde_json::json; +use std::convert::TryFrom; +use std::env; +use std::str::FromStr; +use std::sync::Mutex; +use std::thread; + +const NUCLEUS_TESTNET_RPC_URLS: &[&str] = &["http://localhost:26657"]; + +const BOB_PASSPHRASE: &str = "iris test seed"; +const ALICE_PASSPHRASE: &str = "iris test2 seed"; + +lazy_static! { + /// Makes sure that tests sending transactions run sequentially to prevent account sequence + /// mismatches as some addresses are used in multiple tests. + static ref SEQUENCE_LOCK: Mutex<()> = Mutex::new(()); +} + +#[test] +fn swap_nucleus_with_doc() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); + let bob_passphrase = String::from(BOB_PASSPHRASE); + let alice_passphrase = String::from(ALICE_PASSPHRASE); + + let coins = json!([nucleus_testnet_conf(), doc_conf()]); + + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("ALICE_TRADE_IP") .ok(), + "rpcip": env::var("ALICE_TRADE_IP") .ok(), + "passphrase": alice_passphrase, + "coins": coins, + "seednodes": [mm_bob.my_seed_addr()], + "rpc_password": "password", + "skip_startup_checks": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + dbg!(block_on(enable_tendermint( + &mm_bob, + "NUCLEUS-TEST", + &[], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_tendermint( + &mm_alice, + "NUCLEUS-TEST", + &[], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_electrum(&mm_bob, "DOC", false, DOC_ELECTRUM_ADDRS,))); + + dbg!(block_on(enable_electrum(&mm_alice, "DOC", false, DOC_ELECTRUM_ADDRS,))); + + block_on(trade_base_rel_tendermint( + mm_bob, + mm_alice, + "NUCLEUS-TEST", + "DOC", + 1, + 2, + 0.008, + )); +} + +#[test] +fn swap_nucleus_with_eth() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); + let bob_passphrase = String::from(BOB_PASSPHRASE); + let alice_passphrase = String::from(ALICE_PASSPHRASE); + const BOB_ETH_ADDRESS: &str = "0x7b338250f990954E3Ab034ccD32a917c2F607C2d"; + const ALICE_ETH_ADDRESS: &str = "0x37602b7a648b207ACFD19E67253f57669bEA4Ad8"; + + fill_eth( + Address::from_str(BOB_ETH_ADDRESS).unwrap(), + U256::from(10).pow(U256::from(20)), + ); + + fill_eth( + Address::from_str(ALICE_ETH_ADDRESS).unwrap(), + U256::from(10).pow(U256::from(20)), + ); + + let coins = json!([nucleus_testnet_conf(), eth_dev_conf()]); + + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("ALICE_TRADE_IP") .ok(), + "rpcip": env::var("ALICE_TRADE_IP") .ok(), + "passphrase": alice_passphrase, + "coins": coins, + "seednodes": [mm_bob.my_seed_addr()], + "rpc_password": "password", + "skip_startup_checks": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + dbg!(block_on(enable_tendermint( + &mm_bob, + "NUCLEUS-TEST", + &[], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_tendermint( + &mm_alice, + "NUCLEUS-TEST", + &[], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + let swap_contract = swap_contract_checksum(); + + dbg!(block_on(enable_eth_coin( + &mm_bob, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + dbg!(block_on(enable_eth_coin( + &mm_alice, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false + ))); + + block_on(trade_base_rel_tendermint( + mm_bob, + mm_alice, + "NUCLEUS-TEST", + "ETH", + 1, + 2, + 0.008, + )); +} + +#[test] +fn swap_doc_with_iris_ibc_nucleus() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); + let bob_passphrase = String::from(BOB_PASSPHRASE); + let alice_passphrase = String::from(ALICE_PASSPHRASE); + + let coins = json!([nucleus_testnet_conf(), iris_ibc_nucleus_testnet_conf(), doc_conf()]); + + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": bob_passphrase, + "coins": coins, + "rpc_password": "password", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 8999, + "dht": "on", + "myipaddr": env::var("ALICE_TRADE_IP") .ok(), + "rpcip": env::var("ALICE_TRADE_IP") .ok(), + "passphrase": alice_passphrase, + "coins": coins, + "seednodes": [mm_bob.my_seed_addr()], + "rpc_password": "password", + "skip_startup_checks": true, + }), + "password".into(), + None, + ) + .unwrap(); + + thread::sleep(Duration::from_secs(1)); + + dbg!(block_on(enable_tendermint( + &mm_bob, + "NUCLEUS-TEST", + &["IRIS-IBC-NUCLEUS-TEST"], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_tendermint( + &mm_alice, + "NUCLEUS-TEST", + &["IRIS-IBC-NUCLEUS-TEST"], + NUCLEUS_TESTNET_RPC_URLS, + false + ))); + + dbg!(block_on(enable_electrum(&mm_bob, "DOC", false, DOC_ELECTRUM_ADDRS))); + + dbg!(block_on(enable_electrum(&mm_alice, "DOC", false, DOC_ELECTRUM_ADDRS))); + + block_on(trade_base_rel_tendermint( + mm_bob, + mm_alice, + "DOC", + "IRIS-IBC-NUCLEUS-TEST", + 1, + 2, + 0.008, + )); +} + +pub async fn trade_base_rel_tendermint( + mut mm_bob: MarketMakerIt, + mut mm_alice: MarketMakerIt, + base: &str, + rel: &str, + maker_price: i32, + taker_price: i32, + volume: f64, +) { + log!("Issue bob {}/{} sell request", base, rel); + let rc = mm_bob + .rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": maker_price, + "volume": volume + })) + .await + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let mut uuids = vec![]; + + common::log::info!( + "Trigger alice subscription to {}/{} orderbook topic first and sleep for 1 second", + base, + rel + ); + let rc = mm_alice + .rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": base, + "rel": rel, + })) + .await + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + Timer::sleep(1.).await; + common::log::info!("Issue alice {}/{} buy request", base, rel); + let rc = mm_alice + .rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": base, + "rel": rel, + "volume": volume, + "price": taker_price + })) + .await + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + let buy_json: serde_json::Value = serde_json::from_str(&rc.1).unwrap(); + uuids.push(buy_json["result"]["uuid"].as_str().unwrap().to_owned()); + + // ensure the swaps are started + let expected_log = format!("Entering the taker_swap_loop {base}/{rel}"); + mm_alice + .wait_for_log(5., |log| log.contains(&expected_log)) + .await + .unwrap(); + let expected_log = format!("Entering the maker_swap_loop {base}/{rel}"); + mm_bob + .wait_for_log(5., |log| log.contains(&expected_log)) + .await + .unwrap(); + + for uuid in uuids.iter() { + // ensure the swaps are indexed to the SQLite database + let expected_log = format!("Inserting new swap {uuid} to the SQLite database"); + mm_alice + .wait_for_log(5., |log| log.contains(&expected_log)) + .await + .unwrap(); + mm_bob + .wait_for_log(5., |log| log.contains(&expected_log)) + .await + .unwrap() + } + + for uuid in uuids.iter() { + match mm_bob + .wait_for_log(180., |log| log.contains(&format!("[swap uuid={uuid}] Finished"))) + .await + { + Ok(_) => (), + Err(_) => { + log!("{}", mm_bob.log_as_utf8().unwrap()); + }, + } + + match mm_alice + .wait_for_log(180., |log| log.contains(&format!("[swap uuid={uuid}] Finished"))) + .await + { + Ok(_) => (), + Err(_) => { + log!("{}", mm_alice.log_as_utf8().unwrap()); + }, + } + + log!("Waiting a few second for the fresh swap status to be saved.."); + Timer::sleep(5.).await; + + log!("{}", mm_alice.log_as_utf8().unwrap()); + log!("Checking alice/taker status.."); + check_my_swap_status( + &mm_alice, + uuid, + BigDecimal::try_from(volume).unwrap(), + BigDecimal::try_from(volume).unwrap(), + ) + .await; + + log!("{}", mm_bob.log_as_utf8().unwrap()); + log!("Checking bob/maker status.."); + check_my_swap_status( + &mm_bob, + uuid, + BigDecimal::try_from(volume).unwrap(), + BigDecimal::try_from(volume).unwrap(), + ) + .await; + } + + log!("Waiting 3 seconds for nodes to broadcast their swaps data.."); + Timer::sleep(3.).await; + + for uuid in uuids.iter() { + log!("Checking alice status.."); + wait_check_stats_swap_status(&mm_alice, uuid, 240).await; + + log!("Checking bob status.."); + wait_check_stats_swap_status(&mm_bob, uuid, 240).await; + } + + log!("Checking alice recent swaps.."); + check_recent_swaps(&mm_alice, uuids.len()).await; + log!("Checking bob recent swaps.."); + check_recent_swaps(&mm_bob, uuids.len()).await; + log!("Get {}/{} orderbook", base, rel); + let rc = mm_bob + .rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": base, + "rel": rel, + })) + .await + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: OrderbookResponse = serde_json::from_str(&rc.1).unwrap(); + log!("{}/{} orderbook {:?}", base, rel, bob_orderbook); + + assert_eq!(0, bob_orderbook.bids.len(), "{base} {rel} bids must be empty"); + assert_eq!(0, bob_orderbook.asks.len(), "{base} {rel} asks must be empty"); + + mm_bob.stop().await.unwrap(); + mm_alice.stop().await.unwrap(); +} diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 480b93063b..2472ba9518 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -812,458 +812,3 @@ fn test_tendermint_remove_delegation() { let undelegation_entry = undelegation_info["entries"].as_array().unwrap().last().unwrap(); assert_eq!(undelegation_entry["balance"], "0.15"); } - -mod swap { - use super::*; - - use crate::docker_tests::eth_docker_tests::fill_eth; - use crate::docker_tests::eth_docker_tests::swap_contract; - use crate::integration_tests_common::enable_electrum; - use common::executor::Timer; - use common::log; - use compatible_time::Duration; - use ethereum_types::{Address, U256}; - use mm2_rpc::data::legacy::OrderbookResponse; - use mm2_test_helpers::for_tests::{ - check_my_swap_status, check_recent_swaps, doc_conf, enable_eth_coin, iris_ibc_nucleus_testnet_conf, - nucleus_testnet_conf, wait_check_stats_swap_status, DOC_ELECTRUM_ADDRS, - }; - use std::convert::TryFrom; - use std::env; - use std::str::FromStr; - - const BOB_PASSPHRASE: &str = "iris test seed"; - const ALICE_PASSPHRASE: &str = "iris test2 seed"; - - #[test] - fn swap_nucleus_with_doc() { - let _lock = SEQUENCE_LOCK.lock().unwrap(); - let bob_passphrase = String::from(BOB_PASSPHRASE); - let alice_passphrase = String::from(ALICE_PASSPHRASE); - - let coins = json!([nucleus_testnet_conf(), doc_conf()]); - - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 8999, - "dht": "on", - "myipaddr": env::var("BOB_TRADE_IP") .ok(), - "rpcip": env::var("BOB_TRADE_IP") .ok(), - "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), - "passphrase": bob_passphrase, - "coins": coins, - "rpc_password": "password", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "password".into(), - None, - ) - .unwrap(); - - thread::sleep(Duration::from_secs(1)); - - let mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 8999, - "dht": "on", - "myipaddr": env::var("ALICE_TRADE_IP") .ok(), - "rpcip": env::var("ALICE_TRADE_IP") .ok(), - "passphrase": alice_passphrase, - "coins": coins, - "seednodes": [mm_bob.my_seed_addr()], - "rpc_password": "password", - "skip_startup_checks": true, - }), - "password".into(), - None, - ) - .unwrap(); - - thread::sleep(Duration::from_secs(1)); - - dbg!(block_on(enable_tendermint( - &mm_bob, - "NUCLEUS-TEST", - &[], - NUCLEUS_TESTNET_RPC_URLS, - false - ))); - - dbg!(block_on(enable_tendermint( - &mm_alice, - "NUCLEUS-TEST", - &[], - NUCLEUS_TESTNET_RPC_URLS, - false - ))); - - dbg!(block_on(enable_electrum(&mm_bob, "DOC", false, DOC_ELECTRUM_ADDRS,))); - - dbg!(block_on(enable_electrum(&mm_alice, "DOC", false, DOC_ELECTRUM_ADDRS,))); - - block_on(trade_base_rel_tendermint( - mm_bob, - mm_alice, - "NUCLEUS-TEST", - "DOC", - 1, - 2, - 0.008, - )); - } - - #[test] - fn swap_nucleus_with_eth() { - let _lock = SEQUENCE_LOCK.lock().unwrap(); - let bob_passphrase = String::from(BOB_PASSPHRASE); - let alice_passphrase = String::from(ALICE_PASSPHRASE); - const BOB_ETH_ADDRESS: &str = "0x7b338250f990954E3Ab034ccD32a917c2F607C2d"; - const ALICE_ETH_ADDRESS: &str = "0x37602b7a648b207ACFD19E67253f57669bEA4Ad8"; - - fill_eth( - Address::from_str(BOB_ETH_ADDRESS).unwrap(), - U256::from(10).pow(U256::from(20)), - ); - - fill_eth( - Address::from_str(ALICE_ETH_ADDRESS).unwrap(), - U256::from(10).pow(U256::from(20)), - ); - - let coins = json!([nucleus_testnet_conf(), crate::eth_dev_conf()]); - - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 8999, - "dht": "on", - "myipaddr": env::var("BOB_TRADE_IP") .ok(), - "rpcip": env::var("BOB_TRADE_IP") .ok(), - "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), - "passphrase": bob_passphrase, - "coins": coins, - "rpc_password": "password", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "password".into(), - None, - ) - .unwrap(); - - thread::sleep(Duration::from_secs(1)); - - let mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 8999, - "dht": "on", - "myipaddr": env::var("ALICE_TRADE_IP") .ok(), - "rpcip": env::var("ALICE_TRADE_IP") .ok(), - "passphrase": alice_passphrase, - "coins": coins, - "seednodes": [mm_bob.my_seed_addr()], - "rpc_password": "password", - "skip_startup_checks": true, - }), - "password".into(), - None, - ) - .unwrap(); - - thread::sleep(Duration::from_secs(1)); - - dbg!(block_on(enable_tendermint( - &mm_bob, - "NUCLEUS-TEST", - &[], - NUCLEUS_TESTNET_RPC_URLS, - false - ))); - - dbg!(block_on(enable_tendermint( - &mm_alice, - "NUCLEUS-TEST", - &[], - NUCLEUS_TESTNET_RPC_URLS, - false - ))); - - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - - dbg!(block_on(enable_eth_coin( - &mm_bob, - "ETH", - &[crate::GETH_RPC_URL], - &swap_contract, - None, - false - ))); - - dbg!(block_on(enable_eth_coin( - &mm_alice, - "ETH", - &[crate::GETH_RPC_URL], - &swap_contract, - None, - false - ))); - - block_on(trade_base_rel_tendermint( - mm_bob, - mm_alice, - "NUCLEUS-TEST", - "ETH", - 1, - 2, - 0.008, - )); - } - - #[test] - fn swap_doc_with_iris_ibc_nucleus() { - let _lock = SEQUENCE_LOCK.lock().unwrap(); - let bob_passphrase = String::from(BOB_PASSPHRASE); - let alice_passphrase = String::from(ALICE_PASSPHRASE); - - let coins = json!([nucleus_testnet_conf(), iris_ibc_nucleus_testnet_conf(), doc_conf()]); - - let mm_bob = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 8999, - "dht": "on", - "myipaddr": env::var("BOB_TRADE_IP") .ok(), - "rpcip": env::var("BOB_TRADE_IP") .ok(), - "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), - "passphrase": bob_passphrase, - "coins": coins, - "rpc_password": "password", - "i_am_seed": true, - "is_bootstrap_node": true - }), - "password".into(), - None, - ) - .unwrap(); - - thread::sleep(Duration::from_secs(1)); - - let mm_alice = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 8999, - "dht": "on", - "myipaddr": env::var("ALICE_TRADE_IP") .ok(), - "rpcip": env::var("ALICE_TRADE_IP") .ok(), - "passphrase": alice_passphrase, - "coins": coins, - "seednodes": [mm_bob.my_seed_addr()], - "rpc_password": "password", - "skip_startup_checks": true, - }), - "password".into(), - None, - ) - .unwrap(); - - thread::sleep(Duration::from_secs(1)); - - dbg!(block_on(enable_tendermint( - &mm_bob, - "NUCLEUS-TEST", - &["IRIS-IBC-NUCLEUS-TEST"], - NUCLEUS_TESTNET_RPC_URLS, - false - ))); - - dbg!(block_on(enable_tendermint( - &mm_alice, - "NUCLEUS-TEST", - &["IRIS-IBC-NUCLEUS-TEST"], - NUCLEUS_TESTNET_RPC_URLS, - false - ))); - - dbg!(block_on(enable_electrum(&mm_bob, "DOC", false, DOC_ELECTRUM_ADDRS))); - - dbg!(block_on(enable_electrum(&mm_alice, "DOC", false, DOC_ELECTRUM_ADDRS))); - - block_on(trade_base_rel_tendermint( - mm_bob, - mm_alice, - "DOC", - "IRIS-IBC-NUCLEUS-TEST", - 1, - 2, - 0.008, - )); - } - - pub async fn trade_base_rel_tendermint( - mut mm_bob: MarketMakerIt, - mut mm_alice: MarketMakerIt, - base: &str, - rel: &str, - maker_price: i32, - taker_price: i32, - volume: f64, - ) { - log!("Issue bob {}/{} sell request", base, rel); - let rc = mm_bob - .rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "setprice", - "base": base, - "rel": rel, - "price": maker_price, - "volume": volume - })) - .await - .unwrap(); - assert!(rc.0.is_success(), "!setprice: {}", rc.1); - - let mut uuids = vec![]; - - common::log::info!( - "Trigger alice subscription to {}/{} orderbook topic first and sleep for 1 second", - base, - rel - ); - let rc = mm_alice - .rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "orderbook", - "base": base, - "rel": rel, - })) - .await - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - Timer::sleep(1.).await; - common::log::info!("Issue alice {}/{} buy request", base, rel); - let rc = mm_alice - .rpc(&json!({ - "userpass": mm_alice.userpass, - "method": "buy", - "base": base, - "rel": rel, - "volume": volume, - "price": taker_price - })) - .await - .unwrap(); - assert!(rc.0.is_success(), "!buy: {}", rc.1); - let buy_json: serde_json::Value = serde_json::from_str(&rc.1).unwrap(); - uuids.push(buy_json["result"]["uuid"].as_str().unwrap().to_owned()); - - // ensure the swaps are started - let expected_log = format!("Entering the taker_swap_loop {base}/{rel}"); - mm_alice - .wait_for_log(5., |log| log.contains(&expected_log)) - .await - .unwrap(); - let expected_log = format!("Entering the maker_swap_loop {base}/{rel}"); - mm_bob - .wait_for_log(5., |log| log.contains(&expected_log)) - .await - .unwrap(); - - for uuid in uuids.iter() { - // ensure the swaps are indexed to the SQLite database - let expected_log = format!("Inserting new swap {uuid} to the SQLite database"); - mm_alice - .wait_for_log(5., |log| log.contains(&expected_log)) - .await - .unwrap(); - mm_bob - .wait_for_log(5., |log| log.contains(&expected_log)) - .await - .unwrap() - } - - for uuid in uuids.iter() { - match mm_bob - .wait_for_log(180., |log| log.contains(&format!("[swap uuid={uuid}] Finished"))) - .await - { - Ok(_) => (), - Err(_) => { - log!("{}", mm_bob.log_as_utf8().unwrap()); - }, - } - - match mm_alice - .wait_for_log(180., |log| log.contains(&format!("[swap uuid={uuid}] Finished"))) - .await - { - Ok(_) => (), - Err(_) => { - log!("{}", mm_alice.log_as_utf8().unwrap()); - }, - } - - log!("Waiting a few second for the fresh swap status to be saved.."); - Timer::sleep(5.).await; - - log!("{}", mm_alice.log_as_utf8().unwrap()); - log!("Checking alice/taker status.."); - check_my_swap_status( - &mm_alice, - uuid, - BigDecimal::try_from(volume).unwrap(), - BigDecimal::try_from(volume).unwrap(), - ) - .await; - - log!("{}", mm_bob.log_as_utf8().unwrap()); - log!("Checking bob/maker status.."); - check_my_swap_status( - &mm_bob, - uuid, - BigDecimal::try_from(volume).unwrap(), - BigDecimal::try_from(volume).unwrap(), - ) - .await; - } - - log!("Waiting 3 seconds for nodes to broadcast their swaps data.."); - Timer::sleep(3.).await; - - for uuid in uuids.iter() { - log!("Checking alice status.."); - wait_check_stats_swap_status(&mm_alice, uuid, 240).await; - - log!("Checking bob status.."); - wait_check_stats_swap_status(&mm_bob, uuid, 240).await; - } - - log!("Checking alice recent swaps.."); - check_recent_swaps(&mm_alice, uuids.len()).await; - log!("Checking bob recent swaps.."); - check_recent_swaps(&mm_bob, uuids.len()).await; - log!("Get {}/{} orderbook", base, rel); - let rc = mm_bob - .rpc(&json!({ - "userpass": mm_bob.userpass, - "method": "orderbook", - "base": base, - "rel": rel, - })) - .await - .unwrap(); - assert!(rc.0.is_success(), "!orderbook: {}", rc.1); - - let bob_orderbook: OrderbookResponse = serde_json::from_str(&rc.1).unwrap(); - log!("{}/{} orderbook {:?}", base, rel, bob_orderbook); - - assert_eq!(0, bob_orderbook.bids.len(), "{base} {rel} bids must be empty"); - assert_eq!(0, bob_orderbook.asks.len(), "{base} {rel} asks must be empty"); - - mm_bob.stop().await.unwrap(); - mm_alice.stop().await.unwrap(); - } -} diff --git a/mm2src/mm2_main/tests/docker_tests/utxo_ordermatch_v1_tests.rs b/mm2src/mm2_main/tests/docker_tests/utxo_ordermatch_v1_tests.rs new file mode 100644 index 0000000000..6173e3ba9f --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/utxo_ordermatch_v1_tests.rs @@ -0,0 +1,1916 @@ +// UTXO Ordermatching V1 Tests +// +// This module contains UTXO-only ordermatching tests that were extracted from docker_tests_inner.rs +// These tests focus on orderbook behavior, order lifecycle, balance-driven updates, and matching logic. +// They do NOT require ETH/ERC20 containers - only MYCOIN/MYCOIN1 UTXO containers. +// +// Gated by: docker-tests-ordermatch + +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::utxo::{ + fill_address, generate_utxo_coin_with_privkey, generate_utxo_coin_with_random_privkey, rmd160_from_priv, +}; +use crate::integration_tests_common::*; +use coins::{ConfirmPaymentInput, MarketCoinOps, MmCoin, WithdrawRequest}; +use common::{block_on, block_on_f01, executor::Timer, wait_until_sec}; +use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; +use mm2_number::{BigDecimal, BigRational}; +use mm2_test_helpers::for_tests::{ + check_my_swap_status_amounts, mm_dump, mycoin1_conf, mycoin_conf, MarketMakerIt, Mm2TestConf, +}; +use mm2_test_helpers::structs::*; +use serde_json::{json, Value as Json}; +use std::collections::HashMap; +use std::convert::TryInto; +use std::env; +use std::thread; +use std::time::Duration; + +// ============================================================================= +// Order Lifecycle Tests +// Tests for order creation, cancellation, and balance-driven updates +// ============================================================================= + +#[test] +// https://github.com/KomodoPlatform/atomicDEX-API/issues/554 +fn order_should_be_cancelled_when_entire_balance_is_withdrawn() { + let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "999", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + let bob_uuid = json["result"]["uuid"].as_str().unwrap().to_owned(); + + log!("Get MYCOIN/MYCOIN1 orderbook"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {:?}", bob_orderbook); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + let withdraw = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "withdraw", + "coin": "MYCOIN", + "max": true, + "to": "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF", + }))) + .unwrap(); + assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); + + let withdraw: Json = serde_json::from_str(&withdraw.1).unwrap(); + + let send_raw = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "send_raw_transaction", + "coin": "MYCOIN", + "tx_hex": withdraw["tx_hex"], + }))) + .unwrap(); + assert!(send_raw.0.is_success(), "!send_raw: {}", send_raw.1); + + thread::sleep(Duration::from_secs(32)); + + log!("Get MYCOIN/MYCOIN1 orderbook"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {}", serde_json::to_string(&bob_orderbook).unwrap()); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 0, "MYCOIN/MYCOIN1 orderbook must have exactly 0 asks"); + + log!("Get my orders"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + let orders: Json = serde_json::from_str(&rc.1).unwrap(); + log!("my_orders {}", serde_json::to_string(&orders).unwrap()); + assert!( + orders["result"]["maker_orders"].as_object().unwrap().is_empty(), + "maker_orders must be empty" + ); + + let rmd160 = rmd160_from_priv(priv_key); + let order_path = mm_bob.folder.join(format!( + "DB/{}/ORDERS/MY/MAKER/{}.json", + hex::encode(rmd160.take()), + bob_uuid, + )); + log!("Order path {}", order_path.display()); + assert!(!order_path.exists()); + block_on(mm_bob.stop()).unwrap(); +} + +#[test] +fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_after_update() { + let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": "alice passphrase", + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "999", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + log!("Get MYCOIN/MYCOIN1 orderbook"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {:?}", bob_orderbook); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + let withdraw = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "withdraw", + "coin": "MYCOIN", + "amount": "499.99999481", + "to": "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF", + }))) + .unwrap(); + assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); + + let withdraw: Json = serde_json::from_str(&withdraw.1).unwrap(); + + let send_raw = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "send_raw_transaction", + "coin": "MYCOIN", + "tx_hex": withdraw["tx_hex"], + }))) + .unwrap(); + assert!(send_raw.0.is_success(), "!send_raw: {}", send_raw.1); + + thread::sleep(Duration::from_secs(32)); + + log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {}", serde_json::to_string(&bob_orderbook).unwrap()); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + let order_volume = asks[0]["maxvolume"].as_str().unwrap(); + assert_eq!("500", order_volume); // 1000.0 - (499.99999481 + 0.00000274 txfee) = (500.0 + 0.00000274 txfee) + + log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {}", serde_json::to_string(&alice_orderbook).unwrap()); + let asks = alice_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Alice MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + let order_volume = asks[0]["maxvolume"].as_str().unwrap(); + assert_eq!("500", order_volume); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_before_update() { + let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "myipaddr": env::var("BOB_TRADE_IP") .ok(), + "rpcip": env::var("BOB_TRADE_IP") .ok(), + "canbind": env::var("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": "alice passphrase", + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "999", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + log!("Get MYCOIN/MYCOIN1 orderbook"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {:?}", bob_orderbook); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + thread::sleep(Duration::from_secs(2)); + log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {}", serde_json::to_string(&alice_orderbook).unwrap()); + let asks = alice_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Alice MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + let withdraw = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "withdraw", + "coin": "MYCOIN", + "amount": "499.99999481", + "to": "R9imXLs1hEcU9KbFDQq2hJEEJ1P5UoekaF", + }))) + .unwrap(); + assert!(withdraw.0.is_success(), "!withdraw: {}", withdraw.1); + + let withdraw: Json = serde_json::from_str(&withdraw.1).unwrap(); + + let send_raw = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "send_raw_transaction", + "coin": "MYCOIN", + "tx_hex": withdraw["tx_hex"], + }))) + .unwrap(); + assert!(send_raw.0.is_success(), "!send_raw: {}", send_raw.1); + + thread::sleep(Duration::from_secs(32)); + + log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {}", serde_json::to_string(&bob_orderbook).unwrap()); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + let order_volume = asks[0]["maxvolume"].as_str().unwrap(); + assert_eq!("500", order_volume); // 1000.0 - (499.99999481 + 0.00000245 txfee) = (500.0 + 0.00000274 txfee) + + log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {}", serde_json::to_string(&alice_orderbook).unwrap()); + let asks = alice_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Alice MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + let order_volume = asks[0]["maxvolume"].as_str().unwrap(); + assert_eq!("500", order_volume); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +// ============================================================================= +// Partial Fill Tests +// Tests for order updates when partially matched +// ============================================================================= + +#[test] +fn test_order_should_be_updated_when_matched_partially() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "1000", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "500", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + + log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {}", serde_json::to_string(&bob_orderbook).unwrap()); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + let order_volume = asks[0]["maxvolume"].as_str().unwrap(); + assert_eq!("500", order_volume); + + log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {}", serde_json::to_string(&alice_orderbook).unwrap()); + let asks = alice_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Alice MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +// ============================================================================= +// Order Volume Tests +// Tests for setprice max volume and volume constraints +// ============================================================================= + +#[test] +fn test_set_price_max() { + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + // the result of equation x + 0.00001 = 1 + "volume": { + "numer":"99999", + "denom":"100000" + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + // it is slightly more than previous volume so it should fail + "volume": { + "numer":"100000", + "denom":"100000" + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "setprice success, but should fail: {}", rc.1); + block_on(mm_alice.stop()).unwrap(); +} + +// ============================================================================= +// Order Restart/Persistence Tests +// Tests for maker order kickstart on MM restart +// ============================================================================= + +#[test] +fn test_maker_order_should_kick_start_and_appear_in_orderbook_on_restart() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut bob_conf = json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }); + let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + // mm_bob using same DB dir that should kick start the order + bob_conf["dbdir"] = mm_bob.folder.join("DB").to_str().unwrap().into(); + bob_conf["log"] = mm_bob.folder.join("mm2_dup.log").to_str().unwrap().into(); + block_on(mm_bob.stop()).unwrap(); + + let mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); + let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); + + thread::sleep(Duration::from_secs(2)); + + log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); + let rc = block_on(mm_bob_dup.rpc(&json!({ + "userpass": mm_bob_dup.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("Bob orderbook {:?}", bob_orderbook); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "Bob MYCOIN/MYCOIN1 orderbook must have exactly 1 asks"); +} + +#[test] +fn test_maker_order_should_not_kick_start_and_appear_in_orderbook_if_balance_is_withdrawn() { + let (_ctx, coin, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut bob_conf = json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }); + let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let res: SetPriceResponse = serde_json::from_str(&rc.1).unwrap(); + let uuid = res.result.uuid; + + // mm_bob using same DB dir that should kick start the order + bob_conf["dbdir"] = mm_bob.folder.join("DB").to_str().unwrap().into(); + bob_conf["log"] = mm_bob.folder.join("mm2_dup.log").to_str().unwrap().into(); + block_on(mm_bob.stop()).unwrap(); + + let withdraw = block_on_f01(coin.withdraw(WithdrawRequest::new_max( + "MYCOIN".to_string(), + "RRYmiZSDo3UdHHqj1rLKf8cbJroyv9NxXw".to_string(), + ))) + .unwrap(); + block_on_f01(coin.send_raw_tx(&hex::encode(&withdraw.tx.tx_hex().unwrap().0))).unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: withdraw.tx.tx_hex().unwrap().0.to_owned(), + confirmations: 1, + requires_nota: false, + wait_until: wait_until_sec(10), + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); + let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); + + thread::sleep(Duration::from_secs(2)); + + log!("Get MYCOIN/MYCOIN1 orderbook on Bob side"); + let rc = block_on(mm_bob_dup.rpc(&json!({ + "userpass": mm_bob_dup.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("Bob orderbook {:?}", bob_orderbook); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert!(asks.is_empty(), "Bob MYCOIN/MYCOIN1 orderbook must not have asks"); + + let rc = block_on(mm_bob_dup.rpc(&json!({ + "userpass": mm_bob_dup.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + + let res: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); + assert!(res.result.maker_orders.is_empty(), "Bob maker orders must be empty"); + + let order_path = mm_bob.folder.join(format!( + "DB/{}/ORDERS/MY/MAKER/{}.json", + hex::encode(rmd160_from_priv(bob_priv_key).take()), + uuid + )); + + log!("Order path {}", order_path.display()); + assert!(!order_path.exists()); +} + +#[test] +fn test_maker_order_kick_start_should_trigger_subscription_and_match() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + + let relay_conf = json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": "relay", + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }); + let relay = MarketMakerIt::start(relay_conf, "pass".to_string(), None).unwrap(); + let (_relay_dump_log, _relay_dump_dashboard) = mm_dump(&relay.log_path); + + let mut bob_conf = json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", relay.ip)], + }); + let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", relay.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + // mm_bob using same DB dir that should kick start the order + bob_conf["dbdir"] = mm_bob.folder.join("DB").to_str().unwrap().into(); + bob_conf["log"] = mm_bob.folder.join("mm2_dup.log").to_str().unwrap().into(); + block_on(mm_bob.stop()).unwrap(); + + let mut mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); + let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob_dup, "MYCOIN1", &[], None))); + + log!("Give restarted Bob 2 seconds to kickstart the order"); + thread::sleep(Duration::from_secs(2)); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 1, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_bob_dup.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); +} + +// ============================================================================= +// Same Private Key Edge Cases +// Tests for edge cases when using the same private key across nodes +// ============================================================================= + +#[test] +fn test_orders_should_match_on_both_nodes_with_same_priv() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice_1 = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_1_dump_log, _alice_1_dump_dashboard) = mm_dump(&mm_alice_1.log_path); + + let mut mm_alice_2 = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_2_dump_log, _alice_2_dump_dashboard) = mm_dump(&mm_alice_2.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_1, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_1, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_2, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice_2, "MYCOIN1", &[], None))); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_alice_1.rpc(&json!({ + "userpass": mm_alice_1.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_alice_1.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + + let rc = block_on(mm_alice_2.rpc(&json!({ + "userpass": mm_alice_2.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_alice_2.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice_1.stop()).unwrap(); + block_on(mm_alice_2.stop()).unwrap(); +} + +#[test] +fn test_maker_and_taker_order_created_with_same_priv_should_not_match() { + let (_ctx, coin, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, coin1, _) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1000.into()); + fill_address(&coin1, &coin.my_address().unwrap(), 1000.into(), 30); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_1_dump_log, _alice_1_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_bob.wait_for_log(5., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap_err(); + block_on(mm_alice.wait_for_log(5., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap_err(); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +// ============================================================================= +// Order Conversion and Cancellation Tests +// Tests for taker-to-maker order conversion and proper cleanup +// ============================================================================= + +#[test] +fn test_taker_order_converted_to_maker_should_cancel_properly_when_matched() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "sell", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 1, + "timeout": 2, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + + log!("Give Bob 4 seconds to convert order to maker"); + block_on(Timer::sleep(4.)); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 1, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + + log!("Give Bob 2 seconds to cancel the order"); + thread::sleep(Duration::from_secs(2)); + log!("Get my_orders on Bob side"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + let my_orders_json: Json = serde_json::from_str(&rc.1).unwrap(); + let maker_orders: HashMap = + serde_json::from_value(my_orders_json["result"]["maker_orders"].clone()).unwrap(); + assert!(maker_orders.is_empty()); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("Bob orderbook {:?}", bob_orderbook); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 0, "Bob MYCOIN/MYCOIN1 orderbook must be empty"); + + log!("Get MYCOIN/MYCOIN1 orderbook on Alice side"); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let alice_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("Alice orderbook {:?}", alice_orderbook); + let asks = alice_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 0, "Alice MYCOIN/MYCOIN1 orderbook must be empty"); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +// ============================================================================= +// Best Price Matching Tests +// Tests for ensuring taker matches with best available price +// ============================================================================= + +// https://github.com/KomodoPlatform/atomicDEX-API/issues/1053 +#[test] +fn test_taker_should_match_with_best_price_buy() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 4000.into()); + let (_ctx, _, eve_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + let mut mm_eve = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(eve_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_eve_dump_log, _eve_dump_dashboard) = mm_dump(&mm_eve.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN1", &[], None))); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 2, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_eve.rpc(&json!({ + "userpass": mm_eve.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + // subscribe alice to the orderbook topic to not miss eve's message + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!alice orderbook: {}", rc.1); + log!("alice orderbook {}", rc.1); + + thread::sleep(Duration::from_secs(1)); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 3, + "volume": "1000", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + let alice_buy: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); + + block_on(mm_eve.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + + thread::sleep(Duration::from_secs(2)); + + block_on(check_my_swap_status_amounts( + &mm_alice, + alice_buy.result.uuid, + 1000.into(), + 1000.into(), + )); + block_on(check_my_swap_status_amounts( + &mm_eve, + alice_buy.result.uuid, + 1000.into(), + 1000.into(), + )); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + block_on(mm_eve.stop()).unwrap(); +} + +// https://github.com/KomodoPlatform/atomicDEX-API/issues/1053 +#[test] +fn test_taker_should_match_with_best_price_sell() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 4000.into()); + let (_ctx, _, eve_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 2000.into()); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + let mut mm_eve = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(eve_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_eve_dump_log, _eve_dump_dashboard) = mm_dump(&mm_eve.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_eve, "MYCOIN1", &[], None))); + + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 2, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_eve.rpc(&json!({ + "userpass": mm_eve.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + // subscribe alice to the orderbook topic to not miss eve's message + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!alice orderbook: {}", rc.1); + log!("alice orderbook {}", rc.1); + + thread::sleep(Duration::from_secs(1)); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "sell", + "base": "MYCOIN1", + "rel": "MYCOIN", + "price": "0.1", + "volume": "1000", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let alice_sell: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); + + block_on(mm_eve.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + + thread::sleep(Duration::from_secs(2)); + + block_on(check_my_swap_status_amounts( + &mm_alice, + alice_sell.result.uuid, + 1000.into(), + 1000.into(), + )); + block_on(check_my_swap_status_amounts( + &mm_eve, + alice_sell.result.uuid, + 1000.into(), + 1000.into(), + )); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + block_on(mm_eve.stop()).unwrap(); +} + +// ============================================================================= +// RPC Response Format Tests +// Tests for validating RPC response formats (UTXO-only variants) +// ============================================================================= + +#[test] +fn test_set_price_response_format() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + log!("Issue bob MYCOIN/MYCOIN1 sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 0.1 + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let rc_json: Json = serde_json::from_str(&rc.1).unwrap(); + let _: BigDecimal = serde_json::from_value(rc_json["result"]["max_base_vol"].clone()).unwrap(); + let _: BigDecimal = serde_json::from_value(rc_json["result"]["min_base_vol"].clone()).unwrap(); + let _: BigDecimal = serde_json::from_value(rc_json["result"]["price"].clone()).unwrap(); + + let _: BigRational = serde_json::from_value(rc_json["result"]["max_base_vol_rat"].clone()).unwrap(); + let _: BigRational = serde_json::from_value(rc_json["result"]["min_base_vol_rat"].clone()).unwrap(); + let _: BigRational = serde_json::from_value(rc_json["result"]["price_rat"].clone()).unwrap(); +} + +#[test] +fn test_buy_response_format() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), privkey); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + log!("Issue bob buy request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 0.1, + "base_confs": 5, + "base_nota": true, + "rel_confs": 4, + "rel_nota": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + let _: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); +} + +#[test] +fn test_sell_response_format() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + log!("Issue bob sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "sell", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 0.1, + "base_confs": 5, + "base_nota": true, + "rel_confs": 4, + "rel_nota": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let _: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); +} + +#[test] +fn test_my_orders_response_format() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN1", 10000.into(), privkey); + generate_utxo_coin_with_privkey("MYCOIN", 10000.into(), privkey); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + log!("Issue bob buy request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 0.1, + "base_confs": 5, + "base_nota": true, + "rel_confs": 4, + "rel_nota": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + log!("Issue bob setprice request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 0.1, + "base_confs": 5, + "base_nota": true, + "rel_confs": 4, + "rel_nota": false, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + log!("Issue bob my_orders request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + + let _: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); +} + +// ============================================================================= +// Min Volume and Dust Tests +// Tests for order min_volume constraints and dust thresholds +// ============================================================================= + +#[test] +fn test_buy_min_volume() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN1", 1000.into(), privkey); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + let min_volume: BigDecimal = "0.1".parse().unwrap(); + log!("Issue bob MYCOIN/MYCOIN1 buy request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": "2", + "volume": "1", + "min_volume": min_volume, + "order_type": { + "type": "GoodTillCancelled" + }, + "timeout": 2, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let response: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(min_volume, response.result.min_volume); + + log!("Wait for 4 seconds for Bob order to be converted to maker"); + block_on(Timer::sleep(4.)); + + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + let my_orders: MyOrdersRpcResult = serde_json::from_str(&rc.1).unwrap(); + assert_eq!( + 1, + my_orders.result.maker_orders.len(), + "maker_orders must have exactly 1 order" + ); + assert!(my_orders.result.taker_orders.is_empty(), "taker_orders must be empty"); + let maker_order = my_orders.result.maker_orders.get(&response.result.uuid).unwrap(); + + let expected_min_volume: BigDecimal = "0.2".parse().unwrap(); + assert_eq!(expected_min_volume, maker_order.min_base_vol); +} + +#[test] +fn test_sell_min_volume() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + let min_volume: BigDecimal = "0.1".parse().unwrap(); + log!("Issue bob MYCOIN/MYCOIN1 sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "sell", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": "1", + "volume": "1", + "min_volume": min_volume, + "order_type": { + "type": "GoodTillCancelled" + }, + "timeout": 2, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let rc_json: Json = serde_json::from_str(&rc.1).unwrap(); + let uuid: String = serde_json::from_value(rc_json["result"]["uuid"].clone()).unwrap(); + let min_volume_response: BigDecimal = serde_json::from_value(rc_json["result"]["min_volume"].clone()).unwrap(); + assert_eq!(min_volume, min_volume_response); + + log!("Wait for 4 seconds for Bob order to be converted to maker"); + block_on(Timer::sleep(4.)); + + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + let my_orders: Json = serde_json::from_str(&rc.1).unwrap(); + let my_maker_orders: HashMap = + serde_json::from_value(my_orders["result"]["maker_orders"].clone()).unwrap(); + let my_taker_orders: HashMap = + serde_json::from_value(my_orders["result"]["taker_orders"].clone()).unwrap(); + assert_eq!(1, my_maker_orders.len(), "maker_orders must have exactly 1 order"); + assert!(my_taker_orders.is_empty(), "taker_orders must be empty"); + let maker_order = my_maker_orders.get(&uuid).unwrap(); + let min_volume_maker: BigDecimal = serde_json::from_value(maker_order["min_base_vol"].clone()).unwrap(); + assert_eq!(min_volume, min_volume_maker); +} + +#[test] +fn test_setprice_min_volume_dust() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + + let coins = json! ([ + {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"dust":10000000,"protocol":{"type":"UTXO"}}, + mycoin1_conf(1000), + ]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + log!("Issue bob MYCOIN/MYCOIN1 sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": "1", + "volume": "1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let response: SetPriceResponse = serde_json::from_str(&rc.1).unwrap(); + let expected_min = BigDecimal::from(1); + assert_eq!(expected_min, response.result.min_base_vol); + + log!("Issue bob MYCOIN/MYCOIN1 sell request less than dust"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": "1", + // Less than dust, should fial + "volume": 0.01, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "!setprice: {}", rc.1); +} + +#[test] +fn test_sell_min_volume_dust() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + + let coins = json! ([ + {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"dust":10000000,"protocol":{"type":"UTXO"}}, + mycoin1_conf(1000), + ]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + // Enable coins + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + log!("Issue bob MYCOIN/MYCOIN1 sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "sell", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": "1", + "volume": "1", + "order_type": { + "type": "FillOrKill" + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let response: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); + let expected_min = BigDecimal::from(1); + assert_eq!(response.result.min_volume, expected_min); + + log!("Issue bob MYCOIN/MYCOIN1 sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "sell", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": "1", + // Less than dust + "volume": 0.01, + "order_type": { + "type": "FillOrKill" + } + }))) + .unwrap(); + assert!(!rc.0.is_success(), "!sell: {}", rc.1); +} + +// ============================================================================= +// P2P Infrastructure Tests +// These tests verify P2P networking behavior (UTXO-based, coin-agnostic) +// ============================================================================= + +#[test] +fn test_peer_time_sync_validation() { + let timeoffset_tolerable = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() - 1; + let timeoffset_too_big = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() + 1; + + let start_peers_with_time_offset = |offset: i64| -> (Json, Json) { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 10.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 10.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let bob_conf = Mm2TestConf::seednode(&hex::encode(bob_priv_key), &coins); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + bob_conf.conf, + bob_conf.rpc_password, + None, + &[], + )) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + let alice_conf = + Mm2TestConf::light_node(&hex::encode(alice_priv_key), &coins, &[mm_bob.ip.to_string().as_str()]); + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password, + None, + &[("TEST_TIMESTAMP_OFFSET", offset.to_string().as_str())], + )) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + let res_bob = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!(res_bob.0.is_success(), "!get_directly_connected_peers: {}", res_bob.1); + let bob_peers = serde_json::from_str::(&res_bob.1).unwrap(); + + let res_alice = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!( + res_alice.0.is_success(), + "!get_directly_connected_peers: {}", + res_alice.1 + ); + let alice_peers = serde_json::from_str::(&res_alice.1).unwrap(); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + (bob_peers, alice_peers) + }; + + // check with small time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_tolerable); + assert!( + bob_peers["result"].as_object().unwrap().len() == 1, + "bob must have one peer" + ); + assert!( + alice_peers["result"].as_object().unwrap().len() == 1, + "alice must have one peer" + ); + + // check with too big time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_too_big); + assert!( + bob_peers["result"].as_object().unwrap().is_empty(), + "bob must have no peers" + ); + assert!( + alice_peers["result"].as_object().unwrap().is_empty(), + "alice must have no peers" + ); +} diff --git a/mm2src/mm2_main/tests/docker_tests/utxo_swaps_v1_tests.rs b/mm2src/mm2_main/tests/docker_tests/utxo_swaps_v1_tests.rs new file mode 100644 index 0000000000..b1da14ddb9 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/utxo_swaps_v1_tests.rs @@ -0,0 +1,2285 @@ +// UTXO Swaps V1 Tests +// +// This module contains UTXO-only swap tests that were extracted from docker_tests_inner.rs +// These tests focus on UTXO swap mechanics, payment lifecycle, and related functionality. +// They do NOT require ETH/ERC20 containers - only MYCOIN/MYCOIN1 UTXO containers. +// +// Gated by: docker-tests-swaps + +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::swap::trade_base_rel; +use crate::docker_tests::helpers::utxo::{ + fill_address, generate_utxo_coin_with_privkey, generate_utxo_coin_with_random_privkey, rmd160_from_priv, + utxo_coin_from_privkey, +}; +use crate::integration_tests_common::*; +use bitcrypto::dhash160; +use chain::OutPoint; +use coins::utxo::rpc_clients::UnspentInfo; +use coins::utxo::{GetUtxoListOps, UtxoCommonOps}; +use coins::{ + ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, RefundPaymentArgs, SearchForSwapTxSpendInput, + SendPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TransactionEnum, +}; +use common::{block_on, block_on_f01, executor::Timer, now_sec, wait_until_sec}; +use mm2_number::{BigDecimal, MmNumber}; +use mm2_test_helpers::for_tests::{ + get_locked_amount, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, + MarketMakerIt, Mm2TestConf, +}; +use mm2_test_helpers::structs::*; +use serde_json::{json, Value as Json}; +use std::collections::HashMap; +use std::str::FromStr; +use std::thread; +use std::time::Duration; + +// ============================================================================= +// UTXO Swap Spend/Refund Mechanics Tests +// Tests for searching swap tx spend status (refunded vs spent) +// ============================================================================= + +#[test] +fn test_search_for_swap_tx_spend_native_was_refunded_taker() { + let timeout = wait_until_sec(120); + let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let my_public_key = coin.my_public_key().unwrap(); + + let time_lock = now_sec() - 3600; + let taker_payment_args = SendPaymentArgs { + time_lock_duration: 0, + time_lock, + other_pubkey: my_public_key, + secret_hash: &[0; 20], + amount: 1u64.into(), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let tx = block_on(coin.send_taker_payment(taker_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + let maker_refunds_payment_args = RefundPaymentArgs { + payment_tx: &tx.tx_hex(), + time_lock, + other_pubkey: my_public_key, + tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &[0; 20], + }, + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + let refund_tx = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let search_input = SearchForSwapTxSpendInput { + time_lock, + other_pub: coin.my_public_key().unwrap(), + secret_hash: &[0; 20], + tx: &tx.tx_hex(), + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) + .unwrap() + .unwrap(); + assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); +} + +#[test] +fn test_for_non_existent_tx_hex_utxo() { + // This test shouldn't wait till timeout! + let timeout = wait_until_sec(120); + let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + // bad transaction hex + let tx = hex::decode("0400008085202f8902bf17bf7d1daace52e08f732a6b8771743ca4b1cb765a187e72fd091a0aabfd52000000006a47304402203eaaa3c4da101240f80f9c5e9de716a22b1ec6d66080de6a0cca32011cd77223022040d9082b6242d6acf9a1a8e658779e1c655d708379862f235e8ba7b8ca4e69c6012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffffff023ca13c0e9e085dd13f481f193e8a3e8fd609020936e98b5587342d994f4d020000006b483045022100c0ba56adb8de923975052312467347d83238bd8d480ce66e8b709a7997373994022048507bcac921fdb2302fa5224ce86e41b7efc1a2e20ae63aa738dfa99b7be826012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0300e1f5050000000017a9141ee6d4c38a3c078eab87ad1a5e4b00f21259b10d87000000000000000016611400000000000000000000000000000000000000001b94d736000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac2d08e35e000000000000000000000000000000").unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx, + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + let actual = block_on_f01(coin.wait_for_confirmations(confirm_payment_input)) + .err() + .unwrap(); + assert!(actual.contains( + "Tx d342ff9da528a2e262bddf2b6f9a27d1beb7aeb03f0fc8d9eac2987266447e44 was not found on chain after 10 tries" + )); +} + +#[test] +fn test_search_for_swap_tx_spend_native_was_refunded_maker() { + let timeout = wait_until_sec(120); + let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let my_public_key = coin.my_public_key().unwrap(); + + let time_lock = now_sec() - 3600; + let maker_payment_args = SendPaymentArgs { + time_lock_duration: 0, + time_lock, + other_pubkey: my_public_key, + secret_hash: &[0; 20], + amount: 1u64.into(), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let tx = block_on(coin.send_maker_payment(maker_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + let maker_refunds_payment_args = RefundPaymentArgs { + payment_tx: &tx.tx_hex(), + time_lock, + other_pubkey: my_public_key, + tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &[0; 20], + }, + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + let refund_tx = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let search_input = SearchForSwapTxSpendInput { + time_lock, + other_pub: coin.my_public_key().unwrap(), + secret_hash: &[0; 20], + tx: &tx.tx_hex(), + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) + .unwrap() + .unwrap(); + assert_eq!(FoundSwapTxSpend::Refunded(refund_tx), found); +} + +#[test] +fn test_search_for_taker_swap_tx_spend_native_was_spent_by_maker() { + let timeout = wait_until_sec(120); + let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let secret = [0; 32]; + let my_pubkey = coin.my_public_key().unwrap(); + + let secret_hash = dhash160(&secret); + let time_lock = now_sec() - 3600; + let taker_payment_args = SendPaymentArgs { + time_lock_duration: 0, + time_lock, + other_pubkey: my_pubkey, + secret_hash: secret_hash.as_slice(), + amount: 1u64.into(), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let tx = block_on(coin.send_taker_payment(taker_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + let maker_spends_payment_args = SpendPaymentArgs { + other_payment_tx: &tx.tx_hex(), + time_lock, + other_pubkey: my_pubkey, + secret: &secret, + secret_hash: secret_hash.as_slice(), + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + let spend_tx = block_on(coin.send_maker_spends_taker_payment(maker_spends_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: spend_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let search_input = SearchForSwapTxSpendInput { + time_lock, + other_pub: coin.my_public_key().unwrap(), + secret_hash: &*dhash160(&secret), + tx: &tx.tx_hex(), + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) + .unwrap() + .unwrap(); + assert_eq!(FoundSwapTxSpend::Spent(spend_tx), found); +} + +#[test] +fn test_search_for_maker_swap_tx_spend_native_was_spent_by_taker() { + let timeout = wait_until_sec(120); + let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let secret = [0; 32]; + let my_pubkey = coin.my_public_key().unwrap(); + + let time_lock = now_sec() - 3600; + let secret_hash = dhash160(&secret); + let maker_payment_args = SendPaymentArgs { + time_lock_duration: 0, + time_lock, + other_pubkey: my_pubkey, + secret_hash: secret_hash.as_slice(), + amount: 1u64.into(), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let tx = block_on(coin.send_maker_payment(maker_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + let taker_spends_payment_args = SpendPaymentArgs { + other_payment_tx: &tx.tx_hex(), + time_lock, + other_pubkey: my_pubkey, + secret: &secret, + secret_hash: secret_hash.as_slice(), + swap_contract_address: &None, + swap_unique_data: &[], + watcher_reward: false, + }; + let spend_tx = block_on(coin.send_taker_spends_maker_payment(taker_spends_payment_args)).unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: spend_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + block_on_f01(coin.wait_for_confirmations(confirm_payment_input)).unwrap(); + + let search_input = SearchForSwapTxSpendInput { + time_lock, + other_pub: coin.my_public_key().unwrap(), + secret_hash: &*dhash160(&secret), + tx: &tx.tx_hex(), + search_from_block: 0, + swap_contract_address: &None, + swap_unique_data: &[], + }; + let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) + .unwrap() + .unwrap(); + assert_eq!(FoundSwapTxSpend::Spent(spend_tx), found); +} + +#[test] +fn test_one_hundred_maker_payments_in_a_row_native() { + let timeout = 30; + let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + let secret = [0; 32]; + let my_pubkey = coin.my_public_key().unwrap(); + + let time_lock = now_sec() - 3600; + let mut unspents = vec![]; + let mut sent_tx = vec![]; + for i in 0..100 { + let maker_payment_args = SendPaymentArgs { + time_lock_duration: 0, + time_lock: time_lock + i, + other_pubkey: my_pubkey, + secret_hash: &*dhash160(&secret), + amount: 1.into(), + swap_contract_address: &coin.swap_contract_address(), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let tx = block_on(coin.send_maker_payment(maker_payment_args)).unwrap(); + if let TransactionEnum::UtxoTx(tx) = tx { + unspents.push(UnspentInfo { + outpoint: OutPoint { + hash: tx.hash(), + index: 2, + }, + value: tx.outputs[2].value, + height: None, + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), + }); + sent_tx.push(tx); + } + } + + let recently_sent = block_on(coin.as_ref().recently_spent_outpoints.lock()); + + unspents = recently_sent + .replace_spent_outputs_with_cache(unspents.into_iter().collect()) + .into_iter() + .collect(); + + let last_tx = sent_tx.last().unwrap(); + let expected_unspent = UnspentInfo { + outpoint: OutPoint { + hash: last_tx.hash(), + index: 2, + }, + value: last_tx.outputs[2].value, + height: None, + script: last_tx.outputs[2].script_pubkey.clone().into(), + }; + assert_eq!(vec![expected_unspent], unspents); +} + +// ============================================================================= +// UTXO-only Swap and Trade Tests +// Tests for complete swap flows using only MYCOIN/MYCOIN1 +// ============================================================================= + +#[test] +fn test_trade_base_rel_mycoin_mycoin1_coins() { + trade_base_rel(("MYCOIN", "MYCOIN1")); +} + +#[test] +fn test_trade_base_rel_mycoin_mycoin1_coins_burnkey_as_alice() { + // Trade with burn pubkey set as Alice's pubkey (for testing purposes) + // Uses the SET_BURN_PUBKEY_TO_ALICE flag via trade_base_rel + use crate::docker_tests::helpers::env::SET_BURN_PUBKEY_TO_ALICE; + SET_BURN_PUBKEY_TO_ALICE.set(true); + trade_base_rel(("MYCOIN", "MYCOIN1")); + SET_BURN_PUBKEY_TO_ALICE.set(false); +} + +// ============================================================================= +// Max Volume Tests +// Tests for max_taker_vol and max_maker_vol RPCs +// ============================================================================= + +#[test] +fn test_get_max_taker_vol() { + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "max_taker_vol", + "coin": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); + let json: MaxTakerVolResponse = serde_json::from_str(&rc.1).unwrap(); + let expected = MmNumber::from((77699596737u64, 77800000000u64)).to_fraction(); + assert_eq!(json.result, expected); + assert_eq!(json.coin, "MYCOIN1"); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "sell", + "base": "MYCOIN1", + "rel": "MYCOIN", + "price": 1, + "volume": json.result, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +fn test_get_max_taker_vol_dex_fee_min_tx_amount() { + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", "0.00532845".parse().unwrap()); + let coins = json!([mycoin_conf(10000), mycoin1_conf(10000)]); + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "max_taker_vol", + "coin": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["numer"], Json::from("105331")); + assert_eq!(json["result"]["denom"], Json::from("20000000")); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "sell", + "base": "MYCOIN1", + "rel": "MYCOIN", + "price": 1, + "volume": { + "numer": json["result"]["numer"], + "denom": json["result"]["denom"], + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +fn test_get_max_taker_vol_dust_threshold() { + let (_ctx, coin, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", "0.0014041".parse().unwrap()); + let coins = json!([ + mycoin_conf(10000), + {"coin":"MYCOIN1","asset":"MYCOIN1","txversion":4,"overwintered":1,"txfee":10000,"protocol":{"type":"UTXO"},"dust":72800} + ]); + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm.log_path); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "max_taker_vol", + "coin": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!max_taker_vol {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + let result: MmNumber = serde_json::from_value(json["result"].clone()).unwrap(); + assert!(result.is_zero()); + + fill_address(&coin, &coin.my_address().unwrap(), "0.0002".parse().unwrap(), 30); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "max_taker_vol", + "coin": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["numer"], Json::from("3973")); + assert_eq!(json["result"]["denom"], Json::from("5000000")); + + block_on(mm.stop()).unwrap(); +} + +#[test] +fn test_get_max_taker_vol_with_kmd() { + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); + let coins = json!([mycoin_conf(10000), mycoin1_conf(10000), kmd_conf(10000)]); + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + let electrum = block_on(enable_electrum( + &mm_alice, + "KMD", + false, + &[ + "electrum1.cipig.net:10001", + "electrum2.cipig.net:10001", + "electrum3.cipig.net:10001", + ], + )); + log!("{:?}", electrum); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "max_taker_vol", + "coin": "MYCOIN1", + "trade_with": "KMD", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(json["result"]["numer"], Json::from("2589865579")); + assert_eq!(json["result"]["denom"], Json::from("2593000000")); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "sell", + "base": "MYCOIN1", + "rel": "KMD", + "price": 1, + "volume": { + "numer": json["result"]["numer"], + "denom": json["result"]["denom"], + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +fn test_get_max_maker_vol() { + let (_ctx, _, priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(priv_key)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + let expected_volume = MmNumber::from("0.99999726"); + let expected = MaxMakerVolResponse { + coin: "MYCOIN1".to_string(), + volume: MmNumberMultiRepr::from(expected_volume.clone()), + balance: MmNumberMultiRepr::from(1), + locked_by_swaps: MmNumberMultiRepr::from(0), + }; + let actual = block_on(max_maker_vol(&mm, "MYCOIN1")).unwrap::(); + assert_eq!(actual, expected); + + let res = block_on(set_price(&mm, "MYCOIN1", "MYCOIN", "1", "0", true, None)); + assert_eq!(res.result.max_base_vol, expected_volume.to_decimal()); +} + +#[test] +fn test_get_max_maker_vol_error() { + let priv_key = random_secp256k1_secret(); + let coins = json!([mycoin_conf(1000)]); + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(priv_key)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + + let actual_error = block_on(max_maker_vol(&mm, "MYCOIN")).unwrap_err::(); + let expected_error = max_maker_vol_error::NotSufficientBalance { + coin: "MYCOIN".to_owned(), + available: 0.into(), + required: BigDecimal::from(1000) / BigDecimal::from(100_000_000), + locked_by_swaps: None, + }; + assert_eq!(actual_error.error_type, "NotSufficientBalance"); + assert_eq!(actual_error.error_data, Some(expected_error)); +} + +// ============================================================================= +// UTXO Merge and Consolidation Tests +// Tests for UTXO merge functionality and consolidate_utxos RPC +// ============================================================================= + +#[test] +fn test_utxo_merge() { + let timeout = 30; + let (_ctx, coin, privkey) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(privkey)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let native = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "enable", + "coin": "MYCOIN", + "mm2": 1, + "utxo_merge_params": { + "merge_at": 2, + "check_every": 1, + } + }))) + .unwrap(); + assert!(native.0.is_success(), "'enable' failed: {}", native.1); + log!("Enable result {}", native.1); + + block_on(mm_bob.wait_for_log(4., |log| log.contains("Starting UTXO merge loop for coin MYCOIN"))).unwrap(); + + block_on(mm_bob.wait_for_log(4., |log| { + log.contains("UTXO merge of 5 outputs successful for coin=MYCOIN, tx_hash") + })) + .unwrap(); + + thread::sleep(Duration::from_secs(2)); + let address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + let (unspents, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); + assert_eq!(unspents.len(), 1); +} + +#[test] +fn test_utxo_merge_max_merge_at_once() { + let timeout = 30; + let (_ctx, coin, privkey) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + fill_address(&coin, &coin.my_address().unwrap(), 2.into(), timeout); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(privkey)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let native = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "enable", + "coin": "MYCOIN", + "mm2": 1, + "utxo_merge_params": { + "merge_at": 3, + "check_every": 1, + "max_merge_at_once": 4, + } + }))) + .unwrap(); + assert!(native.0.is_success(), "'enable' failed: {}", native.1); + log!("Enable result {}", native.1); + + block_on(mm_bob.wait_for_log(4., |log| log.contains("Starting UTXO merge loop for coin MYCOIN"))).unwrap(); + + block_on(mm_bob.wait_for_log(4., |log| { + log.contains("UTXO merge of 4 outputs successful for coin=MYCOIN, tx_hash") + })) + .unwrap(); + + thread::sleep(Duration::from_secs(2)); + let address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + let (unspents, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); + assert_eq!(unspents.len(), 2); +} + +#[test] +fn test_consolidate_utxos_rpc() { + let timeout = 30; + let utxos = 50; + let (_ctx, coin, privkey) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + + for i in 1..=utxos { + fill_address(&coin, &coin.my_address().unwrap(), i.into(), timeout); + } + + let coins = json!([mycoin_conf(1000)]); + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(privkey)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + + let consolidate_rpc = |merge_at: u32, merge_at_once: u32| { + block_on(mm_bob.rpc(&json!({ + "mmrpc": "2.0", + "userpass": mm_bob.userpass, + "method": "consolidate_utxos", + "params": { + "coin": "MYCOIN", + "merge_conditions": { + "merge_at": merge_at, + "max_merge_at_once": merge_at_once, + }, + "broadcast": true + } + }))) + .unwrap() + }; + + let res = consolidate_rpc(52, 4); + assert!(!res.0.is_success(), "Expected error for merge_at > utxos: {}", res.1); + + let res = consolidate_rpc(30, 4); + assert!(res.0.is_success(), "Consolidate utxos failed: {}", res.1); + + let res: RpcSuccessResponse = + serde_json::from_str(&res.1).expect("Expected 'RpcSuccessResponse'"); + assert_eq!(res.result.consolidated_utxos.len(), 4); + for i in 1..=4 { + assert_eq!(res.result.consolidated_utxos[i - 1].value, (i as u32).into()); + } + + thread::sleep(Duration::from_secs(2)); + let address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + let (unspents, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); + assert_eq!(unspents.len(), 51 - 4 + 1); +} + +#[test] +fn test_fetch_utxos_rpc() { + let timeout = 30; + let (_ctx, coin, privkey) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + + for i in 1..=10 { + fill_address(&coin, &coin.my_address().unwrap(), i.into(), timeout); + } + + let coins = json!([mycoin_conf(1000)]); + let mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(privkey)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + + let fetch_utxo_rpc = || { + let res = block_on(mm_bob.rpc(&json!({ + "mmrpc": "2.0", + "userpass": mm_bob.userpass, + "method": "fetch_utxos", + "params": { + "coin": "MYCOIN" + } + }))) + .unwrap(); + assert!(res.0.is_success(), "Fetch UTXOs failed: {}", res.1); + let res: RpcSuccessResponse = + serde_json::from_str(&res.1).expect("Expected 'RpcSuccessResponse'"); + res.result + }; + + let res = fetch_utxo_rpc(); + assert!(res.total_count == 11); + + fill_address(&coin, &coin.my_address().unwrap(), 100.into(), timeout); + thread::sleep(Duration::from_secs(2)); + + let res = fetch_utxo_rpc(); + assert!(res.total_count == 12); + assert!(res.addresses[0].utxos.iter().any(|utxo| utxo.value == 100.into())); +} + +// ============================================================================= +// Withdraw Tests (UTXO-only) +// Tests for withdraw RPC with insufficient balance +// ============================================================================= + +#[test] +fn test_withdraw_not_sufficient_balance() { + let privkey = random_secp256k1_secret(); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(privkey)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm.log_path); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + + let amount = BigDecimal::from(1); + let withdraw = block_on(mm.rpc(&json!({ + "mmrpc": "2.0", + "userpass": mm.userpass, + "method": "withdraw", + "params": { + "coin": "MYCOIN", + "to": "RJTYiYeJ8eVvJ53n2YbrVmxWNNMVZjDGLh", + "amount": amount, + }, + "id": 0, + }))) + .unwrap(); + + assert!(withdraw.0.is_client_error(), "MYCOIN withdraw: {}", withdraw.1); + log!("error: {:?}", withdraw.1); + let error: RpcErrorResponse = + serde_json::from_str(&withdraw.1).expect("Expected 'RpcErrorResponse'"); + let expected_error = withdraw_error::NotSufficientBalance { + coin: "MYCOIN".to_owned(), + available: 0.into(), + required: amount, + }; + assert_eq!(error.error_type, "NotSufficientBalance"); + assert_eq!(error.error_data, Some(expected_error)); + + let balance = BigDecimal::from(1) / BigDecimal::from(2); + let (_ctx, coin) = utxo_coin_from_privkey("MYCOIN", privkey); + fill_address(&coin, &coin.my_address().unwrap(), balance.clone(), 30); + + let txfee = BigDecimal::from_str("0.00000211").unwrap(); + let withdraw = block_on(mm.rpc(&json!({ + "mmrpc": "2.0", + "userpass": mm.userpass, + "method": "withdraw", + "params": { + "coin": "MYCOIN", + "to": "RJTYiYeJ8eVvJ53n2YbrVmxWNNMVZjDGLh", + "amount": balance, + }, + "id": 0, + }))) + .unwrap(); + + assert!(withdraw.0.is_client_error(), "MYCOIN withdraw: {}", withdraw.1); + log!("error: {:?}", withdraw.1); + let error: RpcErrorResponse = + serde_json::from_str(&withdraw.1).expect("Expected 'RpcErrorResponse'"); + let expected_error = withdraw_error::NotSufficientBalance { + coin: "MYCOIN".to_owned(), + available: balance.clone(), + required: balance + txfee, + }; + assert_eq!(error.error_type, "NotSufficientBalance"); + assert_eq!(error.error_data, Some(expected_error)); +} + +// ============================================================================= +// Locked Amount Tests +// Tests for locked_amount RPC during swaps +// ============================================================================= + +#[test] +fn test_locked_amount() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let bob_conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(bob_priv_key)), &coins); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let alice_conf = Mm2TestConf::light_node( + &format!("0x{}", hex::encode(alice_priv_key)), + &coins, + &[&mm_bob.ip.to_string()], + ); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + + block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN", "MYCOIN1")], + 1., + 1., + 777., + )); + + let locked_bob = block_on(get_locked_amount(&mm_bob, "MYCOIN")); + assert_eq!(locked_bob.coin, "MYCOIN"); + + let expected_result: MmNumberMultiRepr = MmNumber::from("777.00000274").into(); + assert_eq!(expected_result, locked_bob.locked_amount); + + let locked_alice = block_on(get_locked_amount(&mm_alice, "MYCOIN1")); + assert_eq!(locked_alice.coin, "MYCOIN1"); + + let expected_result: MmNumberMultiRepr = MmNumber::from("778.00000519").into(); + assert_eq!(expected_result, locked_alice.locked_amount); +} + +// ============================================================================= +// Swap Lifecycle Tests +// Tests for swap stopping, order transformation, etc. +// ============================================================================= + +#[test] +fn swaps_should_stop_on_stop_rpc() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let bob_conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(bob_priv_key)), &coins); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let alice_conf = Mm2TestConf::light_node( + &format!("0x{}", hex::encode(alice_priv_key)), + &coins, + &[&mm_bob.ip.to_string()], + ); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + + block_on(start_swaps( + &mut mm_bob, + &mut mm_alice, + &[("MYCOIN", "MYCOIN1")], + 1., + 1., + 0.0001, + )); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +fn test_fill_or_kill_taker_order_should_not_transform_to_maker() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + log!("Issue bob MYCOIN/MYCOIN1 sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "sell", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 0.1, + "order_type": { + "type": "FillOrKill" + }, + "timeout": 2, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let sell_json: Json = serde_json::from_str(&rc.1).unwrap(); + let order_type = sell_json["result"]["order_type"]["type"].as_str(); + assert_eq!(order_type, Some("FillOrKill")); + + log!("Wait for 4 seconds for Bob order to be cancelled"); + block_on(Timer::sleep(4.)); + + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + let my_orders: Json = serde_json::from_str(&rc.1).unwrap(); + let my_maker_orders: HashMap = + serde_json::from_value(my_orders["result"]["maker_orders"].clone()).unwrap(); + let my_taker_orders: HashMap = + serde_json::from_value(my_orders["result"]["taker_orders"].clone()).unwrap(); + assert!(my_maker_orders.is_empty(), "maker_orders must be empty"); + assert!(my_taker_orders.is_empty(), "taker_orders must be empty"); +} + +#[test] +fn test_gtc_taker_order_should_transform_to_maker() { + let privkey = random_secp256k1_secret(); + generate_utxo_coin_with_privkey("MYCOIN", 1000.into(), privkey); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000),]); + + let conf = Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("MM log path: {}", mm.log_path.display()); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + + log!("Issue bob MYCOIN/MYCOIN1 sell request"); + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "sell", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": 0.1, + "order_type": { + "type": "GoodTillCancelled" + }, + "timeout": 2, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let rc_json: Json = serde_json::from_str(&rc.1).unwrap(); + let uuid: String = serde_json::from_value(rc_json["result"]["uuid"].clone()).unwrap(); + + log!("Wait for 4 seconds for Bob order to be converted to maker"); + block_on(Timer::sleep(4.)); + + let rc = block_on(mm.rpc(&json! ({ + "userpass": mm.userpass, + "method": "my_orders", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_orders: {}", rc.1); + let my_orders: Json = serde_json::from_str(&rc.1).unwrap(); + let my_maker_orders: HashMap = + serde_json::from_value(my_orders["result"]["maker_orders"].clone()).unwrap(); + let my_taker_orders: HashMap = + serde_json::from_value(my_orders["result"]["taker_orders"].clone()).unwrap(); + assert_eq!( + 1, + my_maker_orders.len(), + "maker_orders must have exactly 1 order, but has {:?}", + my_maker_orders + ); + assert!(my_taker_orders.is_empty(), "taker_orders must be empty"); + assert!(my_maker_orders.contains_key(&uuid)); +} + +// ============================================================================= +// Buy/Sell with Locked Coins Tests +// Tests for order placement when coins are locked by other swaps +// ============================================================================= + +#[test] +fn test_buy_when_coins_locked_by_other_swap() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": { + "numer":"77699596737", + "denom":"77800000000" + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + thread::sleep(Duration::from_secs(6)); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": { + "numer":"77699599999", + "denom":"77800000000" + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "buy success, but should fail: {}", rc.1); + assert!(rc.1.contains("Not enough MYCOIN1 for swap"), "{}", rc.1); + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +fn test_sell_when_coins_locked_by_other_swap() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "sell", + "base": "MYCOIN1", + "rel": "MYCOIN", + "price": 1, + "volume": { + "numer":"77699596737", + "denom":"77800000000" + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + thread::sleep(Duration::from_secs(6)); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "sell", + "base": "MYCOIN1", + "rel": "MYCOIN", + "price": 1, + "volume": { + "numer":"77699599999", + "denom":"77800000000" + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "sell success, but should fail: {}", rc.1); + assert!(rc.1.contains("Not enough MYCOIN1 for swap"), "{}", rc.1); + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +fn test_buy_max() { + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 1.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": { + "numer":"77699596737", + "denom":"77800000000" + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": { + "numer":"77699596738", + "denom":"77800000000" + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "buy success, but should fail: {}", rc.1); + block_on(mm_alice.stop()).unwrap(); +} + +// ============================================================================= +// Setprice Max Volume Tests +// Tests for setprice with max parameter and volume calculations +// ============================================================================= + +#[test] +// https://github.com/KomodoPlatform/atomicDEX-API/issues/471 +fn test_match_and_trade_setprice_max() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 2000.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + + let mut mm_alice = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + let json: Json = serde_json::from_str(&rc.1).unwrap(); + let bob_uuid = json["result"]["uuid"].as_str().unwrap().to_owned(); + + log!("Get MYCOIN/MYCOIN1 orderbook"); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "orderbook", + "base": "MYCOIN", + "rel": "MYCOIN1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + + let bob_orderbook: Json = serde_json::from_str(&rc.1).unwrap(); + log!("orderbook {:?}", bob_orderbook); + let asks = bob_orderbook["asks"].as_array().unwrap(); + assert_eq!(asks.len(), 1, "MYCOIN/MYCOIN1 orderbook must have exactly 1 ask"); + assert_eq!(asks[0]["maxvolume"], Json::from("999.99999726")); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "buy", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": 1, + "volume": "999.99999", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!buy: {}", rc.1); + + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + + thread::sleep(Duration::from_secs(3)); + + let rmd160 = rmd160_from_priv(bob_priv_key); + let order_path = mm_bob.folder.join(format!( + "DB/{}/ORDERS/MY/MAKER/{}.json", + hex::encode(rmd160.take()), + bob_uuid, + )); + log!("Order path {}", order_path.display()); + assert!(!order_path.exists()); + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +#[test] +// https://github.com/KomodoPlatform/atomicDEX-API/issues/888 +fn test_max_taker_vol_swap() { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 50.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(bob_priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + &[("MYCOIN_FEE_DISCOUNT", "")], + )) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(alice_priv_key)), + "coins": coins, + "rpc_password": "pass", + "seednodes": vec![format!("{}", mm_bob.ip)], + }), + "pass".to_string(), + None, + &[("MYCOIN_FEE_DISCOUNT", "")], + )) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_bob, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN", &[], None))); + log!("{:?}", block_on(enable_native(&mm_alice, "MYCOIN1", &[], None))); + let price = MmNumber::from((100, 1620)); + let rc = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "setprice", + "base": "MYCOIN", + "rel": "MYCOIN1", + "price": price, + "max": true, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!setprice: {}", rc.1); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "orderbook", + "base": "MYCOIN1", + "rel": "MYCOIN", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!orderbook: {}", rc.1); + log!("{}", rc.1); + thread::sleep(Duration::from_secs(3)); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "max_taker_vol", + "coin": "MYCOIN1", + "trade_with": "MYCOIN", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!max_taker_vol: {}", rc.1); + let vol: MaxTakerVolResponse = serde_json::from_str(&rc.1).unwrap(); + let expected_vol = MmNumber::from((1294999865579, 25930000000)); + + let actual_vol = MmNumber::from(vol.result.clone()); + log!("actual vol {}", actual_vol.to_decimal()); + log!("expected vol {}", expected_vol.to_decimal()); + + assert_eq!(expected_vol, actual_vol); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "sell", + "base": "MYCOIN1", + "rel": "MYCOIN", + "price": "16", + "volume": vol.result, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!sell: {}", rc.1); + let sell_res: BuyOrSellRpcResult = serde_json::from_str(&rc.1).unwrap(); + + block_on(mm_bob.wait_for_log(22., |log| log.contains("Entering the maker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + block_on(mm_alice.wait_for_log(22., |log| log.contains("Entering the taker_swap_loop MYCOIN/MYCOIN1"))).unwrap(); + + thread::sleep(Duration::from_secs(3)); + + let rc = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "my_swap_status", + "params": { + "uuid": sell_res.result.uuid + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "!my_swap_status: {}", rc.1); + + let status_response: Json = serde_json::from_str(&rc.1).unwrap(); + let events_array = status_response["result"]["events"].as_array().unwrap(); + let first_event_type = events_array[0]["event"]["type"].as_str().unwrap(); + assert_eq!("Started", first_event_type); + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + +// ============================================================================= +// Trade Preimage Tests +// Tests for trade_preimage RPC - fee estimation before swap execution +// ============================================================================= + +#[test] +fn test_maker_trade_preimage() { + let priv_key = random_secp256k1_secret(); + + let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); + let my_address = mycoin.my_address().expect("!my_address"); + fill_address(&mycoin, &my_address, 10.into(), 30); + + let (_ctx, mycoin1) = utxo_coin_from_privkey("MYCOIN1", priv_key); + let my_address = mycoin1.my_address().expect("!my_address"); + fill_address(&mycoin1, &my_address, 20.into(), 30); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "setprice", + "price": 1, + "max": true, + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); + let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000274", false); // txfee from get_sender_trade_fee + let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000992", true); + let volume = MmNumber::from("9.99999726"); // 1.0 - 0.00000274 from calc_max_maker_vol + + let my_coin_total = TotalTradeFeeForTest::new("MYCOIN", "0.00000274", "0.00000274"); + let my_coin1_total = TotalTradeFeeForTest::new("MYCOIN1", "0.00000992", "0"); + + let expected = TradePreimageResult::MakerPreimage(MakerPreimage { + base_coin_fee, + rel_coin_fee, + volume: Some(volume.to_decimal()), + volume_rat: Some(volume.to_ratio()), + volume_fraction: Some(volume.to_fraction()), + total_fees: vec![my_coin_total, my_coin1_total], + }); + + let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); + actual.result.sort_total_fees(); + assert_eq!(expected, actual.result); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN1", + "rel": "MYCOIN", + "swap_method": "setprice", + "price": 1, + "max": true, + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); + let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); + actual.result.sort_total_fees(); + + let base_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000548", false); + let rel_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000496", true); + let volume = MmNumber::from("19.99999452"); + + let my_coin_total = TotalTradeFeeForTest::new("MYCOIN", "0.00000496", "0"); + let my_coin1_total = TotalTradeFeeForTest::new("MYCOIN1", "0.00000548", "0.00000548"); + let expected = TradePreimageResult::MakerPreimage(MakerPreimage { + base_coin_fee, + rel_coin_fee, + volume: Some(volume.to_decimal()), + volume_rat: Some(volume.to_ratio()), + volume_fraction: Some(volume.to_fraction()), + total_fees: vec![my_coin_total, my_coin1_total], + }); + + actual.result.sort_total_fees(); + assert_eq!(expected, actual.result); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN1", + "rel": "MYCOIN", + "swap_method": "setprice", + "price": 1, + "volume": "19.99999109", // actually try max value (balance - txfee = 20.0 - 0.00000823) + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); + let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); + actual.result.sort_total_fees(); + + let base_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000891", false); // txfee updated for calculated max volume (not 616) + let rel_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000496", true); + + let total_my_coin = TotalTradeFeeForTest::new("MYCOIN", "0.00000496", "0"); + let total_my_coin1 = TotalTradeFeeForTest::new("MYCOIN1", "0.00000891", "0.00000891"); + + let expected = TradePreimageResult::MakerPreimage(MakerPreimage { + base_coin_fee, + rel_coin_fee, + volume: None, + volume_rat: None, + volume_fraction: None, + total_fees: vec![total_my_coin, total_my_coin1], + }); + + actual.result.sort_total_fees(); + assert_eq!(expected, actual.result); +} + +#[test] +fn test_taker_trade_preimage() { + let priv_key = random_secp256k1_secret(); + + let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); + let my_address = mycoin.my_address().expect("!my_address"); + fill_address(&mycoin, &my_address, 10.into(), 30); + + let (_ctx, mycoin1) = utxo_coin_from_privkey("MYCOIN1", priv_key); + let my_address = mycoin1.my_address().expect("!my_address"); + fill_address(&mycoin1, &my_address, 20.into(), 30); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + + // `max` field is not supported for `buy/sell` swap methods + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "sell", + "max": true, + "price": 1, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + + let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(actual.error_type, "InvalidParam", "Unexpected error_type: {}", rc.1); + let expected = trade_preimage_error::InvalidParam { + param: "max".to_owned(), + reason: "'max' cannot be used with 'sell' or 'buy' method".to_owned(), + }; + assert_eq!(actual.error_data, Some(expected)); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "sell", + "volume": "7.77", + "price": "2", + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); + + let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); + actual.result.sort_total_fees(); + + let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000274", false); + let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000992", true); + let taker_fee = TradeFeeForTest::new("MYCOIN", "0.01", false); + let fee_to_send_taker_fee = TradeFeeForTest::new("MYCOIN", "0.00000245", false); + + let my_coin_total_fee = TotalTradeFeeForTest::new("MYCOIN", "0.01000519", "0.01000519"); + let my_coin1_total_fee = TotalTradeFeeForTest::new("MYCOIN1", "0.00000992", "0"); + + let expected = TradePreimageResult::TakerPreimage(TakerPreimage { + base_coin_fee, + rel_coin_fee, + taker_fee, + fee_to_send_taker_fee, + total_fees: vec![my_coin_total_fee, my_coin1_total_fee], + }); + assert_eq!(expected, actual.result); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "buy", + "volume": "7.77", + "price": "2", + }, + }))) + .unwrap(); + assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); + let mut actual: RpcSuccessResponse = serde_json::from_str(&rc.1).unwrap(); + actual.result.sort_total_fees(); + + let base_coin_fee = TradeFeeForTest::new("MYCOIN", "0.00000496", true); + let rel_coin_fee = TradeFeeForTest::new("MYCOIN1", "0.00000548", false); // fee to send taker payment + let taker_fee = TradeFeeForTest::new("MYCOIN1", "0.02", false); + let fee_to_send_taker_fee = TradeFeeForTest::new("MYCOIN1", "0.0000049", false); + + let my_coin_total_fee = TotalTradeFeeForTest::new("MYCOIN", "0.00000496", "0"); + let my_coin1_total_fee = TotalTradeFeeForTest::new("MYCOIN1", "0.02001038", "0.02001038"); // taker_fee + rel_coin_fee + fee_to_send_taker_fee + + let expected = TradePreimageResult::TakerPreimage(TakerPreimage { + base_coin_fee, + rel_coin_fee, + taker_fee, + fee_to_send_taker_fee, + total_fees: vec![my_coin_total_fee, my_coin1_total_fee], + }); + assert_eq!(expected, actual.result); +} + +#[test] +fn test_trade_preimage_not_sufficient_balance() { + #[track_caller] + fn expect_not_sufficient_balance( + res: &str, + available: BigDecimal, + required: BigDecimal, + locked_by_swaps: Option, + ) { + let actual: RpcErrorResponse = serde_json::from_str(res).unwrap(); + assert_eq!(actual.error_type, "NotSufficientBalance"); + let expected = trade_preimage_error::NotSufficientBalance { + coin: "MYCOIN".to_owned(), + available, + required, + locked_by_swaps, + }; + assert_eq!(actual.error_data, Some(expected)); + } + + let priv_key = random_secp256k1_secret(); + let fill_balance_functor = |amount: BigDecimal| { + let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); + let my_address = mycoin.my_address().expect("!my_address"); + fill_address(&mycoin, &my_address, amount, 30); + }; + + let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + + fill_balance_functor(MmNumber::from("0.00001273").to_decimal()); // volume < txfee + dust = 274 + 1000 + // Try sell the max amount with the zero balance. + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "setprice", + "price": 1, + "max": true, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + let available = MmNumber::from("0.00001273").to_decimal(); + // Required at least 0.00001274 MYCOIN to pay the transaction_fee(0.00000274) and to send a value not less than dust(0.00001) and not less than min_trading_vol (10 * dust). + let required = MmNumber::from("0.00001274").to_decimal(); // TODO: this is not true actually: we can't create orders less that min_trading_vol = 10 * dust + expect_not_sufficient_balance(&rc.1, available, required, Some(MmNumber::from("0").to_decimal())); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "setprice", + "price": 1, + "volume": 0.1, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + // Required 0.00001 MYCOIN to pay the transaction fee and the specified 0.1 volume. + let available = MmNumber::from("0.00001273").to_decimal(); + let required = MmNumber::from("0.1000024").to_decimal(); + expect_not_sufficient_balance(&rc.1, available, required, None); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "setprice", + "price": 1, + "max": true, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + // balance(0.00001273) + let available = MmNumber::from("0.00001273").to_decimal(); + // required min_tx_amount(0.00001) + transaction_fee(0.00000274) + let required = MmNumber::from("0.00001274").to_decimal(); + expect_not_sufficient_balance(&rc.1, available, required, Some(MmNumber::from("0").to_decimal())); + + fill_balance_functor(MmNumber::from("7.770085").to_decimal()); + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "sell", + "price": 1, + "volume": 7.77, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + let available = MmNumber::from("7.77009773").to_decimal(); + // `required = volume + fee_to_send_taker_payment + dex_fee + fee_to_send_dex_fee`, + // where `volume = 7.77`, `fee_to_send_taker_payment = 0.00000393, fee_to_send_dex_fee = 0.00000422`, `dex_fee = 0.01`. + // Please note `dex_fee = 7.77 / 777` with dex_fee = 0.01 + // required = 7.77 + 0.01 (dex_fee) + (0.00000393 + 0.00000422) = 7.78000815 + let required = MmNumber::from("7.78000815"); + expect_not_sufficient_balance(&rc.1, available, required.to_decimal(), Some(BigDecimal::from(0))); +} + +/// This test ensures that `trade_preimage` will not succeed on input that will fail on `buy/sell/setprice`. +/// https://github.com/KomodoPlatform/atomicDEX-API/issues/902 +#[test] +fn test_trade_preimage_additional_validation() { + let priv_key = random_secp256k1_secret(); + + let (_ctx, mycoin1) = utxo_coin_from_privkey("MYCOIN1", priv_key); + let my_address = mycoin1.my_address().expect("!my_address"); + fill_address(&mycoin1, &my_address, 20.into(), 30); + + let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); + let my_address = mycoin.my_address().expect("!my_address"); + fill_address(&mycoin, &my_address, 10.into(), 30); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); + + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + + // Price is too low + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "setprice", + "price": 0, + "volume": 0.1, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(actual.error_type, "PriceTooLow"); + // currently the minimum price is any value above 0 + let expected = trade_preimage_error::PriceTooLow { + price: BigDecimal::from(0), + threshold: BigDecimal::from(0), + }; + assert_eq!(actual.error_data, Some(expected)); + + // volume 0.00001 is too low, min trading volume 0.0001 + let low_volume = BigDecimal::from(1) / BigDecimal::from(100_000); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "setprice", + "price": 1, + "volume": low_volume, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(actual.error_type, "VolumeTooLow"); + // Min MYCOIN trading volume is 0.0001. + let volume_threshold = BigDecimal::from(1) / BigDecimal::from(10_000); + let expected = trade_preimage_error::VolumeTooLow { + coin: "MYCOIN".to_owned(), + volume: low_volume.clone(), + threshold: volume_threshold, + }; + assert_eq!(actual.error_data, Some(expected)); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "sell", + "price": 1, + "volume": low_volume, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(actual.error_type, "VolumeTooLow"); + // Min MYCOIN trading volume is 0.0001. + let volume_threshold = BigDecimal::from(1) / BigDecimal::from(10_000); + let expected = trade_preimage_error::VolumeTooLow { + coin: "MYCOIN".to_owned(), + volume: low_volume, + threshold: volume_threshold, + }; + assert_eq!(actual.error_data, Some(expected)); + + // rel volume is too low + // Min MYCOIN trading volume is 0.0001. + let volume = BigDecimal::from(1) / BigDecimal::from(10_000); + let low_price = BigDecimal::from(1) / BigDecimal::from(10); + // Min MYCOIN1 trading volume is 0.0001, but the actual volume is 0.00001 + let low_rel_volume = &volume * &low_price; + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "trade_preimage", + "params": { + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "sell", + "price": low_price, + "volume": volume, + }, + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + let actual: RpcErrorResponse = serde_json::from_str(&rc.1).unwrap(); + assert_eq!(actual.error_type, "VolumeTooLow"); + // Min MYCOIN1 trading volume is 0.0001. + let volume_threshold = BigDecimal::from(1) / BigDecimal::from(10_000); + let expected = trade_preimage_error::VolumeTooLow { + coin: "MYCOIN1".to_owned(), + volume: low_rel_volume, + threshold: volume_threshold, + }; + assert_eq!(actual.error_data, Some(expected)); +} + +#[test] +fn test_trade_preimage_legacy() { + let priv_key = random_secp256k1_secret(); + let (_ctx, mycoin) = utxo_coin_from_privkey("MYCOIN", priv_key); + let my_address = mycoin.my_address().expect("!my_address"); + fill_address(&mycoin, &my_address, 10.into(), 30); + let (_ctx, mycoin1) = utxo_coin_from_privkey("MYCOIN1", priv_key); + let my_address = mycoin1.my_address().expect("!my_address"); + fill_address(&mycoin1, &my_address, 20.into(), 30); + + let coins = json!([mycoin_conf(1000), mycoin1_conf(2000)]); + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(priv_key)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".to_string(), + None, + ) + .unwrap(); + let (_dump_log, _dump_dashboard) = mm_dump(&mm.log_path); + + log!("{:?}", block_on(enable_native(&mm, "MYCOIN1", &[], None))); + log!("{:?}", block_on(enable_native(&mm, "MYCOIN", &[], None))); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "trade_preimage", + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "setprice", + "max": true, + "price": "1", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); + let _: TradePreimageResponse = serde_json::from_str(&rc.1).unwrap(); + + // vvv test a taker method vvv + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "trade_preimage", + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "sell", + "volume": "7.77", + "price": "2", + }))) + .unwrap(); + assert!(rc.0.is_success(), "!trade_preimage: {}", rc.1); + let _: TradePreimageResponse = serde_json::from_str(&rc.1).unwrap(); + + // vvv test the error response vvv + + // `max` field is not supported for `buy/sell` swap methods + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "trade_preimage", + "base": "MYCOIN", + "rel": "MYCOIN1", + "swap_method": "sell", + "max": true, + "price": "1", + }))) + .unwrap(); + assert!(!rc.0.is_success(), "trade_preimage success, but should fail: {}", rc.1); + assert!(rc + .1 + .contains("Incorrect use of the 'max' parameter: 'max' cannot be used with 'sell' or 'buy' method")); +} diff --git a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs index 1e9b366fc3..5735d6be60 100644 --- a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs @@ -12,6 +12,7 @@ use lazy_static::lazy_static; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_number::MmNumber; use mm2_test_helpers::for_tests::zombie_conf_for_docker; +use serde_json::json; use tempfile::TempDir; use tokio::sync::Mutex; diff --git a/mm2src/mm2_main/tests/docker_tests_main.rs b/mm2src/mm2_main/tests/docker_tests_main.rs index c85ffe4c05..9899f1da46 100644 --- a/mm2src/mm2_main/tests/docker_tests_main.rs +++ b/mm2src/mm2_main/tests/docker_tests_main.rs @@ -7,45 +7,28 @@ #[cfg(test)] #[macro_use] extern crate common; -#[cfg(test)] +#[cfg(all(test, feature = "docker-tests-qrc20"))] #[macro_use] extern crate gstuff; #[cfg(test)] #[macro_use] extern crate lazy_static; #[cfg(test)] -#[macro_use] -extern crate serde_json; -#[cfg(test)] extern crate ser_error_derive; #[cfg(test)] extern crate test; -use common::custom_futures::timeout::FutureTimerExt; -use std::env; -use std::io::{BufRead, BufReader}; -use std::path::PathBuf; -use std::process::Command; -use std::time::Duration; -use test::{test_main, StaticBenchFn, StaticTestFn, TestDescAndFn}; +use test::TestDescAndFn; mod docker_tests; + +// Sia tests are gated on docker-tests-sia feature to prevent them from running in other docker test jobs +#[cfg(feature = "docker-tests-sia")] mod sia_tests; -use docker_tests::docker_tests_common::*; -use docker_tests::qrc20_tests::{qtum_docker_node, QtumDockerOps, QTUM_REGTEST_DOCKER_IMAGE_WITH_TAG}; -use sia_tests::utils::wait_for_dsia_node_ready; #[allow(dead_code)] mod integration_tests_common; -const ENV_VAR_NO_UTXO_DOCKER: &str = "_KDF_NO_UTXO_DOCKER"; -const ENV_VAR_NO_QTUM_DOCKER: &str = "_KDF_NO_QTUM_DOCKER"; -const ENV_VAR_NO_SLP_DOCKER: &str = "_KDF_NO_SLP_DOCKER"; -const ENV_VAR_NO_ETH_DOCKER: &str = "_KDF_NO_ETH_DOCKER"; -const ENV_VAR_NO_COSMOS_DOCKER: &str = "_KDF_NO_COSMOS_DOCKER"; -const ENV_VAR_NO_ZOMBIE_DOCKER: &str = "_KDF_NO_ZOMBIE_DOCKER"; -const ENV_VAR_NO_SIA_DOCKER: &str = "_KDF_NO_SIA_DOCKER"; - // AP: custom test runner is intended to initialize the required environment (e.g. coin daemons in the docker containers) // and then gracefully clear it by dropping the RAII docker container handlers // I've tried to use static for such singleton initialization but it turned out that despite @@ -55,231 +38,5 @@ const ENV_VAR_NO_SIA_DOCKER: &str = "_KDF_NO_SIA_DOCKER"; // Windows - https://github.com/KomodoPlatform/komodo/blob/master/zcutil/fetch-params.bat // Linux and MacOS - https://github.com/KomodoPlatform/komodo/blob/master/zcutil/fetch-params.sh pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { - // pretty_env_logger::try_init(); - let mut containers = vec![]; - // skip Docker containers initialization if we are intended to run test_mm_start only - if env::var("_MM2_TEST_CONF").is_err() { - let mut images = vec![]; - - let disable_utxo: bool = env::var(ENV_VAR_NO_UTXO_DOCKER).is_ok(); - let disable_slp: bool = env::var(ENV_VAR_NO_SLP_DOCKER).is_ok(); - let disable_qtum: bool = env::var(ENV_VAR_NO_QTUM_DOCKER).is_ok(); - let disable_eth: bool = env::var(ENV_VAR_NO_ETH_DOCKER).is_ok(); - let disable_cosmos: bool = env::var(ENV_VAR_NO_COSMOS_DOCKER).is_ok(); - let disable_zombie: bool = env::var(ENV_VAR_NO_ZOMBIE_DOCKER).is_ok(); - let disable_sia: bool = env::var(ENV_VAR_NO_SIA_DOCKER).is_ok(); - - if !disable_utxo || !disable_slp { - images.push(UTXO_ASSET_DOCKER_IMAGE_WITH_TAG) - } - if !disable_qtum { - images.push(QTUM_REGTEST_DOCKER_IMAGE_WITH_TAG); - } - if !disable_eth { - images.push(GETH_DOCKER_IMAGE_WITH_TAG); - } - if !disable_cosmos { - images.push(NUCLEUS_IMAGE); - images.push(ATOM_IMAGE_WITH_TAG); - images.push(IBC_RELAYER_IMAGE_WITH_TAG); - } - if !disable_zombie { - images.push(ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG); - } - - if !disable_sia { - images.push(SIA_DOCKER_IMAGE_WITH_TAG); - } - - for image in images { - pull_docker_image(image); - remove_docker_containers(image); - } - - let (nucleus_node, atom_node, ibc_relayer_node) = if !disable_cosmos { - let runtime_dir = prepare_runtime_dir().unwrap(); - let nucleus_node = nucleus_node(runtime_dir.clone()); - let atom_node = atom_node(runtime_dir.clone()); - let ibc_relayer_node = ibc_relayer_node(runtime_dir); - (Some(nucleus_node), Some(atom_node), Some(ibc_relayer_node)) - } else { - (None, None, None) - }; - let (utxo_node, utxo_node1) = if !disable_utxo { - let utxo_node = utxo_asset_docker_node("MYCOIN", 8000); - let utxo_node1 = utxo_asset_docker_node("MYCOIN1", 8001); - (Some(utxo_node), Some(utxo_node1)) - } else { - (None, None) - }; - let qtum_node = if !disable_qtum { - let qtum_node = qtum_docker_node(9000); - Some(qtum_node) - } else { - None - }; - let for_slp_node = if !disable_slp { - let for_slp_node = utxo_asset_docker_node("FORSLP", 10000); - Some(for_slp_node) - } else { - None - }; - let geth_node = if !disable_eth { - let geth_node = geth_docker_node("ETH", 8545); - Some(geth_node) - } else { - None - }; - let zombie_node = if !disable_zombie { - let zombie_node = zombie_asset_docker_node(7090); - Some(zombie_node) - } else { - None - }; - - let sia_node = if !disable_sia { - let sia_node = sia_docker_node("SIA", 9980); - Some(sia_node) - } else { - None - }; - - if let (Some(utxo_node), Some(utxo_node1)) = (utxo_node, utxo_node1) { - let utxo_ops = UtxoAssetDockerOps::from_ticker("MYCOIN"); - let utxo_ops1 = UtxoAssetDockerOps::from_ticker("MYCOIN1"); - utxo_ops.wait_ready(4); - utxo_ops1.wait_ready(4); - containers.push(utxo_node); - containers.push(utxo_node1); - } - if let Some(qtum_node) = qtum_node { - let qtum_ops = QtumDockerOps::new(); - qtum_ops.wait_ready(2); - qtum_ops.initialize_contracts(); - containers.push(qtum_node); - } - if let Some(for_slp_node) = for_slp_node { - let for_slp_ops = BchDockerOps::from_ticker("FORSLP"); - for_slp_ops.wait_ready(4); - for_slp_ops.initialize_slp(); - containers.push(for_slp_node); - } - if let Some(geth_node) = geth_node { - wait_for_geth_node_ready(); - init_geth_node(); - containers.push(geth_node); - } - if let Some(zombie_node) = zombie_node { - let zombie_ops = ZCoinAssetDockerOps::new(); - zombie_ops.wait_ready(4); - containers.push(zombie_node); - } - if let (Some(nucleus_node), Some(atom_node), Some(ibc_relayer_node)) = - (nucleus_node, atom_node, ibc_relayer_node) - { - prepare_ibc_channels(ibc_relayer_node.container.id()); - thread::sleep(Duration::from_secs(10)); - wait_until_relayer_container_is_ready(ibc_relayer_node.container.id()); - containers.push(nucleus_node); - containers.push(atom_node); - containers.push(ibc_relayer_node); - } - if let Some(sia_node) = sia_node { - block_on(wait_for_dsia_node_ready()); - containers.push(sia_node); - } - } - // detect if docker is installed - // skip the tests that use docker if not installed - let owned_tests: Vec<_> = tests - .iter() - .map(|t| match t.testfn { - StaticTestFn(f) => TestDescAndFn { - testfn: StaticTestFn(f), - desc: t.desc.clone(), - }, - StaticBenchFn(f) => TestDescAndFn { - testfn: StaticBenchFn(f), - desc: t.desc.clone(), - }, - _ => panic!("non-static tests passed to lp_coins test runner"), - }) - .collect(); - let args: Vec = env::args().collect(); - test_main(&args, owned_tests, None); -} - -fn wait_for_geth_node_ready() { - let mut attempts = 0; - loop { - if attempts >= 5 { - panic!("Failed to connect to Geth node after several attempts."); - } - match block_on(GETH_WEB3.eth().block_number().timeout(Duration::from_secs(6))) { - Ok(Ok(block_number)) => { - log!("Geth node is ready, latest block number: {:?}", block_number); - break; - }, - Ok(Err(e)) => { - log!("Failed to connect to Geth node: {:?}, retrying...", e); - }, - Err(_) => { - log!("Connection to Geth node timed out, retrying..."); - }, - } - attempts += 1; - thread::sleep(Duration::from_secs(1)); - } -} - -fn pull_docker_image(name: &str) { - Command::new("docker") - .arg("pull") - .arg(name) - .status() - .expect("Failed to execute docker command"); -} - -fn remove_docker_containers(name: &str) { - let stdout = Command::new("docker") - .arg("ps") - .arg("-f") - .arg(format!("ancestor={name}")) - .arg("-q") - .output() - .expect("Failed to execute docker command"); - - let reader = BufReader::new(stdout.stdout.as_slice()); - let ids: Vec<_> = reader.lines().map(|line| line.unwrap()).collect(); - if !ids.is_empty() { - Command::new("docker") - .arg("rm") - .arg("-f") - .args(ids) - .status() - .expect("Failed to execute docker command"); - } -} - -fn prepare_runtime_dir() -> std::io::Result { - let project_root = { - let mut current_dir = std::env::current_dir().unwrap(); - current_dir.pop(); - current_dir.pop(); - current_dir - }; - - let containers_state_dir = project_root.join(".docker/container-state"); - assert!(containers_state_dir.exists()); - let containers_runtime_dir = project_root.join(".docker/container-runtime"); - - // Remove runtime directory if it exists to copy containers files to a clean directory - if containers_runtime_dir.exists() { - std::fs::remove_dir_all(&containers_runtime_dir).unwrap(); - } - - // Copy container files to runtime directory - mm2_io::fs::copy_dir_all(&containers_state_dir, &containers_runtime_dir).unwrap(); - - Ok(containers_runtime_dir) + docker_tests::runner::docker_tests_runner_impl(tests) } diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index 4f302bfa6b..073c63bbec 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -2356,7 +2356,9 @@ fn test_metrics_method() { .expect(r#"Couldn't find a metric with key = "traffic.out" and label: coin = "RICK" in received json"#); } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_electrum_tx_history() { fn get_tx_history_request_count(mm: &MarketMakerIt) -> u64 { @@ -2777,7 +2779,9 @@ fn test_convert_eth_address() { assert!(rc.1.contains("Address must be prefixed with 0x")); } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_add_delegation_qtum() { let coins = json!([{ @@ -2927,7 +2931,9 @@ fn test_remove_delegation_qtum() { ); } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_query_delegations_info_qtum() { let coins = json!([{ @@ -3410,7 +3416,9 @@ fn qrc20_activate_electrum() { assert_eq!(electrum_json["balance"].as_str(), Some("139")); } +// TODO: Re-enable once Electrum servers are dockerized: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2708 #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_qrc20_withdraw() { // corresponding private key: [3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, 172, 110, 180, 13, 123, 179, 10, 49] @@ -4112,7 +4120,21 @@ fn test_update_maker_order() { let max_base_vol = BigDecimal::from_str(update_maker_order_json["result"]["max_base_vol"].as_str().unwrap()).unwrap(); assert_eq!(update_maker_order_json["result"]["price"], Json::from("2")); - assert_eq!(max_base_vol, max_volume); + // Approximate comparison: fee/balance can change slightly between the my_balance/trade_preimage + // calls above and the update_maker_order call + let diff = if max_base_vol > max_volume { + &max_base_vol - &max_volume + } else { + &max_volume - &max_base_vol + }; + let tolerance = BigDecimal::from_str("0.0001").unwrap(); + assert!( + diff < tolerance, + "max_base_vol {} differs from expected {} by more than {}", + max_base_vol, + max_volume, + tolerance + ); block_on(mm_bob.stop()).unwrap(); } @@ -5592,8 +5614,12 @@ fn test_sign_verify_message_eth_with_derivation_path() { "0x36b91a54f905f2dd88ecfd7f4a539710c699eaab2b425ba79ad959c29ec26492011674981da72d68ac0ab72bb35661a13c42bce314ecdfff0e44174f82a7ee2501"; assert_eq!(expected_signature, response.signature); - let address0 = match result.wallet_balance { - EnableCoinBalanceMap::HD(bal) => bal.accounts[0].addresses[0].address.clone(), + // Addresses were used before, so they are included in the activation result. + let (address0, address1) = match result.wallet_balance { + EnableCoinBalanceMap::HD(bal) => ( + bal.accounts[0].addresses[0].address.clone(), + bal.accounts[0].addresses[1].address.clone(), + ), EnableCoinBalanceMap::Iguana(_) => panic!("Expected HD"), }; let response = block_on(verify_message(&mm_bob, "ETH", expected_signature, &address0)); @@ -5603,8 +5629,6 @@ fn test_sign_verify_message_eth_with_derivation_path() { assert!(response.is_valid); // Test address 1. - let get_new_address = block_on(get_new_address(&mm_bob, "ETH", 0, Some(Bip44Chain::External))); - assert!(get_new_address.new_address.balance.contains_key("ETH")); let response = block_on(sign_message( &mm_bob, "ETH", @@ -5621,12 +5645,7 @@ fn test_sign_verify_message_eth_with_derivation_path() { "0xc8aa1d54c311e38edc815308dc67018aecbd6d4008a88b9af7aba9c98997b7b56f9e6eab64b3c496c6fff1762ae0eba8228370b369d505dd9087cded0a4d947a01"; assert_eq!(expected_signature, response.signature); - let response = block_on(verify_message( - &mm_bob, - "ETH", - expected_signature, - &get_new_address.new_address.address, - )); + let response = block_on(verify_message(&mm_bob, "ETH", expected_signature, &address1)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; diff --git a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs index 86bbfbca53..3939133238 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -1321,7 +1321,9 @@ fn setprice_min_volume_should_be_displayed_in_orderbook() { assert_eq!(min_volume, "1", "Alice MORTY/RICK ask must display correct min_volume"); } +// TODO: Re-enable or rewrite as part of orderbook sync improvements: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2626 #[test] +#[ignore] fn test_order_cancellation_received_before_creation() { let coins = json!([rick_conf(), morty_conf()]); diff --git a/mm2src/mm2_main/tests/sia_tests/docker_functional_tests.rs b/mm2src/mm2_main/tests/sia_tests/docker_functional_tests.rs index c9881ce1a3..c1ca2310fa 100644 --- a/mm2src/mm2_main/tests/sia_tests/docker_functional_tests.rs +++ b/mm2src/mm2_main/tests/sia_tests/docker_functional_tests.rs @@ -1,4 +1,5 @@ -use crate::docker_tests::docker_tests_common::{fund_privkey_utxo, random_secp256k1_secret}; +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::utxo::fund_privkey_utxo; use super::utils::*; @@ -6,7 +7,7 @@ use coins::siacoin::{ApiClientHelpers, SiaTransactionTypes}; use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::{start_swaps, wait_for_swap_finished_or_err}; use serde::Deserialize; -use serde_json::Value as Json; +use serde_json::{json, Value as Json}; use std::str::FromStr; diff --git a/mm2src/mm2_main/tests/sia_tests/mod.rs b/mm2src/mm2_main/tests/sia_tests/mod.rs index f5e712ddaa..9d8471286e 100644 --- a/mm2src/mm2_main/tests/sia_tests/mod.rs +++ b/mm2src/mm2_main/tests/sia_tests/mod.rs @@ -1,3 +1,8 @@ +//! Sia docker tests - requires `docker-tests-sia` feature. +//! +//! This module is gated at the crate level in docker_tests_main.rs with +//! `#[cfg(feature = "docker-tests-sia")]`. + mod docker_functional_tests; mod short_locktime_tests; diff --git a/mm2src/mm2_main/tests/sia_tests/short_locktime_tests.rs b/mm2src/mm2_main/tests/sia_tests/short_locktime_tests.rs index 7033535533..73308bc220 100644 --- a/mm2src/mm2_main/tests/sia_tests/short_locktime_tests.rs +++ b/mm2src/mm2_main/tests/sia_tests/short_locktime_tests.rs @@ -1,4 +1,5 @@ -use crate::docker_tests::docker_tests_common::{fund_privkey_utxo, random_secp256k1_secret}; +use crate::docker_tests::helpers::env::random_secp256k1_secret; +use crate::docker_tests::helpers::utxo::fund_privkey_utxo; use super::utils::*; diff --git a/mm2src/mm2_main/tests/sia_tests/utils.rs b/mm2src/mm2_main/tests/sia_tests/utils.rs index f609fbd70c..bb5a9b1085 100644 --- a/mm2src/mm2_main/tests/sia_tests/utils.rs +++ b/mm2src/mm2_main/tests/sia_tests/utils.rs @@ -4,7 +4,7 @@ pub use coins::siacoin::sia_rust::utils::V2TransactionBuilder; use coins::siacoin::{ApiClientHelpers, SiaApiClient, SiaClient, SiaClientConf}; use keys::hash::H256; -use crate::docker_tests::docker_tests_common::SIA_RPC_PARAMS; +use crate::docker_tests::helpers::sia::SIA_RPC_PARAMS; use common::custom_futures::timeout::FutureTimerExt; use common::executor::Timer; use mm2_rpc::data::legacy::CoinInitResponse; @@ -12,7 +12,7 @@ use mm2_test_helpers::for_tests::{MarketMakerIt, Mm2TestConf}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; -use serde_json::Value as Json; +use serde_json::{json, Value as Json}; use std::collections::HashMap; use std::net::IpAddr; use std::time::Duration; @@ -315,7 +315,11 @@ pub async fn wait_for_dsia_node_ready() { let client = init_sia_client().await.unwrap(); // Mine 155 blocks to begin because coinbase maturity is 150 + log!("Mining 155 blocks to Charlie's address..."); client.mine_blocks(155, &CHARLIE_SIA_ADDRESS).await.unwrap(); + // Verify blocks were mined + let height = client.current_height().await.unwrap(); + log!("Mining complete. Current height: {}", height); // Spawn a loop that will keep mining blocks every 10 seconds to advance the chain // and get the swap tests running. diff --git a/mm2src/mm2_main/tests/sia_tests/utils/komodod_client.rs b/mm2src/mm2_main/tests/sia_tests/utils/komodod_client.rs index 319c25bf1b..6d40980f3b 100644 --- a/mm2src/mm2_main/tests/sia_tests/utils/komodod_client.rs +++ b/mm2src/mm2_main/tests/sia_tests/utils/komodod_client.rs @@ -8,6 +8,7 @@ use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; use http::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; use reqwest::Client as ReqwestClient; +use serde_json::json; use std::net::IpAddr; use std::time::Duration; use url::Url; diff --git a/mm2src/mm2_p2p/AGENTS.md b/mm2src/mm2_p2p/AGENTS.md index da9babd2f6..b9aed32a30 100644 --- a/mm2src/mm2_p2p/AGENTS.md +++ b/mm2src/mm2_p2p/AGENTS.md @@ -1,5 +1,7 @@ # mm2_p2p — Peer-to-Peer Networking +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + libp2p-based networking layer for decentralized communication between KDF nodes. ## Responsibilities diff --git a/mm2src/mm2_test_helpers/src/electrums.rs b/mm2src/mm2_test_helpers/src/electrums.rs index a410daf1c2..e4fda709f3 100644 --- a/mm2src/mm2_test_helpers/src/electrums.rs +++ b/mm2src/mm2_test_helpers/src/electrums.rs @@ -73,7 +73,9 @@ pub fn tbtc_electrums() -> Vec { } #[cfg(target_arch = "wasm32")] -pub fn tqtum_electrums() -> Vec { vec![json!({ "url": "electrum3.cipig.net:30071", "protocol": "WSS" })] } +pub fn tqtum_electrums() -> Vec { + vec![json!({ "url": "electrum3.cipig.net:30071", "protocol": "WSS" })] +} #[cfg(not(target_arch = "wasm32"))] pub fn tqtum_electrums() -> Vec { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index ca48513a21..d103e9c6ad 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -778,10 +778,10 @@ pub fn btc_with_sync_starting_header() -> Json { }, "spv_conf": { "starting_block_header": { - "height": 764064, - "hash": "00000000000000000006da48b920343944908861fa05b28824922d9e60aaa94d", - "bits": 386375189, - "time": 1668986059, + "height": 872928, + "hash": "00000000000000000001dc2f171d19c36ad8afb972287230900b2a352184402a", + "bits": 386053475, + "time": 1733153640, }, "max_stored_block_headers": 3000, "validation_params": { @@ -2714,7 +2714,10 @@ pub async fn wait_check_stats_swap_status(mm: &MarketMakerIt, uuid: &str, timeou if !response.0.is_success() { Timer::sleep(1.).await; if get_utc_timestamp() > wait_until { - panic!("Timed out waiting for swap stats status uuid={}, latest status={}", uuid, response.1); + panic!( + "Timed out waiting for swap stats status uuid={}, latest status={}", + uuid, response.1 + ); } continue; } @@ -3677,7 +3680,9 @@ pub async fn task_enable_eth_with_tokens( timeout: u64, path_to_address: Option, ) -> EthWithTokensActivationResult { - let init = task_enable_eth_with_tokens_init(mm, platform_coin, tokens, swap_contract_address, nodes, path_to_address).await; + let init = + task_enable_eth_with_tokens_init(mm, platform_coin, tokens, swap_contract_address, nodes, path_to_address) + .await; let init: RpcV2Response = json::from_value(init).unwrap(); let timeout = wait_until_ms(timeout * 1000); diff --git a/mm2src/mm2_test_helpers/src/lib.rs b/mm2src/mm2_test_helpers/src/lib.rs index ef4e81eb9b..769c7ea2cd 100644 --- a/mm2src/mm2_test_helpers/src/lib.rs +++ b/mm2src/mm2_test_helpers/src/lib.rs @@ -1,4 +1,5 @@ -#[macro_use] extern crate serde_derive; +#[macro_use] +extern crate serde_derive; pub mod electrums; pub mod for_tests; diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index d1f601eb04..6599dc7840 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -402,7 +402,9 @@ pub struct TradePreimageResponse { } impl TradePreimageResponse { - pub fn sort_total_fees(&mut self) { self.result.sort_total_fees() } + pub fn sort_total_fees(&mut self) { + self.result.sort_total_fees() + } } #[derive(Debug, Deserialize)] diff --git a/mm2src/trading_api/src/lib.rs b/mm2src/trading_api/src/lib.rs index 183e6d9bcd..b54187f782 100644 --- a/mm2src/trading_api/src/lib.rs +++ b/mm2src/trading_api/src/lib.rs @@ -1,3 +1,7 @@ //! This module is for indirect connection to third-party trading APIs, processing their results and errors +// TODO: Remove this allow when Rust 1.92 regression is fixed. +// See: https://github.com/rust-lang/rust/issues/147648 +#![allow(unused_assignments)] + pub mod one_inch_api; diff --git a/mm2src/trezor/AGENTS.md b/mm2src/trezor/AGENTS.md index 024cd524fa..4e57b50103 100644 --- a/mm2src/trezor/AGENTS.md +++ b/mm2src/trezor/AGENTS.md @@ -1,5 +1,7 @@ # trezor — Hardware Wallet Integration +> **Note:** Always follow the root `/CLAUDE.md` for global conventions (fmt, clippy, error handling, etc.). + Trezor hardware wallet API for UTXO and EVM transaction signing. Handles device communication, user interactions (PIN/passphrase/button), and transaction signing protocols. ## Responsibilities diff --git a/scripts/ci/docker-test-nodes-setup.sh b/scripts/ci/docker-test-nodes-setup.sh new file mode 100755 index 0000000000..35c21a4246 --- /dev/null +++ b/scripts/ci/docker-test-nodes-setup.sh @@ -0,0 +1,205 @@ +#!/bin/bash +# +# Setup script for KDF docker test nodes +# +# This script prepares the container runtime directory and configuration files +# needed by the docker-compose test environment. +# +# Usage: +# ./scripts/ci/docker-test-nodes-setup.sh [--skip-cosmos] [--skip-sia] +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +CONTAINER_STATE_DIR="$PROJECT_ROOT/.docker/container-state" +CONTAINER_RUNTIME_DIR="$PROJECT_ROOT/.docker/container-runtime" + +SKIP_COSMOS=false +SKIP_SIA=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --skip-cosmos) + SKIP_COSMOS=true + shift + ;; + --skip-sia) + SKIP_SIA=true + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +echo "=== KDF Docker Test Nodes Setup ===" +echo "Project root: $PROJECT_ROOT" + +# ============================================================================ +# Prepare runtime directory for Cosmos nodes +# ============================================================================ + +if [ "$SKIP_COSMOS" = false ]; then + echo "" + echo "Preparing Cosmos node runtime directories..." + + if [ ! -d "$CONTAINER_STATE_DIR" ]; then + echo "ERROR: Container state directory not found: $CONTAINER_STATE_DIR" + exit 1 + fi + + # Remove existing runtime directory to start fresh + if [ -d "$CONTAINER_RUNTIME_DIR" ]; then + echo "Removing existing runtime directory..." + rm -rf "$CONTAINER_RUNTIME_DIR" + fi + + # Copy container state to runtime directory + echo "Copying container state to runtime directory..." + cp -r "$CONTAINER_STATE_DIR" "$CONTAINER_RUNTIME_DIR" + + # Set proper permissions + chmod -R 755 "$CONTAINER_RUNTIME_DIR" + + echo "Cosmos node data prepared at: $CONTAINER_RUNTIME_DIR" +else + echo "Skipping Cosmos node setup (--skip-cosmos)" +fi + +# ============================================================================ +# Prepare Sia configuration +# ============================================================================ + +if [ "$SKIP_SIA" = false ]; then + echo "" + echo "Preparing Sia node configuration..." + + SIA_CONFIG_DIR="$CONTAINER_RUNTIME_DIR/sia-config" + mkdir -p "$SIA_CONFIG_DIR" + + # Write walletd.yml + cat > "$SIA_CONFIG_DIR/walletd.yml" << 'EOF' +http: + address: :9980 + password: password + publicEndpoints: false +index: + mode: full +log: + stdout: + enabled: true + level: debug + format: human +EOF + + # Write ci_network.json + cat > "$SIA_CONFIG_DIR/ci_network.json" << 'EOF' +{ + "network": { + "name": "komodo-ci", + "initialCoinbase": "300000000000000000000000000000", + "minimumCoinbase": "30000000000000000000000000000", + "initialTarget": "0100000000000000000000000000000000000000000000000000000000000000", + "blockInterval": 60000000000, + "maturityDelay": 10, + "hardforkDevAddr": { + "height": 1, + "oldAddress": "000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69", + "newAddress": "000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69" + }, + "hardforkTax": { + "height": 2 + }, + "hardforkStorageProof": { + "height": 5 + }, + "hardforkOak": { + "height": 10, + "fixHeight": 12, + "genesisTimestamp": "2023-01-13T00:53:20-08:00" + }, + "hardforkASIC": { + "height": 20, + "oakTime": 600000000000, + "oakTarget": "0100000000000000000000000000000000000000000000000000000000000000", + "nonceFactor": 1009 + }, + "hardforkFoundation": { + "height": 30, + "primaryAddress": "053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807", + "failsafeAddress": "000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69" + }, + "hardforkV2": { + "allowHeight": 0, + "requireHeight": 7777777, + "finalCutHeight": 8888888 + } + }, + "genesis": { + "parentID": "0000000000000000000000000000000000000000000000000000000000000000", + "nonce": 0, + "timestamp": "2023-01-13T00:53:20-08:00", + "minerPayouts": null, + "transactions": [ + { + "id": "268ef8627241b3eb505cea69b21379c4b91c21dfc4b3f3f58c66316249058cfd", + "siacoinOutputs": [ + { + "value": "1000000000000000000000000000000000000", + "address": "a0cfbc1089d129f52d00bc0b0fac190d4d87976a1d7f34da7ca0c295c99a628de344d19ad469" + } + ], + "siafundOutputs": [ + { + "value": 10000, + "address": "053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807" + } + ] + } + ] + } +} +EOF + + echo "Sia configuration written to: $SIA_CONFIG_DIR" +else + echo "Skipping Sia setup (--skip-sia)" +fi + +# ============================================================================ +# Export environment variables for docker-compose +# ============================================================================ + +echo "" +echo "=== Environment Variables ===" +echo "Set these environment variables before running docker-compose:" +echo "" +echo " export KDF_CONTAINER_RUNTIME_DIR=$CONTAINER_RUNTIME_DIR" +echo " export ZCASH_PARAMS_PATH=\${HOME}/.zcash-params" +echo "" +echo "Or use the defaults in the compose file." + +# ============================================================================ +# Summary +# ============================================================================ + +echo "" +echo "=== Setup Complete ===" +echo "" +echo "To start the test nodes:" +echo " docker compose -f .docker/test-nodes.yml --profile all up -d" +echo "" +echo "To start specific profiles:" +echo " docker compose -f .docker/test-nodes.yml --profile utxo --profile evm up -d" +echo "" +echo "To view logs:" +echo " docker compose -f .docker/test-nodes.yml logs -f" +echo "" +echo "To stop and cleanup:" +echo " docker compose -f .docker/test-nodes.yml down -v"