diff --git a/.github/actions/cargo-cache/action.yml b/.github/actions/cargo-cache/action.yml new file mode 100644 index 0000000000..89069e7797 --- /dev/null +++ b/.github/actions/cargo-cache/action.yml @@ -0,0 +1,20 @@ +name: 'Set up cargo cache' +description: 'Sets up the cargo cache for the workflow' +runs: + using: 'composite' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up cargo cache + uses: actions/cache@v3 + continue-on-error: false + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- diff --git a/.github/workflows/adex-cli.yml b/.github/workflows/adex-cli.yml index 02390e203a..1265d28d46 100644 --- a/.github/workflows/adex-cli.yml +++ b/.github/workflows/adex-cli.yml @@ -20,7 +20,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -33,6 +33,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Start checking code format and lint run: | cargo fmt --manifest-path ./mm2src/adex_cli/Cargo.toml --all -- --check @@ -49,7 +52,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -62,9 +65,12 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Start testing run: | - cargo test --manifest-path ./mm2src/adex_cli/Cargo.toml + cargo test --manifest-path ./mm2src/adex_cli/Cargo.toml --no-fail-fast build: timeout-minutes: 60 @@ -77,7 +83,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -90,6 +96,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Start building run: | cargo build --manifest-path ./mm2src/adex_cli/Cargo.toml diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index d7a4c2f959..90873357ea 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -23,7 +23,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -41,6 +41,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -100,6 +103,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -147,6 +153,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | if (test-path "./MM_VERSION") { @@ -196,6 +205,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -235,7 +247,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -257,6 +269,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -305,6 +320,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -344,7 +362,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -366,6 +384,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -407,7 +428,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -429,6 +450,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -472,7 +496,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH diff --git a/.github/workflows/fmt-and-lint.yml b/.github/workflows/fmt-and-lint.yml index b00adec115..093cdb823b 100644 --- a/.github/workflows/fmt-and-lint.yml +++ b/.github/workflows/fmt-and-lint.yml @@ -25,6 +25,9 @@ jobs: sudo apt-get -y install libudev-dev if: matrix.os == 'ubuntu-latest' + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: fmt check run: cargo fmt -- --check @@ -42,5 +45,8 @@ jobs: rustup default nightly-2022-10-29 rustup target add wasm32-unknown-unknown + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: wasm code lint run: cargo clippy --target wasm32-unknown-unknown -- --D warnings diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 53b55ca6a6..0a7474dd08 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -5,11 +5,14 @@ on: types: - opened - edited + - reopened - synchronize + - labeled + - unlabeled jobs: main: - name: Validate PR title + name: Validate PR runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@v5.2.0 @@ -40,3 +43,13 @@ jobs: echo "PR title is too long (greater than 72 characters)" exit 1 fi + + - name: Check PR labels + if: > + (contains(toJson(github.event.pull_request.labels.*.name), 'under review') == false && + contains(toJson(github.event.pull_request.labels.*.name), 'in progress') == false) || + (contains(toJson(github.event.pull_request.labels.*.name), 'under review') == true && + contains(toJson(github.event.pull_request.labels.*.name), 'in progress') == true) + run: | + echo "PR must have "exactly one" of these labels: [ 'under review', 'in progress' ]." + exit 1 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 381b51fc08..020d518434 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -23,7 +23,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -41,6 +41,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -92,6 +95,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -133,6 +139,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | if (test-path "./MM_VERSION") { @@ -176,6 +185,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -209,7 +221,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -231,6 +243,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -273,6 +288,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -306,7 +324,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -328,6 +346,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION @@ -363,7 +384,7 @@ jobs: - name: pre scripts for ci container run: | - git config --global --add safe.directory /__w/atomicDEX-API/atomicDEX-API + git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework echo "/bin" >> $GITHUB_PATH echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH @@ -385,6 +406,9 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Build run: | rm -f ./MM_VERSION diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0101ea30cc..4a939b19b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,10 +25,13 @@ jobs: rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal rustup default nightly-2022-10-29 + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Test run: | # wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash - cargo test --bins --lib + cargo test --bins --lib --no-fail-fast mac-x86-64-unit: timeout-minutes: 90 @@ -47,10 +50,13 @@ jobs: rustup default nightly-2022-10-29 rustup target add x86_64-apple-darwin + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Test run: | # wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash - cargo test --bins --lib --target x86_64-apple-darwin + cargo test --bins --lib --target x86_64-apple-darwin --no-fail-fast win-x86-64-unit: timeout-minutes: 90 @@ -68,6 +74,9 @@ jobs: rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal rustup default nightly-2022-10-29 + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Test run: | # Invoke-WebRequest -Uri https://github.com/KomodoPlatform/komodo/raw/d456be35acd1f8584e1e4f971aea27bd0644d5c5/zcutil/wget64.exe -OutFile \wget64.exe @@ -86,7 +95,7 @@ jobs: # Restart-Service docker # Get-Service docker - cargo test --bins --lib + cargo test --bins --lib --no-fail-fast linux-x86-64-mm2-integration: timeout-minutes: 90 @@ -104,8 +113,11 @@ jobs: rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal rustup default nightly-2022-10-29 + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Test - run: cargo test --test 'mm2_tests_main' + run: cargo test --test 'mm2_tests_main' --no-fail-fast # https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits # https://github.com/KomodoPlatform/atomicDEX-API/actions/runs/4419618128/jobs/7748266141#step:4:1790 @@ -145,8 +157,11 @@ jobs: rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal rustup default nightly-2022-10-29 + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Test - run: cargo test --test 'mm2_tests_main' + run: cargo test --test 'mm2_tests_main' --no-fail-fast docker-tests: timeout-minutes: 90 @@ -164,10 +179,13 @@ jobs: rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal rustup default nightly-2022-10-29 + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Test run: | wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash - cargo test --test 'docker_tests_main' --features run-docker-tests + cargo test --test 'docker_tests_main' --features run-docker-tests --no-fail-fast wasm: timeout-minutes: 90 @@ -197,5 +215,8 @@ jobs: sudo tar -xzvf geckodriver-v0.32.2-linux64.tar.gz -C /bin sudo chmod +x /bin/geckodriver + - name: Cargo cache + uses: ./.github/actions/cargo-cache + - name: Test run: WASM_BINDGEN_TEST_TIMEOUT=360 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main diff --git a/.github/workflows/validate-mm2-version.yml b/.github/workflows/validate-mm2-version.yml new file mode 100644 index 0000000000..fd8c588c81 --- /dev/null +++ b/.github/workflows/validate-mm2-version.yml @@ -0,0 +1,25 @@ +name: Validate mm2 version + +on: + pull_request: + branches: + - main + +jobs: + validate_version_change: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3 + + - name: Install dependencies + run: pip install toml + + - name: Run version check + run: ./scripts/ci/validate-version-bump.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f852cf6b..0c06f7c875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,56 @@ +## v1.0.6-beta - 2023-07-24 + +**Features:** +- Komodo DeFi Framework was introduced in [#1903](https://github.com/KomodoPlatform/atomicDEX-API/issues/1903) + - The project/repo was renamed from AtomicDEX-API to Komodo-DeFi Framework + - The readme file, logo, links, and some references in the documentation were updated to reflect the new name/brand + - CI was updated to use the new name where it's needed. +- Swap watcher nodes [#1431](https://github.com/KomodoPlatform/atomicDEX-API/issues/1431) + - Using watcher nodes for swaps were enabled by default for UTXO coins in [#1859](https://github.com/KomodoPlatform/atomicDEX-API/pull/1859) + - `use_watchers` configuration was set to true by default. It was later disabled in [#1897](https://github.com/KomodoPlatform/atomicDEX-API/pull/1897) due to this issue [#1887](https://github.com/KomodoPlatform/atomicDEX-API/issues/1887) + - All nodes doing a swap will broadcast a watcher message after the taker payment is sent if the swapped coins are supported by watchers (currently only UTXO). This was also disabled in [#1897](https://github.com/KomodoPlatform/atomicDEX-API/pull/1897) due to this issue [#1887](https://github.com/KomodoPlatform/atomicDEX-API/issues/1887) + - This update also fixes an issue that caused nodes to broadcast two consecutive watcher messages after the taker payment was sent. +- NFT integration [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900) + - Cache support was added for sqlite (non-wasm targets) in [#1833](https://github.com/KomodoPlatform/atomicDEX-API/pull/1833) + - IndexedDb support for wasm was added in [#1877](https://github.com/KomodoPlatform/atomicDEX-API/pull/1877) + - DB unit tests were added in [#1877](https://github.com/KomodoPlatform/atomicDEX-API/pull/1877) + - Handling of `bafy` in IPFS Moralis links in a correct way was done in [#1877](https://github.com/KomodoPlatform/atomicDEX-API/pull/1877) + - `get_uri_meta` function was added to optimize the retrieval of `UriMeta` from `token_uri` and `metadata` in [#1877](https://github.com/KomodoPlatform/atomicDEX-API/pull/1877) + - `protect_from_spam` feature was added to redact URLs in specific fields and flag them as possible spam in [#1877](https://github.com/KomodoPlatform/atomicDEX-API/pull/1877) + - Address is now used instead of string in NFT and transaction objects in [#1914](https://github.com/KomodoPlatform/atomicDEX-API/pull/1914) + - `guard: Arc>` from struct `NftCtx` is added to lock nft functions which uses db in [#1914](https://github.com/KomodoPlatform/atomicDEX-API/pull/1914) + - IndexedDB Cursor collect method was used to fix uncaught Error in [#1914](https://github.com/KomodoPlatform/atomicDEX-API/pull/1914) +- HTTPS support was added for the RPC server in [#1861](https://github.com/KomodoPlatform/atomicDEX-API/pull/1861) +- Adex-CLI [#1682](https://github.com/KomodoPlatform/atomicDEX-API/issues/1682) + - New commands `enable`, `get-enabled`, `orderbook`,`sell`, `buy` were added to adex-cli in [#1768](https://github.com/KomodoPlatform/atomicDEX-API/pull/1768) + +**Enhancements/Fixes:** +- Some RUSTSEC advisories where resolved in [#1853](https://github.com/KomodoPlatform/atomicDEX-API/pull/1853) +- ARRR/ZCOIN code was refactored to be compiled in WebAssembly (WASM) in [#1805](https://github.com/KomodoPlatform/atomicDEX-API/pull/1805) + - The PR for this paves the way for subsequent implementation of the empty/todo functions related to WASM storage and other functionalities. +- Orderbook response now returns the right age for the age field, this was fixed in [#1851](https://github.com/KomodoPlatform/atomicDEX-API/pull/1851) +- A bug that caused `best_orders` rpc to return `is_mine: false` for the user's orders was fixed in [#1846](https://github.com/KomodoPlatform/atomicDEX-API/pull/1846) + - An optional parameter `exclude_mine` was also added to the `best_orders` request that allows users to exclude their own orders from the response. + - `exclude_mine` defaults to false to maintain the same behaviour before the PR. +- Watchtower integration tests were moved to the new ethereum testnet and the ignore attributes were removed in [#1846](https://github.com/KomodoPlatform/atomicDEX-API/pull/1846) + - The PR also adds a new test case for watcher rewards. + - It also fixes the unstable `send_and_refund_eth_payment`, `send_and_refund_erc20_payment`, `test_nonce_lock` and `test_withdraw_and_send tests` tests that were failing due to concurrency issues. +- Infrastructure DNS rotation for default seednodes was done in [#1868](https://github.com/KomodoPlatform/atomicDEX-API/pull/1868) +- Price endpoints were updated in [#1869](https://github.com/KomodoPlatform/atomicDEX-API/pull/1869) +- A fix removed the passed config string from the error logs during mm2 initialization if there was a deserialization error was done in [#1872](https://github.com/KomodoPlatform/atomicDEX-API/pull/1872) +- The time needed for CI completion was reduced by caching the downloaded dependencies in [#1880](https://github.com/KomodoPlatform/atomicDEX-API/pull/1880) +- Label validation on PRs was added. This validation will only succeed if one of the following labels is used but not both: `under review` or `in progress` [#1881](https://github.com/KomodoPlatform/atomicDEX-API/pull/1881) +- `orderbook` mod of adex-cli was refactored by moving it from the internal `response_handler` to its appropriate folder, enhancing code organization and clarity in [#1879](https://github.com/KomodoPlatform/atomicDEX-API/pull/1879) +- A bug was fixed for adex-cli to allow starting if configuration does not exist in [#1889](https://github.com/KomodoPlatform/atomicDEX-API/pull/1889) +- IBC and standard withdrawals for Cosmos now allow users to specify the gas price and gas limit for each transaction [#1894](https://github.com/KomodoPlatform/atomicDEX-API/pull/1894) +- A fix was introduced to adex-cli to allow starting mm2 from cli under regular user in macOS [#1856](https://github.com/KomodoPlatform/atomicDEX-API/pull/1856) +- The repo logo was updated to be visible in GitHub light theme in [#1904](https://github.com/KomodoPlatform/atomicDEX-API/issues/1904) +- A CI job was added to check if mm2 version was bumped before merging any pull request to main in [#1899](https://github.com/KomodoPlatform/atomicDEX-API/issues/1899) +- All CI tests now run with the `--no-fail-fast` flag, allowing other tests to proceed despite any failures [#1907](https://github.com/KomodoPlatform/atomicDEX-API/issues/1907) +- Index out of bounds errors in the `tx_details_by_hash` functions was fixed in [#1915](https://github.com/KomodoPlatform/atomicDEX-API/issues/1915) +- Adex-CLI `test_activation_scheme` was fixed by removing the old file in [#1920](https://github.com/KomodoPlatform/atomicDEX-API/issues/1920) + + ## v1.0.5-beta - 2023-06-08 **Features:** diff --git a/Cargo.lock b/Cargo.lock index fb78ef9e37..eba3d3c01d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,17 +42,6 @@ dependencies = [ "generic-array 0.14.5", ] -[[package]] -name = "aes" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" -dependencies = [ - "aes-soft", - "aesni", - "cipher 0.2.5", -] - [[package]] name = "aes" version = "0.7.5" @@ -60,7 +49,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if 1.0.0", - "cipher 0.3.0", + "cipher", "cpufeatures 0.2.1", "opaque-debug 0.3.0", ] @@ -72,46 +61,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" dependencies = [ "aead", - "aes 0.7.5", - "cipher 0.3.0", + "aes", + "cipher", "ctr", "ghash", "subtle 2.4.0", ] [[package]] -name = "aes-soft" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" -dependencies = [ - "cipher 0.2.5", - "opaque-debug 0.3.0", -] - -[[package]] -name = "aesni" -version = "0.10.0" +name = "ahash" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "cipher 0.2.5", - "opaque-debug 0.3.0", + "getrandom 0.2.9", + "once_cell", + "version_check", ] [[package]] name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - -[[package]] -name = "ahash" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "getrandom 0.2.9", + "cfg-if 1.0.0", "once_cell", "version_check", ] @@ -260,15 +234,6 @@ dependencies = [ "pin-project-lite 0.2.9", ] -[[package]] -name = "atomic-shim" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77" -dependencies = [ - "crossbeam-utils 0.8.8", -] - [[package]] name = "atomicdex-gossipsub" version = "0.20.0" @@ -380,12 +345,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base-x" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" - [[package]] name = "base16ct" version = "0.1.1" @@ -431,6 +390,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "base64ct" version = "1.5.1" @@ -469,7 +434,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aaf33151a6429fe9211d1b276eafdf70cdff28b071e76c0b0e1503221ea3744" dependencies = [ - "num-bigint 0.4.3", + "num-bigint", "num-integer", "num-traits", "serde", @@ -574,9 +539,9 @@ dependencies = [ [[package]] name = "blake2" -version = "0.10.4" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ "digest 0.10.3", ] @@ -650,12 +615,12 @@ dependencies = [ [[package]] name = "block-modes" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" dependencies = [ "block-padding 0.2.1", - "cipher 0.2.5", + "cipher", ] [[package]] @@ -902,7 +867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01b72a433d0cf2aef113ba70f62634c56fddb0f244e6377185c56a7cadbd8f91" dependencies = [ "cfg-if 1.0.0", - "cipher 0.3.0", + "cipher", "cpufeatures 0.2.1", "zeroize", ] @@ -915,7 +880,7 @@ checksum = "3b84ed6d1d5f7aa9bdde921a5090e0ca4d934d250ea3b402a5fab3a994e28a2a" dependencies = [ "aead", "chacha20", - "cipher 0.3.0", + "cipher", "poly1305", "zeroize", ] @@ -948,15 +913,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cipher" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" -dependencies = [ - "generic-array 0.14.5", -] - [[package]] name = "cipher" version = "0.3.0" @@ -1076,6 +1032,7 @@ dependencies = [ "mm2_metrics", "mm2_net", "mm2_number", + "mm2_rpc", "mm2_test_helpers", "mocktopus", "num-traits", @@ -1085,6 +1042,7 @@ dependencies = [ "prost-build", "protobuf", "rand 0.7.3", + "regex", "rlp", "rmp-serde", "rpc", @@ -1183,7 +1141,6 @@ dependencies = [ "futures 0.1.29", "futures 0.3.15", "futures-timer", - "getrandom 0.2.9", "gstuff", "hex 0.4.3", "http 0.2.7", @@ -1270,12 +1227,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" -[[package]] -name = "const_fn" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402da840495de3f976eaefc3485b7f5eb5b0bf9761f9a47be27fe975b3b8c2ec" - [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1636,7 +1587,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" dependencies = [ - "cipher 0.3.0", + "cipher", ] [[package]] @@ -1917,12 +1868,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "dlopen" version = "0.1.8" @@ -2098,9 +2043,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "atty", "humantime", @@ -2112,7 +2057,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" dependencies = [ "blake2b_simd", "byteorder 1.4.3", @@ -2381,13 +2326,14 @@ dependencies = [ [[package]] name = "fpe" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25080721bbcd2cd4d765b7d607ea350425fa087ce53cd3e31afcacdab850352" +checksum = "cd910db5f9ca4dc3116f8c46367825807aa2b942f72565f16b4be0b208a00a9e" dependencies = [ - "aes 0.6.0", "block-modes", - "num-bigint 0.3.2", + "cipher", + "libm", + "num-bigint", "num-integer", "num-traits", ] @@ -2697,9 +2643,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" dependencies = [ "bytes 1.1.0", "fnv", @@ -2731,38 +2677,38 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash 0.4.7", + "ahash 0.7.6", ] [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" dependencies = [ "ahash 0.7.6", ] [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.7.6", + "ahash 0.8.3", ] [[package]] name = "hashlink" -version = "0.6.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" +checksum = "0761a1b9491c4f2e3d66aa0f62d0fba0af9a0e2852e4d48ea506632a4b56e6aa" dependencies = [ - "hashbrown 0.9.1", + "hashbrown 0.13.2", ] [[package]] @@ -2939,9 +2885,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "httparse" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -2978,9 +2924,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes 1.1.0", "futures-channel", @@ -2993,7 +2939,7 @@ dependencies = [ "httpdate", "itoa 1.0.1", "pin-project-lite 0.2.9", - "socket2 0.4.4", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -3123,12 +3069,12 @@ checksum = "5a9d968042a4902e08810946fc7cd5851eb75e80301342305af755ca06cb82ce" [[package]] name = "indexmap" -version = "1.7.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", - "hashbrown 0.11.2", + "hashbrown 0.12.1", ] [[package]] @@ -3180,7 +3126,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98" dependencies = [ - "socket2 0.4.4", + "socket2 0.4.9", "widestring", "winapi", "winreg", @@ -3371,6 +3317,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "libp2p" version = "0.45.1" @@ -3618,7 +3570,7 @@ dependencies = [ "libc", "libp2p-core", "log 0.4.17", - "socket2 0.4.4", + "socket2 0.4.9", "tokio", ] @@ -3763,9 +3715,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.20.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd" +checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" dependencies = [ "cc", "pkg-config", @@ -3918,10 +3870,10 @@ dependencies = [ ] [[package]] -name = "mach" -version = "0.3.2" +name = "mach2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" dependencies = [ "libc", ] @@ -4001,26 +3953,27 @@ dependencies = [ [[package]] name = "metrics" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142c53885123b68d94108295a09d4afe1a1388ed95b54d5dacd9a454753030f2" +checksum = "aa8ebbd1a9e57bbab77b9facae7f5136aea44c356943bf9a198f647da64285d6" dependencies = [ - "ahash 0.7.6", + "ahash 0.8.3", "metrics-macros", + "portable-atomic", ] [[package]] name = "metrics-exporter-prometheus" -version = "0.10.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953cbbb6f9ba4b9304f4df79b98cdc9d14071ed93065a9fca11c00c5d9181b66" +checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" dependencies = [ + "base64 0.21.2", "hyper", "indexmap", "ipnet", "metrics", "metrics-util", - "parking_lot 0.11.1", "quanta", "thiserror", "tokio", @@ -4029,31 +3982,29 @@ dependencies = [ [[package]] name = "metrics-macros" -version = "0.5.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e30813093f757be5cf21e50389a24dc7dbb22c49f23b7e8f51d69b508a5ffa" +checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ "proc-macro2 1.0.58", "quote 1.0.27", - "syn 1.0.95", + "syn 2.0.16", ] [[package]] name = "metrics-util" -version = "0.13.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1f4b69bef1e2b392b2d4a12902f2af90bb438ba4a66aa222d1023fa6561b50" +checksum = "111cb375987443c3de8d503580b536f77dc8416d32db62d9456db5d93bd7ac47" dependencies = [ "aho-corasick", - "atomic-shim", "crossbeam-epoch 0.9.5", "crossbeam-utils 0.8.8", - "hashbrown 0.11.2", + "hashbrown 0.13.2", "indexmap", "metrics", "num_cpus", "ordered-float", - "parking_lot 0.11.1", "quanta", "radix_trie", "sketches-ddsketch", @@ -4107,7 +4058,6 @@ dependencies = [ "env_logger", "futures 0.3.15", "futures-rustls 0.21.1", - "getrandom 0.2.9", "hex 0.4.3", "lazy_static", "libp2p", @@ -4129,7 +4079,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "1.0.5-beta" +version = "1.0.6-beta" dependencies = [ "chrono", "common", @@ -4160,6 +4110,7 @@ dependencies = [ "db_common", "derive_more", "futures 0.3.15", + "futures-rustls 0.21.1", "gstuff", "hex 0.4.3", "lazy_static", @@ -4340,10 +4291,13 @@ dependencies = [ "primitives", "rand 0.6.5", "rand 0.7.3", + "rcgen", "regex", "rmp-serde", "rpc", "rpc_task", + "rustls 0.20.4", + "rustls-pemfile 1.0.2", "script", "secp256k1 0.20.3", "ser_error", @@ -4422,6 +4376,7 @@ dependencies = [ "derive_more", "ethkey", "futures 0.3.15", + "futures-util", "gstuff", "http 0.2.7", "hyper", @@ -4431,8 +4386,11 @@ dependencies = [ "mm2_err_handle", "prost", "rand 0.7.3", + "rustls 0.20.4", "serde", "serde_json", + "tokio", + "tokio-rustls", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -4444,7 +4402,7 @@ name = "mm2_number" version = "0.1.0" dependencies = [ "bigdecimal", - "num-bigint 0.4.3", + "num-bigint", "num-rational", "num-traits", "paste", @@ -4462,10 +4420,13 @@ dependencies = [ "gstuff", "http 0.2.7", "mm2_err_handle", + "mm2_number", + "rpc", "ser_error", "ser_error_derive", "serde", "serde_json", + "uuid 1.2.2", ] [[package]] @@ -4487,6 +4448,7 @@ dependencies = [ "mm2_metrics", "mm2_net", "mm2_number", + "mm2_rpc", "rand 0.7.3", "regex", "rpc", @@ -4621,17 +4583,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "num-bigint" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d0a3d5e207573f948a9e5376662aa743a2ea13f7c50a554d7af443a73fbfeba" -dependencies = [ - "autocfg 1.1.0", - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.3" @@ -4672,7 +4623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ "autocfg 1.1.0", - "num-bigint 0.4.3", + "num-bigint", "num-integer", "num-traits", "serde", @@ -4680,9 +4631,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg 1.1.0", ] @@ -4763,9 +4714,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "ordered-float" -version = "2.10.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +checksum = "2fc2dbde8f8a79f2102cc474ceb0ad68e3b80b85289ea62389b60e66777e4213" dependencies = [ "num-traits", ] @@ -5007,6 +4958,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.0", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -5015,9 +4975,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "petgraph" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b305cc4569dd4e8765bab46261f67ef5d4d11a4b6e745100ee5dad8948b46c" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", "indexmap", @@ -5094,9 +5054,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.17" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "poly1305" @@ -5121,6 +5081,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc59d1bcc64fc5d021d67521f818db868368028108d37f0e98d74e33f68297b5" + [[package]] name = "ppv-lite86" version = "0.2.8" @@ -5204,12 +5170,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" version = "0.4.30" @@ -5342,16 +5302,16 @@ dependencies = [ [[package]] name = "quanta" -version = "0.9.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" dependencies = [ "crossbeam-utils 0.8.8", "libc", - "mach", + "mach2", "once_cell", "raw-cpuid", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -5689,6 +5649,18 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rcgen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" +dependencies = [ + "pem", + "ring", + "time 0.3.11", + "yasna", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -5868,19 +5840,20 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f10b46df14cf1ee1ac7baa4d2fbc2c52c0622a4b82fa8740e37bc452ac0184f" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" dependencies = [ "byteorder 1.4.3", "num-traits", + "paste", ] [[package]] name = "rmp-serde" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c1ee98f14fe8b8e9c5ea13d25da7b2a1796169202c57a09d7288de90d56222b" +checksum = "4ce7d70c926fe472aed493b902010bccc17fa9f7284145cb8772fd22fdb052d8" dependencies = [ "byteorder 1.4.3", "rmp", @@ -5941,18 +5914,17 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.24.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38ee71cbab2c827ec0ac24e76f82eca723cee92c509a65f67dee393c25112" +checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", - "memchr", "smallvec 1.6.1", - "time 0.2.27", + "time 0.3.11", ] [[package]] @@ -6059,11 +6031,11 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.13.0", + "base64 0.21.2", ] [[package]] @@ -6279,9 +6251,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.136" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] @@ -6308,13 +6280,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2 1.0.58", "quote 1.0.27", - "syn 1.0.95", + "syn 2.0.16", ] [[package]] @@ -6396,12 +6368,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "sha1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" - [[package]] name = "sha2" version = "0.8.2" @@ -6494,9 +6460,9 @@ checksum = "833011ca526bd88f16778d32c699d325a9ad302fa06381cd66f7be63351d3f6d" [[package]] name = "sketches-ddsketch" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d2ecae5fcf33b122e2e6bd520a57ccf152d2dde3b38c71039df1a6867264ee" +checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" [[package]] name = "slab" @@ -6570,9 +6536,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -6866,7 +6832,7 @@ dependencies = [ "rand 0.7.3", "serde", "serde_derive", - "socket2 0.4.4", + "socket2 0.4.9", "solana-logger", "solana-sdk", "solana-version", @@ -7429,70 +7395,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "standback" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" -dependencies = [ - "version_check", -] - [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version 0.2.3", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2 1.0.58", - "quote 1.0.27", - "serde", - "serde_derive", - "syn 1.0.95", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2 1.0.58", - "quote 1.0.27", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn 1.0.95", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "strsim" version = "0.8.0" @@ -7810,9 +7718,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -7890,40 +7798,16 @@ dependencies = [ "winapi", ] -[[package]] -name = "time" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" -dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb", - "time-macros 0.1.1", - "version_check", - "winapi", -] - [[package]] name = "time" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" dependencies = [ + "itoa 1.0.1", "libc", "num_threads", - "time-macros 0.2.4", -] - -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", + "time-macros", ] [[package]] @@ -7932,19 +7816,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" -[[package]] -name = "time-macros-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" -dependencies = [ - "proc-macro-hack", - "proc-macro2 1.0.58", - "quote 1.0.27", - "standback", - "syn 1.0.95", -] - [[package]] name = "tiny-bip39" version = "0.8.2" @@ -8012,7 +7883,7 @@ dependencies = [ "parking_lot 0.12.0", "pin-project-lite 0.2.9", "signal-hook-registry", - "socket2 0.4.4", + "socket2 0.4.9", "tokio-macros", "windows-sys 0.42.0", ] @@ -8116,7 +7987,7 @@ dependencies = [ "pin-project 1.0.10", "prost", "prost-derive", - "rustls-pemfile 1.0.0", + "rustls-pemfile 1.0.2", "tokio", "tokio-rustls", "tokio-stream", @@ -8560,9 +8431,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "void" @@ -8603,12 +8474,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -9035,6 +8900,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time 0.3.11", +] + [[package]] name = "zbase32" version = "0.1.2" @@ -9044,7 +8918,7 @@ checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" dependencies = [ "base64 0.13.0", "bech32", @@ -9060,7 +8934,7 @@ dependencies = [ "protobuf-codegen-pure", "rand_core 0.5.1", "subtle 2.4.0", - "time 0.2.27", + "time 0.3.11", "zcash_note_encryption", "zcash_primitives", ] @@ -9068,17 +8942,18 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" dependencies = [ "bech32", "bs58", "ff 0.8.0", "group 0.8.0", "jubjub", + "libsqlite3-sys", "protobuf", "rand_core 0.5.1", "rusqlite", - "time 0.2.27", + "time 0.3.11", "zcash_client_backend", "zcash_primitives", ] @@ -9086,7 +8961,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.0.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" dependencies = [ "blake2b_simd", "byteorder 1.4.3", @@ -9100,9 +8975,9 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" dependencies = [ - "aes 0.6.0", + "aes", "bitvec 0.18.5", "blake2b_simd", "blake2s_simd", @@ -9130,7 +9005,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" dependencies = [ "bellman", "blake2b_simd", diff --git a/README.md b/README.md index 40f82f6e81..5d17c4e4e4 100755 --- a/README.md +++ b/README.md @@ -1,60 +1,60 @@

- +

- - + + - - downloads + + downloads - - last commit + + last commit - - + +
- - issues + + issues - - issues closed + + issues closed - - pulls + + pulls - - pulls closed + + pulls closed
- - build status + + build status - - release version + + release version chat on Discord - - follow on Twitter + + follow on Twitter

-## What is the AtomicDEX-API? +## What is the Komodo DeFi Framework? -The AtomicDEX API core is open-source [atomic-swap](https://komodoplatform.com/en/academy/atomic-swaps/) software for seamless, decentralised, peer to peer trading between almost every blockchain asset in existence. This software works with propagation of orderbooks and swap states through the [libp2p](https://libp2p.io/) protocol and uses [Hash Time Lock Contracts (HTLCs)](https://en.bitcoinwiki.org/wiki/Hashed_Timelock_Contracts) for ensuring that the two parties in a swap either mutually complete a trade, or funds return to thier original owner. +The Komodo DeFi Framework is open-source [atomic-swap](https://komodoplatform.com/en/academy/atomic-swaps/) software for seamless, decentralised, peer to peer trading between almost every blockchain asset in existence. This software works with propagation of orderbooks and swap states through the [libp2p](https://libp2p.io/) protocol and uses [Hash Time Lock Contracts (HTLCs)](https://en.bitcoinwiki.org/wiki/Hashed_Timelock_Contracts) for ensuring that the two parties in a swap either mutually complete a trade, or funds return to thier original owner. There is no 3rd party intermediary, no proxy tokens, and at all times users remain in sole possession of their private keys. -A [well documented API](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) offers simple access to the underlying services using simple language agnostic JSON structured methods and parameters such that users can communicate with the core in a variety of methods such as [curl](https://developers.komodoplatform.com/basic-docs/atomicdex-api-legacy/buy.html) in CLI, or fully functioning [desktop and mobile applications](https://atomicdex.io/) like [AtomicDEX Desktop](https://github.com/KomodoPlatform/atomicDEX-Desktop). +A [well documented API](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) offers simple access to the underlying services using simple language agnostic JSON structured methods and parameters such that users can communicate with the core in a variety of methods such as [curl](https://developers.komodoplatform.com/basic-docs/atomicdex-api-legacy/buy.html) in CLI, or fully functioning [desktop and mobile applications](https://atomicdex.io/) like [Komodo Wallet Desktop](https://github.com/KomodoPlatform/komodo-wallet-desktop). -For a curated list of AtomicDEX based projects and resources, check out [Awesome AtomicDEX](https://github.com/KomodoPlatform/awesome-atomicdex). +For a curated list of Komodo DeFi Framework based projects and resources, check out [Awesome AtomicDEX](https://github.com/KomodoPlatform/awesome-atomicdex). ## Features @@ -63,7 +63,7 @@ For a curated list of AtomicDEX based projects and resources, check out [Awesome - Query orderbooks for all pairs within the [supported coins](https://github.com/KomodoPlatform/coins/blob/master/coins) - Buy/sell from the orderbook, or create maker orders - Configure automated ["makerbot" trading](https://developers.komodoplatform.com/basic-docs/atomicdex-api-20-dev/start_simple_market_maker_bot.html) with periodic price updates and optional [telegram](https://telegram.org/) alerts - + ## System Requirements @@ -91,7 +91,7 @@ If you want to build from source, the following prerequisites are required: To build, run `cargo build` (or `cargo build -vv` to get verbose build output). -For more detailed instructions, please refer to the [Installation Guide](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html). +For more detailed instructions, please refer to the [Installation Guide](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html). ## Building WASM binary @@ -119,7 +119,7 @@ To facilitate interoperability with the `mm2` service, there is the `adex-cli` c ## Usage -To launch the AtomicDEX API, run `./mm2` (or `mm2.exe` in Windows) +To launch the Komodo DeFi Framework, run `./mm2` (or `mm2.exe` in Windows) To activate a coin: ```bash @@ -178,7 +178,7 @@ Refer to the [Komodo Developer Docs](https://developers.komodoplatform.com/basic ## Disclaimer -This repository contains the `work in progress` code of the brand new AtomicDEX API core (mm2) built mainly on Rust. +This repository contains the `work in progress` code of the brand new Komodo DeFi Framework (mm2) built mainly on Rust. The current state can be considered as a alpha version. **WARNING: Use with test coins only or with assets which value does not exceed an amount you are willing to lose. This is alpha stage software! ** @@ -187,4 +187,3 @@ The current state can be considered as a alpha version. ## Help and troubleshooting If you have any question/want to report a bug/suggest an improvement feel free to [open an issue](https://github.com/artemii235/SuperNET/issues/new) or join the [Komodo Platform Discord](https://discord.gg/PGxVm2y) `dev-marketmaker` channel. - diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 2fa24a183e..4ff9aca963 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to AtomicDEX-API +# Contributing to Komodo DeFi Framework We welcome contribution from everyone in the form of suggestions, bug reports, pull requests, and feedback. Please note we have a code of conduct, please follow it in all your interactions with the project. diff --git a/docs/DEV_ENVIRONMENT.md b/docs/DEV_ENVIRONMENT.md index 0253bdd25e..317bb4eb2c 100644 --- a/docs/DEV_ENVIRONMENT.md +++ b/docs/DEV_ENVIRONMENT.md @@ -1,4 +1,4 @@ -# Setting up the dev environment for AtomicDEX-API to run full tests suite +# Setting up the dev environment for Komodo DeFi Framework to run full tests suite ## Running native tests diff --git a/docs/PR_REVIEW_CHECKLIST.md b/docs/PR_REVIEW_CHECKLIST.md index fcf5d02dd4..74a2373c5d 100644 --- a/docs/PR_REVIEW_CHECKLIST.md +++ b/docs/PR_REVIEW_CHECKLIST.md @@ -1,4 +1,4 @@ -# PR review checklist for AtomicDEX-API +# PR review checklist for Komodo DeFi Framework - [ ] Check that all CI build stages passed successfully. It's acceptable to have unstable tests failing. If you are unsure whether the test is unstable, please clarify it with the team. @@ -12,4 +12,4 @@ the test is unstable, please clarify it with the team. - [ ] Indicate code that is worth moving to a separate module or crate. - [ ] Check if the code can be improved/simplified: it might be overly abstracted or require the additional abstraction layer instead for a better design. - [ ] Follow SOLID if applicable. -- [ ] For PRs targeting release (main) branch check that QA tested and approved it. \ No newline at end of file +- [ ] For PRs targeting release (main) branch check that QA tested and approved it. diff --git a/mm2src/adex_cli/Cargo.lock b/mm2src/adex_cli/Cargo.lock index e4f904a958..37367f7ed8 100644 --- a/mm2src/adex_cli/Cargo.lock +++ b/mm2src/adex_cli/Cargo.lock @@ -15,20 +15,29 @@ dependencies = [ name = "adex-cli" version = "0.1.0" dependencies = [ + "anyhow", + "async-trait", "clap", "common", "derive_more", + "directories", "env_logger 0.7.1", "gstuff", + "http 0.2.9", "inquire", + "itertools", "log 0.4.17", "mm2_net", + "mm2_number", + "mm2_rpc", "passwords", + "rpc", "serde", "serde_json", "sysinfo", "tiny-bip39", "tokio", + "uuid", "winapi", ] @@ -40,17 +49,11 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - -[[package]] -name = "ahash" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "getrandom 0.2.9", + "cfg-if 1.0.0", "once_cell", "version_check", ] @@ -73,6 +76,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -83,12 +92,52 @@ dependencies = [ ] [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstream" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", ] [[package]] @@ -103,6 +152,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.2" @@ -116,19 +171,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] -[[package]] -name = "atomic-shim" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "atty" version = "0.2.14" @@ -170,6 +216,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + [[package]] name = "base64" version = "0.10.1" @@ -179,12 +231,36 @@ dependencies = [ "byteorder", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "bech32" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bitcoin" version = "0.29.2" @@ -202,6 +278,20 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" +[[package]] +name = "bitcrypto" +version = "0.1.0" +dependencies = [ + "groestl", + "primitives", + "ripemd160", + "serialization", + "sha-1", + "sha2", + "sha3", + "siphasher", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -220,15 +310,33 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ + "block-padding", "generic-array", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "bumpalo" version = "3.12.2" @@ -281,6 +389,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chain" +version = "0.1.0" +dependencies = [ + "bitcoin", + "bitcrypto", + "primitives", + "rustc-hex", + "serialization", + "serialization_derive", +] + [[package]] name = "chrono" version = "0.4.24" @@ -298,19 +418,46 @@ dependencies = [ [[package]] name = "clap" -version = "2.34.0" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" dependencies = [ - "ansi_term", - "atty", + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" +dependencies = [ + "anstream", + "anstyle", "bitflags", + "clap_lex", "strsim", - "textwrap", - "unicode-width", - "vec_map", ] +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote 1.0.27", + "syn 2.0.15", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + [[package]] name = "cloudabi" version = "0.0.3" @@ -330,6 +477,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "common" version = "0.1.0" @@ -350,20 +503,20 @@ dependencies = [ "futures 0.1.31", "futures 0.3.28", "futures-timer", - "getrandom 0.2.9", "gstuff", "hex", "http 0.2.9", "http-body 0.1.0", "hyper", "hyper-rustls", + "instant", "itertools", "js-sys", "lazy_static", "libc", "lightning", "log 0.4.17", - "parking_lot 0.12.1", + "parking_lot", "parking_lot_core 0.6.3", "primitive-types", "rand 0.7.3", @@ -381,7 +534,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", - "wasm-timer", "web-sys", "winapi", ] @@ -396,6 +548,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.4.0" @@ -494,7 +652,7 @@ dependencies = [ "crossterm_winapi", "libc", "mio", - "parking_lot 0.12.1", + "parking_lot", "signal-hook", "signal-hook-mio", "winapi", @@ -547,7 +705,7 @@ dependencies = [ "codespan-reporting", "once_cell", "proc-macro2", - "quote", + "quote 1.0.27", "scratch", "syn 2.0.15", ] @@ -565,7 +723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] @@ -589,7 +747,7 @@ checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", "proc-macro2", - "quote", + "quote 1.0.27", "rustc_version 0.4.0", "syn 1.0.109", ] @@ -603,6 +761,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dyn-clone" version = "1.0.11" @@ -653,6 +832,27 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "ethbloom" version = "0.12.1" @@ -812,10 +1012,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] +[[package]] +name = "futures-rustls" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1387e07917c711fb4ee4f48ea0adb04a3c9739e53ef85bf43ae1edc2937a8b" +dependencies = [ + "futures-io", + "rustls 0.19.1", + "webpki 0.21.4", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -883,10 +1094,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if 1.0.0", - "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -895,6 +1104,17 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +[[package]] +name = "groestl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2432787a9b8f0d58dca43fe2240399479b7582dc8afa2126dc7652b864029e47" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + [[package]] name = "gstuff" version = "0.7.4" @@ -926,37 +1146,44 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash 0.4.7", -] +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.7.6", + "ahash", ] [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashlink" -version = "0.6.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" dependencies = [ - "hashbrown 0.9.1", + "hashbrown 0.14.0", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -975,6 +1202,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -1095,7 +1328,7 @@ checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http 0.2.9", "hyper", - "rustls", + "rustls 0.20.8", "tokio", "tokio-rustls", "webpki-roots", @@ -1159,7 +1392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 1.0.109", ] @@ -1196,6 +1429,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", ] [[package]] @@ -1213,6 +1460,18 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1243,6 +1502,32 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keys" +version = "0.1.0" +dependencies = [ + "base58", + "bech32", + "bitcrypto", + "derive_more", + "lazy_static", + "primitives", + "rand 0.6.5", + "rustc-hex", + "secp256k1 0.20.3", + "serde", + "serde_derive", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1257,9 +1542,9 @@ checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libsqlite3-sys" -version = "0.20.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd" +checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" dependencies = [ "cc", "pkg-config", @@ -1284,6 +1569,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "lock_api" version = "0.4.9" @@ -1313,10 +1604,10 @@ dependencies = [ ] [[package]] -name = "mach" -version = "0.3.2" +name = "mach2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" dependencies = [ "libc", ] @@ -1349,26 +1640,27 @@ dependencies = [ [[package]] name = "metrics" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142c53885123b68d94108295a09d4afe1a1388ed95b54d5dacd9a454753030f2" +checksum = "aa8ebbd1a9e57bbab77b9facae7f5136aea44c356943bf9a198f647da64285d6" dependencies = [ - "ahash 0.7.6", + "ahash", "metrics-macros", + "portable-atomic", ] [[package]] name = "metrics-exporter-prometheus" -version = "0.10.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953cbbb6f9ba4b9304f4df79b98cdc9d14071ed93065a9fca11c00c5d9181b66" +checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" dependencies = [ + "base64 0.21.2", "hyper", "indexmap", "ipnet", "metrics", "metrics-util", - "parking_lot 0.11.2", "quanta", "thiserror", "tokio", @@ -1377,31 +1669,29 @@ dependencies = [ [[package]] name = "metrics-macros" -version = "0.5.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e30813093f757be5cf21e50389a24dc7dbb22c49f23b7e8f51d69b508a5ffa" +checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ "proc-macro2", - "quote", - "syn 1.0.109", + "quote 1.0.27", + "syn 2.0.15", ] [[package]] name = "metrics-util" -version = "0.13.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1f4b69bef1e2b392b2d4a12902f2af90bb438ba4a66aa222d1023fa6561b50" +checksum = "111cb375987443c3de8d503580b536f77dc8416d32db62d9456db5d93bd7ac47" dependencies = [ "aho-corasick 0.7.20", - "atomic-shim", "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.11.2", + "hashbrown 0.13.2", "indexmap", "metrics", "num_cpus", "ordered-float", - "parking_lot 0.11.2", "quanta", "radix_trie", "sketches-ddsketch", @@ -1439,6 +1729,7 @@ dependencies = [ "db_common", "derive_more", "futures 0.3.28", + "futures-rustls", "gstuff", "hex", "lazy_static", @@ -1471,7 +1762,7 @@ dependencies = [ name = "mm2_metrics" version = "0.1.0" dependencies = [ - "base64", + "base64 0.10.1", "common", "derive_more", "futures 0.3.28", @@ -1485,7 +1776,6 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-timer", ] [[package]] @@ -1499,6 +1789,7 @@ dependencies = [ "derive_more", "ethkey", "futures 0.3.28", + "futures-util", "gstuff", "http 0.2.9", "hyper", @@ -1508,14 +1799,30 @@ dependencies = [ "mm2_err_handle", "prost", "rand 0.7.3", + "rustls 0.20.8", "serde", "serde_json", + "tokio", + "tokio-rustls", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", ] +[[package]] +name = "mm2_number" +version = "0.1.0" +dependencies = [ + "bigdecimal", + "num-bigint", + "num-rational", + "num-traits", + "paste", + "serde", + "serde_json", +] + [[package]] name = "mm2_rpc" version = "0.1.0" @@ -1526,10 +1833,13 @@ dependencies = [ "gstuff", "http 0.2.9", "mm2_err_handle", + "mm2_number", + "rpc", "ser_error", "ser_error_derive", "serde", "serde_json", + "uuid", ] [[package]] @@ -1559,6 +1869,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1569,6 +1891,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg 1.1.0", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1609,11 +1944,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" -version = "2.10.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +checksum = "2fc2dbde8f8a79f2102cc474ceb0ad68e3b80b85289ea62389b60e66777e4213" dependencies = [ "num-traits", ] @@ -1624,7 +1965,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ddb756ca205bd108aee3c62c6d3c994e1df84a59b9d6d4a5ea42ee1fd5a9a28" dependencies = [ - "arrayvec", + "arrayvec 0.7.2", "bitvec", "byte-slice-cast", "impl-trait-for-tuples", @@ -1640,21 +1981,10 @@ checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote", + "quote 1.0.27", "syn 1.0.109", ] -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.1" @@ -1680,20 +2010,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if 1.0.0", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec 1.10.0", - "winapi", -] - [[package]] name = "parking_lot_core" version = "0.9.7" @@ -1716,6 +2032,12 @@ dependencies = [ "random-pick", ] +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + [[package]] name = "pbkdf2" version = "0.4.0" @@ -1743,6 +2065,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "portable-atomic" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1816,22 +2144,22 @@ dependencies = [ "anyhow", "itertools", "proc-macro2", - "quote", + "quote 1.0.27", "syn 1.0.109", ] [[package]] name = "quanta" -version = "0.9.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" dependencies = [ "crossbeam-utils", "libc", - "mach", + "mach2", "once_cell", "raw-cpuid", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -1842,6 +2170,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" + [[package]] name = "quote" version = "1.0.27" @@ -2072,7 +2406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b86292cf41ccfc96c5de7165c1c53d5b4ac540c5bab9d1857acbe9eba5f1a0b" dependencies = [ "proc-macro-hack", - "quote", + "quote 1.0.27", "syn 2.0.15", ] @@ -2140,6 +2474,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.9", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "regex" version = "1.8.1" @@ -2172,6 +2517,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "ripemd160" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + [[package]] name = "rlp" version = "0.5.2" @@ -2182,18 +2538,33 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rpc" +version = "0.1.0" +dependencies = [ + "chain", + "keys", + "log 0.4.17", + "primitives", + "rustc-hex", + "script", + "serde", + "serde_derive", + "serde_json", + "serialization", +] + [[package]] name = "rusqlite" -version = "0.24.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38ee71cbab2c827ec0ac24e76f82eca723cee92c509a65f67dee393c25112" +checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", - "memchr", "smallvec 1.10.0", ] @@ -2233,6 +2604,33 @@ dependencies = [ "semver 1.0.17", ] +[[package]] +name = "rustix" +version = "0.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64 0.13.1", + "log 0.4.17", + "ring", + "sct 0.6.1", + "webpki 0.21.4", +] + [[package]] name = "rustls" version = "0.20.8" @@ -2240,8 +2638,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "ring", - "sct", - "webpki", + "sct 0.7.0", + "webpki 0.22.0", ] [[package]] @@ -2268,6 +2666,30 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +[[package]] +name = "script" +version = "0.1.0" +dependencies = [ + "bitcrypto", + "blake2b_simd", + "chain", + "keys", + "log 0.4.17", + "primitives", + "serde", + "serialization", +] + +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sct" version = "0.7.0" @@ -2349,7 +2771,7 @@ name = "ser_error_derive" version = "0.1.0" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "ser_error", "syn 1.0.109", ] @@ -2381,7 +2803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] @@ -2404,10 +2826,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] +[[package]] +name = "serialization" +version = "0.1.0" +dependencies = [ + "byteorder", + "derive_more", + "primitives", + "test_helpers", +] + +[[package]] +name = "serialization_derive" +version = "0.1.0" +dependencies = [ + "quote 0.3.15", + "syn 0.11.11", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.9.9" @@ -2421,6 +2874,18 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer", + "digest", + "keccak", + "opaque-debug", +] + [[package]] name = "shared_ref_counter" version = "0.1.0" @@ -2456,10 +2921,16 @@ dependencies = [ ] [[package]] -name = "sketches-ddsketch" +name = "siphasher" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d2ecae5fcf33b122e2e6bd520a57ccf152d2dde3b38c71039df1a6867264ee" +checksum = "833011ca526bd88f16778d32c699d325a9ad302fa06381cd66f7be63351d3f6d" + +[[package]] +name = "sketches-ddsketch" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" [[package]] name = "slab" @@ -2519,9 +2990,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" @@ -2529,6 +3000,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +dependencies = [ + "quote 0.3.15", + "synom", + "unicode-xid", +] + [[package]] name = "syn" version = "1.0.109" @@ -2536,7 +3018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "unicode-ident", ] @@ -2547,10 +3029,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "unicode-ident", ] +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +dependencies = [ + "unicode-xid", +] + [[package]] name = "sysinfo" version = "0.28.4" @@ -2582,12 +3073,10 @@ dependencies = [ ] [[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +name = "test_helpers" +version = "0.1.0" dependencies = [ - "unicode-width", + "hex", ] [[package]] @@ -2606,7 +3095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] @@ -2706,7 +3195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] @@ -2716,9 +3205,9 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls", + "rustls 0.20.8", "tokio", - "webpki", + "webpki 0.22.0", ] [[package]] @@ -2777,7 +3266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] @@ -2841,12 +3330,24 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" + [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.3.2" @@ -2864,12 +3365,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.4" @@ -2892,12 +3387,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2906,9 +3395,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -2916,16 +3405,16 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", - "lazy_static", "log 0.4.17", + "once_cell", "proc-macro2", - "quote", - "syn 1.0.109", + "quote 1.0.27", + "syn 2.0.15", "wasm-bindgen-shared", ] @@ -2943,32 +3432,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote", + "quote 1.0.27", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", - "quote", - "syn 1.0.109", + "quote 1.0.27", + "syn 2.0.15", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-bindgen-test" @@ -2991,32 +3480,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77c5a6f82cc6093a321ca5fb3dc9327fe51675d477b3799b4a9375bac3b7b4c" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", ] [[package]] -name = "wasm-timer" -version = "0.2.5" +name = "web-sys" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" dependencies = [ - "futures 0.3.28", "js-sys", - "parking_lot 0.11.2", - "pin-utils", "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", ] [[package]] -name = "web-sys" -version = "0.3.57" +name = "webpki" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ - "js-sys", - "wasm-bindgen", + "ring", + "untrusted", ] [[package]] @@ -3035,7 +3519,7 @@ version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ - "webpki", + "webpki 0.22.0", ] [[package]] @@ -3244,6 +3728,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", - "quote", + "quote 1.0.27", "syn 2.0.15", ] diff --git a/mm2src/adex_cli/Cargo.toml b/mm2src/adex_cli/Cargo.toml index 2f1abdc0a0..9af19a3ab0 100644 --- a/mm2src/adex_cli/Cargo.toml +++ b/mm2src/adex_cli/Cargo.toml @@ -7,20 +7,30 @@ description = "Provides a CLI interface and facilitates interoperating to komodo # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -clap = "2.33.3" +anyhow = { version = "1.0.71", features = ["std"] } +async-trait = "0.1.68" +clap = { version = "4.2", features = ["derive"] } common = { path = "../common" } derive_more = "0.99" +directories = "5.0" env_logger = "0.7.1" +http = "0.2" gstuff = { version = "=0.7.4" , features = [ "nightly" ]} inquire = "0.6" +itertools = "0.10" log = "0.4" mm2_net = { path = "../mm2_net" } +mm2_number = { path = "../mm2_number" } +mm2_rpc = { path = "../mm2_rpc"} passwords = "3.1" serde = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } sysinfo = "0.28" tiny-bip39 = "0.8.0" tokio = { version = "1.20", features = [ "macros" ] } +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +rpc = { path = "../mm2_bitcoin/rpc" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.3", features = ["processthreadsapi", "winnt"] } + diff --git a/mm2src/adex_cli/src/activation_scheme_db/activation_scheme_impl.rs b/mm2src/adex_cli/src/activation_scheme_db/activation_scheme_impl.rs new file mode 100644 index 0000000000..a2bc0ba451 --- /dev/null +++ b/mm2src/adex_cli/src/activation_scheme_db/activation_scheme_impl.rs @@ -0,0 +1,70 @@ +use anyhow::{anyhow, bail, Result}; +use log::{debug, error}; +use serde_json::Value as Json; +use std::collections::HashMap; + +use super::init_activation_scheme::get_activation_scheme_path; +use crate::helpers::read_json_file; +use crate::logging::{error_anyhow, error_bail}; + +#[derive(Default)] +pub(crate) struct ActivationScheme { + scheme: HashMap, +} + +impl ActivationScheme { + pub(crate) fn get_activation_method(&self, coin: &str) -> Option<&Json> { self.scheme.get(coin) } + + fn init(&mut self) -> Result<()> { + let mut scheme_source: Vec = Self::load_json_file()?; + self.scheme = scheme_source + .iter_mut() + .filter_map(Self::get_coin_activation_command) + .collect(); + Ok(()) + } + + fn get_coin_activation_command(element: &mut Json) -> Option<(String, Json)> { + Self::get_coin_activation_command_impl(element).ok() + } + + fn get_coin_activation_command_impl(element: &mut Json) -> Result<(String, Json)> { + let coin = element + .get_mut("coin") + .ok_or_else(|| error_anyhow!("Failed to get coin pair, no coin value"))? + .as_str() + .ok_or_else(|| error_anyhow!("Failed to get coin pair, coin is not str"))? + .to_string(); + let mut command = element + .get_mut("command") + .ok_or_else(|| error_anyhow!("Failed to get coin pair, no command value"))? + .take(); + command + .as_object_mut() + .ok_or_else(|| error_anyhow!("Failed to get coin pair, command is not object"))? + .remove("userpass"); + Ok((coin, command)) + } + + fn load_json_file() -> Result> { + let activation_scheme_path = get_activation_scheme_path()?; + debug!("Start reading activation_scheme from: {activation_scheme_path:?}"); + + let mut activation_scheme: Json = read_json_file(&activation_scheme_path)?; + + let Json::Array(results) = activation_scheme + .get_mut("results") + .ok_or_else(|| error_anyhow!("Failed to load activation scheme json file, no results section"))? + .take() + else { + error_bail!("Failed to load activation scheme json file, wrong format") + }; + Ok(results) + } +} + +pub(crate) fn get_activation_scheme() -> Result { + let mut activation_scheme = ActivationScheme::default(); + activation_scheme.init()?; + Ok(activation_scheme) +} diff --git a/mm2src/adex_cli/src/activation_scheme_db/init_activation_scheme.rs b/mm2src/adex_cli/src/activation_scheme_db/init_activation_scheme.rs new file mode 100644 index 0000000000..b0dd8c956c --- /dev/null +++ b/mm2src/adex_cli/src/activation_scheme_db/init_activation_scheme.rs @@ -0,0 +1,52 @@ +use anyhow::{anyhow, Result}; +use common::log::{error, info}; +use http::StatusCode; +use itertools::Itertools; +use mm2_net::transport::slurp_url; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::PathBuf; + +use crate::adex_config::AdexConfigImpl; +use crate::error_anyhow; + +const ACTIVATION_SCHEME_FILE: &str = "activation_scheme.json"; +const COIN_ACTIVATION_SOURCE: &str = "https://stats.kmd.io/api/table/coin_activation/"; + +pub(crate) async fn init_activation_scheme() -> Result<()> { + let config_path = get_activation_scheme_path()?; + info!("Start getting activation_scheme from: {config_path:?}"); + + let mut writer = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(config_path) + .map_err(|error| error_anyhow!("Failed to open activation_scheme file to write: {error}"))?; + + let activation_scheme = get_activation_scheme_data().await?; + writer + .write_all(&activation_scheme) + .map_err(|error| error_anyhow!("Failed to write activation_scheme: {error}")) +} + +pub(crate) fn get_activation_scheme_path() -> Result { + let mut config_path = AdexConfigImpl::get_config_dir()?; + config_path.push(ACTIVATION_SCHEME_FILE); + Ok(config_path) +} + +async fn get_activation_scheme_data() -> Result> { + info!("Download activation_scheme from: {COIN_ACTIVATION_SOURCE}"); + match slurp_url(COIN_ACTIVATION_SOURCE).await { + Ok((StatusCode::OK, _, data)) => Ok(data), + Ok((status_code, headers, data)) => Err(error_anyhow!( + "Failed to get activation scheme from: {COIN_ACTIVATION_SOURCE}, bad status: {status_code}, headers: {}, data: {}", + headers.iter().map(|(k, v)| format!("{k}: {v:?}")).join(", "), + String::from_utf8_lossy(&data) + )), + Err(error) => Err(error_anyhow!( + "Failed to get activation_scheme from: {COIN_ACTIVATION_SOURCE}, error: {error}" + )), + } +} diff --git a/mm2src/adex_cli/src/activation_scheme_db/mod.rs b/mm2src/adex_cli/src/activation_scheme_db/mod.rs new file mode 100644 index 0000000000..1dd9ccaa07 --- /dev/null +++ b/mm2src/adex_cli/src/activation_scheme_db/mod.rs @@ -0,0 +1,7 @@ +mod activation_scheme_impl; +mod init_activation_scheme; + +pub(super) use activation_scheme_impl::get_activation_scheme; +#[cfg(test)] +pub(super) use init_activation_scheme::get_activation_scheme_path; +pub(super) use init_activation_scheme::init_activation_scheme; diff --git a/mm2src/adex_cli/src/adex_app.rs b/mm2src/adex_cli/src/adex_app.rs new file mode 100644 index 0000000000..01a3a34385 --- /dev/null +++ b/mm2src/adex_cli/src/adex_app.rs @@ -0,0 +1,25 @@ +use std::env; +use std::io::Write; + +use super::adex_config::AdexConfigImpl; +use super::adex_proc::ResponseHandlerImpl; +use super::cli; + +pub(super) struct AdexApp { + config: AdexConfigImpl, +} + +impl AdexApp { + pub(super) fn new() -> AdexApp { + let config = AdexConfigImpl::read_config().unwrap_or_default(); + AdexApp { config } + } + + pub(super) async fn execute(&self) { + let mut writer = std::io::stdout(); + let response_handler = ResponseHandlerImpl { + writer: (&mut writer as &mut dyn Write).into(), + }; + let _ = cli::Cli::execute(env::args(), &self.config, &response_handler).await; + } +} diff --git a/mm2src/adex_cli/src/adex_config.rs b/mm2src/adex_cli/src/adex_config.rs new file mode 100644 index 0000000000..20bcbb8fdf --- /dev/null +++ b/mm2src/adex_cli/src/adex_config.rs @@ -0,0 +1,160 @@ +use anyhow::{anyhow, bail, Result}; +use directories::ProjectDirs; +use inquire::Password; +use log::{error, info, warn}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::adex_proc::SmartFractPrecision; +use crate::helpers::rewrite_json_file; +use crate::logging::{error_anyhow, warn_bail}; + +const PROJECT_QUALIFIER: &str = "com"; +const PROJECT_COMPANY: &str = "komodoplatform"; +const PROJECT_APP: &str = "adex-cli"; +const ADEX_CFG: &str = "adex_cfg.json"; + +const PRICE_PRECISION_MIN: usize = 8; +const PRICE_PRECISION_MAX: usize = 8; +const VOLUME_PRECISION_MIN: usize = 2; +const VOLUME_PRECISION_MAX: usize = 5; +const VOLUME_PRECISION: SmartFractPrecision = (VOLUME_PRECISION_MIN, VOLUME_PRECISION_MAX); +const PRICE_PRECISION: SmartFractPrecision = (PRICE_PRECISION_MIN, PRICE_PRECISION_MAX); + +pub(super) fn get_config() { + let Ok(adex_cfg) = AdexConfigImpl::from_config_path() else { return; }; + info!("{}", adex_cfg) +} + +pub(super) fn set_config(set_password: bool, rpc_api_uri: Option) -> Result<()> { + assert!(set_password || rpc_api_uri.is_some()); + let mut adex_cfg = AdexConfigImpl::from_config_path().unwrap_or_else(|_| AdexConfigImpl::default()); + + if set_password { + let rpc_password = Password::new("Enter RPC API password:") + .prompt() + .map_err(|error| error_anyhow!("Failed to get rpc_api_password: {error}"))?; + adex_cfg.set_rpc_password(rpc_password); + } + + if let Some(rpc_api_uri) = rpc_api_uri { + adex_cfg.set_rpc_uri(rpc_api_uri); + } + + adex_cfg.write_to_config_path()?; + info!("Configuration has been set"); + + Ok(()) +} + +pub(super) trait AdexConfig { + fn rpc_password(&self) -> Option; + fn rpc_uri(&self) -> Option; + fn orderbook_price_precision(&self) -> &SmartFractPrecision; + fn orderbook_volume_precision(&self) -> &SmartFractPrecision; +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub(super) struct AdexConfigImpl { + #[serde(skip_serializing_if = "Option::is_none")] + rpc_password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + rpc_uri: Option, +} + +impl AdexConfig for AdexConfigImpl { + fn rpc_password(&self) -> Option { self.rpc_password.clone() } + fn rpc_uri(&self) -> Option { self.rpc_uri.clone() } + fn orderbook_price_precision(&self) -> &SmartFractPrecision { &PRICE_PRECISION } + fn orderbook_volume_precision(&self) -> &SmartFractPrecision { &VOLUME_PRECISION } +} + +impl Display for AdexConfigImpl { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !self.is_set() { + return writeln!(f, "adex configuration is not set"); + } + writeln!( + f, + "mm2 RPC URL: {}", + self.rpc_uri.as_ref().expect("Expected rpc_uri is set") + )?; + writeln!(f, "mm2 RPC password: *************")?; + Ok(()) + } +} + +impl AdexConfigImpl { + #[cfg(test)] + pub(super) fn new(rpc_password: &str, rpc_uri: &str) -> Self { + Self { + rpc_password: Some(rpc_password.to_string()), + rpc_uri: Some(rpc_uri.to_string()), + } + } + + #[cfg(not(test))] + pub(super) fn read_config() -> Result { + let config = AdexConfigImpl::from_config_path()?; + if config.rpc_password.is_none() { + warn!("Configuration is not complete, no rpc_password in there"); + } + if config.rpc_uri.is_none() { + warn!("Configuration is not complete, no rpc_uri in there"); + } + Ok(config) + } + + fn is_set(&self) -> bool { self.rpc_uri.is_some() && self.rpc_password.is_some() } + + pub(super) fn get_config_dir() -> Result { + let project_dirs = ProjectDirs::from(PROJECT_QUALIFIER, PROJECT_COMPANY, PROJECT_APP) + .ok_or_else(|| error_anyhow!("Failed to get project_dirs"))?; + let config_path: PathBuf = project_dirs.config_dir().into(); + fs::create_dir_all(&config_path) + .map_err(|error| error_anyhow!("Failed to create config_dir: {config_path:?}, error: {error}"))?; + Ok(config_path) + } + + pub(crate) fn get_config_path() -> Result { + let mut config_path = Self::get_config_dir()?; + config_path.push(ADEX_CFG); + Ok(config_path) + } + + fn from_config_path() -> Result { + let config_path = Self::get_config_path()?; + + if !config_path.exists() { + warn_bail!("Config is not set") + } + Self::read_from(&config_path) + } + + fn write_to_config_path(&self) -> Result<()> { + let config_path = Self::get_config_path()?; + self.write_to(&config_path) + } + + fn read_from(cfg_path: &Path) -> Result { + let adex_path_str = cfg_path.to_str().unwrap_or("Undefined"); + let adex_cfg_file = fs::File::open(cfg_path) + .map_err(|error| error_anyhow!("Failed to open: {adex_path_str}, error: {error}"))?; + + serde_json::from_reader(adex_cfg_file) + .map_err(|error| error_anyhow!("Failed to read adex_cfg to read from: {adex_path_str}, error: {error}")) + } + + fn write_to(&self, cfg_path: &Path) -> Result<()> { + let adex_path_str = cfg_path + .to_str() + .ok_or_else(|| error_anyhow!("Failed to get cfg_path as str"))?; + rewrite_json_file(self, adex_path_str) + } + + fn set_rpc_password(&mut self, rpc_password: String) { self.rpc_password.replace(rpc_password); } + + fn set_rpc_uri(&mut self, rpc_uri: String) { self.rpc_uri.replace(rpc_uri); } +} diff --git a/mm2src/adex_cli/src/adex_proc/adex_proc_impl.rs b/mm2src/adex_cli/src/adex_proc/adex_proc_impl.rs new file mode 100644 index 0000000000..926ee784ff --- /dev/null +++ b/mm2src/adex_cli/src/adex_proc/adex_proc_impl.rs @@ -0,0 +1,161 @@ +use anyhow::{anyhow, bail, Result}; +use log::{error, info, warn}; +use mm2_rpc::data::legacy::{BalanceResponse, CoinInitResponse, GetEnabledResponse, Mm2RpcResult, MmVersionResponse, + OrderbookRequest, OrderbookResponse, SellBuyRequest, SellBuyResponse, Status}; +use serde_json::{json, Value as Json}; + +use super::command::{Command, Dummy, Method}; +use super::response_handler::ResponseHandler; +use super::OrderbookConfig; +use crate::activation_scheme_db::get_activation_scheme; +use crate::adex_config::AdexConfig; +use crate::transport::Transport; +use crate::{error_anyhow, error_bail, warn_anyhow, warn_bail}; + +pub(crate) struct AdexProc<'trp, 'hand, 'cfg, T: Transport, H: ResponseHandler, C: AdexConfig + ?Sized> { + pub(crate) transport: Option<&'trp T>, + pub(crate) response_handler: &'hand H, + pub(crate) config: &'cfg C, +} + +macro_rules! request_legacy { + ($request: ident, $response_ty: ty, $self: ident, $handle_method: ident$ (, $opt:expr)*) => {{ + let transport = $self.transport.ok_or_else(|| warn_anyhow!( concat!("Failed to send: `", stringify!($request), "`, transport is not available")))?; + match transport.send::<_, $response_ty, Json>($request).await { + Ok(Ok(ok)) => $self.response_handler.$handle_method(&ok, $($opt),*), + Ok(Err(error)) => $self.response_handler.print_response(error), + Err(error) => error_bail!( + concat!("Failed to send: `", stringify!($request), "`, error: {}"), + error + ), + } + }}; +} + +impl AdexProc<'_, '_, '_, T, P, C> { + pub(crate) async fn enable(&self, asset: &str) -> Result<()> { + info!("Enabling asset: {asset}"); + + let activation_scheme = get_activation_scheme()?; + let Some(activation_method) = activation_scheme.get_activation_method(asset) else { + warn_bail!("Asset is not known: {asset}") + }; + + let enable = Command::builder() + .flatten_data(activation_method) + .userpass(self.get_rpc_password()?) + .build(); + + request_legacy!(enable, CoinInitResponse, self, on_enable_response) + } + + pub(crate) async fn get_balance(&self, asset: &str) -> Result<()> { + info!("Getting balance, coin: {asset} ..."); + let get_balance = Command::builder() + .method(Method::GetBalance) + .flatten_data(json!({ "coin": asset })) + .userpass(self.get_rpc_password()?) + .build(); + request_legacy!(get_balance, BalanceResponse, self, on_balance_response) + } + + pub(crate) async fn get_enabled(&self) -> Result<()> { + info!("Getting list of enabled coins ..."); + + let get_enabled = Command::::builder() + .method(Method::GetEnabledCoins) + .userpass(self.get_rpc_password()?) + .build(); + request_legacy!( + get_enabled, + Mm2RpcResult, + self, + on_get_enabled_response + ) + } + + pub(crate) async fn get_orderbook(&self, base: &str, rel: &str, orderbook_config: OrderbookConfig) -> Result<()> { + info!("Getting orderbook, base: {base}, rel: {rel} ..."); + + let get_orderbook = Command::builder() + .method(Method::GetOrderbook) + .flatten_data(OrderbookRequest { + base: base.to_string(), + rel: rel.to_string(), + }) + .build(); + + request_legacy!( + get_orderbook, + OrderbookResponse, + self, + on_orderbook_response, + self.config, + orderbook_config + ) + } + + pub(crate) async fn sell(&self, order: SellBuyRequest) -> Result<()> { + info!( + "Selling: {} {} for: {} {} at the price of {} {} per {}", + order.volume, + order.base, + order.volume.clone() * order.price.clone(), + order.rel, + order.price, + order.rel, + order.base, + ); + + let sell = Command::builder() + .userpass(self.get_rpc_password()?) + .method(Method::Sell) + .flatten_data(order) + .build(); + request_legacy!(sell, Mm2RpcResult, self, on_sell_response) + } + + pub(crate) async fn buy(&self, order: SellBuyRequest) -> Result<()> { + info!( + "Buying: {} {} with: {} {} at the price of {} {} per {}", + order.volume, + order.base, + order.volume.clone() * order.price.clone(), + order.rel, + order.price, + order.rel, + order.base, + ); + + let buy = Command::builder() + .userpass(self.get_rpc_password()?) + .method(Method::Buy) + .flatten_data(order) + .build(); + request_legacy!(buy, Mm2RpcResult, self, on_buy_response) + } + + pub(crate) async fn send_stop(&self) -> Result<()> { + info!("Sending stop command"); + let stop_command = Command::::builder() + .userpass(self.get_rpc_password()?) + .method(Method::Stop) + .build(); + request_legacy!(stop_command, Mm2RpcResult, self, on_stop_response) + } + + pub(crate) async fn get_version(self) -> Result<()> { + info!("Request for mm2 version"); + let get_version = Command::::builder() + .userpass(self.get_rpc_password()?) + .method(Method::Version) + .build(); + request_legacy!(get_version, MmVersionResponse, self, on_version_response) + } + + fn get_rpc_password(&self) -> Result { + self.config + .rpc_password() + .ok_or_else(|| error_anyhow!("Failed to get rpc_password, not set")) + } +} diff --git a/mm2src/adex_cli/src/adex_proc/command.rs b/mm2src/adex_cli/src/adex_proc/command.rs new file mode 100644 index 0000000000..461798cf56 --- /dev/null +++ b/mm2src/adex_cli/src/adex_proc/command.rs @@ -0,0 +1,94 @@ +use derive_more::Display; +use serde::Serialize; + +#[derive(Serialize, Clone)] +pub(super) struct Command +where + T: Serialize + Sized, +{ + #[serde(flatten, skip_serializing_if = "Option::is_none")] + flatten_data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + method: Option, + #[serde(skip_serializing_if = "Option::is_none")] + userpass: Option, +} + +#[derive(Serialize, Clone, Display)] +#[serde(rename_all = "lowercase")] +pub(super) enum Method { + Stop, + Version, + #[serde(rename = "my_balance")] + GetBalance, + #[serde(rename = "get_enabled_coins")] + GetEnabledCoins, + #[serde(rename = "orderbook")] + GetOrderbook, + Sell, + Buy, +} + +#[derive(Serialize, Clone, Copy, Display)] +pub(super) struct Dummy {} + +impl Command +where + T: Serialize + Sized, +{ + pub(super) fn builder() -> CommandBuilder { CommandBuilder::new() } +} + +pub(super) struct CommandBuilder { + userpass: Option, + method: Option, + flatten_data: Option, +} + +impl CommandBuilder +where + T: Serialize, +{ + fn new() -> Self { + CommandBuilder { + userpass: None, + method: None, + flatten_data: None, + } + } + + pub(super) fn userpass(&mut self, userpass: String) -> &mut Self { + self.userpass = Some(userpass); + self + } + + pub(super) fn method(&mut self, method: Method) -> &mut Self { + self.method = Some(method); + self + } + + pub(super) fn flatten_data(&mut self, flatten_data: T) -> &mut Self { + self.flatten_data = Some(flatten_data); + self + } + + pub(super) fn build(&mut self) -> Command { + Command { + userpass: self.userpass.take(), + method: self.method.take(), + flatten_data: self.flatten_data.take(), + } + } +} + +impl std::fmt::Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut cmd: Self = self.clone(); + cmd.userpass = self.userpass.as_ref().map(|_| "***********".to_string()); + writeln!( + f, + "{}", + serde_json::to_string(&cmd).unwrap_or_else(|_| "Unknown".to_string()) + ) + } +} diff --git a/mm2src/adex_cli/src/adex_proc/mod.rs b/mm2src/adex_cli/src/adex_proc/mod.rs new file mode 100644 index 0000000000..0d6795a413 --- /dev/null +++ b/mm2src/adex_cli/src/adex_proc/mod.rs @@ -0,0 +1,19 @@ +mod adex_proc_impl; +mod command; +mod response_handler; + +pub(super) use adex_proc_impl::AdexProc; +pub(super) use response_handler::{ResponseHandler, ResponseHandlerImpl, SmartFractPrecision}; + +#[derive(Clone)] +pub(super) struct OrderbookConfig { + pub(super) uuids: bool, + pub(super) min_volume: bool, + pub(super) max_volume: bool, + pub(super) publics: bool, + pub(super) address: bool, + pub(super) age: bool, + pub(super) conf_settings: bool, + pub(super) asks_limit: Option, + pub(super) bids_limit: Option, +} diff --git a/mm2src/adex_cli/src/adex_proc/response_handler.rs b/mm2src/adex_cli/src/adex_proc/response_handler.rs new file mode 100644 index 0000000000..74a0838136 --- /dev/null +++ b/mm2src/adex_cli/src/adex_proc/response_handler.rs @@ -0,0 +1,207 @@ +#[path = "response_handler/orderbook.rs"] mod orderbook; +#[path = "response_handler/smart_fraction_fmt.rs"] +mod smart_fraction_fmt; + +pub(crate) use smart_fraction_fmt::SmartFractPrecision; + +use anyhow::{anyhow, Result}; +use itertools::Itertools; +use log::{error, info}; +use mm2_rpc::data::legacy::{BalanceResponse, CoinInitResponse, GetEnabledResponse, Mm2RpcResult, MmVersionResponse, + OrderbookResponse, SellBuyResponse, Status}; +use serde_json::Value as Json; +use std::cell::RefCell; +use std::fmt::Debug; +use std::io::Write; + +use super::OrderbookConfig; +use crate::adex_config::AdexConfig; +use crate::error_anyhow; +use common::{write_safe::io::WriteSafeIO, write_safe_io, writeln_safe_io}; + +pub(crate) trait ResponseHandler { + fn print_response(&self, response: Json) -> Result<()>; + fn debug_response(&self, response: &T) -> Result<()>; + fn on_orderbook_response( + &self, + orderbook: &OrderbookResponse, + config: &Cfg, + orderbook_config: OrderbookConfig, + ) -> Result<()>; + fn on_get_enabled_response(&self, enabled: &Mm2RpcResult) -> Result<()>; + fn on_version_response(&self, response: &MmVersionResponse) -> Result<()>; + fn on_enable_response(&self, response: &CoinInitResponse) -> Result<()>; + fn on_balance_response(&self, response: &BalanceResponse) -> Result<()>; + fn on_sell_response(&self, response: &Mm2RpcResult) -> Result<()>; + fn on_buy_response(&self, response: &Mm2RpcResult) -> Result<()>; + fn on_stop_response(&self, response: &Mm2RpcResult) -> Result<()>; +} + +pub(crate) struct ResponseHandlerImpl<'a> { + pub(crate) writer: RefCell<&'a mut dyn Write>, +} + +impl ResponseHandler for ResponseHandlerImpl<'_> { + fn print_response(&self, result: Json) -> Result<()> { + let object = result + .as_object() + .ok_or_else(|| error_anyhow!("Failed to cast result as object"))?; + + object + .iter() + .map(SimpleCliTable::from_pair) + .for_each(|value| writeln_safe_io!(self.writer.borrow_mut(), "{}: {:?}", value.key, value.value)); + Ok(()) + } + + fn debug_response(&self, response: &T) -> Result<()> { + info!("{response:?}"); + Ok(()) + } + + fn on_orderbook_response( + &self, + orderbook: &OrderbookResponse, + config: &Cfg, + orderbook_config: OrderbookConfig, + ) -> Result<()> { + let mut writer = self.writer.borrow_mut(); + + let base_vol_head = format!("Volume: {}", orderbook.base); + let rel_price_head = format!("Price: {}", orderbook.rel); + writeln_safe_io!( + writer, + "{}", + orderbook::AskBidRow::new( + base_vol_head.as_str(), + rel_price_head.as_str(), + "Uuid", + "Min volume", + "Max volume", + "Age(sec.)", + "Public", + "Address", + "Order conf (bc,bn:rc,rn)", + &orderbook_config + ) + ); + + let price_prec = config.orderbook_price_precision(); + let vol_prec = config.orderbook_volume_precision(); + + if orderbook.asks.is_empty() { + writeln_safe_io!( + writer, + "{}", + orderbook::AskBidRow::new("", "No asks found", "", "", "", "", "", "", "", &orderbook_config) + ); + } else { + let skip = orderbook + .asks + .len() + .checked_sub(orderbook_config.asks_limit.unwrap_or(usize::MAX)) + .unwrap_or_default(); + + orderbook + .asks + .iter() + .sorted_by(orderbook::cmp_asks) + .skip(skip) + .map(|entry| orderbook::AskBidRow::from_orderbook_entry(entry, vol_prec, price_prec, &orderbook_config)) + .for_each(|row: orderbook::AskBidRow| writeln_safe_io!(writer, "{}", row)); + } + writeln_safe_io!(writer, "{}", orderbook::AskBidRow::new_delimiter(&orderbook_config)); + + if orderbook.bids.is_empty() { + writeln_safe_io!( + writer, + "{}", + orderbook::AskBidRow::new("", "No bids found", "", "", "", "", "", "", "", &orderbook_config) + ); + } else { + orderbook + .bids + .iter() + .sorted_by(orderbook::cmp_bids) + .take(orderbook_config.bids_limit.unwrap_or(usize::MAX)) + .map(|entry| orderbook::AskBidRow::from_orderbook_entry(entry, vol_prec, price_prec, &orderbook_config)) + .for_each(|row: orderbook::AskBidRow| writeln_safe_io!(writer, "{}", row)); + } + Ok(()) + } + + fn on_get_enabled_response(&self, enabled: &Mm2RpcResult) -> Result<()> { + let mut writer = self.writer.borrow_mut(); + writeln_safe_io!(writer, "{:8} {}", "Ticker", "Address"); + for row in &enabled.result { + writeln_safe_io!(writer, "{:8} {}", row.ticker, row.address); + } + Ok(()) + } + + fn on_version_response(&self, response: &MmVersionResponse) -> Result<()> { + let mut writer = self.writer.borrow_mut(); + writeln_safe_io!(writer, "Version: {}", response.result); + writeln_safe_io!(writer, "Datetime: {}", response.datetime); + Ok(()) + } + + fn on_enable_response(&self, response: &CoinInitResponse) -> Result<()> { + let mut writer = self.writer.borrow_mut(); + writeln_safe_io!( + writer, + "coin: {}\naddress: {}\nbalance: {}\nunspendable_balance: {}\nrequired_confirmations: {}\nrequires_notarization: {}", + response.coin, + response.address, + response.balance, + response.unspendable_balance, + response.required_confirmations, + if response.requires_notarization { "Yes" } else { "No" } + ); + if let Some(mature_confirmations) = response.mature_confirmations { + writeln_safe_io!(writer, "mature_confirmations: {}", mature_confirmations); + } + Ok(()) + } + + fn on_balance_response(&self, response: &BalanceResponse) -> Result<()> { + writeln_safe_io!( + self.writer.borrow_mut(), + "coin: {}\nbalance: {}\nunspendable: {}\naddress: {}", + response.coin, + response.balance, + response.unspendable_balance, + response.address + ); + Ok(()) + } + + fn on_sell_response(&self, response: &Mm2RpcResult) -> Result<()> { + writeln_safe_io!(self.writer.borrow_mut(), "Order uuid: {}", response.request.uuid); + Ok(()) + } + + fn on_buy_response(&self, response: &Mm2RpcResult) -> Result<()> { + writeln_safe_io!(self.writer.borrow_mut(), "Buy order uuid: {}", response.request.uuid); + Ok(()) + } + + fn on_stop_response(&self, response: &Mm2RpcResult) -> Result<()> { + writeln_safe_io!(self.writer.borrow_mut(), "Service stopped: {}", response.result); + Ok(()) + } +} + +struct SimpleCliTable<'a> { + key: &'a String, + value: &'a Json, +} + +impl<'a> SimpleCliTable<'a> { + fn from_pair(pair: (&'a String, &'a Json)) -> Self { + SimpleCliTable { + key: pair.0, + value: pair.1, + } + } +} diff --git a/mm2src/adex_cli/src/adex_proc/response_handler/orderbook.rs b/mm2src/adex_cli/src/adex_proc/response_handler/orderbook.rs new file mode 100644 index 0000000000..c3641f5aa5 --- /dev/null +++ b/mm2src/adex_cli/src/adex_proc/response_handler/orderbook.rs @@ -0,0 +1,181 @@ +use mm2_number::bigdecimal::ToPrimitive; +use mm2_rpc::data::legacy::{AggregatedOrderbookEntry, OrderConfirmationsSettings}; +use std::cmp::Ordering; +use std::fmt::{Display, Formatter}; + +use super::{smart_fraction_fmt::{SmartFractPrecision, SmartFractionFmt}, + OrderbookConfig}; + +pub(super) fn cmp_bids(left: &&AggregatedOrderbookEntry, right: &&AggregatedOrderbookEntry) -> Ordering { + let cmp = left.entry.price.cmp(&right.entry.price).reverse(); + if cmp.is_eq() { + return left + .entry + .base_max_volume + .base_max_volume + .cmp(&right.entry.base_max_volume.base_max_volume) + .reverse(); + } + cmp +} + +pub(super) fn cmp_asks(left: &&AggregatedOrderbookEntry, right: &&AggregatedOrderbookEntry) -> Ordering { + let cmp = left.entry.price.cmp(&right.entry.price).reverse(); + if cmp.is_eq() { + return left + .entry + .base_max_volume + .base_max_volume + .cmp(&right.entry.base_max_volume.base_max_volume); + } + cmp +} + +enum AskBidRowVal { + Value(String), + Delim, +} + +pub(super) struct AskBidRow<'a> { + volume: AskBidRowVal, + price: AskBidRowVal, + uuid: AskBidRowVal, + min_volume: AskBidRowVal, + max_volume: AskBidRowVal, + age: AskBidRowVal, + public: AskBidRowVal, + address: AskBidRowVal, + is_mine: AskBidRowVal, + conf_settings: AskBidRowVal, + config: &'a OrderbookConfig, +} + +impl<'a> AskBidRow<'a> { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + volume: &str, + price: &str, + uuid: &str, + min_volume: &str, + max_volume: &str, + age: &str, + public: &str, + address: &str, + conf_settings: &str, + config: &'a OrderbookConfig, + ) -> Self { + Self { + is_mine: AskBidRowVal::Value(String::new()), + volume: AskBidRowVal::Value(volume.to_string()), + price: AskBidRowVal::Value(price.to_string()), + uuid: AskBidRowVal::Value(uuid.to_string()), + min_volume: AskBidRowVal::Value(min_volume.to_string()), + max_volume: AskBidRowVal::Value(max_volume.to_string()), + age: AskBidRowVal::Value(age.to_string()), + public: AskBidRowVal::Value(public.to_string()), + address: AskBidRowVal::Value(address.to_string()), + conf_settings: AskBidRowVal::Value(conf_settings.to_string()), + config, + } + } + + pub(super) fn new_delimiter(config: &'a OrderbookConfig) -> Self { + Self { + is_mine: AskBidRowVal::Delim, + volume: AskBidRowVal::Delim, + price: AskBidRowVal::Delim, + uuid: AskBidRowVal::Delim, + min_volume: AskBidRowVal::Delim, + max_volume: AskBidRowVal::Delim, + age: AskBidRowVal::Delim, + public: AskBidRowVal::Delim, + address: AskBidRowVal::Delim, + conf_settings: AskBidRowVal::Delim, + config, + } + } + + pub(super) fn from_orderbook_entry( + entry: &AggregatedOrderbookEntry, + vol_prec: &SmartFractPrecision, + price_prec: &SmartFractPrecision, + config: &'a OrderbookConfig, + ) -> Self { + AskBidRow { + is_mine: AskBidRowVal::Value((if entry.entry.is_mine { "*" } else { "" }).to_string()), + volume: AskBidRowVal::Value( + SmartFractionFmt::new( + vol_prec.0, + vol_prec.1, + entry.entry.base_max_volume.base_max_volume.to_f64().unwrap(), + ) + .expect("volume smart fraction should be constructed properly") + .to_string(), + ), + price: AskBidRowVal::Value( + SmartFractionFmt::new(price_prec.0, price_prec.1, entry.entry.price.to_f64().unwrap()) + .expect("price smart fraction should be constructed properly") + .to_string(), + ), + uuid: AskBidRowVal::Value(entry.entry.uuid.to_string()), + min_volume: AskBidRowVal::Value( + SmartFractionFmt::new(vol_prec.0, vol_prec.1, entry.entry.min_volume.to_f64().unwrap()) + .expect("min_volume smart fraction should be constructed properly") + .to_string(), + ), + max_volume: AskBidRowVal::Value( + SmartFractionFmt::new(vol_prec.0, vol_prec.1, entry.entry.max_volume.to_f64().unwrap()) + .expect("max_volume smart fraction should be constructed properly") + .to_string(), + ), + age: AskBidRowVal::Value(entry.entry.age.to_string()), + public: AskBidRowVal::Value(entry.entry.pubkey.clone()), + address: AskBidRowVal::Value(entry.entry.address.clone()), + conf_settings: AskBidRowVal::Value( + entry + .entry + .conf_settings + .as_ref() + .map_or("none".to_string(), format_confirmation_settings), + ), + config, + } + } +} + +impl Display for AskBidRow<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + macro_rules! write_ask_bid_row { + ($value: expr, $width: expr, $alignment: literal) => { + if let AskBidRowVal::Value(value) = &$value { + write!(f, concat!("{:", $alignment, "width$} "), value, width = $width)?; + } else { + write!(f, "{:- { + if $config { + write_ask_bid_row!($value, $width, $alignment); + } + }; + } + write_ask_bid_row!(self.is_mine, 1, "<"); + write_ask_bid_row!(self.volume, 15, ">"); + write_ask_bid_row!(self.price, 13, "<"); + write_ask_bid_row!(self.config.uuids, self.uuid, 36, "<"); + write_ask_bid_row!(self.config.min_volume, self.min_volume, 10, "<"); + write_ask_bid_row!(self.config.max_volume, self.max_volume, 10, "<"); + write_ask_bid_row!(self.config.age, self.age, 10, "<"); + write_ask_bid_row!(self.config.publics, self.public, 66, "<"); + write_ask_bid_row!(self.config.address, self.address, 34, "<"); + write_ask_bid_row!(self.config.conf_settings, self.conf_settings, 24, "<"); + Ok(()) + } +} + +fn format_confirmation_settings(settings: &OrderConfirmationsSettings) -> String { + format!( + "{},{}:{},{}", + settings.base_confs, settings.base_nota, settings.rel_confs, settings.rel_nota + ) +} diff --git a/mm2src/adex_cli/src/adex_proc/response_handler/smart_fraction_fmt.rs b/mm2src/adex_cli/src/adex_proc/response_handler/smart_fraction_fmt.rs new file mode 100644 index 0000000000..9c7b9de415 --- /dev/null +++ b/mm2src/adex_cli/src/adex_proc/response_handler/smart_fraction_fmt.rs @@ -0,0 +1,96 @@ +use std::cell::Cell; + +pub(crate) type SmartFractPrecision = (usize, usize); + +pub(super) struct SmartFractionFmt { + precision_min: i32, + precision_max: i32, + num: Cell, +} + +#[derive(Debug)] +pub(super) enum SmartFractionTrimErr { + WrongParams, +} + +impl SmartFractionFmt { + pub(super) fn new(precision_min: usize, precision_max: usize, num: f64) -> Result { + if precision_min == 0 || precision_min > precision_max { + return Err(SmartFractionTrimErr::WrongParams); + } + Ok(Self { + precision_min: precision_min as i32, + precision_max: precision_max as i32, + num: Cell::new(num), + }) + } +} + +impl ToString for SmartFractionFmt { + fn to_string(&self) -> String { + let num = self.num.get(); + let fraction = if num == 0.0 { + 0 + } else { + let fruct_order = (num.log10() - 1.0) as i32; + let fruct_order_abs = fruct_order.abs(); + if fruct_order > 0 { + self.precision_min + } else if (self.precision_min + 1..(self.precision_max + 1)).contains(&fruct_order_abs) { + fruct_order_abs + 1 + } else if fruct_order_abs > self.precision_max { + self.precision_max + } else { + self.precision_min + } + }; + let num = (num * 10_f64.powi(fraction)).trunc() / 10_f64.powi(fraction); + format!("{0:#.1$}", num, fraction as usize) + } +} + +#[test] +fn test_construct_smart_fraction_fmt() { + assert!(SmartFractionFmt::new(0, 5, 0.0).is_err()); + assert!(SmartFractionFmt::new(5, 2, 0.0).is_err()); +} + +#[test] +fn test_smart_fraction_fmt() { + let num = SmartFractionFmt::new(2, 5, 0.0).unwrap(); + assert_eq!(num.to_string(), "0"); + let num = SmartFractionFmt::new(2, 5, 0.1).unwrap(); + assert_eq!(num.to_string(), "0.10"); + let num = SmartFractionFmt::new(2, 5, 0.19909).unwrap(); + assert_eq!(num.to_string(), "0.19"); + let num = SmartFractionFmt::new(2, 5, 0.10001).unwrap(); + assert_eq!(num.to_string(), "0.10"); + let num = SmartFractionFmt::new(2, 5, 0.10991).unwrap(); + assert_eq!(num.to_string(), "0.10"); + let num = SmartFractionFmt::new(2, 5, 0.0011991).unwrap(); + assert_eq!(num.to_string(), "0.0011"); + let num = SmartFractionFmt::new(2, 5, 0.001110000001).unwrap(); + assert_eq!(num.to_string(), "0.0011"); + let num = SmartFractionFmt::new(2, 5, 0.00001700445).unwrap(); + assert_eq!(num.to_string(), "0.000017"); + let num = SmartFractionFmt::new(2, 5, 0.00000199).unwrap(); + assert_eq!(num.to_string(), "0.00000"); + let num = SmartFractionFmt::new(2, 5, 1.0).unwrap(); + assert_eq!(num.to_string(), "1.00"); + let num = SmartFractionFmt::new(2, 5, 1.00001).unwrap(); + assert_eq!(num.to_string(), "1.00"); + let num = SmartFractionFmt::new(2, 5, 1.00000000001).unwrap(); + assert_eq!(num.to_string(), "1.00"); + let num = SmartFractionFmt::new(2, 5, 1.99001).unwrap(); + assert_eq!(num.to_string(), "1.99"); + let num = SmartFractionFmt::new(2, 5, 5000.0).unwrap(); + assert_eq!(num.to_string(), "5000.00"); + + let num = SmartFractionFmt::new(1, 5, 0.10991).unwrap(); + assert_eq!(num.to_string(), "0.1"); + + let num = SmartFractionFmt::new(2, 2, 0.001110000001).unwrap(); + assert_eq!(num.to_string(), "0.00"); + let num = SmartFractionFmt::new(2, 2, 0.101110000001).unwrap(); + assert_eq!(num.to_string(), "0.10"); +} diff --git a/mm2src/adex_cli/src/cli.rs b/mm2src/adex_cli/src/cli.rs index fdbf65161d..3ab9672a1c 100644 --- a/mm2src/adex_cli/src/cli.rs +++ b/mm2src/adex_cli/src/cli.rs @@ -1,113 +1,315 @@ -use clap::{App, Arg, SubCommand}; -use log::error; -use std::env; +use anyhow::Result; +use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum}; +use common::serde_derive::Serialize; +use mm2_number::{bigdecimal::ParseBigDecimalError, BigDecimal, MmNumber}; +use mm2_rpc::data::legacy::{MatchBy, OrderType, SellBuyRequest}; +use rpc::v1::types::H256 as H256Json; +use std::collections::HashSet; +use std::mem::take; +use std::str::FromStr; +use uuid::Uuid; +use crate::adex_config::{get_config, set_config, AdexConfig}; +use crate::adex_proc::{AdexProc, OrderbookConfig, ResponseHandler}; use crate::scenarios::{get_status, init, start_process, stop_process}; +use crate::transport::SlurpTransport; +const MM2_CONFIG_FILE_DEFAULT: &str = "MM2.json"; +const COINS_FILE_DEFAULT: &str = "coins"; +const ORDERBOOK_BIDS_LIMIT: &str = "20"; +const ORDERBOOK_ASKS_LIMIT: &str = "20"; + +#[derive(Subcommand)] enum Command { + #[command(about = "Initialize a predefined coin set and configuration to start mm2 instance with")] Init { + #[arg(long, help = "coin set file path", default_value = COINS_FILE_DEFAULT)] mm_coins_path: String, + #[arg(long, help = "mm2 configuration file path", default_value = MM2_CONFIG_FILE_DEFAULT)] mm_conf_path: String, }, + #[command(about = "Start mm2 instance")] Start { + #[arg(long, help = "mm2 configuration file path")] mm_conf_path: Option, + #[arg(long, help = "coin set file path")] mm_coins_path: Option, + #[arg(long, help = "log file path")] mm_log: Option, }, + #[command(about = "Stop mm2 using API")] Stop, + #[command(about = "Kill mm2 process")] + Kill, + #[command(about = "Get mm2 running status")] Status, + #[command(about = "Gets version of intermediary mm2 service")] + Version, + #[command(subcommand, about = "To manage rpc_password and mm2 RPC URL")] + Config(ConfigSubcommand), + #[command(about = "Puts an asset to the trading index")] + Enable { + #[arg(name = "ASSET", help = "Asset to be included into the trading index")] + asset: String, + }, + #[command(about = "Gets balance of an asset")] + Balance { + #[arg(name = "ASSET", help = "Asset to get balance of")] + asset: String, + }, + #[command(about = "Lists activated assets")] + GetEnabled, + #[command(about = "Gets orderbook")] + Orderbook { + #[command(flatten)] + orderbook_args: OrderbookCliArgs, + }, + Sell { + #[command(flatten)] + order_args: SellOrderCli, + }, + Buy { + #[command(flatten)] + order_args: BuyOrderCli, + }, } -pub fn process_cli() { - let mut app = App::new(env!("CARGO_PKG_NAME")) - .version(env!("CARGO_PKG_VERSION")) - .author(env!("CARGO_PKG_AUTHORS")) - .about(env!("CARGO_PKG_DESCRIPTION")) - .subcommand( - SubCommand::with_name("init") - .about("Initialize predefined mm2 coin set and configuration") - .arg( - Arg::with_name("mm-coins-path") - .long("mm-coins-path") - .value_name("FILE") - .help("coin set file path") - .default_value("coins"), - ) - .arg( - Arg::with_name("mm-conf-path") - .long("mm-conf-path") - .value_name("FILE") - .help("mm2 configuration file path") - .default_value("MM2.json"), - ), - ) - .subcommand( - SubCommand::with_name("start") - .about("Start mm2 service") - .arg( - Arg::with_name("mm-conf-path") - .long("mm-conf-path") - .value_name("FILE") - .help("mm2 configuration file path"), - ) - .arg( - Arg::with_name("mm-coins-path") - .long("mm-coins-path") - .value_name("FILE") - .help("coin set file path"), - ) - .arg( - Arg::with_name("mm-log") - .long("mm-log") - .value_name("FILE") - .help("log file path"), - ), - ) - .subcommand(SubCommand::with_name("stop").about("Stop mm2 instance")) - .subcommand(SubCommand::with_name("status").about("Get mm2 running status")); - - let matches = app.clone().get_matches(); - - let command = match matches.subcommand() { - ("init", Some(init_matches)) => { - let mm_coins_path = init_matches.value_of("mm-coins-path").unwrap_or("coins").to_owned(); - let mm_conf_path = init_matches.value_of("mm-conf-path").unwrap_or("MM2.json").to_owned(); +#[derive(Subcommand)] +enum ConfigSubcommand { + #[command(about = "Sets komodo adex cli configuration")] + Set(SetConfigArgs), + #[command(about = "Gets komodo adex cli configuration")] + Get, +} + +#[derive(Args)] +#[group(required = true, multiple = true)] +struct SetConfigArgs { + #[arg(long, help = "Set if you are going to set up a password")] + set_password: bool, + #[arg(long, name = "URI", help = "Adex RPC API Uri. http://localhost:7783")] + adex_uri: Option, +} + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +pub(super) struct Cli { + #[command(subcommand)] + command: Command, +} + +impl Cli { + pub(super) async fn execute( + args: impl Iterator, + config: &Cfg, + printer: &P, + ) -> Result<()> { + let transport = config.rpc_uri().map(SlurpTransport::new); + + let proc = AdexProc { + transport: transport.as_ref(), + response_handler: printer, + config, + }; + + let mut parsed_cli = Self::parse_from(args); + match &mut parsed_cli.command { Command::Init { - mm_coins_path, - mm_conf_path, - } - }, - ("start", Some(start_matches)) => { - let mm_conf_path = start_matches.value_of("mm-conf-path").map(|s| s.to_owned()); - let mm_coins_path = start_matches.value_of("mm-coins-path").map(|s| s.to_owned()); - let mm_log = start_matches.value_of("mm-log").map(|s| s.to_owned()); + mm_coins_path: coins_file, + mm_conf_path: mm2_cfg_file, + } => init(mm2_cfg_file, coins_file).await, Command::Start { - mm_conf_path, - mm_coins_path, - mm_log, - } - }, - ("stop", _) => Command::Stop, - ("status", _) => Command::Status, - _ => { - let _ = app - .print_long_help() - .map_err(|error| error!("Failed to print_long_help: {error}")); - return; - }, - }; - - match command { - Command::Init { - mm_coins_path: coins_file, - mm_conf_path: mm2_cfg_file, - } => init(&mm2_cfg_file, &coins_file), - Command::Start { - mm_conf_path: mm2_cfg_file, - mm_coins_path: coins_file, - mm_log: log_file, - } => start_process(&mm2_cfg_file, &coins_file, &log_file), - Command::Stop => stop_process(), - Command::Status => get_status(), + mm_conf_path: mm2_cfg_file, + mm_coins_path: coins_file, + mm_log: log_file, + } => start_process(mm2_cfg_file, coins_file, log_file), + Command::Version => proc.get_version().await?, + Command::Kill => stop_process(), + Command::Status => get_status(), + Command::Stop => proc.send_stop().await?, + Command::Config(ConfigSubcommand::Set(SetConfigArgs { set_password, adex_uri })) => { + set_config(*set_password, adex_uri.take())? + }, + Command::Config(ConfigSubcommand::Get) => get_config(), + Command::Enable { asset } => proc.enable(asset).await?, + Command::Balance { asset } => proc.get_balance(asset).await?, + Command::GetEnabled => proc.get_enabled().await?, + Command::Orderbook { ref orderbook_args } => { + proc.get_orderbook( + &orderbook_args.base, + &orderbook_args.rel, + OrderbookConfig::from(orderbook_args), + ) + .await? + }, + Command::Sell { + order_args: SellOrderCli { order_cli }, + } => proc.sell(SellBuyRequest::from(order_cli)).await?, + Command::Buy { + order_args: BuyOrderCli { order_cli }, + } => proc.buy(SellBuyRequest::from(order_cli)).await?, + } + Ok(()) + } +} + +#[derive(Args)] +#[command(about = "Puts a selling coins request")] +struct SellOrderCli { + #[command(flatten)] + order_cli: OrderCli, +} + +#[derive(Args)] +#[command(about = "Puts a buying coins request")] +struct BuyOrderCli { + #[command(flatten)] + order_cli: OrderCli, +} + +#[derive(Args, Serialize, Debug)] +struct OrderbookCliArgs { + #[arg(help = "Base currency of a pair")] + base: String, + #[arg(help = "Related currency, also can be called \"quote currency\" according to exchange terms")] + rel: String, + #[arg(long, help = "Orderbook asks count limitation", default_value = ORDERBOOK_ASKS_LIMIT)] + asks_limit: Option, + #[arg(long, help = "Orderbook bids count limitation", default_value = ORDERBOOK_BIDS_LIMIT)] + bids_limit: Option, + #[arg(long, help = "Enables `uuid` column")] + uuids: bool, + #[arg(long, help = "Enables `min_volume` column")] + min_volume: bool, + #[arg(long, help = "Enables `max_volume` column")] + max_volume: bool, + #[arg(long, help = "Enables `public` column")] + publics: bool, + #[arg(long, help = "Enables `address` column")] + address: bool, + #[arg(long, help = "Enables `age` column")] + age: bool, + #[arg(long, help = "Enables order confirmation settings column")] + conf_settings: bool, +} + +impl From<&OrderbookCliArgs> for OrderbookConfig { + fn from(value: &OrderbookCliArgs) -> Self { + OrderbookConfig { + uuids: value.uuids, + min_volume: value.min_volume, + max_volume: value.max_volume, + publics: value.publics, + address: value.address, + age: value.age, + conf_settings: value.conf_settings, + asks_limit: value.asks_limit, + bids_limit: value.bids_limit, + } + } +} + +#[derive(Args, Serialize, Debug)] +struct OrderCli { + #[arg(help = "Base currency of a pair")] + base: String, + #[arg(help = "Related currency")] + rel: String, + #[arg(help = "Amount of coins the user is willing to sell/buy of the base coin", value_parser=parse_mm_number )] + volume: MmNumber, + #[arg(help = "Price in rel the user is willing to receive/pay per one unit of the base coin", value_parser=parse_mm_number)] + price: MmNumber, + #[arg(long, value_enum, default_value_t = OrderTypeCli::GoodTillCancelled, help="The GoodTillCancelled order is automatically converted to a maker order if not matched in 30 seconds, and this maker order stays in the orderbook until explicitly cancelled. On the other hand, a FillOrKill is cancelled if not matched within 30 seconds")] + order_type: OrderTypeCli, + #[arg(long, + help = "Amount of base coin that will be used as min_volume of GoodTillCancelled order after conversion to maker", + value_parser=parse_mm_number + )] + min_volume: Option, + #[arg(short='u', long="uuid", action = ArgAction::Append, help="The created order is matched using a set of uuid")] + match_uuids: Vec, + #[arg(short='p', + long="public", + value_parser=H256Json::from_str, + action = ArgAction::Append, + help="The created order is matched using a set of publics to select specific nodes (ignored if uuids not empty)")] + match_publics: Vec, + #[arg( + long, + help = "Number of required blockchain confirmations for base coin atomic swap transaction" + )] + base_confs: Option, + #[arg( + long, + help = "Whether dPoW notarization is required for base coin atomic swap transaction" + )] + base_nota: Option, + #[arg( + long, + help = "Number of required blockchain confirmations for rel coin atomic swap transaction" + )] + rel_confs: Option, + #[arg( + long, + help = "Whether dPoW notarization is required for rel coin atomic swap transaction" + )] + rel_nota: Option, + #[arg( + long, + help = "If true, each order's short record history is stored else the only order status will be temporarily stored while in progress" + )] + save_in_history: bool, +} + +fn parse_mm_number(value: &str) -> Result { + let decimal: BigDecimal = BigDecimal::from_str(value)?; + Ok(MmNumber::from(decimal)) +} + +#[derive(Debug, Copy, Clone, ValueEnum, Serialize)] +enum OrderTypeCli { + FillOrKill, + GoodTillCancelled, +} + +impl From for OrderType { + fn from(value: OrderTypeCli) -> Self { + match value { + OrderTypeCli::GoodTillCancelled => OrderType::GoodTillCancelled, + OrderTypeCli::FillOrKill => OrderType::FillOrKill, + } + } +} + +impl From<&mut OrderCli> for SellBuyRequest { + fn from(value: &mut OrderCli) -> Self { + let match_by = if !value.match_uuids.is_empty() { + MatchBy::Orders(HashSet::from_iter(value.match_uuids.drain(..))) + } else if !value.match_publics.is_empty() { + MatchBy::Pubkeys(HashSet::from_iter(value.match_publics.drain(..))) + } else { + MatchBy::Any + }; + + let will_be_substituted = String::new(); + SellBuyRequest { + base: take(&mut value.base), + rel: take(&mut value.rel), + price: take(&mut value.price), + volume: take(&mut value.volume), + timeout: None, + duration: None, + method: will_be_substituted, + gui: None, + dest_pub_key: H256Json::default(), + match_by, + order_type: value.order_type.into(), + base_confs: value.base_confs, + base_nota: value.base_nota, + rel_confs: value.rel_confs, + rel_nota: value.rel_nota, + min_volume: take(&mut value.min_volume), + save_in_history: value.save_in_history, + } } } diff --git a/mm2src/adex_cli/src/helpers.rs b/mm2src/adex_cli/src/helpers.rs new file mode 100644 index 0000000000..a6c93a7774 --- /dev/null +++ b/mm2src/adex_cli/src/helpers.rs @@ -0,0 +1,43 @@ +use anyhow::{anyhow, Result}; +use common::log::error; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::io::Write; +use std::ops::Deref; +use std::path::Path; + +use crate::error_anyhow; + +pub(crate) fn rewrite_data_file(data: T, file: &str) -> Result<()> +where + T: Deref, +{ + let mut writer = fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(file) + .map_err(|error| error_anyhow!("Failed to open {file}: {error}"))?; + + writer + .write(&data) + .map_err(|error| error_anyhow!("Failed to write data into {file}: {error}"))?; + Ok(()) +} + +pub(crate) fn rewrite_json_file(value: &T, file: &str) -> Result<()> +where + T: Serialize, +{ + let data = serde_json::to_vec_pretty(value).map_err(|error| error_anyhow!("Failed to serialize data {error}"))?; + rewrite_data_file(data, file) +} + +pub(crate) fn read_json_file(file: &Path) -> Result +where + T: for<'a> Deserialize<'a>, +{ + let reader = fs::File::open(file).map_err(|error| error_anyhow!("Failed to open {file:?}, error: {error}"))?; + serde_json::from_reader(reader) + .map_err(|error| error_anyhow!("Failed to read json from data: {file:?}, error: {error}")) +} diff --git a/mm2src/adex_cli/src/log.rs b/mm2src/adex_cli/src/log.rs deleted file mode 100644 index 60ffb13180..0000000000 --- a/mm2src/adex_cli/src/log.rs +++ /dev/null @@ -1,13 +0,0 @@ -use log::LevelFilter; -use std::io::Write; - -pub fn init_logging() { - let mut builder = env_logger::builder(); - let level = std::env::var("RUST_LOG") - .map(|s| s.parse().expect("Failed to parse RUST_LOG")) - .unwrap_or(LevelFilter::Info); - builder - .filter_level(level) - .format(|buf, record| writeln!(buf, "{}", record.args())); - builder.init(); -} diff --git a/mm2src/adex_cli/src/logging.rs b/mm2src/adex_cli/src/logging.rs new file mode 100644 index 0000000000..d02c6d6a31 --- /dev/null +++ b/mm2src/adex_cli/src/logging.rs @@ -0,0 +1,27 @@ +#[cfg(not(any(test, target_arch = "wasm32")))] +use log::LevelFilter; +#[cfg(not(any(test, target_arch = "wasm32")))] +use std::io::Write; + +#[cfg(not(any(test, target_arch = "wasm32")))] +pub(super) fn init_logging() { + let mut builder = env_logger::builder(); + let level = std::env::var("RUST_LOG") + .map(|s| s.parse().expect("Failed to parse RUST_LOG")) + .unwrap_or(LevelFilter::Info); + builder + .filter_level(level) + .format(|buf, record| writeln!(buf, "{}", record.args())); + builder.init(); +} + +#[macro_export] +macro_rules! error_anyhow { ($($arg: expr),*) => { { error!($($arg),*); anyhow!("") } } } +#[macro_export] +macro_rules! warn_anyhow { ($($arg: expr),*) => { { warn!($($arg),*); anyhow!("") } } } +#[macro_export] +macro_rules! error_bail { ($($arg: expr),*) => { { error!($($arg),*); bail!("") } } } +#[macro_export] +macro_rules! warn_bail { ($($arg: expr),*) => { { warn!($($arg),*); bail!("") } } } + +pub(super) use {error_anyhow, error_bail, warn_bail}; diff --git a/mm2src/adex_cli/src/main.rs b/mm2src/adex_cli/src/main.rs index eb80a3b868..613597cb57 100644 --- a/mm2src/adex_cli/src/main.rs +++ b/mm2src/adex_cli/src/main.rs @@ -1,12 +1,21 @@ +#[cfg(not(target_arch = "wasm32"))] mod activation_scheme_db; +#[cfg(not(any(test, target_arch = "wasm32")))] mod adex_app; +#[cfg(not(target_arch = "wasm32"))] mod adex_config; +#[cfg(not(target_arch = "wasm32"))] mod adex_proc; #[cfg(not(target_arch = "wasm32"))] mod cli; -#[cfg(not(target_arch = "wasm32"))] mod log; +#[cfg(not(target_arch = "wasm32"))] mod helpers; +mod logging; #[cfg(not(target_arch = "wasm32"))] mod scenarios; +#[cfg(all(not(target_arch = "wasm32"), test))] mod tests; +#[cfg(not(target_arch = "wasm32"))] mod transport; #[cfg(target_arch = "wasm32")] fn main() {} -#[cfg(not(target_arch = "wasm32"))] -fn main() { - log::init_logging(); - cli::process_cli(); +#[cfg(not(any(test, target_arch = "wasm32")))] +#[tokio::main(flavor = "current_thread")] +async fn main() { + logging::init_logging(); + let app = adex_app::AdexApp::new(); + app.execute().await; } diff --git a/mm2src/adex_cli/src/scenarios/helpers.rs b/mm2src/adex_cli/src/scenarios/helpers.rs deleted file mode 100644 index 2282bf4b36..0000000000 --- a/mm2src/adex_cli/src/scenarios/helpers.rs +++ /dev/null @@ -1,34 +0,0 @@ -use common::log::error; -use serde::Serialize; -use std::fs::OpenOptions; -use std::io::Write; -use std::ops::Deref; - -pub fn rewrite_data_file(data: T, file: &str) -> Result<(), ()> -where - T: Deref, -{ - let mut writer = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(file) - .map_err(|error| { - error!("Failed to open {file}: {error}"); - })?; - - writer.write(&data).map_err(|error| { - error!("Failed to write data into {file}: {error}"); - })?; - Ok(()) -} - -pub fn rewrite_json_file(value: &T, file: &str) -> Result<(), ()> -where - T: Serialize, -{ - let data = serde_json::to_vec_pretty(value).map_err(|error| { - error!("Failed to serialize data {error}"); - })?; - rewrite_data_file(data, file) -} diff --git a/mm2src/adex_cli/src/scenarios/init_coins.rs b/mm2src/adex_cli/src/scenarios/init_coins.rs index fefcdd7c66..4b9c6ba79a 100644 --- a/mm2src/adex_cli/src/scenarios/init_coins.rs +++ b/mm2src/adex_cli/src/scenarios/init_coins.rs @@ -1,45 +1,19 @@ +use anyhow::{anyhow, Result}; use common::log::{error, info}; -use derive_more::Display; use mm2_net::transport::slurp_url; -use super::helpers::rewrite_data_file; +use crate::helpers::rewrite_data_file; +use crate::logging::error_anyhow; -#[derive(Clone, Copy, Debug, Display)] -pub enum CoinSet { - Empty, - Full, -} +const FULL_COIN_SET_ADDRESS: &str = "https://raw.githubusercontent.com/KomodoPlatform/coins/master/coins"; -#[tokio::main(flavor = "current_thread")] -pub async fn init_coins(coins_file: &str) -> Result<(), ()> { - const FULL_COIN_SET_ADDRESS: &str = "https://raw.githubusercontent.com/KomodoPlatform/coins/master/coins"; - const EMPTY_COIN_SET_DATA: &[u8] = b"[]\n"; - let coin_set = inquire_coin_set(coins_file)?; - info!("Start getting mm2 coins"); - let coins_data = match coin_set { - CoinSet::Empty => Vec::::from(EMPTY_COIN_SET_DATA), - CoinSet::Full => { - info!("Getting coin set from: {FULL_COIN_SET_ADDRESS}"); - let (_status_code, _headers, data) = slurp_url(FULL_COIN_SET_ADDRESS).await.map_err(|error| { - error!("Failed to get coin set from: {FULL_COIN_SET_ADDRESS}, error: {error}"); - })?; - data - }, - }; +pub(crate) async fn init_coins(coins_file: &str) -> Result<()> { + info!("Getting coin set from: {FULL_COIN_SET_ADDRESS}"); + let (_status_code, _headers, coins_data) = slurp_url(FULL_COIN_SET_ADDRESS) + .await + .map_err(|error| error_anyhow!("Failed to get coin set from: {FULL_COIN_SET_ADDRESS}, error: {error}"))?; rewrite_data_file(coins_data, coins_file)?; info!("Got coins data, written into: {coins_file}"); Ok(()) } - -fn inquire_coin_set(coins_file: &str) -> Result { - inquire::Select::new( - format!("Select one of predefined coin sets to save into: {coins_file}").as_str(), - vec![CoinSet::Empty, CoinSet::Full], - ) - .with_help_message("Information about the currencies: their ticker symbols, names, ports, addresses, etc.") - .prompt() - .map_err(|error| { - error!("Failed to select coin_set: {error}"); - }) -} diff --git a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs index 551cf21598..b21c3f292b 100644 --- a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs +++ b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs @@ -1,4 +1,7 @@ +use anyhow::{anyhow, Result}; use bip39::{Language, Mnemonic, MnemonicType}; +use common::log::{error, info}; +use common::password_policy; use inquire::{validator::Validation, Confirm, CustomType, CustomUserError, Text}; use passwords::PasswordGenerator; use serde::Serialize; @@ -6,11 +9,10 @@ use std::net::Ipv4Addr; use std::ops::Not; use std::path::Path; -use super::helpers; use super::inquire_extentions::{InquireOption, DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER, DEFAULT_OPTION_BOOL_FORMATTER, OPTION_BOOL_PARSER}; -use common::log::{error, info}; -use common::password_policy; +use crate::helpers; +use crate::logging::error_anyhow; const DEFAULT_NET_ID: u16 = 7777; const DEFAULT_GID: &str = "adex-cli"; @@ -18,7 +20,7 @@ const DEFAULT_OPTION_PLACEHOLDER: &str = "Tap enter to skip"; const RPC_PORT_MIN: u16 = 1024; const RPC_PORT_MAX: u16 = 49151; -pub fn init_mm2_cfg(cfg_file: &str) -> Result<(), ()> { +pub(crate) fn init_mm2_cfg(cfg_file: &str) -> Result<()> { let mut mm2_cfg = Mm2Cfg::new(); info!("Start collecting mm2_cfg into: {cfg_file}"); mm2_cfg.inquire()?; @@ -29,31 +31,31 @@ pub fn init_mm2_cfg(cfg_file: &str) -> Result<(), ()> { } #[derive(Serialize)] -pub struct Mm2Cfg { - pub gui: Option, - pub netid: Option, - pub rpc_password: Option, +struct Mm2Cfg { + gui: Option, + netid: Option, + rpc_password: Option, #[serde(rename = "passphrase", skip_serializing_if = "Option::is_none")] - pub seed_phrase: Option, - pub allow_weak_password: Option, + seed_phrase: Option, + allow_weak_password: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub dbdir: Option, + dbdir: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub rpcip: Option, + rpcip: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub rpcport: Option, + rpcport: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub rpc_local_only: Option, + rpc_local_only: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub i_am_seed: Option, + i_am_seed: Option, #[serde(skip_serializing_if = "Vec::::is_empty")] - pub seednodes: Vec, + seednodes: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub hd_account_id: Option, + hd_account_id: Option, } impl Mm2Cfg { - pub fn new() -> Mm2Cfg { + fn new() -> Mm2Cfg { Mm2Cfg { gui: None, netid: None, @@ -70,7 +72,7 @@ impl Mm2Cfg { } } - fn inquire(&mut self) -> Result<(), ()> { + fn inquire(&mut self) -> Result<()> { self.inquire_gui()?; self.inquire_net_id()?; self.inquire_seed_phrase()?; @@ -87,7 +89,7 @@ impl Mm2Cfg { } #[inline] - fn inquire_dbdir(&mut self) -> Result<(), ()> { + fn inquire_dbdir(&mut self) -> Result<()> { let is_reachable_dir = |dbdir: &InquireOption| -> Result { match dbdir { InquireOption::None => Ok(Validation::Valid), @@ -108,35 +110,35 @@ impl Mm2Cfg { .with_help_message("AtomicDEX API database path. Optional, defaults to a subfolder named DB in the path of your mm2 binary") .with_validator(is_reachable_dir) .prompt() - .map_err(|error| { - error!("Failed to get dbdir: {error}"); - })?.into(); + .map_err(|error| + error_anyhow!("Failed to get dbdir: {error}") + )?.into(); Ok(()) } #[inline] - fn inquire_gui(&mut self) -> Result<(), ()> { + fn inquire_gui(&mut self) -> Result<()> { self.gui = Some(DEFAULT_GID.into()); info!("> gui is set by default: {DEFAULT_GID}"); Ok(()) } #[inline] - fn inquire_net_id(&mut self) -> Result<(), ()> { + fn inquire_net_id(&mut self) -> Result<()> { self.netid = CustomType::::new("What is the network `mm2` is going to be a part, netid:") .with_default(DEFAULT_NET_ID) .with_help_message(r#"Network ID number, telling the AtomicDEX API which network to join. 7777 is the current main network, though alternative netids can be used for testing or "private" trades"#) .with_placeholder(format!("{DEFAULT_NET_ID}").as_str()) .prompt() - .map_err(|error| { - error!("Failed to get netid: {error}"); - })?.into(); + .map_err(|error| + error_anyhow!("Failed to get netid: {error}") + )?.into(); Ok(()) } #[inline] - fn inquire_seed_phrase(&mut self) -> Result<(), ()> { + fn inquire_seed_phrase(&mut self) -> Result<()> { let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); let default_password: &str = mnemonic.phrase(); self.seed_phrase = Text::new("What is the seed phrase:") @@ -156,16 +158,14 @@ impl Mm2Cfg { Your passphrase; this is the source of each of your coins' private keys. KEEP IT SAFE!", ) .prompt() - .map_err(|error| { - error!("Failed to get passphrase: {error}"); - }) + .map_err(|error| error_anyhow!("Failed to get passphrase: {error}")) .map(|value| if "none" == value { None } else { Some(value) })?; Ok(()) } #[inline] - fn inquire_rpc_password(&mut self) -> Result<(), ()> { + fn inquire_rpc_password(&mut self) -> Result<()> { let allow_weak_password = self.allow_weak_password; let validator = move |password: &str| { if let Some(false) = allow_weak_password { @@ -185,14 +185,12 @@ impl Mm2Cfg { .with_default(default_password.as_str()) .with_placeholder(default_password.as_str()) .prompt() - .map_err(|error| { - error!("Failed to get rpc_password: {error}"); - })? + .map_err(|error| error_anyhow!("Failed to get rpc_password: {error}"))? .into(); Ok(()) } - fn generate_password() -> Result { + fn generate_password() -> Result { let pg = PasswordGenerator { length: 8, numbers: true, @@ -207,40 +205,38 @@ impl Mm2Cfg { while password_policy::password_policy(&password).is_err() { password = pg .generate_one() - .map_err(|error| error!("Failed to generate password: {error}"))?; + .map_err(|error| error_anyhow!("Failed to generate password: {error}"))?; } Ok(password) } #[inline] - fn inquire_allow_weak_password(&mut self) -> Result<(), ()> { + fn inquire_allow_weak_password(&mut self) -> Result<()> { self.allow_weak_password = Confirm::new("Allow weak password:") .with_default(false) .with_placeholder("No") .with_help_message(r#"If true, will allow low entropy rpc_password. If false rpc_password must not have 3 of the same characters in a row, must be at least 8 characters long, must contain at least one of each of the following: numeric, uppercase, lowercase, special character (e.g. !#$*). It also can not contain the word "password", or the chars <, >, and &. Defaults to false."#) .prompt() - .map_err(|error| { - error!("Failed to get allow_weak_password: {error}"); - })? + .map_err(|error| + error_anyhow!("Failed to get allow_weak_password: {error}") + )? .into(); Ok(()) } #[inline] - fn inquire_rpcip(&mut self) -> Result<(), ()> { + fn inquire_rpcip(&mut self) -> Result<()> { self.rpcip = CustomType::>::new("What is rpcip:") .with_placeholder(DEFAULT_OPTION_PLACEHOLDER) .with_help_message("IP address to bind to for RPC server. Optional, defaults to 127.0.0.1") .prompt() - .map_err(|error| { - error!("Failed to get rpcip: {error}"); - })? + .map_err(|error| error_anyhow!("Failed to get rpcip: {error}"))? .into(); Ok(()) } #[inline] - fn inquire_rpcport(&mut self) -> Result<(), ()> { + fn inquire_rpcport(&mut self) -> Result<()> { let validator = |value: &InquireOption| -> Result { match value { InquireOption::None => Ok(Validation::Valid), @@ -260,15 +256,13 @@ impl Mm2Cfg { .with_validator(validator) .with_placeholder(DEFAULT_OPTION_PLACEHOLDER) .prompt() - .map_err(|error| { - error!("Failed to get rpcport: {error}"); - })? + .map_err(|error| error_anyhow!("Failed to get rpcport: {error}"))? .into(); Ok(()) } #[inline] - fn inquire_rpc_local_only(&mut self) -> Result<(), ()> { + fn inquire_rpc_local_only(&mut self) -> Result<()> { self.rpc_local_only = CustomType::>::new("What is rpc_local_only:") .with_parser(OPTION_BOOL_PARSER) .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) @@ -276,14 +270,14 @@ impl Mm2Cfg { .with_default(InquireOption::None) .with_help_message("If false the AtomicDEX API will allow rpc methods sent from external IP addresses. Optional, defaults to true. Warning: Only use this if you know what you are doing, and have put the appropriate security measures in place.") .prompt() - .map_err(|error| { - error!("Failed to get rpc_local_only: {error}"); - })?.into(); + .map_err(|error| + error_anyhow!("Failed to get rpc_local_only: {error}") + )?.into(); Ok(()) } #[inline] - fn inquire_i_am_a_seed(&mut self) -> Result<(), ()> { + fn inquire_i_am_a_seed(&mut self) -> Result<()> { self.i_am_seed = CustomType::>::new("What is i_am_a_seed:") .with_parser(OPTION_BOOL_PARSER) .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) @@ -291,23 +285,23 @@ impl Mm2Cfg { .with_default(InquireOption::None) .with_help_message("Runs AtomicDEX API as a seed node mode (acting as a relay for AtomicDEX API clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (7777) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other AtomicDEX API clients using the same netID.") .prompt() - .map_err(|error| { - error!("Failed to get i_am_a_seed: {error}"); - })?.into(); + .map_err(|error| + error_anyhow!("Failed to get i_am_a_seed: {error}") + )?.into(); Ok(()) } #[inline] - fn inquire_seednodes(&mut self) -> Result<(), ()> { + fn inquire_seednodes(&mut self) -> Result<()> { info!("Reading seed nodes until tap enter is met"); loop { let seednode: Option = CustomType::>::new("What is the next seednode:") .with_help_message("Optional. If operating on a test or private netID, the IP address of at least one seed node is required (on the main network, these are already hardcoded)") .with_placeholder(DEFAULT_OPTION_PLACEHOLDER) .prompt() - .map_err(|error| { - error!("Failed to get seed node: {error}"); - })?.into(); + .map_err(|error| + error_anyhow!("Failed to get seed node: {error}") + )?.into(); let Some(seednode) = seednode else { break; }; @@ -317,14 +311,14 @@ impl Mm2Cfg { } #[inline] - fn inquire_hd_account_id(&mut self) -> Result<(), ()> { + fn inquire_hd_account_id(&mut self) -> Result<()> { self.hd_account_id = CustomType::>::new("What is hd_account_id:") .with_help_message(r#"Optional. If this value is set, the AtomicDEX-API will work in only the HD derivation mode, coins will need to have a coin derivation path entry in the coins file for activation. The hd_account_id value effectively takes its place in the full derivation as follows: m/44'/COIN_ID'/'/CHAIN/ADDRESS_ID"#) .with_placeholder(DEFAULT_OPTION_PLACEHOLDER) .prompt() - .map_err(|error| { - error!("Failed to get hd_account_id: {error}"); - })? + .map_err(|error| + error_anyhow!("Failed to get hd_account_id: {}", error) + )? .into(); Ok(()) } diff --git a/mm2src/adex_cli/src/scenarios/inquire_extentions.rs b/mm2src/adex_cli/src/scenarios/inquire_extentions.rs index 9416fe01a5..e1d40d7a6b 100644 --- a/mm2src/adex_cli/src/scenarios/inquire_extentions.rs +++ b/mm2src/adex_cli/src/scenarios/inquire_extentions.rs @@ -2,7 +2,7 @@ use inquire::parser::DEFAULT_BOOL_PARSER; use std::str::FromStr; #[derive(Clone)] -pub enum InquireOption { +pub(super) enum InquireOption { Some(T), None, } @@ -40,8 +40,8 @@ impl ToString for InquireOption { } } -pub type OptionBoolFormatter<'a> = &'a dyn Fn(OptionalConfirm) -> String; -pub const DEFAULT_OPTION_BOOL_FORMATTER: OptionBoolFormatter = &|ans| -> String { +type OptionBoolFormatter<'a> = &'a dyn Fn(OptionalConfirm) -> String; +pub(super) const DEFAULT_OPTION_BOOL_FORMATTER: OptionBoolFormatter = &|ans| -> String { match ans { InquireOption::None => String::new(), InquireOption::Some(true) => String::from("yes"), @@ -49,15 +49,15 @@ pub const DEFAULT_OPTION_BOOL_FORMATTER: OptionBoolFormatter = &|ans| -> String } }; -pub type OptionBoolParser<'a> = &'a dyn Fn(&str) -> Result, ()>; -pub const OPTION_BOOL_PARSER: OptionBoolParser = &|ans: &str| -> Result, ()> { +type OptionBoolParser<'a> = &'a dyn Fn(&str) -> Result, ()>; +pub(super) const OPTION_BOOL_PARSER: OptionBoolParser = &|ans: &str| -> Result, ()> { if ans.is_empty() { return Ok(InquireOption::None); } DEFAULT_BOOL_PARSER(ans).map(InquireOption::Some) }; -pub const DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER: OptionBoolFormatter = &|ans: InquireOption| match ans { +pub(super) const DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER: OptionBoolFormatter = &|ans: InquireOption| match ans { InquireOption::None => String::from("Tap enter to skip/yes/no"), InquireOption::Some(true) => String::from("none/Yes/no"), InquireOption::Some(false) => String::from("none/yes/No"), diff --git a/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs b/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs index 73a3460bbc..36a864b5f8 100644 --- a/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs +++ b/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs @@ -1,31 +1,34 @@ +use anyhow::{anyhow, Result}; use common::log::{error, info}; use std::env; use std::path::PathBuf; +use crate::error_anyhow; + #[cfg(not(target_os = "macos"))] -pub use sysinfo::{PidExt, ProcessExt, System, SystemExt}; +use sysinfo::{PidExt, ProcessExt, System, SystemExt}; #[cfg(windows)] mod reexport { - pub use std::ffi::CString; - pub use std::mem; - pub use std::mem::size_of; - pub use std::ptr::null; - pub use std::u32; - pub use winapi::um::processthreadsapi::{CreateProcessA, OpenProcess, TerminateProcess, PROCESS_INFORMATION, - STARTUPINFOA}; - pub use winapi::um::winnt::{PROCESS_TERMINATE, SYNCHRONIZE}; - - pub const MM2_BINARY: &str = "mm2.exe"; + pub(super) use std::ffi::CString; + pub(super) use std::mem; + pub(super) use std::mem::size_of; + pub(super) use std::ptr::null; + pub(super) use std::u32; + pub(super) use winapi::um::processthreadsapi::{CreateProcessA, OpenProcess, TerminateProcess, PROCESS_INFORMATION, + STARTUPINFOA}; + pub(super) use winapi::um::winnt::{PROCESS_TERMINATE, SYNCHRONIZE}; + + pub(super) const MM2_BINARY: &str = "mm2.exe"; } #[cfg(windows)] use reexport::*; #[cfg(all(unix, not(target_os = "macos")))] mod unix_not_macos_reexport { - pub use std::process::{Command, Stdio}; + pub(super) use std::process::{Command, Stdio}; - pub const KILL_CMD: &str = "kill"; + pub(super) const KILL_CMD: &str = "kill"; } #[cfg(all(unix, not(target_os = "macos")))] @@ -33,26 +36,26 @@ use unix_not_macos_reexport::*; #[cfg(unix)] mod unix_reexport { - pub const MM2_BINARY: &str = "mm2"; + pub(super) const MM2_BINARY: &str = "mm2"; } #[cfg(unix)] use unix_reexport::*; #[cfg(target_os = "macos")] mod macos_reexport { - pub use std::fs; - pub const LAUNCH_CTL_COOL_DOWN_TIMEOUT_MS: u64 = 500; - pub use common::log::debug; - pub use std::process::{Command, Stdio}; - pub use std::thread::sleep; - pub use std::time::Duration; - pub const LAUNCHCTL_MM2_ID: &str = "com.mm2.daemon"; + pub(super) use std::fs; + pub(super) const LAUNCH_CTL_COOL_DOWN_TIMEOUT_MS: u64 = 500; + pub(super) use std::process::Command; + pub(super) use std::thread::sleep; + pub(super) use std::time::Duration; + pub(super) use sysinfo::{ProcessExt, System, SystemExt}; + pub(super) const LAUNCHCTL_MM2_ID: &str = "com.komodoproject.mm2"; } #[cfg(target_os = "macos")] use macos_reexport::*; #[cfg(not(target_os = "macos"))] -pub fn get_status() { +pub(crate) fn get_status() { let pids = find_proc_by_name(MM2_BINARY); if pids.is_empty() { info!("Process not found: {MM2_BINARY}"); @@ -73,17 +76,15 @@ fn find_proc_by_name(pname: &'_ str) -> Vec { .collect() } -fn get_mm2_binary_path() -> Result { - let mut dir = env::current_exe().map_err(|error| { - error!("Failed to get current binary dir: {error}"); - })?; +fn get_mm2_binary_path() -> Result { + let mut dir = env::current_exe().map_err(|error| error_anyhow!("Failed to get current binary dir: {error}"))?; dir.pop(); dir.push(MM2_BINARY); Ok(dir) } #[cfg(not(target_os = "macos"))] -pub fn start_process(mm2_cfg_file: &Option, coins_file: &Option, log_file: &Option) { +pub(crate) fn start_process(mm2_cfg_file: &Option, coins_file: &Option, log_file: &Option) { if let Some(mm2_cfg_file) = mm2_cfg_file { info!("Set env MM_CONF_PATH as: {mm2_cfg_file}"); env::set_var("MM_CONF_PATH", mm2_cfg_file); @@ -106,7 +107,7 @@ pub fn start_process(mm2_cfg_file: &Option, coins_file: &Option, } #[cfg(all(unix, not(target_os = "macos")))] -pub fn start_process_impl(mm2_binary: PathBuf) { +fn start_process_impl(mm2_binary: PathBuf) { let mut command = Command::new(&mm2_binary); let file_name = mm2_binary.file_name().expect("No file_name in mm2_binary"); let process = match command.stdout(Stdio::null()).stdout(Stdio::null()).spawn() { @@ -122,7 +123,7 @@ pub fn start_process_impl(mm2_binary: PathBuf) { } #[cfg(windows)] -pub fn start_process_impl(mm2_binary: PathBuf) { +fn start_process_impl(mm2_binary: PathBuf) { let Some(program) = mm2_binary.to_str() else { error!("Failed to cast mm2_binary to &str"); return; @@ -161,7 +162,7 @@ pub fn start_process_impl(mm2_binary: PathBuf) { } #[cfg(all(unix, not(target_os = "macos")))] -pub fn stop_process() { +pub(crate) fn stop_process() { let pids = find_proc_by_name(MM2_BINARY); if pids.is_empty() { info!("Process not found: {MM2_BINARY}"); @@ -186,7 +187,7 @@ pub fn stop_process() { } #[cfg(windows)] -pub fn stop_process() { +pub(crate) fn stop_process() { let processes = find_proc_by_name(MM2_BINARY); for pid in processes { info!("Terminate process: {}", pid); @@ -198,7 +199,7 @@ pub fn stop_process() { } #[cfg(target_os = "macos")] -pub fn start_process(mm2_cfg_file: &Option, coins_file: &Option, log_file: &Option) { +pub(crate) fn start_process(mm2_cfg_file: &Option, coins_file: &Option, log_file: &Option) { let Ok(mm2_binary) = get_mm2_binary_path() else { return; }; let Ok(current_dir) = env::current_dir() else { @@ -207,7 +208,6 @@ pub fn start_process(mm2_cfg_file: &Option, coins_file: &Option, }; let Ok(plist_path) = get_plist_path() else {return;}; - let plist = format!( r#" @@ -215,6 +215,11 @@ pub fn start_process(mm2_cfg_file: &Option, coins_file: &Option, Label {} + LimitLoadToSessionType + + Aqua + Background + ProgramArguments {} @@ -251,33 +256,39 @@ pub fn start_process(mm2_cfg_file: &Option, coins_file: &Option, return; } + let Ok(uid) = get_proc_uid() else { return }; match Command::new("launchctl") - .arg("enable") - .arg(format!("system/{LAUNCHCTL_MM2_ID}").as_str()) + .arg("bootstrap") + .arg(format!("user/{}", uid).as_str()) + .arg(&plist_path) .spawn() { - Ok(_) => debug!("Successfully enabled using launchctl, label: {LAUNCHCTL_MM2_ID}"), - Err(error) => error!("Failed to enable process: {error}"), + Ok(_) => info!( + "Successfully bootstraped launchctl: user/{} {}", + uid, + plist_path.display() + ), + Err(error) => error!("Failed to bootstrap process: {error}"), } - match Command::new("launchctl").arg("load").arg(&plist_path).spawn() { - Ok(_) => debug!("Successfully loaded using launchctl, label: {LAUNCHCTL_MM2_ID}"), - Err(error) => error!("Failed to load process: {error}"), - } - - match Command::new("launchctl").args(["start", LAUNCHCTL_MM2_ID]).spawn() { - Ok(_) => info!("Successfully started using launchctl, label: {LAUNCHCTL_MM2_ID}"), - Err(error) => error!("Failed to start process: {error}"), + match Command::new("launchctl") + .arg("kickstart") + .arg("-k") + .arg("-p") + .arg(format!("user/{}/{}", uid, LAUNCHCTL_MM2_ID).as_str()) + .spawn() + { + Ok(_) => info!("Successfully kickstarted launchctl: user/{}/{}", uid, LAUNCHCTL_MM2_ID), + Err(error) => error!("Failed to kickstart process: {error}"), } } #[cfg(target_os = "macos")] -fn get_plist_path() -> Result { +fn get_plist_path() -> Result { match env::current_dir() { - Err(error) => { - error!("Failed to get current_dir to construct plist_path: {error}"); - Err(()) - }, + Err(error) => Err(error_anyhow!( + "Failed to get current_dir to construct plist_path: {error}" + )), Ok(mut current_dir) => { current_dir.push(&format!("{LAUNCHCTL_MM2_ID}.plist")); Ok(current_dir) @@ -286,22 +297,41 @@ fn get_plist_path() -> Result { } #[cfg(target_os = "macos")] -pub fn stop_process() { +pub(crate) fn stop_process() { let Ok(plist_path) = get_plist_path() else { return; }; - - if let Err(error) = Command::new("launchctl").arg("unload").arg(&plist_path).spawn() { - error!("Failed to unload process using launchctl: {}", error); + let Ok(uid) = get_proc_uid() else { return }; + if let Err(error) = Command::new("launchctl") + .arg("bootout") + .arg(format!("user/{}/{}", uid, LAUNCHCTL_MM2_ID)) + .spawn() + { + error!( + "Failed to unload process using launchctl: user/{}/{}, error: {}", + uid, LAUNCHCTL_MM2_ID, error + ); } else { info!("mm2 successfully stopped by launchctl"); } sleep(Duration::from_millis(LAUNCH_CTL_COOL_DOWN_TIMEOUT_MS)); - if let Err(err) = fs::remove_file(&plist_path) { + if let Err(err) = fs::remove_file(plist_path) { error!("Failed to remove plist file: {}", err); } } #[cfg(target_os = "macos")] -pub fn get_status() { +fn get_proc_uid() -> Result { + let pid = sysinfo::get_current_pid().map_err(|e| error_anyhow!("Failed to get current pid: {e}"))?; + let s = System::new_all(); + let proc = s + .process(pid) + .ok_or_else(|| error_anyhow!("Failed to get current process by pid: {pid}"))?; + proc.user_id() + .map(|uid| **uid) + .ok_or_else(|| error_anyhow!("Failed to get uid")) +} + +#[cfg(target_os = "macos")] +pub(crate) fn get_status() { let output = Command::new("launchctl") .args(["list", LAUNCHCTL_MM2_ID]) .output() diff --git a/mm2src/adex_cli/src/scenarios/mod.rs b/mm2src/adex_cli/src/scenarios/mod.rs index 2a9d637f01..070748c83c 100644 --- a/mm2src/adex_cli/src/scenarios/mod.rs +++ b/mm2src/adex_cli/src/scenarios/mod.rs @@ -1,16 +1,23 @@ -mod helpers; mod init_coins; mod init_mm2_cfg; mod inquire_extentions; mod mm2_proc_mng; +use anyhow::Result; use init_coins::init_coins; use init_mm2_cfg::init_mm2_cfg; -pub use mm2_proc_mng::{get_status, start_process, stop_process}; +use log::info; -pub fn init(cfg_file: &str, coins_file: &str) { - if init_mm2_cfg(cfg_file).is_err() { - return; - } - let _ = init_coins(coins_file); +use super::activation_scheme_db::init_activation_scheme; + +pub(super) use mm2_proc_mng::{get_status, start_process, stop_process}; + +pub(super) async fn init(cfg_file: &str, coins_file: &str) { let _ = init_impl(cfg_file, coins_file).await; } + +async fn init_impl(cfg_file: &str, coins_file: &str) -> Result<()> { + init_mm2_cfg(cfg_file)?; + init_coins(coins_file).await?; + init_activation_scheme().await?; + info!("Initialization done"); + Ok(()) } diff --git a/mm2src/adex_cli/src/tests/http_mock_data/balance.http b/mm2src/adex_cli/src/tests/http_mock_data/balance.http new file mode 100644 index 0000000000..dabd5b4748 --- /dev/null +++ b/mm2src/adex_cli/src/tests/http_mock_data/balance.http @@ -0,0 +1,4 @@ +HTTP/1.1 200 OK +content-length: 110 + +{"coin":"RICK","balance":"0.5767226","unspendable_balance":"0","address":"RPFGrvJWjSYN4qYvcXsECW1HoHbvQjowZM"} \ No newline at end of file diff --git a/mm2src/adex_cli/src/tests/http_mock_data/buy.http b/mm2src/adex_cli/src/tests/http_mock_data/buy.http new file mode 100644 index 0000000000..18452b12ee --- /dev/null +++ b/mm2src/adex_cli/src/tests/http_mock_data/buy.http @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +access-control-allow-origin: http://localhost:3000 +content-length: 720 +date: Tue, 02 May 2023 10:09:55 GMT + +{"result":{"base":"MORTY","rel":"RICK","base_amount":"0.01","base_amount_rat":[[1,[1]],[1,[100]]],"rel_amount":"0.005","rel_amount_rat":[[1,[1]],[1,[200]]],"action":"Buy","uuid":"4685e133-dfb3-4b31-8d4c-0ffa79933c8e","method":"request","sender_pubkey":"264fcd9401d797c50fe2f1c7d5fe09bbc10f3838c1d8d6f793061fa5f38b2b4d","dest_pub_key":"0000000000000000000000000000000000000000000000000000000000000000","match_by":{"type":"Any"},"conf_settings":{"base_confs":1,"base_nota":false,"rel_confs":1,"rel_nota":false},"order_type":{"type":"GoodTillCancelled"},"min_volume":"0.0002","min_volume_fraction":{"numer":"1","denom":"5000"},"min_volume_rat":[[1,[1]],[1,[5000]]],"base_orderbook_ticker":null,"rel_orderbook_ticker":null}} \ No newline at end of file diff --git a/mm2src/adex_cli/src/tests/http_mock_data/enable.http b/mm2src/adex_cli/src/tests/http_mock_data/enable.http new file mode 100644 index 0000000000..1c09691d91 --- /dev/null +++ b/mm2src/adex_cli/src/tests/http_mock_data/enable.http @@ -0,0 +1,4 @@ +HTTP/1.1 200 OK +content-length: 188 + +{"result":"success","address":"0x224050fb7EB13Fa0D342F5b245f1237bAB531650","balance":"0.02","unspendable_balance":"0","coin":"ETH","required_confirmations":3,"requires_notarization":false} \ No newline at end of file diff --git a/mm2src/adex_cli/src/tests/http_mock_data/get_enabled.http b/mm2src/adex_cli/src/tests/http_mock_data/get_enabled.http new file mode 100644 index 0000000000..914a2ccb00 --- /dev/null +++ b/mm2src/adex_cli/src/tests/http_mock_data/get_enabled.http @@ -0,0 +1,4 @@ +HTTP/1.1 200 OK +content-length: 279 + +{"result":[{"ticker":"MORTY","address":"RPFGrvJWjSYN4qYvcXsECW1HoHbvQjowZM"},{"ticker":"RICK","address":"RPFGrvJWjSYN4qYvcXsECW1HoHbvQjowZM"},{"ticker":"KMD","address":"RPFGrvJWjSYN4qYvcXsECW1HoHbvQjowZM"},{"ticker":"ETH","address":"0x224050fb7EB13Fa0D342F5b245f1237bAB531650"}]} \ No newline at end of file diff --git a/mm2src/adex_cli/src/tests/http_mock_data/orderbook.http b/mm2src/adex_cli/src/tests/http_mock_data/orderbook.http new file mode 100644 index 0000000000..e0270e2d23 --- /dev/null +++ b/mm2src/adex_cli/src/tests/http_mock_data/orderbook.http @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +access-control-allow-origin: http://localhost:3000 +content-length: 20013 +date: Fri, 21 Apr 2023 10:33:36 GMT + +{"askdepth":0,"asks":[{"coin":"RICK","address":"R9gWj7fzSxZtJZCSDMQz5G5J7x4rg6UmiQ","price":"1","price_rat":[[1,[1]],[1,[1]]],"price_fraction":{"numer":"1","denom":"1"},"maxvolume":"0.23173","max_volume_rat":[[1,[23173]],[1,[100000]]],"max_volume_fraction":{"numer":"23173","denom":"100000"},"min_volume":"0.0001","min_volume_rat":[[1,[1]],[1,[10000]]],"min_volume_fraction":{"numer":"1","denom":"10000"},"pubkey":"022d7424c741213a2b9b49aebdaa10e84419e642a8db0a09e359a3d4c850834846","age":1682073216,"zcredits":0,"uuid":"c7585a1b-6060-4319-9da6-c67321628a06","is_mine":false,"base_max_volume":"0.23173","base_max_volume_fraction":{"numer":"23173","denom":"100000"},"base_max_volume_rat":[[1,[23173]],[1,[100000]]],"base_min_volume":"0.0001","base_min_volume_fraction":{"numer":"1","denom":"10000"},"base_min_volume_rat":[[1,[1]],[1,[10000]]],"rel_max_volume":"0.23173","rel_max_volume_fraction":{"numer":"23173","denom":"100000"},"rel_max_volume_rat":[[1,[23173]],[1,[100000]]],"rel_min_volume":"0.0001","rel_min_volume_fraction":{"numer":"1","denom":"10000"},"rel_min_volume_rat":[[1,[1]],[1,[10000]]],"base_confs":1,"base_nota":false,"rel_confs":1,"rel_nota":false,"base_max_volume_aggr":"340660.26950721","base_max_volume_aggr_fraction":{"numer":"34066026950721","denom":"100000000"},"base_max_volume_aggr_rat":[[1,[2641326145,7931]],[1,[100000000]]],"rel_max_volume_aggr":"340660.26950715","rel_max_volume_aggr_fraction":{"numer":"6813205390143","denom":"20000000"},"rel_max_volume_aggr_rat":[[1,[1387258687,1586]],[1,[20000000]]]},{"coin":"RICK","address":"RB8yufv3YTfdzYnwz5paNnnDynGJG6WsqD","price":"1","price_rat":[[1,[1]],[1,[1]]],"price_fraction":{"numer":"1","denom":"1"},"maxvolume":"340654.03777721","max_volume_rat":[[1,[2018153145,7931]],[1,[100000000]]],"max_volume_fraction":{"numer":"34065403777721","denom":"100000000"},"min_volume":"0.0001","min_volume_rat":[[1,[1]],[1,[10000]]],"min_volume_fraction":{"numer":"1","denom":"10000"},"pubkey":"0315d9c51c657ab1be4ae9d3ab6e76a619d3bccfe830d5363fa168424c0d044732","age":1682073216,"zcredits":0,"uuid":"d69fe2a9-51ca-4d69-96ad-b141a01d8bb4","is_mine":false,"base_max_volume":"340654.03777721","base_max_volume_fraction":{"numer":"34065403777721","denom":"100000000"},"base_max_volume_rat":[[1,[2018153145,7931]],[1,[100000000]]],"base_min_volume":"0.0001","base_min_volume_fraction":{"numer":"1","denom":"10000"},"base_min_volume_rat":[[1,[1]],[1,[10000]]],"rel_max_volume":"340654.03777721","rel_max_volume_fraction":{"numer":"34065403777721","denom":"100000000"},"rel_max_volume_rat":[[1,[2018153145,7931]],[1,[100000000]]],"rel_min_volume":"0.0001","rel_min_volume_fraction":{"numer":"1","denom":"10000"},"rel_min_volume_rat":[[1,[1]],[1,[10000]]],"base_confs":1,"base_nota":false,"rel_confs":1,"rel_nota":false,"base_max_volume_aggr":"340660.03777721","base_max_volume_aggr_fraction":{"numer":"34066003777721","denom":"100000000"},"base_max_volume_aggr_rat":[[1,[2618153145,7931]],[1,[100000000]]],"rel_max_volume_aggr":"340660.03777715","rel_max_volume_aggr_fraction":{"numer":"6813200755543","denom":"20000000"},"rel_max_volume_aggr_rat":[[1,[1382624087,1586]],[1,[20000000]]]},{"coin":"RICK","address":"RMaprYNUp8ErJ9ZAKcxMfpC4ioVycYCCCc","price":"0.99999999","price_rat":[[1,[99999999]],[1,[100000000]]],"price_fraction":{"numer":"99999999","denom":"100000000"},"maxvolume":"2","max_volume_rat":[[1,[2]],[1,[1]]],"max_volume_fraction":{"numer":"2","denom":"1"},"min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","min_volume_rat":[[1,[10000]],[1,[99999999]]],"min_volume_fraction":{"numer":"10000","denom":"99999999"},"pubkey":"037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5","age":1682073216,"zcredits":0,"uuid":"a2337218-7f6f-46a1-892e-6febfb7f5403","is_mine":false,"base_max_volume":"2","base_max_volume_fraction":{"numer":"2","denom":"1"},"base_max_volume_rat":[[1,[2]],[1,[1]]],"base_min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","base_min_volume_fraction":{"numer":"10000","denom":"99999999"},"base_min_volume_rat":[[1,[10000]],[1,[99999999]]],"rel_max_volume":"1.99999998","rel_max_volume_fraction":{"numer":"99999999","denom":"50000000"},"rel_max_volume_rat":[[1,[99999999]],[1,[50000000]]],"rel_min_volume":"0.0001","rel_min_volume_fraction":{"numer":"1","denom":"10000"},"rel_min_volume_rat":[[1,[1]],[1,[10000]]],"base_confs":0,"base_nota":false,"rel_confs":0,"rel_nota":false,"base_max_volume_aggr":"6","base_max_volume_aggr_fraction":{"numer":"6","denom":"1"},"base_max_volume_aggr_rat":[[1,[6]],[1,[1]]],"rel_max_volume_aggr":"5.99999994","rel_max_volume_aggr_fraction":{"numer":"299999997","denom":"50000000"},"rel_max_volume_aggr_rat":[[1,[299999997]],[1,[50000000]]]},{"coin":"RICK","address":"RMaprYNUp8ErJ9ZAKcxMfpC4ioVycYCCCc","price":"0.99999999","price_rat":[[1,[99999999]],[1,[100000000]]],"price_fraction":{"numer":"99999999","denom":"100000000"},"maxvolume":"2","max_volume_rat":[[1,[2]],[1,[1]]],"max_volume_fraction":{"numer":"2","denom":"1"},"min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","min_volume_rat":[[1,[10000]],[1,[99999999]]],"min_volume_fraction":{"numer":"10000","denom":"99999999"},"pubkey":"037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5","age":1682073216,"zcredits":0,"uuid":"c172c295-7fe3-4131-9c81-c3a7182f0617","is_mine":false,"base_max_volume":"2","base_max_volume_fraction":{"numer":"2","denom":"1"},"base_max_volume_rat":[[1,[2]],[1,[1]]],"base_min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","base_min_volume_fraction":{"numer":"10000","denom":"99999999"},"base_min_volume_rat":[[1,[10000]],[1,[99999999]]],"rel_max_volume":"1.99999998","rel_max_volume_fraction":{"numer":"99999999","denom":"50000000"},"rel_max_volume_rat":[[1,[99999999]],[1,[50000000]]],"rel_min_volume":"0.0001","rel_min_volume_fraction":{"numer":"1","denom":"10000"},"rel_min_volume_rat":[[1,[1]],[1,[10000]]],"base_confs":0,"base_nota":false,"rel_confs":0,"rel_nota":false,"base_max_volume_aggr":"4","base_max_volume_aggr_fraction":{"numer":"4","denom":"1"},"base_max_volume_aggr_rat":[[1,[4]],[1,[1]]],"rel_max_volume_aggr":"3.99999996","rel_max_volume_aggr_fraction":{"numer":"99999999","denom":"25000000"},"rel_max_volume_aggr_rat":[[1,[99999999]],[1,[25000000]]]},{"coin":"RICK","address":"RMaprYNUp8ErJ9ZAKcxMfpC4ioVycYCCCc","price":"0.99999999","price_rat":[[1,[99999999]],[1,[100000000]]],"price_fraction":{"numer":"99999999","denom":"100000000"},"maxvolume":"2","max_volume_rat":[[1,[2]],[1,[1]]],"max_volume_fraction":{"numer":"2","denom":"1"},"min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","min_volume_rat":[[1,[10000]],[1,[99999999]]],"min_volume_fraction":{"numer":"10000","denom":"99999999"},"pubkey":"037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5","age":1682073216,"zcredits":0,"uuid":"fbbc44d2-fb50-4b4b-8ac3-d9857cae16b6","is_mine":false,"base_max_volume":"2","base_max_volume_fraction":{"numer":"2","denom":"1"},"base_max_volume_rat":[[1,[2]],[1,[1]]],"base_min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","base_min_volume_fraction":{"numer":"10000","denom":"99999999"},"base_min_volume_rat":[[1,[10000]],[1,[99999999]]],"rel_max_volume":"1.99999998","rel_max_volume_fraction":{"numer":"99999999","denom":"50000000"},"rel_max_volume_rat":[[1,[99999999]],[1,[50000000]]],"rel_min_volume":"0.0001","rel_min_volume_fraction":{"numer":"1","denom":"10000"},"rel_min_volume_rat":[[1,[1]],[1,[10000]]],"base_confs":0,"base_nota":false,"rel_confs":0,"rel_nota":false,"base_max_volume_aggr":"2","base_max_volume_aggr_fraction":{"numer":"2","denom":"1"},"base_max_volume_aggr_rat":[[1,[2]],[1,[1]]],"rel_max_volume_aggr":"1.99999998","rel_max_volume_aggr_fraction":{"numer":"99999999","denom":"50000000"},"rel_max_volume_aggr_rat":[[1,[99999999]],[1,[50000000]]]}],"base":"RICK","biddepth":0,"bids":[{"coin":"MORTY","address":"RSqn8JX4rjee7Tb6WbvHcJy87FTPseb2ND","price":"1.024380249948780987502560950624871952468756402376562179881171891005941405449702929727514853513624257","price_rat":[[1,[5000]],[1,[4881]]],"price_fraction":{"numer":"5000","denom":"4881"},"maxvolume":"0.9897561975005121901249743904937512804753124359762343782011882810899405859455029707027248514648637574","max_volume_rat":[[1,[4831]],[1,[4881]]],"max_volume_fraction":{"numer":"4831","denom":"4881"},"min_volume":"0.0001024380249948780987502560950624871952468756402376562179881171891005941405449702929727514853513624257","min_volume_rat":[[1,[1]],[1,[9762]]],"min_volume_fraction":{"numer":"1","denom":"9762"},"pubkey":"02d6c3e22a419a4034272acb215f1d39cd6a0413cfd83ac0c68f482db80accd89a","age":1682073216,"zcredits":0,"uuid":"c480675b-3352-4159-9b3c-55cb2b1329de","is_mine":false,"base_max_volume":"0.9662","base_max_volume_fraction":{"numer":"4831","denom":"5000"},"base_max_volume_rat":[[1,[4831]],[1,[5000]]],"base_min_volume":"0.0001","base_min_volume_fraction":{"numer":"1","denom":"10000"},"base_min_volume_rat":[[1,[1]],[1,[10000]]],"rel_max_volume":"0.9897561975005121901249743904937512804753124359762343782011882810899405859455029707027248514648637574","rel_max_volume_fraction":{"numer":"4831","denom":"4881"},"rel_max_volume_rat":[[1,[4831]],[1,[4881]]],"rel_min_volume":"0.0001024380249948780987502560950624871952468756402376562179881171891005941405449702929727514853513624257","rel_min_volume_fraction":{"numer":"1","denom":"9762"},"rel_min_volume_rat":[[1,[1]],[1,[9762]]],"base_confs":1,"base_nota":false,"rel_confs":1,"rel_nota":false,"base_max_volume_aggr":"0.9662","base_max_volume_aggr_fraction":{"numer":"4831","denom":"5000"},"base_max_volume_aggr_rat":[[1,[4831]],[1,[5000]]],"rel_max_volume_aggr":"0.9897561975005121901249743904937512804753124359762343782011882810899405859455029707027248514648637574","rel_max_volume_aggr_fraction":{"numer":"4831","denom":"4881"},"rel_max_volume_aggr_rat":[[1,[4831]],[1,[4881]]]},{"coin":"MORTY","address":"RMaprYNUp8ErJ9ZAKcxMfpC4ioVycYCCCc","price":"1.000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","price_rat":[[1,[100000000]],[1,[99999999]]],"price_fraction":{"numer":"100000000","denom":"99999999"},"maxvolume":"2","max_volume_rat":[[1,[2]],[1,[1]]],"max_volume_fraction":{"numer":"2","denom":"1"},"min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","min_volume_rat":[[1,[10000]],[1,[99999999]]],"min_volume_fraction":{"numer":"10000","denom":"99999999"},"pubkey":"037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5","age":1682073216,"zcredits":0,"uuid":"fdb0de9c-e283-48c3-9de6-8117fecf0aff","is_mine":false,"base_max_volume":"1.99999998","base_max_volume_fraction":{"numer":"99999999","denom":"50000000"},"base_max_volume_rat":[[1,[99999999]],[1,[50000000]]],"base_min_volume":"0.0001","base_min_volume_fraction":{"numer":"1","denom":"10000"},"base_min_volume_rat":[[1,[1]],[1,[10000]]],"rel_max_volume":"2","rel_max_volume_fraction":{"numer":"2","denom":"1"},"rel_max_volume_rat":[[1,[2]],[1,[1]]],"rel_min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","rel_min_volume_fraction":{"numer":"10000","denom":"99999999"},"rel_min_volume_rat":[[1,[10000]],[1,[99999999]]],"base_confs":0,"base_nota":false,"rel_confs":0,"rel_nota":false,"base_max_volume_aggr":"2.96619998","base_max_volume_aggr_fraction":{"numer":"148309999","denom":"50000000"},"base_max_volume_aggr_rat":[[1,[148309999]],[1,[50000000]]],"rel_max_volume_aggr":"2.989756197500512190124974390493751280475312435976234378201188281089940585945502970702724851464863757","rel_max_volume_aggr_fraction":{"numer":"14593","denom":"4881"},"rel_max_volume_aggr_rat":[[1,[14593]],[1,[4881]]]},{"coin":"MORTY","address":"RMaprYNUp8ErJ9ZAKcxMfpC4ioVycYCCCc","price":"1.000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","price_rat":[[1,[100000000]],[1,[99999999]]],"price_fraction":{"numer":"100000000","denom":"99999999"},"maxvolume":"2","max_volume_rat":[[1,[2]],[1,[1]]],"max_volume_fraction":{"numer":"2","denom":"1"},"min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","min_volume_rat":[[1,[10000]],[1,[99999999]]],"min_volume_fraction":{"numer":"10000","denom":"99999999"},"pubkey":"037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5","age":1682073216,"zcredits":0,"uuid":"6a3bb75d-8e91-4192-bf50-d8190a69600d","is_mine":false,"base_max_volume":"1.99999998","base_max_volume_fraction":{"numer":"99999999","denom":"50000000"},"base_max_volume_rat":[[1,[99999999]],[1,[50000000]]],"base_min_volume":"0.0001","base_min_volume_fraction":{"numer":"1","denom":"10000"},"base_min_volume_rat":[[1,[1]],[1,[10000]]],"rel_max_volume":"2","rel_max_volume_fraction":{"numer":"2","denom":"1"},"rel_max_volume_rat":[[1,[2]],[1,[1]]],"rel_min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","rel_min_volume_fraction":{"numer":"10000","denom":"99999999"},"rel_min_volume_rat":[[1,[10000]],[1,[99999999]]],"base_confs":0,"base_nota":false,"rel_confs":0,"rel_nota":false,"base_max_volume_aggr":"4.96619996","base_max_volume_aggr_fraction":{"numer":"124154999","denom":"25000000"},"base_max_volume_aggr_rat":[[1,[124154999]],[1,[25000000]]],"rel_max_volume_aggr":"4.989756197500512190124974390493751280475312435976234378201188281089940585945502970702724851464863757","rel_max_volume_aggr_fraction":{"numer":"24355","denom":"4881"},"rel_max_volume_aggr_rat":[[1,[24355]],[1,[4881]]]},{"coin":"MORTY","address":"RMaprYNUp8ErJ9ZAKcxMfpC4ioVycYCCCc","price":"1.000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","price_rat":[[1,[100000000]],[1,[99999999]]],"price_fraction":{"numer":"100000000","denom":"99999999"},"maxvolume":"2","max_volume_rat":[[1,[2]],[1,[1]]],"max_volume_fraction":{"numer":"2","denom":"1"},"min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","min_volume_rat":[[1,[10000]],[1,[99999999]]],"min_volume_fraction":{"numer":"10000","denom":"99999999"},"pubkey":"037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5","age":1682073216,"zcredits":0,"uuid":"b24b40de-e93d-4218-8d93-1940ceadce7f","is_mine":false,"base_max_volume":"1.99999998","base_max_volume_fraction":{"numer":"99999999","denom":"50000000"},"base_max_volume_rat":[[1,[99999999]],[1,[50000000]]],"base_min_volume":"0.0001","base_min_volume_fraction":{"numer":"1","denom":"10000"},"base_min_volume_rat":[[1,[1]],[1,[10000]]],"rel_max_volume":"2","rel_max_volume_fraction":{"numer":"2","denom":"1"},"rel_max_volume_rat":[[1,[2]],[1,[1]]],"rel_min_volume":"0.0001000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000","rel_min_volume_fraction":{"numer":"10000","denom":"99999999"},"rel_min_volume_rat":[[1,[10000]],[1,[99999999]]],"base_confs":0,"base_nota":false,"rel_confs":0,"rel_nota":false,"base_max_volume_aggr":"6.96619994","base_max_volume_aggr_fraction":{"numer":"348309997","denom":"50000000"},"base_max_volume_aggr_rat":[[1,[348309997]],[1,[50000000]]],"rel_max_volume_aggr":"6.989756197500512190124974390493751280475312435976234378201188281089940585945502970702724851464863757","rel_max_volume_aggr_fraction":{"numer":"34117","denom":"4881"},"rel_max_volume_aggr_rat":[[1,[34117]],[1,[4881]]]},{"coin":"MORTY","address":"RB8yufv3YTfdzYnwz5paNnnDynGJG6WsqD","price":"1","price_rat":[[1,[1]],[1,[1]]],"price_fraction":{"numer":"1","denom":"1"},"maxvolume":"32229.14223005","max_volume_rat":[[1,[337750201,150]],[1,[20000000]]],"max_volume_fraction":{"numer":"644582844601","denom":"20000000"},"min_volume":"0.0001","min_volume_rat":[[1,[1]],[1,[10000]]],"min_volume_fraction":{"numer":"1","denom":"10000"},"pubkey":"0315d9c51c657ab1be4ae9d3ab6e76a619d3bccfe830d5363fa168424c0d044732","age":1682073216,"zcredits":0,"uuid":"652a7e97-f42c-4f87-bc26-26bd1a0fea24","is_mine":false,"base_max_volume":"32229.14223005","base_max_volume_fraction":{"numer":"644582844601","denom":"20000000"},"base_max_volume_rat":[[1,[337750201,150]],[1,[20000000]]],"base_min_volume":"0.0001","base_min_volume_fraction":{"numer":"1","denom":"10000"},"base_min_volume_rat":[[1,[1]],[1,[10000]]],"rel_max_volume":"32229.14223005","rel_max_volume_fraction":{"numer":"644582844601","denom":"20000000"},"rel_max_volume_rat":[[1,[337750201,150]],[1,[20000000]]],"rel_min_volume":"0.0001","rel_min_volume_fraction":{"numer":"1","denom":"10000"},"rel_min_volume_rat":[[1,[1]],[1,[10000]]],"base_confs":1,"base_nota":false,"rel_confs":1,"rel_nota":false,"base_max_volume_aggr":"32236.10842999","base_max_volume_aggr_fraction":{"numer":"3223610842999","denom":"100000000"},"base_max_volume_aggr_rat":[[1,[2385370999,750]],[1,[100000000]]],"rel_max_volume_aggr":"32236.13198624750051219012497439049375128047531243597623437820118828108994058594550297070272485146486","rel_max_volume_aggr_fraction":{"numer":"3146891204497481","denom":"97620000000"},"rel_max_volume_aggr_rat":[[1,[3026456649,732692]],[1,[3130719488,22]]]},{"coin":"MORTY","address":"R9gWj7fzSxZtJZCSDMQz5G5J7x4rg6UmiQ","price":"1","price_rat":[[1,[1]],[1,[1]]],"price_fraction":{"numer":"1","denom":"1"},"maxvolume":"0.22784","max_volume_rat":[[1,[712]],[1,[3125]]],"max_volume_fraction":{"numer":"712","denom":"3125"},"min_volume":"0.0001","min_volume_rat":[[1,[1]],[1,[10000]]],"min_volume_fraction":{"numer":"1","denom":"10000"},"pubkey":"022d7424c741213a2b9b49aebdaa10e84419e642a8db0a09e359a3d4c850834846","age":1682073216,"zcredits":0,"uuid":"1082c93c-8c23-4944-b8f1-a92ec703b03a","is_mine":false,"base_max_volume":"0.22784","base_max_volume_fraction":{"numer":"712","denom":"3125"},"base_max_volume_rat":[[1,[712]],[1,[3125]]],"base_min_volume":"0.0001","base_min_volume_fraction":{"numer":"1","denom":"10000"},"base_min_volume_rat":[[1,[1]],[1,[10000]]],"rel_max_volume":"0.22784","rel_max_volume_fraction":{"numer":"712","denom":"3125"},"rel_max_volume_rat":[[1,[712]],[1,[3125]]],"rel_min_volume":"0.0001","rel_min_volume_fraction":{"numer":"1","denom":"10000"},"rel_min_volume_rat":[[1,[1]],[1,[10000]]],"base_confs":1,"base_nota":false,"rel_confs":1,"rel_nota":false,"base_max_volume_aggr":"32236.33626999","base_max_volume_aggr_fraction":{"numer":"3223633626999","denom":"100000000"},"base_max_volume_aggr_rat":[[1,[2408154999,750]],[1,[100000000]]],"rel_max_volume_aggr":"32236.35982624750051219012497439049375128047531243597623437820118828108994058594550297070272485146486","rel_max_volume_aggr_fraction":{"numer":"3146913446238281","denom":"97620000000"},"rel_max_volume_aggr_rat":[[1,[3793360969,732697]],[1,[3130719488,22]]]}],"netid":7777,"numasks":5,"numbids":6,"rel":"MORTY","timestamp":1682073216,"total_asks_base_vol":"340660.26950721","total_asks_base_vol_fraction":{"numer":"34066026950721","denom":"100000000"},"total_asks_base_vol_rat":[[1,[2641326145,7931]],[1,[100000000]]],"total_asks_rel_vol":"340660.26950715","total_asks_rel_vol_fraction":{"numer":"6813205390143","denom":"20000000"},"total_asks_rel_vol_rat":[[1,[1387258687,1586]],[1,[20000000]]],"total_bids_base_vol":"32236.33626999","total_bids_base_vol_fraction":{"numer":"3223633626999","denom":"100000000"},"total_bids_base_vol_rat":[[1,[2408154999,750]],[1,[100000000]]],"total_bids_rel_vol":"32236.35982624750051219012497439049375128047531243597623437820118828108994058594550297070272485146486","total_bids_rel_vol_fraction":{"numer":"3146913446238281","denom":"97620000000"},"total_bids_rel_vol_rat":[[1,[3793360969,732697]],[1,[3130719488,22]]]} \ No newline at end of file diff --git a/mm2src/adex_cli/src/tests/http_mock_data/version.http b/mm2src/adex_cli/src/tests/http_mock_data/version.http new file mode 100644 index 0000000000..a39e217ebf --- /dev/null +++ b/mm2src/adex_cli/src/tests/http_mock_data/version.http @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +content-type: application/json +content-length: 72 + +{"result":"1.0.1-beta_824ca36f3","datetime":"2023-04-06T22:35:43+05:00"} \ No newline at end of file diff --git a/mm2src/adex_cli/src/tests/mod.rs b/mm2src/adex_cli/src/tests/mod.rs new file mode 100644 index 0000000000..1946a838ad --- /dev/null +++ b/mm2src/adex_cli/src/tests/mod.rs @@ -0,0 +1,254 @@ +use std::io::Write; +use std::time::Duration; +use tokio::io::AsyncWriteExt; +use tokio::net::{TcpListener, TcpStream}; + +use crate::activation_scheme_db::{get_activation_scheme, get_activation_scheme_path, init_activation_scheme}; +use crate::adex_config::AdexConfigImpl; +use crate::adex_proc::ResponseHandlerImpl; +use crate::cli::Cli; + +const FAKE_SERVER_COOLDOWN_TIMEOUT_MS: u64 = 10; +const FAKE_SERVER_WARMUP_TIMEOUT_MS: u64 = 100; + +#[tokio::test] +async fn test_get_version() { + tokio::spawn(fake_mm2_server(7784, include_bytes!("http_mock_data/version.http"))); + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_WARMUP_TIMEOUT_MS)).await; + let mut buffer: Vec = vec![]; + let response_handler = ResponseHandlerImpl { + writer: (&mut buffer as &mut dyn Write).into(), + }; + let config = AdexConfigImpl::new("dummy", "http://127.0.0.1:7784"); + let args = vec!["adex-cli", "version"]; + Cli::execute(args.iter().map(|arg| arg.to_string()), &config, &response_handler) + .await + .unwrap(); + + let result = String::from_utf8(buffer).unwrap(); + assert_eq!( + "Version: 1.0.1-beta_824ca36f3\nDatetime: 2023-04-06T22:35:43+05:00\n", + result + ); +} + +#[tokio::test] +async fn test_get_orderbook() { + tokio::spawn(fake_mm2_server(7785, include_bytes!("http_mock_data/orderbook.http"))); + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_WARMUP_TIMEOUT_MS)).await; + let mut buffer: Vec = vec![]; + let response_handler = ResponseHandlerImpl { + writer: (&mut buffer as &mut dyn Write).into(), + }; + let config = AdexConfigImpl::new("dummy", "http://127.0.0.1:7785"); + let args = vec!["adex-cli", "orderbook", "RICK", "MORTY"]; + Cli::execute(args.iter().map(|arg| arg.to_string()), &config, &response_handler) + .await + .unwrap(); + + let result = String::from_utf8(buffer).unwrap(); + assert_eq!(RICK_AND_MORTY_ORDERBOOK, result); +} + +#[tokio::test] +async fn test_get_orderbook_with_uuids() { + tokio::spawn(fake_mm2_server(7786, include_bytes!("http_mock_data/orderbook.http"))); + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_WARMUP_TIMEOUT_MS)).await; + let mut buffer: Vec = vec![]; + let response_handler = ResponseHandlerImpl { + writer: (&mut buffer as &mut dyn Write).into(), + }; + let config = AdexConfigImpl::new("dummy", "http://127.0.0.1:7786"); + let args = vec!["adex-cli", "orderbook", "RICK", "MORTY", "--uuids"]; + Cli::execute(args.iter().map(|arg| arg.to_string()), &config, &response_handler) + .await + .unwrap(); + + let result = String::from_utf8(buffer).unwrap(); + assert_eq!(RICK_AND_MORTY_ORDERBOOK_WITH_UUIDS, result); +} + +#[tokio::test] +async fn test_get_orderbook_with_publics() { + tokio::spawn(fake_mm2_server(7787, include_bytes!("http_mock_data/orderbook.http"))); + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_WARMUP_TIMEOUT_MS)).await; + let mut buffer: Vec = vec![]; + let response_handler = ResponseHandlerImpl { + writer: (&mut buffer as &mut dyn Write).into(), + }; + let config = AdexConfigImpl::new("dummy", "http://127.0.0.1:7787"); + let args = vec!["adex-cli", "orderbook", "RICK", "MORTY", "--publics"]; + Cli::execute(args.iter().map(|arg| arg.to_string()), &config, &response_handler) + .await + .unwrap(); + + let result = String::from_utf8(buffer).unwrap(); + assert_eq!(RICK_AND_MORTY_ORDERBOOK_WITH_PUBLICS, result); +} + +#[tokio::test] +async fn test_get_enabled() { + tokio::spawn(fake_mm2_server(7788, include_bytes!("http_mock_data/get_enabled.http"))); + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_WARMUP_TIMEOUT_MS)).await; + let mut buffer: Vec = vec![]; + let response_handler = ResponseHandlerImpl { + writer: (&mut buffer as &mut dyn Write).into(), + }; + let config = AdexConfigImpl::new("dummy", "http://127.0.0.1:7788"); + let args = vec!["adex-cli", "get-enabled"]; + Cli::execute(args.iter().map(|arg| arg.to_string()), &config, &response_handler) + .await + .unwrap(); + + let result = String::from_utf8(buffer).unwrap(); + assert_eq!(ENABLED_COINS, result); +} + +#[tokio::test] +async fn test_get_balance() { + tokio::spawn(fake_mm2_server(7789, include_bytes!("http_mock_data/balance.http"))); + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_WARMUP_TIMEOUT_MS)).await; + let mut buffer: Vec = vec![]; + let response_handler = ResponseHandlerImpl { + writer: (&mut buffer as &mut dyn Write).into(), + }; + let config = AdexConfigImpl::new("dummy", "http://127.0.0.1:7789"); + let args = vec!["adex-cli", "balance", "RICK"]; + Cli::execute(args.iter().map(|arg| arg.to_string()), &config, &response_handler) + .await + .unwrap(); + + let result = String::from_utf8(buffer).unwrap(); + assert_eq!(RICK_BALANCE, result); +} + +#[tokio::test] +async fn test_enable() { + tokio::spawn(fake_mm2_server(7790, include_bytes!("http_mock_data/enable.http"))); + test_activation_scheme().await; + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_WARMUP_TIMEOUT_MS)).await; + let mut buffer: Vec = vec![]; + let response_handler = ResponseHandlerImpl { + writer: (&mut buffer as &mut dyn Write).into(), + }; + let config = AdexConfigImpl::new("dummy", "http://127.0.0.1:7790"); + let args = vec!["adex-cli", "enable", "ETH"]; + Cli::execute(args.iter().map(|arg| arg.to_string()), &config, &response_handler) + .await + .unwrap(); + + let result = String::from_utf8(buffer).unwrap(); + assert_eq!(ENABLE_OUTPUT, result); +} + +async fn test_activation_scheme() { + let _ = std::fs::remove_file(get_activation_scheme_path().unwrap()); + init_activation_scheme().await.unwrap(); + let scheme = get_activation_scheme().unwrap(); + let kmd_scheme = scheme.get_activation_method("KMD"); + assert!(kmd_scheme.is_some()); + let kmd_scheme = kmd_scheme.unwrap(); + assert_eq!(kmd_scheme.get("method").unwrap().as_str().unwrap(), "electrum"); + assert_ne!(kmd_scheme.get("servers").unwrap().as_array().unwrap().iter().count(), 0); +} + +#[tokio::test] +async fn test_buy_morty_for_rick() { + tokio::spawn(fake_mm2_server(7791, include_bytes!("http_mock_data/buy.http"))); + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_WARMUP_TIMEOUT_MS)).await; + let mut buffer: Vec = vec![]; + let response_handler = ResponseHandlerImpl { + writer: (&mut buffer as &mut dyn Write).into(), + }; + let config = AdexConfigImpl::new("dummy", "http://127.0.0.1:7791"); + let args = vec!["adex-cli", "buy", "MORTY", "RICK", "0.01", "0.5"]; + Cli::execute(args.iter().map(|arg| arg.to_string()), &config, &response_handler) + .await + .unwrap(); + + let result = String::from_utf8(buffer).unwrap(); + assert_eq!("Buy order uuid: 4685e133-dfb3-4b31-8d4c-0ffa79933c8e\n", result); +} + +async fn fake_mm2_server(port: u16, predefined_response: &'static [u8]) { + let server = TcpListener::bind(("0.0.0.0", port)) + .await + .expect("Failed to bind tcp server"); + + if let Ok((stream, _)) = server.accept().await { + tokio::spawn(handle_connection(stream, predefined_response)); + } +} + +async fn handle_connection(mut stream: TcpStream, predefined_response: &'static [u8]) { + let (reader, mut writer) = stream.split(); + reader.readable().await.unwrap(); + writer.write_all(predefined_response).await.unwrap(); + tokio::time::sleep(Duration::from_millis(FAKE_SERVER_COOLDOWN_TIMEOUT_MS)).await; +} + +const RICK_AND_MORTY_ORDERBOOK: &str = r" Volume: RICK Price: MORTY + 0.23 1.00000000 + 340654.03 1.00000000 + 2.00 0.99999999 + 2.00 0.99999999 + 2.00 0.99999999 +- --------------- ------------- + 0.96 1.02438024 + 1.99 1.00000001 + 1.99 1.00000001 + 1.99 1.00000001 + 32229.14 1.00000000 + 0.22 1.00000000 +"; + +const RICK_AND_MORTY_ORDERBOOK_WITH_UUIDS: &str = r" Volume: RICK Price: MORTY Uuid + 0.23 1.00000000 c7585a1b-6060-4319-9da6-c67321628a06 + 340654.03 1.00000000 d69fe2a9-51ca-4d69-96ad-b141a01d8bb4 + 2.00 0.99999999 a2337218-7f6f-46a1-892e-6febfb7f5403 + 2.00 0.99999999 c172c295-7fe3-4131-9c81-c3a7182f0617 + 2.00 0.99999999 fbbc44d2-fb50-4b4b-8ac3-d9857cae16b6 +- --------------- ------------- ------------------------------------ + 0.96 1.02438024 c480675b-3352-4159-9b3c-55cb2b1329de + 1.99 1.00000001 fdb0de9c-e283-48c3-9de6-8117fecf0aff + 1.99 1.00000001 6a3bb75d-8e91-4192-bf50-d8190a69600d + 1.99 1.00000001 b24b40de-e93d-4218-8d93-1940ceadce7f + 32229.14 1.00000000 652a7e97-f42c-4f87-bc26-26bd1a0fea24 + 0.22 1.00000000 1082c93c-8c23-4944-b8f1-a92ec703b03a +"; + +const RICK_AND_MORTY_ORDERBOOK_WITH_PUBLICS: &str = r" Volume: RICK Price: MORTY Public + 0.23 1.00000000 022d7424c741213a2b9b49aebdaa10e84419e642a8db0a09e359a3d4c850834846 + 340654.03 1.00000000 0315d9c51c657ab1be4ae9d3ab6e76a619d3bccfe830d5363fa168424c0d044732 + 2.00 0.99999999 037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5 + 2.00 0.99999999 037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5 + 2.00 0.99999999 037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5 +- --------------- ------------- ------------------------------------------------------------------ + 0.96 1.02438024 02d6c3e22a419a4034272acb215f1d39cd6a0413cfd83ac0c68f482db80accd89a + 1.99 1.00000001 037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5 + 1.99 1.00000001 037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5 + 1.99 1.00000001 037310a8fb9fd8f198a1a21db830252ad681fccda580ed4101f3f6bfb98b34fab5 + 32229.14 1.00000000 0315d9c51c657ab1be4ae9d3ab6e76a619d3bccfe830d5363fa168424c0d044732 + 0.22 1.00000000 022d7424c741213a2b9b49aebdaa10e84419e642a8db0a09e359a3d4c850834846 +"; + +const ENABLED_COINS: &str = r"Ticker Address +MORTY RPFGrvJWjSYN4qYvcXsECW1HoHbvQjowZM +RICK RPFGrvJWjSYN4qYvcXsECW1HoHbvQjowZM +KMD RPFGrvJWjSYN4qYvcXsECW1HoHbvQjowZM +ETH 0x224050fb7EB13Fa0D342F5b245f1237bAB531650 +"; + +const RICK_BALANCE: &str = r"coin: RICK +balance: 0.5767226 +unspendable: 0 +address: RPFGrvJWjSYN4qYvcXsECW1HoHbvQjowZM +"; + +const ENABLE_OUTPUT: &str = r"coin: ETH +address: 0x224050fb7EB13Fa0D342F5b245f1237bAB531650 +balance: 0.02 +unspendable_balance: 0 +required_confirmations: 3 +requires_notarization: No +"; diff --git a/mm2src/adex_cli/src/transport.rs b/mm2src/adex_cli/src/transport.rs new file mode 100644 index 0000000000..f2726908fd --- /dev/null +++ b/mm2src/adex_cli/src/transport.rs @@ -0,0 +1,80 @@ +use anyhow::{anyhow, bail, Result}; +use async_trait::async_trait; +use http::{HeaderMap, StatusCode}; +use log::{error, warn}; +use mm2_net::native_http::slurp_post_json; +use serde::{Deserialize, Serialize}; + +use crate::{error_anyhow, error_bail, warn_bail}; + +#[async_trait] +pub(super) trait Transport { + async fn send(&self, req: ReqT) -> Result> + where + ReqT: Serialize + Send + Sync, + OkT: for<'a> Deserialize<'a>, + ErrT: for<'a> Deserialize<'a>; +} + +pub(super) struct SlurpTransport { + rpc_uri: String, +} + +impl SlurpTransport { + pub(super) fn new(rpc_uri: String) -> SlurpTransport { SlurpTransport { rpc_uri } } +} + +#[async_trait] +impl Transport for SlurpTransport { + async fn send(&self, req: ReqT) -> Result> + where + ReqT: Serialize + Send + Sync, + OkT: for<'a> Deserialize<'a>, + ErrT: for<'a> Deserialize<'a>, + { + let data = serde_json::to_string(&req).expect("Failed to serialize enable request"); + match slurp_post_json(&self.rpc_uri, data).await { + Err(error) => error_bail!("Failed to send json: {error}"), + Ok(resp) => resp.process::(), + } + } +} + +trait Response { + fn process(self) -> Result> + where + OkT: for<'a> Deserialize<'a>, + ErrT: for<'a> Deserialize<'a>; +} + +impl Response for (StatusCode, HeaderMap, Vec) { + fn process(self) -> Result> + where + OkT: for<'a> Deserialize<'a>, + ErrT: for<'a> Deserialize<'a>, + { + let (status, _headers, data) = self; + + match status { + StatusCode::OK => match serde_json::from_slice::(&data) { + Ok(resp_data) => Ok(Ok(resp_data)), + Err(error) => { + let data = String::from_utf8(data) + .map_err(|error| error_anyhow!("Failed to get string from resp data: {error}"))?; + error_bail!("Failed to deserialize response from data: {data:?}, error: {error}") + }, + }, + StatusCode::INTERNAL_SERVER_ERROR => match serde_json::from_slice::(&data) { + Ok(resp_data) => Ok(Err(resp_data)), + Err(error) => { + let data = String::from_utf8(data) + .map_err(|error| error_anyhow!("Failed to get string from resp data: {error}"))?; + error_bail!("Failed to deserialize response from data: {data:?}, error: {error}") + }, + }, + _ => { + warn_bail!("Bad http status: {status}, data: {data:?}") + }, + } + } +} diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 88f8d94b4b..72255359b3 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -67,7 +67,8 @@ mm2_git = { path = "../mm2_git" } mm2_io = { path = "../mm2_io" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } -mm2_number = { path = "../mm2_number" } +mm2_number = { path = "../mm2_number"} +mm2_rpc = { path = "../mm2_rpc" } mocktopus = "0.8.0" num-traits = "0.2" parking_lot = { version = "0.12.0", features = ["nightly"] } @@ -75,6 +76,7 @@ primitives = { path = "../mm2_bitcoin/primitives" } prost = "0.10" protobuf = "2.20" rand = { version = "0.7", features = ["std", "small_rng"] } +regex = "1" rlp = { version = "0.5" } rmp-serde = "0.14.3" rpc = { path = "../mm2_bitcoin/rpc" } @@ -101,6 +103,9 @@ uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } # We don't need the default web3 features at all since we added our own web3 transport using shared HYPER instance. web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.19.0", default-features = false } zbase32 = "0.1.2" +zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } +zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } [target.'cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies] bincode = { version = "1.3.3", default-features = false, optional = true } @@ -124,7 +129,7 @@ web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "Re [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs = { version = "1" } bitcoin = "0.29" -hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } +hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features hyper-rustls = { version = "0.23", default-features = false, features = ["http1", "http2", "webpki-tokio"] } @@ -140,10 +145,7 @@ tokio = { version = "1.20" } tokio-rustls = { version = "0.23" } tonic = { version = "0.7", features = ["tls", "tls-webpki-roots", "compression"] } webpki-roots = { version = "0.22" } -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.0.0" } -zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.0.0" } -zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.0.0" } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.0.0" } +zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } [target.'cfg(windows)'.dependencies] winapi = "0.3" diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 4297f16a1c..59c5736358 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -22,8 +22,7 @@ // use super::eth::Action::{Call, Create}; use crate::lp_price::get_base_price_in_rel; -use crate::nft::nft_structs::{ContractType, ConvertChain, NftListReq, TransactionNftDetails, WithdrawErc1155, - WithdrawErc721}; +use crate::nft::nft_structs::{ContractType, ConvertChain, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; use async_trait::async_trait; use bitcrypto::{keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; @@ -67,7 +66,6 @@ use std::ops::Deref; use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::{Arc, Mutex}; -use url::Url; use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallRequest, FilterBuilder, Log, Trace, TraceFilterBuilder, Transaction as Web3Transaction, TransactionId, U64}; use web3::{self, Web3}; @@ -107,7 +105,7 @@ pub use rlp; mod web3_transport; #[path = "eth/v2_activation.rs"] pub mod v2_activation; -use crate::nft::{find_wallet_amount, WithdrawNftResult}; +use crate::nft::{find_wallet_nft_amount, WithdrawNftResult}; use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; mod nonce; @@ -875,26 +873,19 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { /// `withdraw_erc1155` function returns details of `ERC-1155` transaction including tx hex, /// which should be sent to`send_raw_transaction` RPC to broadcast the transaction. -pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155, url: Url) -> WithdrawNftResult { +pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> WithdrawNftResult { let coin = lp_coinfind_or_err(&ctx, &withdraw_type.chain.to_ticker()).await?; let (to_addr, token_addr, eth_coin) = get_valid_nft_add_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; let my_address = eth_coin.my_address()?; - // todo check amount in nft cache, instead of sending new moralis req - // dont use `get_nft_metadata` for erc1155, it can return info related to other owner. - let nft_req = NftListReq { - chains: vec![withdraw_type.chain], - url, - }; - let wallet_amount = find_wallet_amount( - ctx, - nft_req, - withdraw_type.token_address.clone(), + let wallet_amount = find_wallet_nft_amount( + &ctx, + &withdraw_type.chain, + withdraw_type.token_address.to_lowercase(), withdraw_type.token_id.clone(), ) .await?; - let amount_dec = if withdraw_type.max { wallet_amount.clone() } else { @@ -1390,7 +1381,7 @@ impl SwapOps for EthCoin { } fn is_supported_by_watchers(&self) -> bool { - false + std::env::var("USE_WATCHER_REWARD").is_ok() //self.contract_supports_watchers } } @@ -1797,7 +1788,7 @@ impl WatcherOps for EthCoin { }, }; - let send_contract_reward_on_spend = true; + let send_contract_reward_on_spend = other_coin.is_eth(); Ok(Some(WatcherReward { amount, @@ -5302,6 +5293,10 @@ fn checksum_address(addr: &str) -> String { result } +/// `eth_addr_to_hex` converts Address to hex format. +/// Note: the result will be in lowercase. +pub(crate) fn eth_addr_to_hex(address: &Address) -> String { format!("{:#02x}", address) } + /// Checks that input is valid mixed-case checksum form address /// The input must be 0x prefixed hex string fn is_valid_checksum_addr(addr: &str) -> bool { addr == checksum_address(addr) } @@ -5417,6 +5412,7 @@ impl From for GetEthAddressError { } /// `get_eth_address` returns wallet address for coin with `ETH` protocol type. +/// Note: result address has mixed-case checksum form. pub async fn get_eth_address(ctx: &MmArc, ticker: &str) -> MmResult { let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(ctx)?; // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index e71821f785..5c2d844381 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,9 +1,13 @@ use super::*; use crate::IguanaPrivKey; -use common::{block_on, now_sec_u32}; +use common::{block_on, now_sec_u32, wait_until_sec}; +use crypto::privkey::key_pair_from_seed; +use ethkey::{Generator, Random}; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; -use mm2_test_helpers::for_tests::{eth_jst_testnet_conf, eth_testnet_conf, ETH_DEV_NODE, ETH_DEV_SWAP_CONTRACT, - ETH_DEV_TOKEN_CONTRACT, ETH_MAINNET_NODE, ETH_MAINNET_SWAP_CONTRACT}; +use mm2_test_helpers::{for_tests::{eth_jst_testnet_conf, eth_testnet_conf, ETH_DEV_NODE, ETH_DEV_NODES, + ETH_DEV_SWAP_CONTRACT, ETH_DEV_TOKEN_CONTRACT, ETH_MAINNET_NODE, + ETH_MAINNET_SWAP_CONTRACT}, + get_passphrase}; use mocktopus::mocking::*; /// The gas price for the tests @@ -17,11 +21,62 @@ const GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE: u64 = 53_500_000_000; const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; +lazy_static! { + static ref ETH_DISTRIBUTOR: EthCoin = eth_distributor(); + static ref JST_DISTRIBUTOR: EthCoin = jst_distributor(); + static ref MM_CTX: MmArc = MmCtxBuilder::new().into_mm_arc(); +} + fn check_sum(addr: &str, expected: &str) { let actual = checksum_address(addr); assert_eq!(expected, actual); } +pub fn eth_distributor() -> EthCoin { + let req = json!({ + "method": "enable", + "coin": "ETH", + "urls": ETH_DEV_NODES, + "swap_contract_address": ETH_DEV_SWAP_CONTRACT, + }); + let seed = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); + let keypair = key_pair_from_seed(&seed).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(keypair.private().secret); + block_on(eth_coin_from_conf_and_request( + &MM_CTX, + "ETH", + ð_testnet_conf(), + &req, + CoinProtocol::ETH, + priv_key_policy, + )) + .unwrap() +} + +pub fn jst_distributor() -> EthCoin { + let req = json!({ + "method": "enable", + "coin": "ETH", + "urls": ETH_DEV_NODES, + "swap_contract_address": ETH_DEV_SWAP_CONTRACT, + }); + let seed = get_passphrase!(".env.client", "BOB_PASSPHRASE").unwrap(); + let keypair = key_pair_from_seed(&seed).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(keypair.private().secret); + block_on(eth_coin_from_conf_and_request( + &MM_CTX, + "ETH", + ð_testnet_conf(), + &req, + CoinProtocol::ERC20 { + platform: "ETH".to_string(), + contract_address: ETH_DEV_TOKEN_CONTRACT.to_string(), + }, + priv_key_policy, + )) + .unwrap() +} + fn eth_coin_for_test( coin_type: EthCoinType, urls: &[&str], @@ -31,7 +86,25 @@ fn eth_coin_for_test( &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); + eth_coin_from_keypair(coin_type, urls, fallback_swap_contract, key_pair) +} +fn random_eth_coin_for_test( + coin_type: EthCoinType, + urls: &[&str], + fallback_swap_contract: Option
, +) -> (MmArc, EthCoin) { + let key_pair = Random.generate().unwrap(); + fill_eth(key_pair.address(), 0.001); + eth_coin_from_keypair(coin_type, urls, fallback_swap_contract, key_pair) +} + +fn eth_coin_from_keypair( + coin_type: EthCoinType, + urls: &[&str], + fallback_swap_contract: Option
, + key_pair: KeyPair, +) -> (MmArc, EthCoin) { let mut nodes = vec![]; for url in urls.iter() { nodes.push(HttpTransportNode { @@ -85,6 +158,24 @@ fn eth_coin_for_test( (ctx, eth_coin) } +pub fn fill_eth(to_addr: Address, amount: f64) { + let wei_per_eth: u64 = 1_000_000_000_000_000_000; + let amount_in_wei = (amount * wei_per_eth as f64) as u64; + ETH_DISTRIBUTOR + .send_to_address(to_addr, amount_in_wei.into()) + .wait() + .unwrap(); +} + +pub fn fill_jst(to_addr: Address, amount: f64) { + let wei_per_jst: u64 = 1_000_000_000_000_000_000; + let amount_in_wei = (amount * wei_per_jst as f64) as u64; + JST_DISTRIBUTOR + .send_to_address(to_addr, amount_in_wei.into()) + .wait() + .unwrap(); +} + #[test] /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md#test-cases fn test_check_sum_address() { @@ -214,10 +305,9 @@ fn test_wei_from_big_decimal() { #[test] fn send_and_refund_erc20_payment() { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("d27bfdbb5f89d30f8b7a1d0dcd4e181e54b8a2347836cb59c6570834f820f9a4").unwrap(), - ) - .unwrap(); + let key_pair = Random.generate().unwrap(); + fill_eth(key_pair.address(), 0.001); + fill_jst(key_pair.address(), 0.0001); let transport = Web3Transport::single_node(ETH_DEV_NODE, false); let web3 = Web3::new(transport); @@ -260,16 +350,15 @@ fn send_and_refund_erc20_payment() { time_lock, other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, secret_hash, - amount: "0.001".parse().unwrap(), + amount: "0.0001".parse().unwrap(), swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, watcher_reward: None, - wait_for_confirmation_until: 0, + wait_for_confirmation_until: wait_until_sec(15), }; let payment = coin.send_maker_payment(maker_payment_args).wait().unwrap(); log!("{:?}", payment); - block_on(Timer::sleep(5.)); let swap_id = coin.etomic_swap_id(time_lock, secret_hash); let status = block_on( @@ -296,7 +385,6 @@ fn send_and_refund_erc20_payment() { .wait() .unwrap(); log!("{:?}", refund); - block_on(Timer::sleep(5.)); let status = block_on( coin.payment_status( @@ -311,10 +399,8 @@ fn send_and_refund_erc20_payment() { #[test] fn send_and_refund_eth_payment() { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("0b6d7b4b1b9454f3f0bc76b2988cd672439213a1de23d20a83467131366ba41c").unwrap(), - ) - .unwrap(); + let key_pair = Random.generate().unwrap(); + fill_eth(key_pair.address(), 0.001); let transport = Web3Transport::single_node(ETH_DEV_NODE, false); let web3 = Web3::new(transport); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -353,7 +439,7 @@ fn send_and_refund_eth_payment() { time_lock, other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, secret_hash, - amount: "0.001".parse().unwrap(), + amount: "0.0001".parse().unwrap(), swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, @@ -363,7 +449,6 @@ fn send_and_refund_eth_payment() { let payment = coin.send_maker_payment(send_maker_payment_args).wait().unwrap(); log!("{:?}", payment); - block_on(Timer::sleep(5.)); let swap_id = coin.etomic_swap_id(time_lock, secret_hash); let status = block_on( @@ -391,7 +476,6 @@ fn send_and_refund_eth_payment() { .unwrap(); log!("{:?}", refund); - block_on(Timer::sleep(5.)); let status = block_on( coin.payment_status( @@ -820,7 +904,7 @@ fn test_nonce_lock() { // send several transactions concurrently to check that they are not using same nonce // using real ETH dev node - let (ctx, coin) = eth_coin_for_test(EthCoinType::Eth, ETH_DEV_NODES, None); + let (ctx, coin) = random_eth_coin_for_test(EthCoinType::Eth, ETH_DEV_NODES, None); let mut futures = vec![]; for _ in 0..5 { futures.push( diff --git a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs b/mm2src/coins/hd_wallet_storage/sqlite_storage.rs index 38d1201120..7d015b3d29 100644 --- a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs +++ b/mm2src/coins/hd_wallet_storage/sqlite_storage.rs @@ -1,9 +1,11 @@ +#![allow(deprecated)] // TODO: remove this once rusqlite is >= 0.29 + use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, HDWalletStorageResult}; use async_trait::async_trait; use common::async_blocking; use db_common::owned_named_params; -use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row, NO_PARAMS}; +use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row}; use db_common::sqlite::{query_single_row_with_named_params, AsSqlNamedParams, OwnedSqlNamedParams, SqliteConnShared, SqliteConnWeak}; use derive_more::Display; @@ -228,7 +230,7 @@ impl HDWalletSqliteStorage { async fn init_tables(&self) -> HDWalletStorageResult<()> { let conn_shared = self.get_shared_conn()?; let conn = Self::lock_conn_mutex(&conn_shared)?; - conn.execute(CREATE_HD_ACCOUNT_TABLE, NO_PARAMS) + conn.execute(CREATE_HD_ACCOUNT_TABLE, []) .map(|_| ()) .map_to_mm(HDWalletStorageError::from) } @@ -280,7 +282,7 @@ pub(super) async fn get_all_storage_items(ctx: &MmArc) -> Vec| HDAccountStorageItem::try_from(row)) + .query_map([], |row: &Row<'_>| HDAccountStorageItem::try_from(row)) .unwrap() .collect::, _>>() .unwrap() diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index a2900d2f87..02a0c41cfd 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -1402,7 +1402,7 @@ impl MmCoin for LightningCoin { Some(amt_or_err) => log_err_and_return_false!(amt_or_err), None => return true, }; - let protocol_info = match info.as_ref().map(rmp_serde::from_read_ref::<_, LightningProtocolInfo>) { + let protocol_info = match info.as_ref().map(|t| rmp_serde::from_slice::(t)) { Some(info_or_err) => log_err_and_return_false!(info_or_err), None => return false, }; diff --git a/mm2src/coins/lightning/ln_sql.rs b/mm2src/coins/lightning/ln_sql.rs index d1cbe1bbf8..53eab1ac78 100644 --- a/mm2src/coins/lightning/ln_sql.rs +++ b/mm2src/coins/lightning/ln_sql.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] // TODO: remove this once rusqlite is >= 0.29 + use crate::lightning::ln_db::{ChannelType, ChannelVisibility, ClosedChannelsFilter, DBChannelDetails, DBPaymentsFilter, GetClosedChannelsResult, GetPaymentsResult, HTLCStatus, LightningDB, PaymentInfo, PaymentType}; @@ -5,7 +7,7 @@ use async_trait::async_trait; use common::{async_blocking, now_sec_i64, PagingOptionsEnum}; use db_common::owned_named_params; use db_common::sqlite::rusqlite::types::Type; -use db_common::sqlite::rusqlite::{params, Error as SqlError, Row, ToSql, NO_PARAMS}; +use db_common::sqlite::rusqlite::{params, Error as SqlError, Row, ToSql}; use db_common::sqlite::sql_builder::SqlBuilder; use db_common::sqlite::{h256_option_slice_from_row, h256_slice_from_row, offset_by_id, query_single_row, sql_text_conversion_err, string_from_row, validate_table_name, AsSqlNamedParams, @@ -626,8 +628,8 @@ impl LightningDB for SqliteLightningDB { let sql_payments_history = create_payments_history_table_sql(self.db_ticker.as_str())?; async_blocking(move || { let conn = sqlite_connection.lock().unwrap(); - conn.execute(&sql_channels_history, NO_PARAMS).map(|_| ())?; - conn.execute(&sql_payments_history, NO_PARAMS).map(|_| ())?; + conn.execute(&sql_channels_history, []).map(|_| ())?; + conn.execute(&sql_payments_history, []).map(|_| ())?; Ok(()) }) .await @@ -803,7 +805,7 @@ impl LightningDB for SqliteLightningDB { let mut total_builder = sql_builder.clone(); total_builder.count("id"); let total_sql = total_builder.sql().expect("valid sql"); - let total: isize = conn.query_row(&total_sql, NO_PARAMS, |row| row.get(0))?; + let total: isize = conn.query_row(&total_sql, [], |row| row.get(0))?; let total = total.try_into().expect("count should be always above zero"); let offset = match paging { @@ -988,7 +990,7 @@ impl LightningDB for SqliteLightningDB { let mut total_builder = sql_builder.clone(); total_builder.count("id"); let total_sql = total_builder.sql().expect("valid sql"); - let total: isize = conn.query_row(&total_sql, NO_PARAMS, |row| row.get(0))?; + let total: isize = conn.query_row(&total_sql, [], |row| row.get(0))?; let total = total.try_into().expect("count should be always above zero"); let offset = match paging { diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index b45c2ba617..8f123d6066 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -64,6 +64,7 @@ use mm2_err_handle::prelude::*; use mm2_metrics::MetricsWeak; use mm2_number::{bigdecimal::{BigDecimal, ParseBigDecimalError, Zero}, MmNumber}; +use mm2_rpc::data::legacy::{EnabledCoin, GetEnabledResponse, Mm2RpcResult}; use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -81,6 +82,7 @@ use std::sync::atomic::Ordering as AtomicOrdering; use std::sync::Arc; use std::time::Duration; use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; +use zcash_primitives::transaction::Transaction as ZTransaction; cfg_native! { use crate::lightning::LightningCoin; @@ -91,8 +93,6 @@ cfg_native! { use lightning_invoice::{Invoice, ParseOrSemanticError}; use std::io; use std::path::PathBuf; - use zcash_primitives::transaction::Transaction as ZTransaction; - use z_coin::ZcoinProtocolInfo; } cfg_wasm32! { @@ -287,8 +287,8 @@ use utxo::{BlockchainNetwork, GenerateTxError, UtxoFeeDetails, UtxoTx}; pub mod nft; use nft::nft_errors::GetNftInfoError; -#[cfg(not(target_arch = "wasm32"))] pub mod z_coin; -#[cfg(not(target_arch = "wasm32"))] use z_coin::ZCoin; +pub mod z_coin; +use z_coin::{ZCoin, ZcoinProtocolInfo}; pub type TransactionFut = Box + Send>; pub type BalanceResult = Result>; @@ -360,7 +360,7 @@ impl From for RawTransactionError { } } -#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum GetMyAddressError { CoinsConfCheckError(String), @@ -467,7 +467,6 @@ pub trait Transaction: fmt::Debug + 'static { pub enum TransactionEnum { UtxoTx(UtxoTx), SignedEthTx(SignedEthTx), - #[cfg(not(target_arch = "wasm32"))] ZTransaction(ZTransaction), CosmosTransaction(CosmosTransaction), #[cfg(not(target_arch = "wasm32"))] @@ -476,7 +475,6 @@ pub enum TransactionEnum { ifrom!(TransactionEnum, UtxoTx); ifrom!(TransactionEnum, SignedEthTx); -#[cfg(not(target_arch = "wasm32"))] ifrom!(TransactionEnum, ZTransaction); #[cfg(not(target_arch = "wasm32"))] ifrom!(TransactionEnum, LightningPayment); @@ -496,7 +494,6 @@ impl Deref for TransactionEnum { match self { TransactionEnum::UtxoTx(ref t) => t, TransactionEnum::SignedEthTx(ref t) => t, - #[cfg(not(target_arch = "wasm32"))] TransactionEnum::ZTransaction(ref t) => t, TransactionEnum::CosmosTransaction(ref t) => t, #[cfg(not(target_arch = "wasm32"))] @@ -1072,6 +1069,10 @@ pub enum WithdrawFee { gas_limit: u64, gas_price: u64, }, + CosmosGas { + gas_limit: u64, + gas_price: f64, + }, } pub struct WithdrawSenderAddress { @@ -1925,10 +1926,8 @@ pub enum WithdrawError { available: BigDecimal, required: BigDecimal, }, -} - -impl From for WithdrawError { - fn from(e: GetNftInfoError) -> Self { WithdrawError::GetNftInfoError(e) } + #[display(fmt = "DB error {}", _0)] + DbError(String), } impl HttpStatusCode for WithdrawError { @@ -1957,7 +1956,9 @@ impl HttpStatusCode for WithdrawError { WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, - WithdrawError::Transport(_) | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + WithdrawError::Transport(_) | WithdrawError::InternalError(_) | WithdrawError::DbError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, } } } @@ -2341,7 +2342,6 @@ pub enum MmCoinEnum { QtumCoin(QtumCoin), Qrc20Coin(Qrc20Coin), EthCoin(EthCoin), - #[cfg(not(target_arch = "wasm32"))] ZCoin(ZCoin), Bch(BchCoin), SlpToken(SlpToken), @@ -2427,7 +2427,6 @@ impl From for MmCoinEnum { fn from(c: LightningCoin) -> MmCoinEnum { MmCoinEnum::LightningCoin(c) } } -#[cfg(not(target_arch = "wasm32"))] impl From for MmCoinEnum { fn from(c: ZCoin) -> MmCoinEnum { MmCoinEnum::ZCoin(c) } } @@ -2447,7 +2446,6 @@ impl Deref for MmCoinEnum { MmCoinEnum::TendermintToken(ref c) => c, #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::LightningCoin(ref c) => c, - #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::ZCoin(ref c) => c, MmCoinEnum::Test(ref c) => c, #[cfg(all( @@ -2840,7 +2838,6 @@ pub enum CoinProtocol { token_contract_address: String, decimals: u8, }, - #[cfg(not(target_arch = "wasm32"))] ZHTLC(ZcoinProtocolInfo), } @@ -3093,7 +3090,6 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result return ERR!("TENDERMINT protocol is not supported by lp_coininit"), CoinProtocol::TENDERMINTTOKEN(_) => return ERR!("TENDERMINTTOKEN protocol is not supported by lp_coininit"), - #[cfg(not(target_arch = "wasm32"))] CoinProtocol::ZHTLC { .. } => return ERR!("ZHTLC protocol is not supported by lp_coininit"), #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { .. } => return ERR!("Lightning protocol is not supported by lp_coininit"), @@ -3470,16 +3466,10 @@ pub async fn get_trade_fee(ctx: MmArc, req: Json) -> Result>, S Ok(try_s!(Response::builder().body(res))) } -#[derive(Serialize)] -struct EnabledCoin { - ticker: String, - address: String, -} - pub async fn get_enabled_coins(ctx: MmArc) -> Result>, String> { let coins_ctx: Arc = try_s!(CoinsContext::from_ctx(&ctx)); let coins = coins_ctx.coins.lock().await; - let enabled_coins: Vec<_> = try_s!(coins + let enabled_coins: GetEnabledResponse = try_s!(coins .iter() .map(|(ticker, coin)| { let address = try_s!(coin.inner.my_address()); @@ -3489,8 +3479,7 @@ pub async fn get_enabled_coins(ctx: MmArc) -> Result>, String> }) }) .collect()); - - let res = try_s!(json::to_vec(&json!({ "result": enabled_coins }))); + let res = try_s!(json::to_vec(&Mm2RpcResult::new(enabled_coins))); Ok(try_s!(Response::builder().body(res))) } @@ -3686,7 +3675,6 @@ pub fn address_by_coin_conf_and_pubkey_str( CoinProtocol::SOLANA | CoinProtocol::SPLTOKEN { .. } => { ERR!("Solana pubkey is the public address - you do not need to use this rpc call.") }, - #[cfg(not(target_arch = "wasm32"))] CoinProtocol::ZHTLC { .. } => ERR!("address_by_coin_conf_and_pubkey_str is not supported for ZHTLC protocol!"), } } diff --git a/mm2src/coins/lp_price.rs b/mm2src/coins/lp_price.rs index ba3c0b9b60..1960aa7b16 100644 --- a/mm2src/coins/lp_price.rs +++ b/mm2src/coins/lp_price.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use std::str::Utf8Error; const PRICE_ENDPOINTS: [&str; 2] = [ - "https://prices.komodo.live:1313/api/v2/tickers", + "https://prices.komodo.earth:1313/api/v2/tickers", "https://prices.cipig.net:1717/api/v2/tickers", ]; diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index eb4ea72b33..3d84103f3f 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -1,243 +1,386 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::{MmError, MmResult}; -use std::str::FromStr; +use url::Url; pub(crate) mod nft_errors; pub(crate) mod nft_structs; +pub(crate) mod storage; + #[cfg(any(test, target_arch = "wasm32"))] mod nft_tests; -use crate::WithdrawError; -use nft_errors::{GetInfoFromUriError, GetNftInfoError}; -use nft_structs::{ConvertChain, Nft, NftList, NftListReq, NftMetadataReq, NftTransferHistory, - NftTransferHistoryWrapper, NftTransfersReq, NftWrapper, NftsTransferHistoryList, - TransactionNftDetails, WithdrawNftReq}; +use crate::{get_my_address, MyAddressReq, WithdrawError}; +use nft_errors::{GetInfoFromUriError, GetNftInfoError, UpdateNftError}; +use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftList, NftListReq, NftMetadataReq, + NftTransferHistory, NftTransfersReq, NftTxHistoryFromMoralis, NftsTransferHistoryList, + TransactionNftDetails, UpdateNftReq, WithdrawNftReq}; -use crate::eth::{get_eth_address, withdraw_erc1155, withdraw_erc721}; -use crate::nft::nft_structs::{TransferStatus, UriMeta, WithdrawNftType}; -use common::APPLICATION_JSON; +use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721}; +use crate::nft::nft_errors::ProtectFromSpamError; +use crate::nft::nft_structs::{NftCommon, NftCtx, NftTransferCommon, RefreshMetadataReq, TransferStatus, TxMeta, + UriMeta}; +use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTxHistoryStorageOps}; +use common::{parse_rfc3339_to_timestamp, APPLICATION_JSON}; use ethereum_types::Address; use http::header::ACCEPT; use mm2_err_handle::map_to_mm::MapToMmResult; use mm2_number::BigDecimal; +use regex::Regex; use serde_json::Value as Json; +use std::cmp::Ordering; +use std::str::FromStr; const MORALIS_API_ENDPOINT: &str = "api/v2"; /// query parameters for moralis request: The format of the token ID const MORALIS_FORMAT_QUERY_NAME: &str = "format"; const MORALIS_FORMAT_QUERY_VALUE: &str = "decimal"; -/// query parameters for moralis request: The transfer direction -const MORALIS_DIRECTION_QUERY_NAME: &str = "direction"; -const MORALIS_DIRECTION_QUERY_VALUE: &str = "both"; +/// The minimum block number from which to get the transfers +const MORALIS_FROM_BLOCK_QUERY_NAME: &str = "from_block"; pub type WithdrawNftResult = Result>; /// `get_nft_list` function returns list of NFTs on requested chains owned by user. pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult { + let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; + let _lock = nft_ctx.guard.lock().await; + + let storage = NftStorageBuilder::new(&ctx).build()?; + for chain in req.chains.iter() { + if !NftListStorageOps::is_initialized(&storage, chain).await? { + NftListStorageOps::init(&storage, chain).await?; + } + } + let mut nft_list = storage + .get_nft_list(req.chains, req.max, req.limit, req.page_number) + .await?; + if req.protect_from_spam { + for nft in &mut nft_list.nfts { + protect_from_nft_spam(nft)?; + } + } + drop_mutability!(nft_list); + Ok(nft_list) +} + +/// `get_nft_metadata` function returns info of one specific NFT. +pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult { + let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; + let _lock = nft_ctx.guard.lock().await; + + let storage = NftStorageBuilder::new(&ctx).build()?; + if !NftListStorageOps::is_initialized(&storage, &req.chain).await? { + NftListStorageOps::init(&storage, &req.chain).await?; + } + let mut nft = storage + .get_nft(&req.chain, format!("{:#02x}", req.token_address), req.token_id.clone()) + .await? + .ok_or_else(|| GetNftInfoError::TokenNotFoundInWallet { + token_address: format!("{:#02x}", req.token_address), + token_id: req.token_id.to_string(), + })?; + if req.protect_from_spam { + protect_from_nft_spam(&mut nft)?; + } + drop_mutability!(nft); + Ok(nft) +} + +/// `get_nft_transfers` function returns a transfer history of NFTs on requested chains owned by user. +pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult { + let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; + let _lock = nft_ctx.guard.lock().await; + + let storage = NftStorageBuilder::new(&ctx).build()?; + for chain in req.chains.iter() { + if !NftTxHistoryStorageOps::is_initialized(&storage, chain).await? { + NftTxHistoryStorageOps::init(&storage, chain).await?; + } + } + let mut transfer_history_list = storage + .get_tx_history(req.chains, req.max, req.limit, req.page_number, req.filters) + .await?; + if req.protect_from_spam { + for tx in &mut transfer_history_list.transfer_history { + protect_from_history_spam(tx)?; + } + } + drop_mutability!(transfer_history_list); + Ok(transfer_history_list) +} + +/// `update_nft` function updates cache of nft transfer history and nft list. +pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNftError> { + let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; + let _lock = nft_ctx.guard.lock().await; + + let storage = NftStorageBuilder::new(&ctx).build()?; + for chain in req.chains.iter() { + let tx_history_initialized = NftTxHistoryStorageOps::is_initialized(&storage, chain).await?; + + let from_block = if tx_history_initialized { + let last_tx_block = NftTxHistoryStorageOps::get_last_block_number(&storage, chain).await?; + last_tx_block.map(|b| b + 1) + } else { + NftTxHistoryStorageOps::init(&storage, chain).await?; + None + }; + let nft_transfers = get_moralis_nft_transfers(&ctx, chain, from_block, &req.url).await?; + storage.add_txs_to_history(chain, nft_transfers).await?; + + let nft_block = match NftListStorageOps::get_last_block_number(&storage, chain).await { + Ok(Some(block)) => block, + Ok(None) => { + // if there are no rows in NFT LIST table we can try to get all info from moralis. + let nfts = cache_nfts_from_moralis(&ctx, &storage, chain, &req.url).await?; + update_meta_in_txs(&storage, chain, nfts).await?; + update_txs_with_empty_meta(&storage, chain, &req.url).await?; + continue; + }, + Err(_) => { + // if there is an error, then NFT LIST table doesnt exist, so we need to cache from mroalis. + NftListStorageOps::init(&storage, chain).await?; + let nft_list = get_moralis_nft_list(&ctx, chain, &req.url).await?; + let last_scanned_block = NftTxHistoryStorageOps::get_last_block_number(&storage, chain) + .await? + .unwrap_or(0); + storage + .add_nfts_to_list(chain, nft_list.clone(), last_scanned_block) + .await?; + update_meta_in_txs(&storage, chain, nft_list).await?; + update_txs_with_empty_meta(&storage, chain, &req.url).await?; + continue; + }, + }; + let scanned_block = + storage + .get_last_scanned_block(chain) + .await? + .ok_or_else(|| UpdateNftError::LastScannedBlockNotFound { + last_nft_block: nft_block.to_string(), + })?; + // if both block numbers exist, last scanned block should be equal + // or higher than last block number from NFT LIST table. + if scanned_block < nft_block { + return MmError::err(UpdateNftError::InvalidBlockOrder { + last_scanned_block: scanned_block.to_string(), + last_nft_block: nft_block.to_string(), + }); + } + update_nft_list(ctx.clone(), &storage, chain, scanned_block + 1, &req.url).await?; + update_txs_with_empty_meta(&storage, chain, &req.url).await?; + } + Ok(()) +} + +pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResult<(), UpdateNftError> { + let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; + let _lock = nft_ctx.guard.lock().await; + + let storage = NftStorageBuilder::new(&ctx).build()?; + let moralis_meta = get_moralis_metadata( + format!("{:#02x}", req.token_address), + req.token_id.clone(), + &req.chain, + &req.url, + ) + .await?; + let req = NftMetadataReq { + token_address: req.token_address, + token_id: req.token_id, + chain: req.chain, + protect_from_spam: false, + }; + let mut nft_db = get_nft_metadata(ctx.clone(), req).await?; + let token_uri = check_moralis_ipfs_bafy(moralis_meta.common.token_uri.as_deref()); + let uri_meta = get_uri_meta(token_uri.as_deref(), moralis_meta.common.metadata.as_deref()).await; + nft_db.common.collection_name = moralis_meta.common.collection_name; + nft_db.common.symbol = moralis_meta.common.symbol; + nft_db.common.token_uri = token_uri; + nft_db.common.metadata = moralis_meta.common.metadata; + nft_db.common.last_token_uri_sync = moralis_meta.common.last_token_uri_sync; + nft_db.common.last_metadata_sync = moralis_meta.common.last_metadata_sync; + nft_db.common.possible_spam = moralis_meta.common.possible_spam; + nft_db.uri_meta = uri_meta; + drop_mutability!(nft_db); + storage + .refresh_nft_metadata(&moralis_meta.chain, nft_db.clone()) + .await?; + let tx_meta = TxMeta::from(nft_db.clone()); + storage.update_txs_meta_by_token_addr_id(&nft_db.chain, tx_meta).await?; + Ok(()) +} + +async fn get_moralis_nft_list(ctx: &MmArc, chain: &Chain, url: &Url) -> MmResult, GetNftInfoError> { let mut res_list = Vec::new(); - for chain in req.chains { - let my_address = get_eth_address(&ctx, &chain.to_ticker()).await?; - - let mut uri_without_cursor = req.url.clone(); - uri_without_cursor.set_path(MORALIS_API_ENDPOINT); - uri_without_cursor - .path_segments_mut() - .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? - .push(&my_address.wallet_address) - .push("nft"); - uri_without_cursor - .query_pairs_mut() - .append_pair("chain", &chain.to_string()) - .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE); - drop_mutability!(uri_without_cursor); - - // The cursor returned in the previous response (used for getting the next page). - let mut cursor = String::new(); - loop { - let uri = format!("{}{}", uri_without_cursor, cursor); - let response = send_request_to_uri(uri.as_str()).await?; - if let Some(nfts_list) = response["result"].as_array() { - for nft_json in nfts_list { - let nft_wrapper: NftWrapper = serde_json::from_str(&nft_json.to_string())?; - let uri_meta = try_get_uri_meta(&nft_wrapper.token_uri).await?; - let nft = Nft { - chain, - token_address: nft_wrapper.token_address, - token_id: nft_wrapper.token_id.0, - amount: nft_wrapper.amount.0, - owner_of: nft_wrapper.owner_of, - token_hash: nft_wrapper.token_hash, - block_number_minted: *nft_wrapper.block_number_minted, - block_number: *nft_wrapper.block_number, - contract_type: nft_wrapper.contract_type.map(|v| v.0), - collection_name: nft_wrapper.name, - symbol: nft_wrapper.symbol, - token_uri: nft_wrapper.token_uri, - metadata: nft_wrapper.metadata, - last_token_uri_sync: nft_wrapper.last_token_uri_sync, - last_metadata_sync: nft_wrapper.last_metadata_sync, - minter_address: nft_wrapper.minter_address, - possible_spam: nft_wrapper.possible_spam, - uri_meta, - }; - // collect NFTs from the page - res_list.push(nft); - } - // if cursor is not null, there are other NFTs on next page, - // and we need to send new request with cursor to get info from the next page. - if let Some(cursor_res) = response["cursor"].as_str() { - cursor = format!("{}{}", "&cursor=", cursor_res); - continue; - } else { - break; - } + let my_address = get_eth_address(ctx, &chain.to_ticker()).await?; + + let mut uri_without_cursor = url.clone(); + uri_without_cursor.set_path(MORALIS_API_ENDPOINT); + uri_without_cursor + .path_segments_mut() + .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? + .push(&my_address.wallet_address) + .push("nft"); + uri_without_cursor + .query_pairs_mut() + .append_pair("chain", &chain.to_string()) + .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE); + drop_mutability!(uri_without_cursor); + + // The cursor returned in the previous response (used for getting the next page). + let mut cursor = String::new(); + loop { + let uri = format!("{}{}", uri_without_cursor, cursor); + let response = send_request_to_uri(uri.as_str()).await?; + if let Some(nfts_list) = response["result"].as_array() { + for nft_json in nfts_list { + let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string())?; + let contract_type = match nft_moralis.contract_type { + Some(contract_type) => contract_type, + None => continue, + }; + let nft = build_nft_from_moralis(chain, nft_moralis, contract_type).await; + // collect NFTs from the page + res_list.push(nft); + } + // if cursor is not null, there are other NFTs on next page, + // and we need to send new request with cursor to get info from the next page. + if let Some(cursor_res) = response["cursor"].as_str() { + cursor = format!("{}{}", "&cursor=", cursor_res); + continue; + } else { + break; } } } drop_mutability!(res_list); - let nft_list = NftList { nfts: res_list }; - Ok(nft_list) + Ok(res_list) +} + +async fn get_moralis_nft_transfers( + ctx: &MmArc, + chain: &Chain, + from_block: Option, + url: &Url, +) -> MmResult, GetNftInfoError> { + let mut res_list = Vec::new(); + let my_address = get_eth_address(ctx, &chain.to_ticker()).await?; + + let mut uri_without_cursor = url.clone(); + uri_without_cursor.set_path(MORALIS_API_ENDPOINT); + uri_without_cursor + .path_segments_mut() + .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? + .push(&my_address.wallet_address) + .push("nft") + .push("transfers"); + let from_block = match from_block { + Some(block) => block.to_string(), + None => "1".into(), + }; + uri_without_cursor + .query_pairs_mut() + .append_pair("chain", &chain.to_string()) + .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE) + .append_pair(MORALIS_FROM_BLOCK_QUERY_NAME, &from_block); + drop_mutability!(uri_without_cursor); + + // The cursor returned in the previous response (used for getting the next page). + let mut cursor = String::new(); + let wallet_address = my_address.wallet_address; + loop { + let uri = format!("{}{}", uri_without_cursor, cursor); + let response = send_request_to_uri(uri.as_str()).await?; + if let Some(transfer_list) = response["result"].as_array() { + for transfer in transfer_list { + let transfer_moralis: NftTxHistoryFromMoralis = serde_json::from_str(&transfer.to_string())?; + let contract_type = match transfer_moralis.contract_type { + Some(contract_type) => contract_type, + None => continue, + }; + let status = get_tx_status(&wallet_address, ð_addr_to_hex(&transfer_moralis.common.to_address)); + let block_timestamp = parse_rfc3339_to_timestamp(&transfer_moralis.block_timestamp)?; + let transfer_history = NftTransferHistory { + common: NftTransferCommon { + block_hash: transfer_moralis.common.block_hash, + transaction_hash: transfer_moralis.common.transaction_hash, + transaction_index: transfer_moralis.common.transaction_index, + log_index: transfer_moralis.common.log_index, + value: transfer_moralis.common.value, + transaction_type: transfer_moralis.common.transaction_type, + token_address: transfer_moralis.common.token_address, + token_id: transfer_moralis.common.token_id, + from_address: transfer_moralis.common.from_address, + to_address: transfer_moralis.common.to_address, + amount: transfer_moralis.common.amount, + verified: transfer_moralis.common.verified, + operator: transfer_moralis.common.operator, + possible_spam: transfer_moralis.common.possible_spam, + }, + chain: *chain, + block_number: *transfer_moralis.block_number, + block_timestamp, + contract_type, + token_uri: None, + collection_name: None, + image_url: None, + token_name: None, + status, + }; + // collect NFTs transfers from the page + res_list.push(transfer_history); + } + // if the cursor is not null, there are other NFTs transfers on next page, + // and we need to send new request with cursor to get info from the next page. + if let Some(cursor_res) = response["cursor"].as_str() { + cursor = format!("{}{}", "&cursor=", cursor_res); + continue; + } else { + break; + } + } + } + drop_mutability!(res_list); + Ok(res_list) } -/// `get_nft_metadata` function returns info of one specific NFT. -/// Current implementation sends request to Moralis. -/// Later, after adding caching, metadata lookup can be performed using previously obtained NFTs info without -/// sending new moralis request. The moralis request can be sent as a fallback, if the data was not found in the cache. -/// /// **Caution:** ERC-1155 token can have a total supply more than 1, which means there could be several owners /// of the same token. `get_nft_metadata` returns NFTs info with the most recent owner. /// **Dont** use this function to get specific info about owner address, amount etc, you will get info not related to my_address. -pub async fn get_nft_metadata(_ctx: MmArc, req: NftMetadataReq) -> MmResult { - let mut uri = req.url; +async fn get_moralis_metadata( + token_address: String, + token_id: BigDecimal, + chain: &Chain, + url: &Url, +) -> MmResult { + let mut uri = url.clone(); uri.set_path(MORALIS_API_ENDPOINT); uri.path_segments_mut() .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? .push("nft") - .push(&format!("{:#02x}", &req.token_address)) - .push(&req.token_id.to_string()); + .push(&token_address) + .push(&token_id.to_string()); uri.query_pairs_mut() - .append_pair("chain", &req.chain.to_string()) + .append_pair("chain", &chain.to_string()) .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE); drop_mutability!(uri); let response = send_request_to_uri(uri.as_str()).await?; - let nft_wrapper: NftWrapper = serde_json::from_str(&response.to_string())?; - let uri_meta = try_get_uri_meta(&nft_wrapper.token_uri).await?; - let nft_metadata = Nft { - chain: req.chain, - token_address: nft_wrapper.token_address, - token_id: nft_wrapper.token_id.0, - amount: nft_wrapper.amount.0, - owner_of: nft_wrapper.owner_of, - token_hash: nft_wrapper.token_hash, - block_number_minted: *nft_wrapper.block_number_minted, - block_number: *nft_wrapper.block_number, - contract_type: nft_wrapper.contract_type.map(|v| v.0), - collection_name: nft_wrapper.name, - symbol: nft_wrapper.symbol, - token_uri: nft_wrapper.token_uri, - metadata: nft_wrapper.metadata, - last_token_uri_sync: nft_wrapper.last_token_uri_sync, - last_metadata_sync: nft_wrapper.last_metadata_sync, - minter_address: nft_wrapper.minter_address, - possible_spam: nft_wrapper.possible_spam, - uri_meta, + let nft_moralis: NftFromMoralis = serde_json::from_str(&response.to_string())?; + let contract_type = match nft_moralis.contract_type { + Some(contract_type) => contract_type, + None => return MmError::err(GetNftInfoError::ContractTypeIsNull), }; + let nft_metadata = build_nft_from_moralis(chain, nft_moralis, contract_type).await; Ok(nft_metadata) } -/// `get_nft_transfers` function returns a transfer history of NFTs on requested chains owned by user. -/// Currently doesnt support filters. -pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult { - let mut res_list = Vec::new(); - - for chain in req.chains { - let my_address = get_eth_address(&ctx, &chain.to_ticker()).await?; - - let mut uri_without_cursor = req.url.clone(); - uri_without_cursor.set_path(MORALIS_API_ENDPOINT); - uri_without_cursor - .path_segments_mut() - .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? - .push(&my_address.wallet_address) - .push("nft") - .push("transfers"); - uri_without_cursor - .query_pairs_mut() - .append_pair("chain", &chain.to_string()) - .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE) - .append_pair(MORALIS_DIRECTION_QUERY_NAME, MORALIS_DIRECTION_QUERY_VALUE); - drop_mutability!(uri_without_cursor); - - // The cursor returned in the previous response (used for getting the next page). - let mut cursor = String::new(); - let wallet_address = my_address.wallet_address; - loop { - let uri = format!("{}{}", uri_without_cursor, cursor); - let response = send_request_to_uri(uri.as_str()).await?; - if let Some(transfer_list) = response["result"].as_array() { - for transfer in transfer_list { - let transfer_wrapper: NftTransferHistoryWrapper = serde_json::from_str(&transfer.to_string())?; - let status = get_tx_status(&wallet_address, &transfer_wrapper.to_address); - let req = NftMetadataReq { - token_address: Address::from_str(&transfer_wrapper.token_address) - .map_to_mm(|e| GetNftInfoError::AddressError(e.to_string()))?, - token_id: transfer_wrapper.token_id.clone(), - chain, - url: req.url.clone(), - }; - let nft_meta = get_nft_metadata(ctx.clone(), req).await?; - let transfer_history = NftTransferHistory { - chain, - block_number: *transfer_wrapper.block_number, - block_timestamp: transfer_wrapper.block_timestamp, - block_hash: transfer_wrapper.block_hash, - transaction_hash: transfer_wrapper.transaction_hash, - transaction_index: transfer_wrapper.transaction_index, - log_index: transfer_wrapper.log_index, - value: transfer_wrapper.value.0, - contract_type: transfer_wrapper.contract_type.0, - transaction_type: transfer_wrapper.transaction_type, - token_address: transfer_wrapper.token_address, - token_id: transfer_wrapper.token_id.0, - collection_name: nft_meta.collection_name, - image: nft_meta.uri_meta.image, - token_name: nft_meta.uri_meta.token_name, - from_address: transfer_wrapper.from_address, - to_address: transfer_wrapper.to_address, - status, - amount: transfer_wrapper.amount.0, - verified: transfer_wrapper.verified, - operator: transfer_wrapper.operator, - possible_spam: transfer_wrapper.possible_spam, - }; - // collect NFTs transfers from the page - res_list.push(transfer_history); - } - // if the cursor is not null, there are other NFTs transfers on next page, - // and we need to send new request with cursor to get info from the next page. - if let Some(cursor_res) = response["cursor"].as_str() { - cursor = format!("{}{}", "&cursor=", cursor_res); - continue; - } else { - break; - } - } - } - } - drop_mutability!(res_list); - let transfer_history_list = NftsTransferHistoryList { - transfer_history: res_list, - }; - Ok(transfer_history_list) -} - /// `withdraw_nft` function generates, signs and returns a transaction that transfers NFT /// from my address to recipient's address. /// This method generates a raw transaction which should then be broadcast using `send_raw_transaction`. pub async fn withdraw_nft(ctx: MmArc, req: WithdrawNftReq) -> WithdrawNftResult { - match req.withdraw_type { - WithdrawNftType::WithdrawErc1155(erc1155_withdraw) => withdraw_erc1155(ctx, erc1155_withdraw, req.url).await, - WithdrawNftType::WithdrawErc721(erc721_withdraw) => withdraw_erc721(ctx, erc721_withdraw).await, + match req { + WithdrawNftReq::WithdrawErc1155(erc1155_withdraw) => withdraw_erc1155(ctx, erc1155_withdraw).await, + WithdrawNftReq::WithdrawErc721(erc721_withdraw) => withdraw_erc721(ctx, erc721_withdraw).await, } } @@ -292,36 +435,46 @@ async fn send_request_to_uri(uri: &str) -> MmResult { Ok(response) } -/// This function uses `get_nft_list` method to get the correct info about amount of specific NFT owned by my_address. -pub(crate) async fn find_wallet_amount( - ctx: MmArc, - nft_list: NftListReq, - token_address_req: String, - token_id_req: BigDecimal, -) -> MmResult { - let nft_list = get_nft_list(ctx, nft_list).await?.nfts; - let nft = nft_list - .into_iter() - .find(|nft| nft.token_address == token_address_req && nft.token_id == token_id_req) - .ok_or_else(|| GetNftInfoError::TokenNotFoundInWallet { - token_address: token_address_req, - token_id: token_id_req.to_string(), - })?; - Ok(nft.amount) +/// `check_moralis_ipfs_bafy` inspects a given token URI and modifies it if certain conditions are met. +/// +/// It checks if the URI points to the Moralis IPFS domain `"ipfs.moralis.io"` and starts with a specific path prefix `"/ipfs/bafy"`. +/// If these conditions are satisfied, it modifies the URI to point to the `"ipfs.io"` domain. +/// This is due to certain "bafy"-prefixed hashes being banned on Moralis IPFS gateway due to abuse. +/// +/// If the URI does not meet these conditions or cannot be parsed, it is returned unchanged. +fn check_moralis_ipfs_bafy(token_uri: Option<&str>) -> Option { + token_uri.map(|uri| { + if let Ok(parsed_url) = Url::parse(uri) { + if parsed_url.host_str() == Some("ipfs.moralis.io") && parsed_url.path().starts_with("/ipfs/bafy") { + let parts: Vec<&str> = parsed_url.path().splitn(2, "/ipfs/").collect(); + format!("https://ipfs.io/ipfs/{}", parts[1]) + } else { + uri.to_string() + } + } else { + uri.to_string() + } + }) } -async fn try_get_uri_meta(token_uri: &Option) -> MmResult { - match token_uri { - Some(token_uri) => { - if let Ok(response_meta) = send_request_to_uri(token_uri).await { - let uri_meta_res: UriMeta = serde_json::from_str(&response_meta.to_string())?; - Ok(uri_meta_res) - } else { - Ok(UriMeta::default()) +async fn get_uri_meta(token_uri: Option<&str>, metadata: Option<&str>) -> UriMeta { + let mut uri_meta = UriMeta::default(); + if let Some(token_uri) = token_uri { + if let Ok(response_meta) = send_request_to_uri(token_uri).await { + if let Ok(token_uri_meta) = serde_json::from_value(response_meta) { + uri_meta = token_uri_meta; } - }, - None => Ok(UriMeta::default()), + } } + if let Some(metadata) = metadata { + if let Ok(meta_from_meta) = serde_json::from_str::(metadata) { + uri_meta.try_to_fill_missing_fields_from(meta_from_meta) + } + } + uri_meta.image_url = check_moralis_ipfs_bafy(uri_meta.image_url.as_deref()); + uri_meta.animation_url = check_moralis_ipfs_bafy(uri_meta.animation_url.as_deref()); + drop_mutability!(uri_meta); + uri_meta } fn get_tx_status(my_wallet: &str, to_address: &str) -> TransferStatus { @@ -332,3 +485,428 @@ fn get_tx_status(my_wallet: &str, to_address: &str) -> TransferStatus { TransferStatus::Send } } + +/// `update_nft_list` function gets nft transfers from NFT HISTORY table, iterates through them +/// and updates NFT LIST table info. +async fn update_nft_list( + ctx: MmArc, + storage: &T, + chain: &Chain, + scan_from_block: u64, + url: &Url, +) -> MmResult<(), UpdateNftError> { + let txs = storage.get_txs_from_block(chain, scan_from_block).await?; + let req = MyAddressReq { + coin: chain.to_ticker(), + }; + let my_address = get_my_address(ctx.clone(), req).await?.wallet_address.to_lowercase(); + for tx in txs.into_iter() { + handle_nft_tx(storage, chain, url, tx, &my_address).await?; + } + Ok(()) +} + +async fn handle_nft_tx( + storage: &T, + chain: &Chain, + url: &Url, + tx: NftTransferHistory, + my_address: &str, +) -> MmResult<(), UpdateNftError> { + match (tx.status, tx.contract_type) { + (TransferStatus::Send, ContractType::Erc721) => handle_send_erc721(storage, chain, tx).await, + (TransferStatus::Receive, ContractType::Erc721) => { + handle_receive_erc721(storage, chain, tx, url, my_address).await + }, + (TransferStatus::Send, ContractType::Erc1155) => handle_send_erc1155(storage, chain, tx).await, + (TransferStatus::Receive, ContractType::Erc1155) => { + handle_receive_erc1155(storage, chain, tx, url, my_address).await + }, + } +} + +async fn handle_send_erc721( + storage: &T, + chain: &Chain, + tx: NftTransferHistory, +) -> MmResult<(), UpdateNftError> { + let nft_db = storage + .get_nft( + chain, + eth_addr_to_hex(&tx.common.token_address), + tx.common.token_id.clone(), + ) + .await? + .ok_or_else(|| UpdateNftError::TokenNotFoundInWallet { + token_address: eth_addr_to_hex(&tx.common.token_address), + token_id: tx.common.token_id.to_string(), + })?; + let tx_meta = TxMeta::from(nft_db); + storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + storage + .remove_nft_from_list( + chain, + eth_addr_to_hex(&tx.common.token_address), + tx.common.token_id, + tx.block_number, + ) + .await?; + Ok(()) +} + +async fn handle_receive_erc721( + storage: &T, + chain: &Chain, + tx: NftTransferHistory, + url: &Url, + my_address: &str, +) -> MmResult<(), UpdateNftError> { + let nft = match storage + .get_nft( + chain, + eth_addr_to_hex(&tx.common.token_address), + tx.common.token_id.clone(), + ) + .await? + { + Some(mut nft_db) => { + // An error is raised if user tries to receive an identical ERC-721 token they already own + // and if owner address != from address + if my_address != eth_addr_to_hex(&tx.common.from_address) { + return MmError::err(UpdateNftError::AttemptToReceiveAlreadyOwnedErc721 { + tx_hash: tx.common.transaction_hash, + }); + } + nft_db.block_number = tx.block_number; + drop_mutability!(nft_db); + storage + .update_nft_amount_and_block_number(chain, nft_db.clone()) + .await?; + nft_db + }, + // If token isn't in NFT LIST table then add nft to the table. + None => { + let mut nft = get_moralis_metadata( + eth_addr_to_hex(&tx.common.token_address), + tx.common.token_id, + chain, + url, + ) + .await?; + // sometimes moralis updates Get All NFTs (which also affects Get Metadata) later + // than History by Wallet update + nft.common.owner_of = + Address::from_str(my_address).map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?; + nft.block_number = tx.block_number; + drop_mutability!(nft); + storage + .add_nfts_to_list(chain, vec![nft.clone()], tx.block_number) + .await?; + nft + }, + }; + let tx_meta = TxMeta::from(nft); + storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + Ok(()) +} + +async fn handle_send_erc1155( + storage: &T, + chain: &Chain, + tx: NftTransferHistory, +) -> MmResult<(), UpdateNftError> { + let mut nft_db = storage + .get_nft( + chain, + eth_addr_to_hex(&tx.common.token_address), + tx.common.token_id.clone(), + ) + .await? + .ok_or_else(|| UpdateNftError::TokenNotFoundInWallet { + token_address: eth_addr_to_hex(&tx.common.token_address), + token_id: tx.common.token_id.to_string(), + })?; + match nft_db.common.amount.cmp(&tx.common.amount) { + Ordering::Equal => { + storage + .remove_nft_from_list( + chain, + eth_addr_to_hex(&tx.common.token_address), + tx.common.token_id, + tx.block_number, + ) + .await?; + }, + Ordering::Greater => { + nft_db.common.amount -= tx.common.amount; + storage + .update_nft_amount(chain, nft_db.clone(), tx.block_number) + .await?; + }, + Ordering::Less => { + return MmError::err(UpdateNftError::InsufficientAmountInCache { + amount_list: nft_db.common.amount.to_string(), + amount_history: tx.common.amount.to_string(), + }); + }, + } + let tx_meta = TxMeta::from(nft_db); + storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + Ok(()) +} + +async fn handle_receive_erc1155( + storage: &T, + chain: &Chain, + tx: NftTransferHistory, + url: &Url, + my_address: &str, +) -> MmResult<(), UpdateNftError> { + let nft = match storage + .get_nft( + chain, + eth_addr_to_hex(&tx.common.token_address), + tx.common.token_id.clone(), + ) + .await? + { + Some(mut nft_db) => { + // if owner address == from address, then owner sent tokens to themself, + // which means that the amount will not change. + if my_address != eth_addr_to_hex(&tx.common.from_address) { + nft_db.common.amount += tx.common.amount; + } + nft_db.block_number = tx.block_number; + drop_mutability!(nft_db); + storage + .update_nft_amount_and_block_number(chain, nft_db.clone()) + .await?; + nft_db + }, + // If token isn't in NFT LIST table then add nft to the table. + None => { + let moralis_meta = get_moralis_metadata( + eth_addr_to_hex(&tx.common.token_address), + tx.common.token_id.clone(), + chain, + url, + ) + .await?; + let token_uri = check_moralis_ipfs_bafy(moralis_meta.common.token_uri.as_deref()); + let uri_meta = get_uri_meta(token_uri.as_deref(), moralis_meta.common.metadata.as_deref()).await; + let nft = Nft { + common: NftCommon { + token_address: moralis_meta.common.token_address, + token_id: moralis_meta.common.token_id, + amount: tx.common.amount, + owner_of: Address::from_str(my_address) + .map_to_mm(|e| UpdateNftError::InvalidHexString(e.to_string()))?, + token_hash: moralis_meta.common.token_hash, + collection_name: moralis_meta.common.collection_name, + symbol: moralis_meta.common.symbol, + token_uri, + metadata: moralis_meta.common.metadata, + last_token_uri_sync: moralis_meta.common.last_token_uri_sync, + last_metadata_sync: moralis_meta.common.last_metadata_sync, + minter_address: moralis_meta.common.minter_address, + possible_spam: moralis_meta.common.possible_spam, + }, + chain: *chain, + block_number_minted: moralis_meta.block_number_minted, + block_number: tx.block_number, + contract_type: moralis_meta.contract_type, + uri_meta, + }; + storage.add_nfts_to_list(chain, [nft.clone()], tx.block_number).await?; + nft + }, + }; + let tx_meta = TxMeta::from(nft); + storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + Ok(()) +} + +/// `find_wallet_nft_amount` function returns NFT amount of cached NFT. +/// Note: in db **token_address** is kept in **lowercase**, because Moralis returns all addresses in lowercase. +pub(crate) async fn find_wallet_nft_amount( + ctx: &MmArc, + chain: &Chain, + token_address: String, + token_id: BigDecimal, +) -> MmResult { + let nft_ctx = NftCtx::from_ctx(ctx).map_err(GetNftInfoError::Internal)?; + let _lock = nft_ctx.guard.lock().await; + + let storage = NftStorageBuilder::new(ctx).build()?; + if !NftListStorageOps::is_initialized(&storage, chain).await? { + NftListStorageOps::init(&storage, chain).await?; + } + let nft_meta = storage + .get_nft(chain, token_address.to_lowercase(), token_id.clone()) + .await? + .ok_or_else(|| GetNftInfoError::TokenNotFoundInWallet { + token_address, + token_id: token_id.to_string(), + })?; + Ok(nft_meta.common.amount) +} + +async fn cache_nfts_from_moralis( + ctx: &MmArc, + storage: &T, + chain: &Chain, + url: &Url, +) -> MmResult, UpdateNftError> { + let nft_list = get_moralis_nft_list(ctx, chain, url).await?; + let last_scanned_block = NftTxHistoryStorageOps::get_last_block_number(storage, chain) + .await? + .unwrap_or(0); + storage + .add_nfts_to_list(chain, nft_list.clone(), last_scanned_block) + .await?; + Ok(nft_list) +} + +/// `update_meta_in_txs` function updates only txs related to current nfts in wallet. +async fn update_meta_in_txs(storage: &T, chain: &Chain, nfts: Vec) -> MmResult<(), UpdateNftError> +where + T: NftListStorageOps + NftTxHistoryStorageOps, +{ + for nft in nfts.into_iter() { + let tx_meta = TxMeta::from(nft); + storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + } + Ok(()) +} + +/// `update_txs_with_empty_meta` function updates empty metadata in transfers. +async fn update_txs_with_empty_meta(storage: &T, chain: &Chain, url: &Url) -> MmResult<(), UpdateNftError> +where + T: NftListStorageOps + NftTxHistoryStorageOps, +{ + let nft_token_addr_id = storage.get_txs_with_empty_meta(chain).await?; + for addr_id_pair in nft_token_addr_id.into_iter() { + let nft_meta = get_moralis_metadata(addr_id_pair.token_address, addr_id_pair.token_id, chain, url).await?; + let tx_meta = TxMeta::from(nft_meta); + storage.update_txs_meta_by_token_addr_id(chain, tx_meta).await?; + } + Ok(()) +} + +/// `contains_disallowed_scheme` function checks if the text contains some link. +fn contains_disallowed_url(text: &str) -> Result { + let url_regex = Regex::new( + r"(?:(?:https?|ftp|file|[^:\s]+:)/?|[^:\s]+:/|\b(?:[a-z\d]+\.))(?:(?:[^\s()<>]+|\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))?\))+(?:\((?:[^\s()<>]+|(?:\(?:[^\s()<>]+\)))?\)|[^\s`!()\[\]{};:'.,<>?«»“”‘’]))?", + )?; + Ok(url_regex.is_match(text)) +} + +/// `check_and_redact_if_spam` checks if the text contains any links. +/// It doesn't matter if the link is valid or not, as this is a spam check. +/// If text contains some link, then it is a spam. +fn check_and_redact_if_spam(text: &mut Option) -> Result { + match text { + Some(s) if contains_disallowed_url(s)? => { + *text = Some("URL redacted for user protection".to_string()); + Ok(true) + }, + _ => Ok(false), + } +} + +/// `protect_from_history_spam` function checks and redact spam in `NftTransferHistory`. +/// +/// `collection_name` and `token_name` in `NftTransferHistory` shouldn't contain any links, +/// they must be just an arbitrary text, which represents NFT names. +fn protect_from_history_spam(tx: &mut NftTransferHistory) -> MmResult<(), ProtectFromSpamError> { + let collection_name_spam = check_and_redact_if_spam(&mut tx.collection_name)?; + let token_name_spam = check_and_redact_if_spam(&mut tx.token_name)?; + + if collection_name_spam || token_name_spam { + tx.common.possible_spam = true; + } + Ok(()) +} + +/// `protect_from_nft_spam` function checks and redact spam in `Nft`. +/// +/// `collection_name` and `token_name` in `Nft` shouldn't contain any links, +/// they must be just an arbitrary text, which represents NFT names. +/// `symbol` also must be a text or sign that represents a symbol. +fn protect_from_nft_spam(nft: &mut Nft) -> MmResult<(), ProtectFromSpamError> { + let collection_name_spam = check_and_redact_if_spam(&mut nft.common.collection_name)?; + let symbol_spam = check_and_redact_if_spam(&mut nft.common.symbol)?; + let token_name_spam = check_and_redact_if_spam(&mut nft.uri_meta.token_name)?; + let meta_spam = check_nft_metadata_for_spam(nft)?; + + if collection_name_spam || symbol_spam || token_name_spam || meta_spam { + nft.common.possible_spam = true; + } + Ok(()) +} +/// `check_nft_metadata_for_spam` function checks and redact spam in `metadata` field from `Nft`. +/// +/// **note:** `token_name` is usually called `name` in `metadata`. +fn check_nft_metadata_for_spam(nft: &mut Nft) -> MmResult { + if let Some(Ok(mut metadata)) = nft + .common + .metadata + .as_ref() + .map(|t| serde_json::from_str::>(t)) + { + if check_spam_and_redact_metadata_field(&mut metadata, "name")? { + nft.common.metadata = Some(serde_json::to_string(&metadata)?); + return Ok(true); + } + } + Ok(false) +} + +/// The `check_spam_and_redact_metadata_field` function scans a specified field in a JSON metadata object for potential spam. +/// +/// This function checks the provided `metadata` map for a field matching the `field` parameter. +/// If this field is found and its value contains some link, it's considered to contain spam. +/// To protect users, function redacts field containing spam link. +/// The function returns `true` if it detected spam link, or `false` otherwise. +fn check_spam_and_redact_metadata_field( + metadata: &mut serde_json::Map, + field: &str, +) -> MmResult { + match metadata.get(field).and_then(|v| v.as_str()) { + Some(text) if contains_disallowed_url(text)? => { + metadata.insert( + field.to_string(), + serde_json::Value::String("URL redacted for user protection".to_string()), + ); + Ok(true) + }, + _ => Ok(false), + } +} + +async fn build_nft_from_moralis(chain: &Chain, nft_moralis: NftFromMoralis, contract_type: ContractType) -> Nft { + let token_uri = check_moralis_ipfs_bafy(nft_moralis.common.token_uri.as_deref()); + let uri_meta = get_uri_meta(token_uri.as_deref(), nft_moralis.common.metadata.as_deref()).await; + Nft { + common: NftCommon { + token_address: nft_moralis.common.token_address, + token_id: nft_moralis.common.token_id, + amount: nft_moralis.common.amount, + owner_of: nft_moralis.common.owner_of, + token_hash: nft_moralis.common.token_hash, + collection_name: nft_moralis.common.collection_name, + symbol: nft_moralis.common.symbol, + token_uri, + metadata: nft_moralis.common.metadata, + last_token_uri_sync: nft_moralis.common.last_token_uri_sync, + last_metadata_sync: nft_moralis.common.last_metadata_sync, + minter_address: nft_moralis.common.minter_address, + possible_spam: nft_moralis.common.possible_spam, + }, + chain: *chain, + block_number_minted: nft_moralis.block_number_minted.map(|v| v.0), + block_number: *nft_moralis.block_number, + contract_type, + uri_meta, + } +} diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs index 1ca9d054c8..719c481dfc 100644 --- a/mm2src/coins/nft/nft_errors.rs +++ b/mm2src/coins/nft/nft_errors.rs @@ -1,5 +1,7 @@ use crate::eth::GetEthAddressError; -use common::HttpStatusCode; +use crate::nft::storage::{CreateNftStorageError, NftStorageError}; +use crate::{GetMyAddressError, WithdrawError}; +use common::{HttpStatusCode, ParseRfc3339Err}; use derive_more::Display; use enum_from::EnumFromStringify; use http::StatusCode; @@ -21,7 +23,7 @@ pub enum GetNftInfoError { Internal(String), GetEthAddressError(GetEthAddressError), #[display( - fmt = "Token: token_address {}, token_id {} was not find in wallet", + fmt = "Token: token_address {}, token_id {} was not found in wallet", token_address, token_id )] @@ -29,7 +31,28 @@ pub enum GetNftInfoError { token_address: String, token_id: String, }, - AddressError(String), + #[display(fmt = "DB error {}", _0)] + DbError(String), + ParseRfc3339Err(ParseRfc3339Err), + #[display(fmt = "The contract type is required and should not be null.")] + ContractTypeIsNull, + ProtectFromSpamError(ProtectFromSpamError), +} + +impl From for WithdrawError { + fn from(e: GetNftInfoError) -> Self { WithdrawError::GetNftInfoError(e) } +} + +impl From for GetNftInfoError { + fn from(e: SlurpError) -> Self { + let error_str = e.to_string(); + match e { + SlurpError::ErrorDeserializing { .. } => GetNftInfoError::InvalidResponse(error_str), + SlurpError::Transport { .. } | SlurpError::Timeout { .. } => GetNftInfoError::Transport(error_str), + SlurpError::InvalidRequest(_) => GetNftInfoError::InvalidRequest(error_str), + SlurpError::Internal(_) => GetNftInfoError::Internal(error_str), + } + } } impl From for GetNftInfoError { @@ -49,6 +72,18 @@ impl From for GetNftInfoError { fn from(e: GetEthAddressError) -> Self { GetNftInfoError::GetEthAddressError(e) } } +impl From for GetNftInfoError { + fn from(e: CreateNftStorageError) -> Self { + match e { + CreateNftStorageError::Internal(err) => GetNftInfoError::Internal(err), + } + } +} + +impl From for GetNftInfoError { + fn from(err: T) -> Self { GetNftInfoError::DbError(format!("{:?}", err)) } +} + impl From for GetNftInfoError { fn from(e: GetInfoFromUriError) -> Self { match e { @@ -60,16 +95,114 @@ impl From for GetNftInfoError { } } +impl From for GetNftInfoError { + fn from(e: ParseRfc3339Err) -> Self { GetNftInfoError::ParseRfc3339Err(e) } +} + +impl From for GetNftInfoError { + fn from(e: ProtectFromSpamError) -> Self { GetNftInfoError::ProtectFromSpamError(e) } +} + impl HttpStatusCode for GetNftInfoError { fn status_code(&self) -> StatusCode { match self { GetNftInfoError::InvalidRequest(_) => StatusCode::BAD_REQUEST, - GetNftInfoError::InvalidResponse(_) => StatusCode::FAILED_DEPENDENCY, + GetNftInfoError::InvalidResponse(_) | GetNftInfoError::ParseRfc3339Err(_) => StatusCode::FAILED_DEPENDENCY, + GetNftInfoError::ContractTypeIsNull => StatusCode::NOT_FOUND, GetNftInfoError::Transport(_) | GetNftInfoError::Internal(_) | GetNftInfoError::GetEthAddressError(_) | GetNftInfoError::TokenNotFoundInWallet { .. } - | GetNftInfoError::AddressError(_) => StatusCode::INTERNAL_SERVER_ERROR, + | GetNftInfoError::DbError(_) + | GetNftInfoError::ProtectFromSpamError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum UpdateNftError { + #[display(fmt = "DB error {}", _0)] + DbError(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), + GetNftInfoError(GetNftInfoError), + GetMyAddressError(GetMyAddressError), + #[display( + fmt = "Token: token_address {}, token_id {} was not found in wallet", + token_address, + token_id + )] + TokenNotFoundInWallet { + token_address: String, + token_id: String, + }, + #[display( + fmt = "Insufficient amount NFT token in the cache: amount in list table before transfer {}, transferred {}", + amount_list, + amount_history + )] + InsufficientAmountInCache { + amount_list: String, + amount_history: String, + }, + #[display( + fmt = "Last scanned nft block {} should be >= last block number {} in nft table", + last_scanned_block, + last_nft_block + )] + InvalidBlockOrder { + last_scanned_block: String, + last_nft_block: String, + }, + #[display( + fmt = "Last scanned block not found, while the last NFT block exists: {}", + last_nft_block + )] + LastScannedBlockNotFound { + last_nft_block: String, + }, + #[display(fmt = "Attempt to receive duplicate ERC721 token in transaction hash: {}", tx_hash)] + AttemptToReceiveAlreadyOwnedErc721 { + tx_hash: String, + }, + #[display(fmt = "Invalid hex string: {}", _0)] + InvalidHexString(String), +} + +impl From for UpdateNftError { + fn from(e: CreateNftStorageError) -> Self { + match e { + CreateNftStorageError::Internal(err) => UpdateNftError::Internal(err), + } + } +} + +impl From for UpdateNftError { + fn from(e: GetNftInfoError) -> Self { UpdateNftError::GetNftInfoError(e) } +} + +impl From for UpdateNftError { + fn from(e: GetMyAddressError) -> Self { UpdateNftError::GetMyAddressError(e) } +} + +impl From for UpdateNftError { + fn from(err: T) -> Self { UpdateNftError::DbError(format!("{:?}", err)) } +} + +impl HttpStatusCode for UpdateNftError { + fn status_code(&self) -> StatusCode { + match self { + UpdateNftError::DbError(_) + | UpdateNftError::Internal(_) + | UpdateNftError::GetNftInfoError(_) + | UpdateNftError::GetMyAddressError(_) + | UpdateNftError::TokenNotFoundInWallet { .. } + | UpdateNftError::InsufficientAmountInCache { .. } + | UpdateNftError::InvalidBlockOrder { .. } + | UpdateNftError::LastScannedBlockNotFound { .. } + | UpdateNftError::AttemptToReceiveAlreadyOwnedErc721 { .. } + | UpdateNftError::InvalidHexString(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -95,7 +228,16 @@ impl From for GetInfoFromUriError { match e { SlurpError::ErrorDeserializing { .. } => GetInfoFromUriError::InvalidResponse(error_str), SlurpError::Transport { .. } | SlurpError::Timeout { .. } => GetInfoFromUriError::Transport(error_str), - SlurpError::Internal(_) | SlurpError::InvalidRequest(_) => GetInfoFromUriError::Internal(error_str), + SlurpError::InvalidRequest(_) => GetInfoFromUriError::InvalidRequest(error_str), + SlurpError::Internal(_) => GetInfoFromUriError::Internal(error_str), } } } + +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize)] +pub enum ProtectFromSpamError { + #[from_stringify("regex::Error")] + RegexError(String), + #[from_stringify("serde_json::Error")] + SerdeError(String), +} diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index 11b01b844c..3db093540c 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -1,30 +1,62 @@ +use crate::nft::eth_addr_to_hex; use crate::{TransactionType, TxFeeDetails, WithdrawFee}; +use common::ten; use ethereum_types::Address; +use futures::lock::Mutex as AsyncMutex; +use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_number::BigDecimal; use rpc::v1::types::Bytes as BytesJson; use serde::Deserialize; use serde_json::Value as Json; use std::fmt; +use std::num::NonZeroUsize; use std::str::FromStr; +use std::sync::Arc; use url::Url; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::{ConstructibleDb, SharedDb}; + +#[cfg(target_arch = "wasm32")] +use crate::nft::storage::wasm::nft_idb::NftCacheIDB; + #[derive(Debug, Deserialize)] pub struct NftListReq { pub(crate) chains: Vec, - pub(crate) url: Url, + #[serde(default)] + pub(crate) max: bool, + #[serde(default = "ten")] + pub(crate) limit: usize, + pub(crate) page_number: Option, + #[serde(default)] + pub(crate) protect_from_spam: bool, } #[derive(Debug, Deserialize)] pub struct NftMetadataReq { + pub(crate) token_address: Address, + pub(crate) token_id: BigDecimal, + pub(crate) chain: Chain, + #[serde(default)] + pub(crate) protect_from_spam: bool, +} + +#[derive(Debug, Deserialize)] +pub struct RefreshMetadataReq { pub(crate) token_address: Address, pub(crate) token_id: BigDecimal, pub(crate) chain: Chain, pub(crate) url: Url, } +#[derive(Debug, Display)] +pub enum ParseChainTypeError { + UnsupportedChainType, +} + #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "UPPERCASE")] -pub(crate) enum Chain { +pub enum Chain { Avalanche, Bsc, Eth, @@ -32,18 +64,6 @@ pub(crate) enum Chain { Polygon, } -impl fmt::Display for Chain { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self { - Chain::Avalanche => write!(f, "AVALANCHE"), - Chain::Bsc => write!(f, "BSC"), - Chain::Eth => write!(f, "ETH"), - Chain::Fantom => write!(f, "FANTOM"), - Chain::Polygon => write!(f, "POLYGON"), - } - } -} - pub(crate) trait ConvertChain { fn to_ticker(&self) -> String; } @@ -60,12 +80,40 @@ impl ConvertChain for Chain { } } +impl fmt::Display for Chain { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Chain::Avalanche => write!(f, "AVALANCHE"), + Chain::Bsc => write!(f, "BSC"), + Chain::Eth => write!(f, "ETH"), + Chain::Fantom => write!(f, "FANTOM"), + Chain::Polygon => write!(f, "POLYGON"), + } + } +} + +impl FromStr for Chain { + type Err = ParseChainTypeError; + + #[inline] + fn from_str(s: &str) -> Result { + match s { + "AVALANCHE" => Ok(Chain::Avalanche), + "BSC" => Ok(Chain::Bsc), + "ETH" => Ok(Chain::Eth), + "FANTOM" => Ok(Chain::Fantom), + "POLYGON" => Ok(Chain::Polygon), + _ => Err(ParseChainTypeError::UnsupportedChainType), + } + } +} + #[derive(Debug, Display)] pub(crate) enum ParseContractTypeError { UnsupportedContractType, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "UPPERCASE")] pub(crate) enum ContractType { Erc1155, @@ -85,27 +133,77 @@ impl FromStr for ContractType { } } -#[derive(Debug, Default, Deserialize, Serialize)] +impl fmt::Display for ContractType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ContractType::Erc1155 => write!(f, "ERC1155"), + ContractType::Erc721 => write!(f, "ERC721"), + } + } +} + +/// `UriMeta` structure is the object which we create from `token_uri` and `metadata`. +/// +/// `token_uri` and `metadata` usually contain either `image` or `image_url` with image url. +/// But most often nft creators use only `image` name for this value (from my observation), +/// less often they use both parameters with the same url. +/// +/// I suspect this is because some APIs only look for one of these image url names, so nft creators try to satisfy all sides. +/// In any case, since there is no clear standard, we have to look for both options, +/// when we build `UriMeta` from `token_uri` or `metadata`. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub(crate) struct UriMeta { - pub(crate) image: Option, - #[serde(rename(deserialize = "name"))] + #[serde(rename = "image")] + pub(crate) raw_image_url: Option, + pub(crate) image_url: Option, + #[serde(rename = "name")] pub(crate) token_name: Option, - description: Option, - attributes: Option, - animation_url: Option, + pub(crate) description: Option, + pub(crate) attributes: Option, + pub(crate) animation_url: Option, + pub(crate) external_url: Option, + pub(crate) image_details: Option, } -#[derive(Debug, Serialize)] -pub struct Nft { - pub(crate) chain: Chain, - pub(crate) token_address: String, +impl UriMeta { + /// `try_to_fill_missing_fields_from` function doesnt change `raw_image_url` field. + /// It tries to update `image_url` field instead, if it is None. + /// As `image` is the original name of `raw_image_url` field in data from `token_uri` or `metadata`, + /// try to find **Some()** in this field first. + pub(crate) fn try_to_fill_missing_fields_from(&mut self, other: UriMeta) { + if self.image_url.is_none() { + self.image_url = other.raw_image_url.or(other.image_url); + } + if self.token_name.is_none() { + self.token_name = other.token_name; + } + if self.description.is_none() { + self.description = other.description; + } + if self.attributes.is_none() { + self.attributes = other.attributes; + } + if self.animation_url.is_none() { + self.animation_url = other.animation_url; + } + if self.external_url.is_none() { + self.external_url = other.external_url; + } + if self.image_details.is_none() { + self.image_details = other.image_details; + } + } +} + +/// [`NftCommon`] structure contains common fields from [`Nft`] and [`NftFromMoralis`] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NftCommon { + pub(crate) token_address: Address, pub(crate) token_id: BigDecimal, pub(crate) amount: BigDecimal, - pub(crate) owner_of: String, - pub(crate) token_hash: String, - pub(crate) block_number_minted: u64, - pub(crate) block_number: u64, - pub(crate) contract_type: Option, + pub(crate) owner_of: Address, + pub(crate) token_hash: Option, + #[serde(rename = "name")] pub(crate) collection_name: Option, pub(crate) symbol: Option, pub(crate) token_uri: Option, @@ -113,30 +211,29 @@ pub struct Nft { pub(crate) last_token_uri_sync: Option, pub(crate) last_metadata_sync: Option, pub(crate) minter_address: Option, - pub(crate) possible_spam: Option, + #[serde(default)] + pub(crate) possible_spam: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Nft { + #[serde(flatten)] + pub(crate) common: NftCommon, + pub(crate) chain: Chain, + pub(crate) block_number_minted: Option, + pub(crate) block_number: u64, + pub(crate) contract_type: ContractType, pub(crate) uri_meta: UriMeta, } -/// This structure is for deserializing NFT json to struct. -/// Its needed to convert fields properly, because all fields in json have string type. +/// This structure is for deserializing moralis NFT json to struct. #[derive(Debug, Deserialize)] -pub(crate) struct NftWrapper { - pub(crate) token_address: String, - pub(crate) token_id: SerdeStringWrap, - pub(crate) amount: SerdeStringWrap, - pub(crate) owner_of: String, - pub(crate) token_hash: String, - pub(crate) block_number_minted: SerdeStringWrap, +pub(crate) struct NftFromMoralis { + #[serde(flatten)] + pub(crate) common: NftCommon, + pub(crate) block_number_minted: Option>, pub(crate) block_number: SerdeStringWrap, - pub(crate) contract_type: Option>, - pub(crate) name: Option, - pub(crate) symbol: Option, - pub(crate) token_uri: Option, - pub(crate) metadata: Option, - pub(crate) last_token_uri_sync: Option, - pub(crate) last_metadata_sync: Option, - pub(crate) minter_address: Option, - pub(crate) possible_spam: Option, + pub(crate) contract_type: Option, } #[derive(Debug)] @@ -165,6 +262,8 @@ impl std::ops::Deref for SerdeStringWrap { #[derive(Debug, Serialize)] pub struct NftList { pub(crate) nfts: Vec, + pub(crate) skipped: usize, + pub(crate) total: usize, } #[derive(Clone, Deserialize)] @@ -188,16 +287,10 @@ pub struct WithdrawErc721 { pub(crate) fee: Option, } -#[derive(Clone, Deserialize)] -pub struct WithdrawNftReq { - pub(crate) url: Url, - pub(crate) withdraw_type: WithdrawNftType, -} - #[derive(Clone, Deserialize)] #[serde(tag = "type", content = "withdraw_data")] #[serde(rename_all = "snake_case")] -pub enum WithdrawNftType { +pub enum WithdrawNftReq { WithdrawErc1155(WithdrawErc1155), WithdrawErc721(WithdrawErc721), } @@ -232,65 +325,161 @@ pub struct TransactionNftDetails { #[derive(Debug, Deserialize)] pub struct NftTransfersReq { pub(crate) chains: Vec, - pub(crate) url: Url, + pub(crate) filters: Option, + #[serde(default)] + pub(crate) max: bool, + #[serde(default = "ten")] + pub(crate) limit: usize, + pub(crate) page_number: Option, + #[serde(default)] + pub(crate) protect_from_spam: bool, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Display)] +pub(crate) enum ParseTransferStatusError { + UnsupportedTransferStatus, +} + +#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Serialize)] pub(crate) enum TransferStatus { Receive, Send, } -#[derive(Debug, Serialize)] -pub(crate) struct NftTransferHistory { - pub(crate) chain: Chain, - pub(crate) block_number: u64, - pub(crate) block_timestamp: String, - pub(crate) block_hash: String, +impl FromStr for TransferStatus { + type Err = ParseTransferStatusError; + + #[inline] + fn from_str(s: &str) -> Result { + match s { + "Receive" => Ok(TransferStatus::Receive), + "Send" => Ok(TransferStatus::Send), + _ => Err(ParseTransferStatusError::UnsupportedTransferStatus), + } + } +} + +impl fmt::Display for TransferStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TransferStatus::Receive => write!(f, "Receive"), + TransferStatus::Send => write!(f, "Send"), + } + } +} + +/// [`NftTransferCommon`] structure contains common fields from [`NftTransferHistory`] and [`NftTxHistoryFromMoralis`] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NftTransferCommon { + pub(crate) block_hash: Option, /// Transaction hash in hexadecimal format pub(crate) transaction_hash: String, - pub(crate) transaction_index: u64, - pub(crate) log_index: u64, - pub(crate) value: BigDecimal, - pub(crate) contract_type: ContractType, - pub(crate) transaction_type: String, - pub(crate) token_address: String, + pub(crate) transaction_index: Option, + pub(crate) log_index: Option, + pub(crate) value: Option, + pub(crate) transaction_type: Option, + pub(crate) token_address: Address, pub(crate) token_id: BigDecimal, + pub(crate) from_address: Address, + pub(crate) to_address: Address, + pub(crate) amount: BigDecimal, + pub(crate) verified: Option, + pub(crate) operator: Option, + #[serde(default)] + pub(crate) possible_spam: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NftTransferHistory { + #[serde(flatten)] + pub(crate) common: NftTransferCommon, + pub(crate) chain: Chain, + pub(crate) block_number: u64, + pub(crate) block_timestamp: u64, + pub(crate) contract_type: ContractType, + pub(crate) token_uri: Option, pub(crate) collection_name: Option, - pub(crate) image: Option, + pub(crate) image_url: Option, pub(crate) token_name: Option, - pub(crate) from_address: String, - pub(crate) to_address: String, pub(crate) status: TransferStatus, - pub(crate) amount: BigDecimal, - pub(crate) verified: u64, - pub(crate) operator: Option, - pub(crate) possible_spam: Option, } +/// This structure is for deserializing moralis NFT transaction json to struct. #[derive(Debug, Deserialize)] -pub(crate) struct NftTransferHistoryWrapper { +pub(crate) struct NftTxHistoryFromMoralis { + #[serde(flatten)] + pub(crate) common: NftTransferCommon, pub(crate) block_number: SerdeStringWrap, pub(crate) block_timestamp: String, - pub(crate) block_hash: String, - /// Transaction hash in hexadecimal format - pub(crate) transaction_hash: String, - pub(crate) transaction_index: u64, - pub(crate) log_index: u64, - pub(crate) value: SerdeStringWrap, - pub(crate) contract_type: SerdeStringWrap, - pub(crate) transaction_type: String, - pub(crate) token_address: String, - pub(crate) token_id: SerdeStringWrap, - pub(crate) from_address: String, - pub(crate) to_address: String, - pub(crate) amount: SerdeStringWrap, - pub(crate) verified: u64, - pub(crate) operator: Option, - pub(crate) possible_spam: Option, + pub(crate) contract_type: Option, } #[derive(Debug, Serialize)] pub struct NftsTransferHistoryList { pub(crate) transfer_history: Vec, + pub(crate) skipped: usize, + pub(crate) total: usize, +} + +#[derive(Copy, Clone, Debug, Deserialize)] +pub struct NftTxHistoryFilters { + #[serde(default)] + pub receive: bool, + #[serde(default)] + pub(crate) send: bool, + pub(crate) from_date: Option, + pub(crate) to_date: Option, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateNftReq { + pub(crate) chains: Vec, + pub(crate) url: Url, +} + +#[derive(Debug, Deserialize, Eq, Hash, PartialEq)] +pub struct NftTokenAddrId { + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, +} + +#[derive(Debug)] +pub struct TxMeta { + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) token_uri: Option, + pub(crate) collection_name: Option, + pub(crate) image_url: Option, + pub(crate) token_name: Option, +} + +impl From for TxMeta { + fn from(nft_db: Nft) -> Self { + TxMeta { + token_address: eth_addr_to_hex(&nft_db.common.token_address), + token_id: nft_db.common.token_id, + token_uri: nft_db.common.token_uri, + collection_name: nft_db.common.collection_name, + image_url: nft_db.uri_meta.image_url, + token_name: nft_db.uri_meta.token_name, + } + } +} + +pub(crate) struct NftCtx { + pub(crate) guard: Arc>, + #[cfg(target_arch = "wasm32")] + pub(crate) nft_cache_db: SharedDb, +} + +impl NftCtx { + pub(crate) fn from_ctx(ctx: &MmArc) -> Result, String> { + Ok(try_s!(from_ctx(&ctx.nft_ctx, move || { + Ok(NftCtx { + guard: Arc::new(AsyncMutex::new(())), + #[cfg(target_arch = "wasm32")] + nft_cache_db: ConstructibleDb::new(ctx).into_shared(), + }) + }))) + } } diff --git a/mm2src/coins/nft/nft_tests.rs b/mm2src/coins/nft/nft_tests.rs index 1cbdacef24..7beb9e5f12 100644 --- a/mm2src/coins/nft/nft_tests.rs +++ b/mm2src/coins/nft/nft_tests.rs @@ -1,79 +1,192 @@ const NFT_LIST_URL_TEST: &str = "https://moralis-proxy.komodo.earth/api/v2/0x394d86994f954ed931b86791b62fe64f4c5dac37/nft?chain=POLYGON&format=decimal"; -const NFT_HISTORY_URL_TEST: &str = "https://moralis-proxy.komodo.earth/api/v2/0x394d86994f954ed931b86791b62fe64f4c5dac37/nft/transfers?chain=POLYGON&format=decimal&direction=both"; +const NFT_HISTORY_URL_TEST: &str = "https://moralis-proxy.komodo.earth/api/v2/0x394d86994f954ed931b86791b62fe64f4c5dac37/nft/transfers?chain=POLYGON&format=decimal"; const NFT_METADATA_URL_TEST: &str = "https://moralis-proxy.komodo.earth/api/v2/nft/0xed55e4477b795eaa9bb4bca24df42214e1a05c18/1111777?chain=POLYGON&format=decimal"; const TEST_WALLET_ADDR_EVM: &str = "0x394d86994f954ed931b86791b62fe64f4c5dac37"; #[cfg(all(test, not(target_arch = "wasm32")))] mod native_tests { - use crate::nft::nft_structs::{NftTransferHistoryWrapper, NftWrapper, UriMeta}; + use crate::eth::eth_addr_to_hex; + use crate::nft::nft_structs::{NftFromMoralis, NftTxHistoryFromMoralis, UriMeta}; use crate::nft::nft_tests::{NFT_HISTORY_URL_TEST, NFT_LIST_URL_TEST, NFT_METADATA_URL_TEST, TEST_WALLET_ADDR_EVM}; - use crate::nft::send_request_to_uri; + use crate::nft::storage::db_test_helpers::*; + use crate::nft::{check_and_redact_if_spam, check_moralis_ipfs_bafy, check_nft_metadata_for_spam, + send_request_to_uri}; use common::block_on; #[test] - fn test_moralis_nft_list() { - let response = block_on(send_request_to_uri(NFT_LIST_URL_TEST)).unwrap(); - let nfts_list = response["result"].as_array().unwrap(); - for nft_json in nfts_list { - let nft_wrapper: NftWrapper = serde_json::from_str(&nft_json.to_string()).unwrap(); - assert_eq!(TEST_WALLET_ADDR_EVM, nft_wrapper.owner_of); - } + fn test_moralis_ipfs_bafy() { + let uri = + "https://ipfs.moralis.io:2053/ipfs/bafybeifnek24coy5xj5qabdwh24dlp5omq34nzgvazkfyxgnqms4eidsiq/1.json"; + let res_uri = check_moralis_ipfs_bafy(Some(uri)); + let expected = "https://ipfs.io/ipfs/bafybeifnek24coy5xj5qabdwh24dlp5omq34nzgvazkfyxgnqms4eidsiq/1.json"; + assert_eq!(expected, res_uri.unwrap()); } #[test] - fn test_moralis_nft_transfer_history() { - let response = block_on(send_request_to_uri(NFT_HISTORY_URL_TEST)).unwrap(); - let mut transfer_list = response["result"].as_array().unwrap().clone(); - assert!(!transfer_list.is_empty()); - let first_tx = transfer_list.remove(transfer_list.len() - 1); - let transfer_wrapper: NftTransferHistoryWrapper = serde_json::from_str(&first_tx.to_string()).unwrap(); - assert_eq!(TEST_WALLET_ADDR_EVM, transfer_wrapper.to_address); + fn test_invalid_moralis_ipfs_link() { + let uri = "example.com/bafy?1=ipfs.moralis.io&e=https://"; + let res_uri = check_moralis_ipfs_bafy(Some(uri)); + assert_eq!(uri, res_uri.unwrap()); } #[test] - fn test_moralis_nft_metadata() { - let response = block_on(send_request_to_uri(NFT_METADATA_URL_TEST)).unwrap(); - let nft_wrapper: NftWrapper = serde_json::from_str(&response.to_string()).unwrap(); - assert_eq!(41237364, *nft_wrapper.block_number_minted); - let token_uri = nft_wrapper.token_uri.unwrap(); + fn test_check_for_spam() { + let mut spam_text = Some("https://arweave.net".to_string()); + assert!(check_and_redact_if_spam(&mut spam_text).unwrap()); + let url_redacted = "URL redacted for user protection"; + assert_eq!(url_redacted, spam_text.unwrap()); + + let mut spam_text = Some("ftp://123path ".to_string()); + assert!(check_and_redact_if_spam(&mut spam_text).unwrap()); + let url_redacted = "URL redacted for user protection"; + assert_eq!(url_redacted, spam_text.unwrap()); + + let mut spam_text = Some("/192.168.1.1/some.example.org?type=A".to_string()); + assert!(check_and_redact_if_spam(&mut spam_text).unwrap()); + let url_redacted = "URL redacted for user protection"; + assert_eq!(url_redacted, spam_text.unwrap()); + + let mut spam_text = Some(r"C:\Users\path\".to_string()); + assert!(check_and_redact_if_spam(&mut spam_text).unwrap()); + let url_redacted = "URL redacted for user protection"; + assert_eq!(url_redacted, spam_text.unwrap()); + + let mut valid_text = Some("Hello my name is NFT (The best ever!)".to_string()); + assert!(!check_and_redact_if_spam(&mut valid_text).unwrap()); + assert_eq!("Hello my name is NFT (The best ever!)", valid_text.unwrap()); + + let mut nft = nft(); + assert!(check_nft_metadata_for_spam(&mut nft).unwrap()); + let meta_redacted = "{\"name\":\"URL redacted for user protection\",\"image\":\"https://tikimetadata.s3.amazonaws.com/tiki_box.png\"}"; + assert_eq!(meta_redacted, nft.common.metadata.unwrap()) + } + + #[test] + fn test_moralis_requests() { + let response_nft_list = block_on(send_request_to_uri(NFT_LIST_URL_TEST)).unwrap(); + let nfts_list = response_nft_list["result"].as_array().unwrap(); + for nft_json in nfts_list { + let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string()).unwrap(); + assert_eq!(TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&nft_moralis.common.owner_of)); + } + + let response_tx_history = block_on(send_request_to_uri(NFT_HISTORY_URL_TEST)).unwrap(); + let mut transfer_list = response_tx_history["result"].as_array().unwrap().clone(); + assert!(!transfer_list.is_empty()); + let first_tx = transfer_list.remove(transfer_list.len() - 1); + let transfer_moralis: NftTxHistoryFromMoralis = serde_json::from_str(&first_tx.to_string()).unwrap(); + assert_eq!( + TEST_WALLET_ADDR_EVM, + eth_addr_to_hex(&transfer_moralis.common.to_address) + ); + + let response_meta = block_on(send_request_to_uri(NFT_METADATA_URL_TEST)).unwrap(); + let nft_moralis: NftFromMoralis = serde_json::from_str(&response_meta.to_string()).unwrap(); + assert_eq!(41237364, *nft_moralis.block_number_minted.unwrap()); + let token_uri = nft_moralis.common.token_uri.unwrap(); let uri_response = block_on(send_request_to_uri(token_uri.as_str())).unwrap(); serde_json::from_str::(&uri_response.to_string()).unwrap(); } + + #[test] + fn test_add_get_nfts() { block_on(test_add_get_nfts_impl()) } + + #[test] + fn test_last_nft_blocks() { block_on(test_last_nft_blocks_impl()) } + + #[test] + fn test_nft_list() { block_on(test_nft_list_impl()) } + + #[test] + fn test_remove_nft() { block_on(test_remove_nft_impl()) } + + #[test] + fn test_refresh_metadata() { block_on(test_refresh_metadata_impl()) } + + #[test] + fn test_nft_amount() { block_on(test_nft_amount_impl()) } + + #[test] + fn test_add_get_txs() { block_on(test_add_get_txs_impl()) } + + #[test] + fn test_last_tx_block() { block_on(test_last_tx_block_impl()) } + + #[test] + fn test_tx_history() { block_on(test_tx_history_impl()) } + + #[test] + fn test_tx_history_filters() { block_on(test_tx_history_filters_impl()) } + + #[test] + fn test_get_update_tx_meta() { block_on(test_get_update_tx_meta_impl()) } } #[cfg(target_arch = "wasm32")] mod wasm_tests { - use crate::nft::nft_structs::{NftTransferHistoryWrapper, NftWrapper}; + use crate::eth::eth_addr_to_hex; + use crate::nft::nft_structs::{NftFromMoralis, NftTxHistoryFromMoralis}; use crate::nft::nft_tests::{NFT_HISTORY_URL_TEST, NFT_LIST_URL_TEST, NFT_METADATA_URL_TEST, TEST_WALLET_ADDR_EVM}; use crate::nft::send_request_to_uri; + use crate::nft::storage::db_test_helpers::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] - async fn test_moralis_nft_list() { - let response = send_request_to_uri(NFT_LIST_URL_TEST).await.unwrap(); - let nfts_list = response["result"].as_array().unwrap(); + async fn test_moralis_requests() { + let response_nft_list = send_request_to_uri(NFT_LIST_URL_TEST).await.unwrap(); + let nfts_list = response_nft_list["result"].as_array().unwrap(); for nft_json in nfts_list { - let nft_wrapper: NftWrapper = serde_json::from_str(&nft_json.to_string()).unwrap(); - assert_eq!(TEST_WALLET_ADDR_EVM, nft_wrapper.owner_of); + let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string()).unwrap(); + assert_eq!(TEST_WALLET_ADDR_EVM, eth_addr_to_hex(&nft_moralis.common.owner_of)); } - } - #[wasm_bindgen_test] - async fn test_moralis_nft_transfer_history() { - let response = send_request_to_uri(NFT_HISTORY_URL_TEST).await.unwrap(); - let mut transfer_list = response["result"].as_array().unwrap().clone(); + let response_tx_history = send_request_to_uri(NFT_HISTORY_URL_TEST).await.unwrap(); + let mut transfer_list = response_tx_history["result"].as_array().unwrap().clone(); assert!(!transfer_list.is_empty()); let first_tx = transfer_list.remove(transfer_list.len() - 1); - let transfer_wrapper: NftTransferHistoryWrapper = serde_json::from_str(&first_tx.to_string()).unwrap(); - assert_eq!(TEST_WALLET_ADDR_EVM, transfer_wrapper.to_address); + let transfer_moralis: NftTxHistoryFromMoralis = serde_json::from_str(&first_tx.to_string()).unwrap(); + assert_eq!( + TEST_WALLET_ADDR_EVM, + eth_addr_to_hex(&transfer_moralis.common.to_address) + ); + + let response_meta = send_request_to_uri(NFT_METADATA_URL_TEST).await.unwrap(); + let nft_moralis: NftFromMoralis = serde_json::from_str(&response_meta.to_string()).unwrap(); + assert_eq!(41237364, *nft_moralis.block_number_minted.unwrap()); } #[wasm_bindgen_test] - async fn test_moralis_nft_metadata() { - let response = send_request_to_uri(NFT_METADATA_URL_TEST).await.unwrap(); - let nft_wrapper: NftWrapper = serde_json::from_str(&response.to_string()).unwrap(); - assert_eq!(41237364, *nft_wrapper.block_number_minted); - } + async fn test_add_get_nfts() { test_add_get_nfts_impl().await } + + #[wasm_bindgen_test] + async fn test_last_nft_blocks() { test_last_nft_blocks_impl().await } + + #[wasm_bindgen_test] + async fn test_nft_list() { test_nft_list_impl().await } + + #[wasm_bindgen_test] + async fn test_remove_nft() { test_remove_nft_impl().await } + + #[wasm_bindgen_test] + async fn test_nft_amount() { test_nft_amount_impl().await } + + #[wasm_bindgen_test] + async fn test_refresh_metadata() { test_refresh_metadata_impl().await } + + #[wasm_bindgen_test] + async fn test_add_get_txs() { test_add_get_txs_impl().await } + + #[wasm_bindgen_test] + async fn test_last_tx_block() { test_last_tx_block_impl().await } + + #[wasm_bindgen_test] + async fn test_tx_history() { test_tx_history_impl().await } + + #[wasm_bindgen_test] + async fn test_tx_history_filters() { test_tx_history_filters_impl().await } + + #[wasm_bindgen_test] + async fn test_get_update_tx_meta() { test_get_update_tx_meta_impl().await } } diff --git a/mm2src/coins/nft/storage/db_test_helpers.rs b/mm2src/coins/nft/storage/db_test_helpers.rs new file mode 100644 index 0000000000..44e0ede9e1 --- /dev/null +++ b/mm2src/coins/nft/storage/db_test_helpers.rs @@ -0,0 +1,581 @@ +use crate::eth::eth_addr_to_hex; +use crate::nft::nft_structs::{Chain, ContractType, Nft, NftCommon, NftTransferCommon, NftTransferHistory, + NftTxHistoryFilters, TransferStatus, TxMeta, UriMeta}; +use crate::nft::storage::{NftListStorageOps, NftStorageBuilder, NftTxHistoryStorageOps, RemoveNftResult}; +use ethereum_types::Address; +use mm2_number::BigDecimal; +use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; +use std::num::NonZeroUsize; +use std::str::FromStr; + +cfg_wasm32! { + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); +} + +const TOKEN_ADD: &str = "0xfd913a305d70a60aac4faac70c739563738e1f81"; +const TOKEN_ID: &str = "214300044414"; +const TX_HASH: &str = "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe"; + +pub(crate) fn nft() -> Nft { + Nft { + common: NftCommon { + token_address: Address::from_str("0x5c7d6712dfaf0cb079d48981781c8705e8417ca0").unwrap(), + token_id: Default::default(), + amount: BigDecimal::from_str("2").unwrap(), + owner_of: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + token_hash: Some("b34ddf294013d20a6d70691027625839".to_string()), + collection_name: None, + symbol: None, + token_uri: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.json".to_string()), + metadata: Some( + "{\"name\":\"https://arweave.net\",\"image\":\"https://tikimetadata.s3.amazonaws.com/tiki_box.png\"}" + .to_string(), + ), + last_token_uri_sync: Some("2023-02-07T17:10:08.402Z".to_string()), + last_metadata_sync: Some("2023-02-07T17:10:16.858Z".to_string()), + minter_address: Some("ERC1155 tokens don't have a single minter".to_string()), + possible_spam: false, + }, + chain: Chain::Bsc, + block_number_minted: Some(25465916), + block_number: 25919780, + contract_type: ContractType::Erc1155, + + uri_meta: UriMeta { + image_url: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.png".to_string()), + raw_image_url: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.png".to_string()), + token_name: None, + description: Some("Born to usher in Bull markets.".to_string()), + attributes: None, + animation_url: None, + external_url: None, + image_details: None, + }, + } +} + +fn tx() -> NftTransferHistory { + NftTransferHistory { + common: NftTransferCommon { + block_hash: Some("0x3d68b78391fb3cf8570df27036214f7e9a5a6a45d309197936f51d826041bfe7".to_string()), + transaction_hash: "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe".to_string(), + transaction_index: Some(198), + log_index: Some(495), + value: Default::default(), + transaction_type: Some("Single".to_string()), + token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), + token_id: BigDecimal::from_str("214300047252").unwrap(), + from_address: Address::from_str("0x6fad0ec6bb76914b2a2a800686acc22970645820").unwrap(), + to_address: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + amount: BigDecimal::from_str("1").unwrap(), + verified: Some(1), + operator: None, + possible_spam: false, + }, + chain: Chain::Bsc, + block_number: 28056726, + block_timestamp: 1683627432, + contract_type: ContractType::Erc721, + token_uri: None, + collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), + image_url: Some("https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string()), + token_name: Some("Nebula Nodes".to_string()), + status: TransferStatus::Receive, + } +} + +fn nft_list() -> Vec { + let nft = Nft { + common: NftCommon { + token_address: Address::from_str("0x5c7d6712dfaf0cb079d48981781c8705e8417ca0").unwrap(), + token_id: Default::default(), + amount: BigDecimal::from_str("2").unwrap(), + owner_of: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + token_hash: Some("b34ddf294013d20a6d70691027625839".to_string()), + collection_name: None, + symbol: None, + token_uri: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.json".to_string()), + metadata: Some("{\"name\":\"Tiki box\"}".to_string()), + last_token_uri_sync: Some("2023-02-07T17:10:08.402Z".to_string()), + last_metadata_sync: Some("2023-02-07T17:10:16.858Z".to_string()), + minter_address: Some("ERC1155 tokens don't have a single minter".to_string()), + possible_spam: false, + }, + chain: Chain::Bsc, + block_number_minted: Some(25465916), + block_number: 25919780, + contract_type: ContractType::Erc1155, + uri_meta: UriMeta { + image_url: Some("https://tikimetadata.s3.amazonaws.com/tiki_box.png".to_string()), + raw_image_url: None, + token_name: None, + description: Some("Born to usher in Bull markets.".to_string()), + attributes: None, + animation_url: None, + external_url: None, + image_details: None, + }, + }; + + let nft1 = Nft { + common: NftCommon { + token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), + token_id: BigDecimal::from_str("214300047252").unwrap(), + amount: BigDecimal::from_str("1").unwrap(), + owner_of: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + token_hash: Some("c5d1cfd75a0535b0ec750c0156e6ddfe".to_string()), + collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), + symbol: Some("BMBBBF".to_string()), + token_uri: Some("https://public.nftstatic.com/static/nft/BSC/BMBBBF/214300047252".to_string()), + metadata: Some( + "{\"image\":\"https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png\"}" + .to_string(), + ), + last_token_uri_sync: Some("2023-02-16T16:35:52.392Z".to_string()), + last_metadata_sync: Some("2023-02-16T16:36:04.283Z".to_string()), + minter_address: Some("0xdbdeb0895f3681b87fb3654b5cf3e05546ba24a9".to_string()), + possible_spam: false, + }, + chain: Chain::Bsc, + + block_number_minted: Some(25721963), + block_number: 28056726, + contract_type: ContractType::Erc721, + uri_meta: UriMeta { + image_url: Some( + "https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string(), + ), + raw_image_url: None, + token_name: Some("Nebula Nodes".to_string()), + description: Some("Interchain nodes".to_string()), + attributes: None, + animation_url: None, + external_url: None, + image_details: None, + }, + }; + + let nft2 = Nft { + common: NftCommon { + token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), + token_id: BigDecimal::from_str("214300044414").unwrap(), + amount: BigDecimal::from_str("1").unwrap(), + owner_of: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + token_hash: Some("125f8f4e952e107c257960000b4b250e".to_string()), + collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), + symbol: Some("BMBBBF".to_string()), + token_uri: Some("https://public.nftstatic.com/static/nft/BSC/BMBBBF/214300044414".to_string()), + metadata: Some( + "{\"image\":\"https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png\"}" + .to_string(), + ), + last_token_uri_sync: Some("2023-02-19T19:12:09.471Z".to_string()), + last_metadata_sync: Some("2023-02-19T19:12:18.080Z".to_string()), + minter_address: Some("0xdbdeb0895f3681b87fb3654b5cf3e05546ba24a9".to_string()), + possible_spam: false, + }, + chain: Chain::Bsc, + + block_number_minted: Some(25810308), + block_number: 28056721, + contract_type: ContractType::Erc721, + uri_meta: UriMeta { + image_url: Some( + "https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string(), + ), + raw_image_url: None, + token_name: Some("Nebula Nodes".to_string()), + description: Some("Interchain nodes".to_string()), + attributes: None, + animation_url: None, + external_url: None, + image_details: None, + }, + }; + vec![nft, nft1, nft2] +} + +fn nft_tx_history() -> Vec { + let tx = NftTransferHistory { + common: NftTransferCommon { + block_hash: Some("0xcb41654fc5cf2bf5d7fd3f061693405c74d419def80993caded0551ecfaeaae5".to_string()), + transaction_hash: "0x9c16b962f63eead1c5d2355cc9037dde178b14b53043c57eb40c27964d22ae6a".to_string(), + transaction_index: Some(57), + log_index: Some(139), + value: Default::default(), + transaction_type: Some("Single".to_string()), + token_address: Address::from_str("0x5c7d6712dfaf0cb079d48981781c8705e8417ca0").unwrap(), + token_id: Default::default(), + from_address: Address::from_str("0x4ff0bbc9b64d635a4696d1a38554fb2529c103ff").unwrap(), + to_address: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + amount: BigDecimal::from_str("1").unwrap(), + verified: Some(1), + operator: Some("0x4ff0bbc9b64d635a4696d1a38554fb2529c103ff".to_string()), + possible_spam: false, + }, + chain: Chain::Bsc, + block_number: 25919780, + block_timestamp: 1677166110, + contract_type: ContractType::Erc1155, + token_uri: None, + collection_name: None, + image_url: None, + token_name: None, + status: TransferStatus::Receive, + }; + + let tx1 = NftTransferHistory { + common: NftTransferCommon { + block_hash: Some("0x3d68b78391fb3cf8570df27036214f7e9a5a6a45d309197936f51d826041bfe7".to_string()), + transaction_hash: "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe".to_string(), + transaction_index: Some(198), + log_index: Some(495), + value: Default::default(), + transaction_type: Some("Single".to_string()), + token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), + token_id: BigDecimal::from_str("214300047252").unwrap(), + from_address: Address::from_str("0x6fad0ec6bb76914b2a2a800686acc22970645820").unwrap(), + to_address: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + amount: BigDecimal::from_str("1").unwrap(), + verified: Some(1), + operator: None, + possible_spam: false, + }, + chain: Chain::Bsc, + block_number: 28056726, + block_timestamp: 1683627432, + contract_type: ContractType::Erc721, + + token_uri: None, + collection_name: None, + image_url: None, + token_name: None, + + status: TransferStatus::Receive, + }; + + let tx2 = NftTransferHistory { + common: NftTransferCommon { + block_hash: Some("0x326db41c5a4fd5f033676d95c590ced18936ef2ef6079e873b23af087fd966c6".to_string()), + transaction_hash: "0x981bad702cc6e088f0e9b5e7287ff7a3487b8d269103cee3b9e5803141f63f91".to_string(), + transaction_index: Some(83), + log_index: Some(201), + value: Default::default(), + transaction_type: Some("Single".to_string()), + token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), + token_id: BigDecimal::from_str("214300044414").unwrap(), + from_address: Address::from_str("0x6fad0ec6bb76914b2a2a800686acc22970645820").unwrap(), + to_address: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), + amount: BigDecimal::from_str("1").unwrap(), + verified: Some(1), + operator: None, + possible_spam: false, + }, + chain: Chain::Bsc, + block_number: 28056721, + block_timestamp: 1683627417, + + contract_type: ContractType::Erc721, + + token_uri: None, + collection_name: Some("Binance NFT Mystery Box-Back to Blockchain Future".to_string()), + image_url: Some("https://public.nftstatic.com/static/nft/res/4df0a5da04174e1e9be04b22a805f605.png".to_string()), + token_name: Some("Nebula Nodes".to_string()), + + status: TransferStatus::Receive, + }; + vec![tx, tx1, tx2] +} + +async fn init_nft_list_storage(chain: &Chain) -> impl NftListStorageOps + NftTxHistoryStorageOps { + let ctx = mm_ctx_with_custom_db(); + let storage = NftStorageBuilder::new(&ctx).build().unwrap(); + NftListStorageOps::init(&storage, chain).await.unwrap(); + let is_initialized = NftListStorageOps::is_initialized(&storage, chain).await.unwrap(); + assert!(is_initialized); + storage +} + +async fn init_nft_history_storage(chain: &Chain) -> impl NftListStorageOps + NftTxHistoryStorageOps { + let ctx = mm_ctx_with_custom_db(); + let storage = NftStorageBuilder::new(&ctx).build().unwrap(); + NftTxHistoryStorageOps::init(&storage, chain).await.unwrap(); + let is_initialized = NftTxHistoryStorageOps::is_initialized(&storage, chain).await.unwrap(); + assert!(is_initialized); + storage +} + +pub(crate) async fn test_add_get_nfts_impl() { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); + + let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); + let nft = storage + .get_nft(&chain, TOKEN_ADD.to_string(), token_id) + .await + .unwrap() + .unwrap(); + assert_eq!(nft.block_number, 28056721); +} + +pub(crate) async fn test_last_nft_blocks_impl() { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); + + let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); + let nft = storage + .get_nft(&chain, TOKEN_ADD.to_string(), token_id) + .await + .unwrap() + .unwrap(); + assert_eq!(nft.block_number, 28056721); +} + +pub(crate) async fn test_nft_list_impl() { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); + + let nft_list = storage + .get_nft_list(vec![chain], false, 1, Some(NonZeroUsize::new(2).unwrap())) + .await + .unwrap(); + assert_eq!(nft_list.nfts.len(), 1); + let nft = nft_list.nfts.get(0).unwrap(); + assert_eq!(nft.block_number, 28056721); + assert_eq!(nft_list.skipped, 1); + assert_eq!(nft_list.total, 3); +} + +pub(crate) async fn test_remove_nft_impl() { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let nft_list = nft_list(); + storage.add_nfts_to_list(&chain, nft_list, 28056726).await.unwrap(); + + let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); + let remove_rslt = storage + .remove_nft_from_list(&chain, TOKEN_ADD.to_string(), token_id, 28056800) + .await + .unwrap(); + assert_eq!(remove_rslt, RemoveNftResult::NftRemoved); + let list_len = storage + .get_nft_list(vec![chain], true, 1, None) + .await + .unwrap() + .nfts + .len(); + assert_eq!(list_len, 2); + let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); + assert_eq!(last_scanned_block, 28056800); +} + +pub(crate) async fn test_nft_amount_impl() { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let mut nft = nft(); + storage + .add_nfts_to_list(&chain, vec![nft.clone()], 25919780) + .await + .unwrap(); + + nft.common.amount -= BigDecimal::from(1); + storage.update_nft_amount(&chain, nft.clone(), 25919800).await.unwrap(); + let amount = storage + .get_nft_amount( + &chain, + eth_addr_to_hex(&nft.common.token_address), + nft.common.token_id.clone(), + ) + .await + .unwrap() + .unwrap(); + assert_eq!(amount, "1"); + let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); + assert_eq!(last_scanned_block, 25919800); + + nft.common.amount += BigDecimal::from(1); + nft.block_number = 25919900; + storage + .update_nft_amount_and_block_number(&chain, nft.clone()) + .await + .unwrap(); + let amount = storage + .get_nft_amount(&chain, eth_addr_to_hex(&nft.common.token_address), nft.common.token_id) + .await + .unwrap() + .unwrap(); + assert_eq!(amount, "2"); + let last_scanned_block = storage.get_last_scanned_block(&chain).await.unwrap().unwrap(); + assert_eq!(last_scanned_block, 25919900); +} + +pub(crate) async fn test_refresh_metadata_impl() { + let chain = Chain::Bsc; + let storage = init_nft_list_storage(&chain).await; + let new_symbol = "NEW_SYMBOL"; + let mut nft = nft(); + storage + .add_nfts_to_list(&chain, vec![nft.clone()], 25919780) + .await + .unwrap(); + nft.common.symbol = Some(new_symbol.to_string()); + drop_mutability!(nft); + let token_add = eth_addr_to_hex(&nft.common.token_address); + let token_id = nft.common.token_id.clone(); + storage.refresh_nft_metadata(&chain, nft).await.unwrap(); + let nft_upd = storage.get_nft(&chain, token_add, token_id).await.unwrap().unwrap(); + assert_eq!(new_symbol.to_string(), nft_upd.common.symbol.unwrap()); +} + +pub(crate) async fn test_add_get_txs_impl() { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let txs = nft_tx_history(); + storage.add_txs_to_history(&chain, txs).await.unwrap(); + + let token_id = BigDecimal::from_str(TOKEN_ID).unwrap(); + let tx1 = storage + .get_txs_by_token_addr_id(&chain, TOKEN_ADD.to_string(), token_id) + .await + .unwrap() + .get(0) + .unwrap() + .clone(); + assert_eq!(tx1.block_number, 28056721); + let tx2 = storage + .get_tx_by_tx_hash(&chain, TX_HASH.to_string()) + .await + .unwrap() + .unwrap(); + assert_eq!(tx2.block_number, 28056726); + let tx_from = storage.get_txs_from_block(&chain, 28056721).await.unwrap(); + assert_eq!(tx_from.len(), 2); +} + +pub(crate) async fn test_last_tx_block_impl() { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let txs = nft_tx_history(); + storage.add_txs_to_history(&chain, txs).await.unwrap(); + + let last_block = NftTxHistoryStorageOps::get_last_block_number(&storage, &chain) + .await + .unwrap() + .unwrap(); + assert_eq!(last_block, 28056726); +} + +pub(crate) async fn test_tx_history_impl() { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let txs = nft_tx_history(); + storage.add_txs_to_history(&chain, txs).await.unwrap(); + + let tx_history = storage + .get_tx_history(vec![chain], false, 1, Some(NonZeroUsize::new(2).unwrap()), None) + .await + .unwrap(); + assert_eq!(tx_history.transfer_history.len(), 1); + let tx = tx_history.transfer_history.get(0).unwrap(); + assert_eq!(tx.block_number, 28056721); + assert_eq!(tx_history.skipped, 1); + assert_eq!(tx_history.total, 3); +} + +pub(crate) async fn test_tx_history_filters_impl() { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let txs = nft_tx_history(); + storage.add_txs_to_history(&chain, txs).await.unwrap(); + + let filters = NftTxHistoryFilters { + receive: true, + send: false, + from_date: None, + to_date: None, + }; + + let filters1 = NftTxHistoryFilters { + receive: false, + send: false, + from_date: None, + to_date: Some(1677166110), + }; + + let filters2 = NftTxHistoryFilters { + receive: false, + send: false, + from_date: Some(1677166110), + to_date: Some(1683627417), + }; + + let tx_history = storage + .get_tx_history(vec![chain], true, 1, None, Some(filters)) + .await + .unwrap(); + assert_eq!(tx_history.transfer_history.len(), 3); + let tx = tx_history.transfer_history.get(0).unwrap(); + assert_eq!(tx.block_number, 28056726); + + let tx_history1 = storage + .get_tx_history(vec![chain], true, 1, None, Some(filters1)) + .await + .unwrap(); + assert_eq!(tx_history1.transfer_history.len(), 1); + let tx1 = tx_history1.transfer_history.get(0).unwrap(); + assert_eq!(tx1.block_number, 25919780); + + let tx_history2 = storage + .get_tx_history(vec![chain], true, 1, None, Some(filters2)) + .await + .unwrap(); + assert_eq!(tx_history2.transfer_history.len(), 2); + let tx_0 = tx_history2.transfer_history.get(0).unwrap(); + assert_eq!(tx_0.block_number, 28056721); + let tx_1 = tx_history2.transfer_history.get(1).unwrap(); + assert_eq!(tx_1.block_number, 25919780); +} + +pub(crate) async fn test_get_update_tx_meta_impl() { + let chain = Chain::Bsc; + let storage = init_nft_history_storage(&chain).await; + let txs = nft_tx_history(); + storage.add_txs_to_history(&chain, txs).await.unwrap(); + + let vec_token_add_id = storage.get_txs_with_empty_meta(&chain).await.unwrap(); + assert_eq!(vec_token_add_id.len(), 2); + + let token_add = "0x5c7d6712dfaf0cb079d48981781c8705e8417ca0".to_string(); + let tx_meta = TxMeta { + token_address: token_add.clone(), + token_id: Default::default(), + token_uri: None, + collection_name: None, + image_url: None, + token_name: Some("Tiki box".to_string()), + }; + storage.update_txs_meta_by_token_addr_id(&chain, tx_meta).await.unwrap(); + let tx_upd = storage + .get_txs_by_token_addr_id(&chain, token_add, Default::default()) + .await + .unwrap(); + let tx_upd = tx_upd.get(0).unwrap(); + assert_eq!(tx_upd.token_name, Some("Tiki box".to_string())); + + let tx_meta = tx(); + storage.update_tx_meta_by_hash(&chain, tx_meta).await.unwrap(); + let tx_by_hash = storage + .get_tx_by_tx_hash(&chain, TX_HASH.to_string()) + .await + .unwrap() + .unwrap(); + assert_eq!(tx_by_hash.token_name, Some("Nebula Nodes".to_string())) +} diff --git a/mm2src/coins/nft/storage/mod.rs b/mm2src/coins/nft/storage/mod.rs new file mode 100644 index 0000000000..d5f4319a2a --- /dev/null +++ b/mm2src/coins/nft/storage/mod.rs @@ -0,0 +1,186 @@ +use crate::nft::nft_structs::{Chain, Nft, NftList, NftTokenAddrId, NftTransferHistory, NftTxHistoryFilters, + NftsTransferHistoryList, TxMeta}; +use crate::WithdrawError; +use async_trait::async_trait; +use derive_more::Display; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::MmResult; +use mm2_err_handle::mm_error::{NotEqual, NotMmError}; +use mm2_number::BigDecimal; +use serde::{Deserialize, Serialize}; +use std::num::NonZeroUsize; + +#[cfg(any(test, target_arch = "wasm32"))] +pub(crate) mod db_test_helpers; +#[cfg(not(target_arch = "wasm32"))] pub(crate) mod sql_storage; +#[cfg(target_arch = "wasm32")] pub(crate) mod wasm; + +#[derive(Debug, PartialEq)] +pub enum RemoveNftResult { + NftRemoved, + NftDidNotExist, +} + +pub trait NftStorageError: std::fmt::Debug + NotMmError + NotEqual + Send {} + +impl From for WithdrawError { + fn from(err: T) -> Self { WithdrawError::DbError(format!("{:?}", err)) } +} + +#[async_trait] +pub trait NftListStorageOps { + type Error: NftStorageError; + + /// Initializes tables in storage for the specified chain type. + async fn init(&self, chain: &Chain) -> MmResult<(), Self::Error>; + + /// Whether tables are initialized for the specified chain. + async fn is_initialized(&self, chain: &Chain) -> MmResult; + + async fn get_nft_list( + &self, + chains: Vec, + max: bool, + limit: usize, + page_number: Option, + ) -> MmResult; + + async fn add_nfts_to_list(&self, chain: &Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send; + + async fn get_nft( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error>; + + async fn remove_nft_from_list( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + scanned_block: u64, + ) -> MmResult; + + async fn get_nft_amount( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error>; + + async fn refresh_nft_metadata(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error>; + + /// `get_last_block_number` function returns the height of last block in NFT LIST table + async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error>; + + /// `get_last_scanned_block` function returns the height of last scanned block + /// when token was added or removed from MFT LIST table. + async fn get_last_scanned_block(&self, chain: &Chain) -> MmResult, Self::Error>; + + /// `update_nft_amount` function sets a new amount of a particular token in NFT LIST table + async fn update_nft_amount(&self, chain: &Chain, nft: Nft, scanned_block: u64) -> MmResult<(), Self::Error>; + + async fn update_nft_amount_and_block_number(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error>; +} + +#[async_trait] +pub trait NftTxHistoryStorageOps { + type Error: NftStorageError; + + /// Initializes tables in storage for the specified chain type. + async fn init(&self, chain: &Chain) -> MmResult<(), Self::Error>; + + /// Whether tables are initialized for the specified chain. + async fn is_initialized(&self, chain: &Chain) -> MmResult; + + async fn get_tx_history( + &self, + chains: Vec, + max: bool, + limit: usize, + page_number: Option, + filters: Option, + ) -> MmResult; + + async fn add_txs_to_history(&self, chain: &Chain, txs: I) -> MmResult<(), Self::Error> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send; + + async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error>; + + /// `get_txs_from_block` function returns transfers sorted by + /// block_number in ascending order. It is needed to update the NFT LIST table correctly. + async fn get_txs_from_block( + &self, + chain: &Chain, + from_block: u64, + ) -> MmResult, Self::Error>; + + async fn get_txs_by_token_addr_id( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error>; + + async fn get_tx_by_tx_hash( + &self, + chain: &Chain, + transaction_hash: String, + ) -> MmResult, Self::Error>; + + async fn update_tx_meta_by_hash(&self, chain: &Chain, tx: NftTransferHistory) -> MmResult<(), Self::Error>; + + async fn update_txs_meta_by_token_addr_id(&self, chain: &Chain, tx_meta: TxMeta) -> MmResult<(), Self::Error>; + + async fn get_txs_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error>; +} + +#[derive(Debug, Deserialize, Display, Serialize)] +pub enum CreateNftStorageError { + Internal(String), +} + +impl From for WithdrawError { + fn from(e: CreateNftStorageError) -> Self { + match e { + CreateNftStorageError::Internal(err) => WithdrawError::InternalError(err), + } + } +} + +/// `NftStorageBuilder` is used to create an instance that implements the [`NftListStorageOps`] +/// and [`NftTxHistoryStorageOps`] traits.Also has guard to lock write operations. +pub struct NftStorageBuilder<'a> { + ctx: &'a MmArc, +} + +impl<'a> NftStorageBuilder<'a> { + #[inline] + pub fn new(ctx: &MmArc) -> NftStorageBuilder<'_> { NftStorageBuilder { ctx } } + + /// `build` function is used to build nft storage which implements [`NftListStorageOps`] and [`NftTxHistoryStorageOps`] traits. + #[inline] + pub fn build(&self) -> MmResult { + #[cfg(target_arch = "wasm32")] + return wasm::wasm_storage::IndexedDbNftStorage::new(self.ctx); + #[cfg(not(target_arch = "wasm32"))] + sql_storage::SqliteNftStorage::new(self.ctx) + } +} + +/// `get_offset_limit` function calculates offset and limit for final result if we use pagination. +fn get_offset_limit(max: bool, limit: usize, page_number: Option, total_count: usize) -> (usize, usize) { + if max { + return (0, total_count); + } + match page_number { + Some(page) => ((page.get() - 1) * limit, limit), + None => (0, limit), + } +} diff --git a/mm2src/coins/nft/storage/sql_storage.rs b/mm2src/coins/nft/storage/sql_storage.rs new file mode 100644 index 0000000000..e37780111d --- /dev/null +++ b/mm2src/coins/nft/storage/sql_storage.rs @@ -0,0 +1,889 @@ +use crate::nft::eth_addr_to_hex; +use crate::nft::nft_structs::{Chain, ConvertChain, Nft, NftList, NftTokenAddrId, NftTransferHistory, + NftTxHistoryFilters, NftsTransferHistoryList, TxMeta}; +use crate::nft::storage::{get_offset_limit, CreateNftStorageError, NftListStorageOps, NftStorageError, + NftTxHistoryStorageOps, RemoveNftResult}; +use async_trait::async_trait; +use common::async_blocking; +use db_common::sql_build::{SqlCondition, SqlQuery}; +use db_common::sqlite::rusqlite::types::{FromSqlError, Type}; +use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row, Statement}; +use db_common::sqlite::sql_builder::SqlBuilder; +use db_common::sqlite::{query_single_row, string_from_row, validate_table_name, CHECK_TABLE_EXISTS_SQL}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::map_to_mm::MapToMmResult; +use mm2_err_handle::mm_error::{MmError, MmResult}; +use mm2_number::BigDecimal; +use serde_json::{self as json}; +use std::convert::TryInto; +use std::num::NonZeroUsize; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +fn nft_list_table_name(chain: &Chain) -> String { chain.to_ticker() + "_nft_list" } + +fn nft_tx_history_table_name(chain: &Chain) -> String { chain.to_ticker() + "_nft_tx_history" } + +fn scanned_nft_blocks_table_name() -> String { "scanned_nft_blocks".to_string() } + +fn create_nft_list_table_sql(chain: &Chain) -> MmResult { + let table_name = nft_list_table_name(chain); + validate_table_name(&table_name)?; + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + token_address VARCHAR(256) NOT NULL, + token_id VARCHAR(256) NOT NULL, + chain TEXT NOT NULL, + amount VARCHAR(256) NOT NULL, + block_number INTEGER NOT NULL, + contract_type TEXT NOT NULL, + details_json TEXT, + PRIMARY KEY (token_address, token_id) + );", + table_name + ); + Ok(sql) +} + +fn create_tx_history_table_sql(chain: &Chain) -> MmResult { + let table_name = nft_tx_history_table_name(chain); + validate_table_name(&table_name)?; + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + transaction_hash VARCHAR(256) PRIMARY KEY, + chain TEXT NOT NULL, + block_number INTEGER NOT NULL, + block_timestamp INTEGER NOT NULL, + contract_type TEXT NOT NULL, + token_address VARCHAR(256) NOT NULL, + token_id VARCHAR(256) NOT NULL, + status TEXT NOT NULL, + amount VARCHAR(256) NOT NULL, + token_uri TEXT, + collection_name TEXT, + image_url TEXT, + token_name TEXT, + details_json TEXT + );", + table_name + ); + Ok(sql) +} + +fn create_scanned_nft_blocks_sql() -> MmResult { + let table_name = scanned_nft_blocks_table_name(); + validate_table_name(&table_name)?; + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + chain TEXT PRIMARY KEY, + last_scanned_block INTEGER DEFAULT 0 + );", + table_name + ); + Ok(sql) +} + +impl NftStorageError for SqlError {} + +#[derive(Clone)] +pub struct SqliteNftStorage(Arc>); + +impl SqliteNftStorage { + pub fn new(ctx: &MmArc) -> MmResult { + let sqlite_connection = ctx + .sqlite_connection + .ok_or(MmError::new(CreateNftStorageError::Internal( + "sqlite_connection is not initialized".to_owned(), + )))?; + Ok(SqliteNftStorage(sqlite_connection.clone())) + } +} + +fn get_nft_list_builder_preimage(chains: Vec) -> MmResult { + let union_sql_strings = chains + .iter() + .map(|chain| { + let table_name = nft_list_table_name(chain); + validate_table_name(&table_name)?; + let sql_builder = SqlBuilder::select_from(table_name.as_str()); + let sql_string = sql_builder + .sql() + .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))? + .trim_end_matches(';') + .to_string(); + Ok(sql_string) + }) + .collect::, SqlError>>()?; + let union_alias_sql = format!("({}) AS nft_list", union_sql_strings.join(" UNION ALL ")); + let mut final_sql_builder = SqlBuilder::select_from(union_alias_sql); + final_sql_builder.order_desc("nft_list.block_number"); + drop_mutability!(final_sql_builder); + Ok(final_sql_builder) +} + +fn get_nft_tx_builder_preimage( + chains: Vec, + filters: Option, +) -> MmResult { + let union_sql_strings = chains + .into_iter() + .map(|chain| { + let table_name = nft_tx_history_table_name(&chain); + validate_table_name(&table_name)?; + let sql_builder = nft_history_table_builder_preimage(table_name.as_str(), filters)?; + let sql_string = sql_builder + .sql() + .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))? + .trim_end_matches(';') + .to_string(); + Ok(sql_string) + }) + .collect::, SqlError>>()?; + let union_alias_sql = format!("({}) AS nft_history", union_sql_strings.join(" UNION ALL ")); + let mut final_sql_builder = SqlBuilder::select_from(union_alias_sql); + final_sql_builder.order_desc("nft_history.block_timestamp"); + drop_mutability!(final_sql_builder); + Ok(final_sql_builder) +} + +fn nft_history_table_builder_preimage( + table_name: &str, + filters: Option, +) -> Result { + let mut sql_builder = SqlBuilder::select_from(table_name); + if let Some(filters) = filters { + if filters.send && !filters.receive { + sql_builder.and_where_eq("status", "'Send'"); + } else if filters.receive && !filters.send { + sql_builder.and_where_eq("status", "'Receive'"); + } + if let Some(date) = filters.from_date { + sql_builder.and_where(format!("block_timestamp >= {}", date)); + } + if let Some(date) = filters.to_date { + sql_builder.and_where(format!("block_timestamp <= {}", date)); + } + } + drop_mutability!(sql_builder); + Ok(sql_builder) +} + +fn finalize_nft_list_sql_builder( + mut sql_builder: SqlBuilder, + offset: usize, + limit: usize, +) -> MmResult { + let sql = sql_builder + .field("nft_list.details_json") + .offset(offset) + .limit(limit) + .sql() + .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))?; + Ok(sql) +} + +fn finalize_nft_history_sql_builder( + mut sql_builder: SqlBuilder, + offset: usize, + limit: usize, +) -> MmResult { + let sql = sql_builder + .field("nft_history.details_json") + .offset(offset) + .limit(limit) + .sql() + .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))?; + Ok(sql) +} + +fn nft_from_row(row: &Row<'_>) -> Result { + let json_string: String = row.get(0)?; + json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) +} + +fn tx_history_from_row(row: &Row<'_>) -> Result { + let json_string: String = row.get(0)?; + json::from_str(&json_string).map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e))) +} + +fn token_address_id_from_row(row: &Row<'_>) -> Result { + let token_address: String = row.get("token_address")?; + let token_id_str: String = row.get("token_id")?; + let token_id = BigDecimal::from_str(&token_id_str).map_err(|_| SqlError::from(FromSqlError::InvalidType))?; + Ok(NftTokenAddrId { + token_address, + token_id, + }) +} + +fn insert_nft_in_list_sql(chain: &Chain) -> MmResult { + let table_name = nft_list_table_name(chain); + validate_table_name(&table_name)?; + + let sql = format!( + "INSERT INTO {} ( + token_address, token_id, chain, amount, block_number, contract_type, details_json + ) VALUES ( + ?1, ?2, ?3, ?4, ?5, ?6, ?7 + );", + table_name + ); + Ok(sql) +} + +fn insert_tx_in_history_sql(chain: &Chain) -> MmResult { + let table_name = nft_tx_history_table_name(chain); + validate_table_name(&table_name)?; + + let sql = format!( + "INSERT INTO {} ( + transaction_hash, chain, block_number, block_timestamp, contract_type, + token_address, token_id, status, amount, collection_name, image_url, token_name, details_json + ) VALUES ( + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13 + );", + table_name + ); + Ok(sql) +} + +fn upsert_last_scanned_block_sql() -> MmResult { + let table_name = scanned_nft_blocks_table_name(); + validate_table_name(&table_name)?; + let sql = format!( + "INSERT OR REPLACE INTO {} (chain, last_scanned_block) VALUES (?1, ?2);", + table_name + ); + Ok(sql) +} + +fn update_details_json_by_token_add_id_sql(chain: &Chain, table_name_creator: F) -> MmResult +where + F: FnOnce(&Chain) -> String, +{ + let table_name = table_name_creator(chain); + + validate_table_name(&table_name)?; + let sql = format!( + "UPDATE {} SET details_json = ?1 WHERE token_address = ?2 AND token_id = ?3;", + table_name + ); + Ok(sql) +} + +fn update_meta_by_tx_hash_sql(chain: &Chain) -> MmResult { + let table_name = nft_tx_history_table_name(chain); + + validate_table_name(&table_name)?; + let sql = format!( + "UPDATE {} SET token_uri = ?1, collection_name = ?2, image_url = ?3, token_name = ?4, details_json = ?5 WHERE transaction_hash = ?6;", + table_name + ); + Ok(sql) +} + +fn update_nft_amount_sql(chain: &Chain, table_name_creator: F) -> MmResult +where + F: FnOnce(&Chain) -> String, +{ + let table_name = table_name_creator(chain); + + validate_table_name(&table_name)?; + let sql = format!( + "UPDATE {} SET amount = ?1, details_json = ?2 WHERE token_address = ?3 AND token_id = ?4;", + table_name + ); + Ok(sql) +} + +fn update_nft_amount_and_block_number_sql(chain: &Chain, table_name_creator: F) -> MmResult +where + F: FnOnce(&Chain) -> String, +{ + let table_name = table_name_creator(chain); + + validate_table_name(&table_name)?; + let sql = format!( + "UPDATE {} SET amount = ?1, block_number = ?2, details_json = ?3 WHERE token_address = ?4 AND token_id = ?5;", + table_name + ); + Ok(sql) +} + +fn get_nft_metadata_sql(chain: &Chain) -> MmResult { + let table_name = nft_list_table_name(chain); + validate_table_name(&table_name)?; + let sql = format!( + "SELECT details_json FROM {} WHERE token_address=?1 AND token_id=?2", + table_name + ); + Ok(sql) +} + +fn select_last_block_number_sql(chain: &Chain, table_name_creator: F) -> MmResult +where + F: FnOnce(&Chain) -> String, +{ + let table_name = table_name_creator(chain); + validate_table_name(&table_name)?; + let sql = format!( + "SELECT block_number FROM {} ORDER BY block_number DESC LIMIT 1", + table_name + ); + Ok(sql) +} + +fn select_last_scanned_block_sql() -> MmResult { + let table_name = scanned_nft_blocks_table_name(); + validate_table_name(&table_name)?; + let sql = format!("SELECT last_scanned_block FROM {} WHERE chain=?1", table_name,); + Ok(sql) +} + +fn get_nft_amount_sql(chain: &Chain, table_name_creator: F) -> MmResult +where + F: FnOnce(&Chain) -> String, +{ + let table_name = table_name_creator(chain); + validate_table_name(&table_name)?; + let sql = format!( + "SELECT amount FROM {} WHERE token_address=?1 AND token_id=?2", + table_name + ); + Ok(sql) +} + +fn delete_nft_sql(chain: &Chain, table_name_creator: F) -> Result> +where + F: FnOnce(&Chain) -> String, +{ + let table_name = table_name_creator(chain); + validate_table_name(&table_name)?; + let sql = format!("DELETE FROM {} WHERE token_address=?1 AND token_id=?2", table_name); + Ok(sql) +} + +fn block_number_from_row(row: &Row<'_>) -> Result { row.get::<_, i64>(0) } + +fn nft_amount_from_row(row: &Row<'_>) -> Result { row.get(0) } + +fn get_txs_from_block_builder<'a>( + conn: &'a Connection, + chain: &'a Chain, + from_block: u64, +) -> MmResult, SqlError> { + let table_name = nft_tx_history_table_name(chain); + validate_table_name(table_name.as_str())?; + let mut sql_builder = SqlQuery::select_from(conn, table_name.as_str())?; + sql_builder + .sql_builder() + .and_where(format!("block_number >= '{}'", from_block)) + .order_asc("block_number") + .field("details_json"); + drop_mutability!(sql_builder); + Ok(sql_builder) +} + +fn get_txs_by_token_addr_id_statement<'a>(conn: &'a Connection, chain: &'a Chain) -> MmResult, SqlError> { + let table_name = nft_tx_history_table_name(chain); + validate_table_name(table_name.as_str())?; + let sql_query = format!( + "SELECT details_json FROM {} WHERE token_address = ? AND token_id = ?", + table_name + ); + let stmt = conn.prepare(&sql_query)?; + Ok(stmt) +} + +fn get_txs_with_empty_meta_builder<'a>(conn: &'a Connection, chain: &'a Chain) -> MmResult, SqlError> { + let table_name = nft_tx_history_table_name(chain); + validate_table_name(table_name.as_str())?; + let mut sql_builder = SqlQuery::select_from(conn, table_name.as_str())?; + sql_builder + .sql_builder() + .distinct() + .field("token_address") + .field("token_id") + .and_where_is_null("token_uri") + .and_where_is_null("collection_name") + .and_where_is_null("image_url") + .and_where_is_null("token_name"); + drop_mutability!(sql_builder); + Ok(sql_builder) +} + +fn get_tx_by_tx_hash_sql(chain: &Chain) -> MmResult { + let table_name = nft_tx_history_table_name(chain); + validate_table_name(&table_name)?; + let sql = format!("SELECT details_json FROM {} WHERE transaction_hash=?1", table_name); + Ok(sql) +} + +#[async_trait] +impl NftListStorageOps for SqliteNftStorage { + type Error = SqlError; + + async fn init(&self, chain: &Chain) -> MmResult<(), Self::Error> { + let selfi = self.clone(); + let sql_nft_list = create_nft_list_table_sql(chain)?; + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + conn.execute(&sql_nft_list, []).map(|_| ())?; + conn.execute(&create_scanned_nft_blocks_sql()?, []).map(|_| ())?; + Ok(()) + }) + .await + } + + async fn is_initialized(&self, chain: &Chain) -> MmResult { + let table_name = nft_list_table_name(chain); + validate_table_name(&table_name)?; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let nft_list_initialized = query_single_row(&conn, CHECK_TABLE_EXISTS_SQL, [table_name], string_from_row)?; + let scanned_nft_blocks_initialized = query_single_row( + &conn, + CHECK_TABLE_EXISTS_SQL, + [scanned_nft_blocks_table_name()], + string_from_row, + )?; + Ok(nft_list_initialized.is_some() && scanned_nft_blocks_initialized.is_some()) + }) + .await + } + + async fn get_nft_list( + &self, + chains: Vec, + max: bool, + limit: usize, + page_number: Option, + ) -> MmResult { + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let sql_builder = get_nft_list_builder_preimage(chains)?; + let total_count_builder_sql = sql_builder + .clone() + .count("*") + .sql() + .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))?; + let total: isize = conn + .prepare(&total_count_builder_sql)? + .query_row([], |row| row.get(0))?; + let count_total = total.try_into().expect("count should not be failed"); + + let (offset, limit) = get_offset_limit(max, limit, page_number, count_total); + let sql = finalize_nft_list_sql_builder(sql_builder, offset, limit)?; + let nfts = conn + .prepare(&sql)? + .query_map([], nft_from_row)? + .collect::, _>>()?; + let result = NftList { + nfts, + skipped: offset, + total: count_total, + }; + Ok(result) + }) + .await + } + + async fn add_nfts_to_list(&self, chain: &Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send, + { + let selfi = self.clone(); + let chain = *chain; + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + + for nft in nfts { + let nft_json = json::to_string(&nft).expect("serialization should not fail"); + let params = [ + Some(eth_addr_to_hex(&nft.common.token_address)), + Some(nft.common.token_id.to_string()), + Some(nft.chain.to_string()), + Some(nft.common.amount.to_string()), + Some(nft.block_number.to_string()), + Some(nft.contract_type.to_string()), + Some(nft_json), + ]; + sql_transaction.execute(&insert_nft_in_list_sql(&chain)?, params)?; + } + let scanned_block_params = [chain.to_ticker(), last_scanned_block.to_string()]; + sql_transaction.execute(&upsert_last_scanned_block_sql()?, scanned_block_params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn get_nft( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error> { + let sql = get_nft_metadata_sql(chain)?; + let params = [token_address, token_id.to_string()]; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, params, nft_from_row).map_to_mm(SqlError::from) + }) + .await + } + + async fn remove_nft_from_list( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + scanned_block: u64, + ) -> MmResult { + let sql = delete_nft_sql(chain, nft_list_table_name)?; + let params = [token_address, token_id.to_string()]; + let scanned_block_params = [chain.to_ticker(), scanned_block.to_string()]; + let selfi = self.clone(); + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let rows_num = sql_transaction.execute(&sql, params)?; + + let remove_nft_result = if rows_num > 0 { + RemoveNftResult::NftRemoved + } else { + RemoveNftResult::NftDidNotExist + }; + sql_transaction.execute(&upsert_last_scanned_block_sql()?, scanned_block_params)?; + sql_transaction.commit()?; + Ok(remove_nft_result) + }) + .await + } + + async fn get_nft_amount( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error> { + let sql = get_nft_amount_sql(chain, nft_list_table_name)?; + let params = [token_address, token_id.to_string()]; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, params, nft_amount_from_row).map_to_mm(SqlError::from) + }) + .await + } + + async fn refresh_nft_metadata(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error> { + let sql = update_details_json_by_token_add_id_sql(chain, nft_list_table_name)?; + let nft_json = json::to_string(&nft).expect("serialization should not fail"); + let selfi = self.clone(); + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = [ + nft_json, + eth_addr_to_hex(&nft.common.token_address), + nft.common.token_id.to_string(), + ]; + sql_transaction.execute(&sql, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { + let sql = select_last_block_number_sql(chain, nft_list_table_name)?; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, [], block_number_from_row).map_to_mm(SqlError::from) + }) + .await? + .map(|b| b.try_into()) + .transpose() + .map_to_mm(|e| SqlError::FromSqlConversionFailure(2, Type::Integer, Box::new(e))) + } + + async fn get_last_scanned_block(&self, chain: &Chain) -> MmResult, Self::Error> { + let sql = select_last_scanned_block_sql()?; + let params = [chain.to_ticker()]; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, params, block_number_from_row).map_to_mm(SqlError::from) + }) + .await? + .map(|b| b.try_into()) + .transpose() + .map_to_mm(|e| SqlError::FromSqlConversionFailure(2, Type::Integer, Box::new(e))) + } + + async fn update_nft_amount(&self, chain: &Chain, nft: Nft, scanned_block: u64) -> MmResult<(), Self::Error> { + let sql = update_nft_amount_sql(chain, nft_list_table_name)?; + let nft_json = json::to_string(&nft).expect("serialization should not fail"); + let scanned_block_params = [chain.to_ticker(), scanned_block.to_string()]; + let selfi = self.clone(); + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = [ + Some(nft.common.amount.to_string()), + Some(nft_json), + Some(eth_addr_to_hex(&nft.common.token_address)), + Some(nft.common.token_id.to_string()), + ]; + sql_transaction.execute(&sql, params)?; + sql_transaction.execute(&upsert_last_scanned_block_sql()?, scanned_block_params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_nft_amount_and_block_number(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error> { + let sql = update_nft_amount_and_block_number_sql(chain, nft_list_table_name)?; + let nft_json = json::to_string(&nft).expect("serialization should not fail"); + let scanned_block_params = [chain.to_ticker(), nft.block_number.to_string()]; + let selfi = self.clone(); + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + let params = [ + Some(nft.common.amount.to_string()), + Some(nft.block_number.to_string()), + Some(nft_json), + Some(eth_addr_to_hex(&nft.common.token_address)), + Some(nft.common.token_id.to_string()), + ]; + sql_transaction.execute(&sql, params)?; + sql_transaction.execute(&upsert_last_scanned_block_sql()?, scanned_block_params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } +} + +#[async_trait] +impl NftTxHistoryStorageOps for SqliteNftStorage { + type Error = SqlError; + + async fn init(&self, chain: &Chain) -> MmResult<(), Self::Error> { + let selfi = self.clone(); + let sql_tx_history = create_tx_history_table_sql(chain)?; + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + conn.execute(&sql_tx_history, []).map(|_| ())?; + Ok(()) + }) + .await + } + + async fn is_initialized(&self, chain: &Chain) -> MmResult { + let table_name = nft_tx_history_table_name(chain); + validate_table_name(&table_name)?; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let nft_list_initialized = query_single_row(&conn, CHECK_TABLE_EXISTS_SQL, [table_name], string_from_row)?; + Ok(nft_list_initialized.is_some()) + }) + .await + } + + async fn get_tx_history( + &self, + chains: Vec, + max: bool, + limit: usize, + page_number: Option, + filters: Option, + ) -> MmResult { + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let sql_builder = get_nft_tx_builder_preimage(chains, filters)?; + let total_count_builder_sql = sql_builder + .clone() + .count("*") + .sql() + .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))?; + let total: isize = conn + .prepare(&total_count_builder_sql)? + .query_row([], |row| row.get(0))?; + let count_total = total.try_into().expect("count should not be failed"); + + let (offset, limit) = get_offset_limit(max, limit, page_number, count_total); + let sql = finalize_nft_history_sql_builder(sql_builder, offset, limit)?; + let txs = conn + .prepare(&sql)? + .query_map([], tx_history_from_row)? + .collect::, _>>()?; + let result = NftsTransferHistoryList { + transfer_history: txs, + skipped: offset, + total: count_total, + }; + Ok(result) + }) + .await + } + + async fn add_txs_to_history(&self, chain: &Chain, txs: I) -> MmResult<(), Self::Error> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send, + { + let selfi = self.clone(); + let chain = *chain; + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + + for tx in txs { + let tx_json = json::to_string(&tx).expect("serialization should not fail"); + let params = [ + Some(tx.common.transaction_hash), + Some(tx.chain.to_string()), + Some(tx.block_number.to_string()), + Some(tx.block_timestamp.to_string()), + Some(tx.contract_type.to_string()), + Some(eth_addr_to_hex(&tx.common.token_address)), + Some(tx.common.token_id.to_string()), + Some(tx.status.to_string()), + Some(tx.common.amount.to_string()), + tx.collection_name, + tx.image_url, + tx.token_name, + Some(tx_json), + ]; + sql_transaction.execute(&insert_tx_in_history_sql(&chain)?, params)?; + } + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { + let sql = select_last_block_number_sql(chain, nft_tx_history_table_name)?; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, [], block_number_from_row).map_to_mm(SqlError::from) + }) + .await? + .map(|b| b.try_into()) + .transpose() + .map_to_mm(|e| SqlError::FromSqlConversionFailure(2, Type::Integer, Box::new(e))) + } + + async fn get_txs_from_block( + &self, + chain: &Chain, + from_block: u64, + ) -> MmResult, Self::Error> { + let selfi = self.clone(); + let chain = *chain; + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let sql_builder = get_txs_from_block_builder(&conn, &chain, from_block)?; + let txs = sql_builder.query(tx_history_from_row)?; + Ok(txs) + }) + .await + } + + async fn get_txs_by_token_addr_id( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error> { + let selfi = self.clone(); + let chain = *chain; + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let mut stmt = get_txs_by_token_addr_id_statement(&conn, &chain)?; + let txs = stmt + .query_map([token_address, token_id.to_string()], tx_history_from_row)? + .collect::, _>>()?; + Ok(txs) + }) + .await + } + + async fn get_tx_by_tx_hash( + &self, + chain: &Chain, + transaction_hash: String, + ) -> MmResult, Self::Error> { + let sql = get_tx_by_tx_hash_sql(chain)?; + let selfi = self.clone(); + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + query_single_row(&conn, &sql, [transaction_hash], tx_history_from_row).map_to_mm(SqlError::from) + }) + .await + } + + async fn update_tx_meta_by_hash(&self, chain: &Chain, tx: NftTransferHistory) -> MmResult<(), Self::Error> { + let sql = update_meta_by_tx_hash_sql(chain)?; + let tx_json = json::to_string(&tx).expect("serialization should not fail"); + let params = [ + tx.token_uri, + tx.collection_name, + tx.image_url, + tx.token_name, + Some(tx_json), + Some(tx.common.transaction_hash), + ]; + let selfi = self.clone(); + async_blocking(move || { + let mut conn = selfi.0.lock().unwrap(); + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&sql, params)?; + sql_transaction.commit()?; + Ok(()) + }) + .await + } + + async fn update_txs_meta_by_token_addr_id(&self, chain: &Chain, tx_meta: TxMeta) -> MmResult<(), Self::Error> { + let selfi = self.clone(); + let txs = selfi + .get_txs_by_token_addr_id(chain, tx_meta.token_address, tx_meta.token_id) + .await?; + for mut tx in txs.into_iter() { + tx.token_uri = tx_meta.token_uri.clone(); + tx.collection_name = tx_meta.collection_name.clone(); + tx.image_url = tx_meta.image_url.clone(); + tx.token_name = tx_meta.token_name.clone(); + drop_mutability!(tx); + selfi.update_tx_meta_by_hash(chain, tx).await?; + } + Ok(()) + } + + async fn get_txs_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error> { + let selfi = self.clone(); + let chain = *chain; + async_blocking(move || { + let conn = selfi.0.lock().unwrap(); + let sql_builder = get_txs_with_empty_meta_builder(&conn, &chain)?; + let token_addr_id_pair = sql_builder.query(token_address_id_from_row)?; + Ok(token_addr_id_pair) + }) + .await + } +} diff --git a/mm2src/coins/nft/storage/wasm/mod.rs b/mm2src/coins/nft/storage/wasm/mod.rs new file mode 100644 index 0000000000..6bbc8738c4 --- /dev/null +++ b/mm2src/coins/nft/storage/wasm/mod.rs @@ -0,0 +1,62 @@ +use crate::nft::storage::NftStorageError; +use mm2_db::indexed_db::{DbTransactionError, InitDbError}; +use mm2_err_handle::prelude::*; +use mm2_number::bigdecimal::ParseBigDecimalError; + +pub(crate) mod nft_idb; +pub(crate) mod wasm_storage; + +pub type WasmNftCacheResult = MmResult; + +impl NftStorageError for WasmNftCacheError {} + +#[derive(Debug, Display)] +pub enum WasmNftCacheError { + ErrorSerializing(String), + ErrorDeserializing(String), + ErrorSaving(String), + ErrorLoading(String), + ErrorClearing(String), + NotSupported(String), + InternalError(String), + GetLastNftBlockError(String), + ParseBigDecimalError(ParseBigDecimalError), +} + +impl From for WasmNftCacheError { + fn from(e: InitDbError) -> Self { + match &e { + InitDbError::NotSupported(_) => WasmNftCacheError::NotSupported(e.to_string()), + InitDbError::EmptyTableList + | InitDbError::DbIsOpenAlready { .. } + | InitDbError::InvalidVersion(_) + | InitDbError::OpeningError(_) + | InitDbError::TypeMismatch { .. } + | InitDbError::UnexpectedState(_) + | InitDbError::UpgradingError { .. } => WasmNftCacheError::InternalError(e.to_string()), + } + } +} + +impl From for WasmNftCacheError { + fn from(e: DbTransactionError) -> Self { + match e { + DbTransactionError::ErrorSerializingItem(_) => WasmNftCacheError::ErrorSerializing(e.to_string()), + DbTransactionError::ErrorDeserializingItem(_) => WasmNftCacheError::ErrorDeserializing(e.to_string()), + DbTransactionError::ErrorUploadingItem(_) => WasmNftCacheError::ErrorSaving(e.to_string()), + DbTransactionError::ErrorGettingItems(_) | DbTransactionError::ErrorCountingItems(_) => { + WasmNftCacheError::ErrorLoading(e.to_string()) + }, + DbTransactionError::ErrorDeletingItems(_) => WasmNftCacheError::ErrorClearing(e.to_string()), + DbTransactionError::NoSuchTable { .. } + | DbTransactionError::ErrorCreatingTransaction(_) + | DbTransactionError::ErrorOpeningTable { .. } + | DbTransactionError::ErrorSerializingIndex { .. } + | DbTransactionError::UnexpectedState(_) + | DbTransactionError::TransactionAborted + | DbTransactionError::MultipleItemsByUniqueIndex { .. } + | DbTransactionError::NoSuchIndex { .. } + | DbTransactionError::InvalidIndex { .. } => WasmNftCacheError::InternalError(e.to_string()), + } + } +} diff --git a/mm2src/coins/nft/storage/wasm/nft_idb.rs b/mm2src/coins/nft/storage/wasm/nft_idb.rs new file mode 100644 index 0000000000..287bcab30a --- /dev/null +++ b/mm2src/coins/nft/storage/wasm/nft_idb.rs @@ -0,0 +1,32 @@ +use crate::nft::storage::wasm::wasm_storage::{LastScannedBlockTable, NftListTable, NftTxHistoryTable}; +use async_trait::async_trait; +use mm2_db::indexed_db::InitDbResult; +use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder}; + +const DB_NAME: &str = "nft_cache"; +const DB_VERSION: u32 = 1; +pub type NftCacheIDBLocked<'a> = DbLocked<'a, NftCacheIDB>; + +pub struct NftCacheIDB { + inner: IndexedDb, +} + +#[async_trait] +impl DbInstance for NftCacheIDB { + fn db_name() -> &'static str { DB_NAME } + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .with_table::() + .with_table::() + .build() + .await?; + Ok(NftCacheIDB { inner }) + } +} + +impl NftCacheIDB { + pub(crate) fn get_inner(&self) -> &IndexedDb { &self.inner } +} diff --git a/mm2src/coins/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs new file mode 100644 index 0000000000..6fb341c26e --- /dev/null +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -0,0 +1,699 @@ +use crate::eth::eth_addr_to_hex; +use crate::nft::nft_structs::{Chain, ContractType, Nft, NftCtx, NftList, NftTransferHistory, NftsTransferHistoryList, + TransferStatus, TxMeta}; +use crate::nft::storage::wasm::nft_idb::{NftCacheIDB, NftCacheIDBLocked}; +use crate::nft::storage::wasm::{WasmNftCacheError, WasmNftCacheResult}; +use crate::nft::storage::{get_offset_limit, CreateNftStorageError, NftListStorageOps, NftTokenAddrId, + NftTxHistoryFilters, NftTxHistoryStorageOps, RemoveNftResult}; +use async_trait::async_trait; +use common::is_initial_upgrade; +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{BeBigUint, DbTable, DbUpgrader, MultiIndex, OnUpgradeResult, SharedDb, TableSignature}; +use mm2_err_handle::map_mm_error::MapMmError; +use mm2_err_handle::map_to_mm::MapToMmResult; +use mm2_err_handle::prelude::MmResult; +use mm2_number::BigDecimal; +use num_traits::ToPrimitive; +use serde_json::{self as json, Value as Json}; +use std::collections::HashSet; +use std::num::NonZeroUsize; +use std::str::FromStr; + +#[derive(Clone)] +pub struct IndexedDbNftStorage { + db: SharedDb, +} + +impl IndexedDbNftStorage { + pub fn new(ctx: &MmArc) -> MmResult { + let nft_ctx = NftCtx::from_ctx(ctx).map_to_mm(CreateNftStorageError::Internal)?; + Ok(IndexedDbNftStorage { + db: nft_ctx.nft_cache_db.clone(), + }) + } + + async fn lock_db(&self) -> WasmNftCacheResult> { + self.db.get_or_initialize().await.mm_err(WasmNftCacheError::from) + } + + fn take_nft_according_to_paging_opts( + mut nfts: Vec, + max: bool, + limit: usize, + page_number: Option, + ) -> WasmNftCacheResult { + let total_count = nfts.len(); + nfts.sort_by(|a, b| b.block_number.cmp(&a.block_number)); + let (offset, limit) = get_offset_limit(max, limit, page_number, total_count); + Ok(NftList { + nfts: nfts.into_iter().skip(offset).take(limit).collect(), + skipped: offset, + total: total_count, + }) + } + + fn take_txs_according_to_paging_opts( + mut txs: Vec, + max: bool, + limit: usize, + page_number: Option, + ) -> WasmNftCacheResult { + let total_count = txs.len(); + txs.sort_by(|a, b| b.block_timestamp.cmp(&a.block_timestamp)); + let (offset, limit) = get_offset_limit(max, limit, page_number, total_count); + Ok(NftsTransferHistoryList { + transfer_history: txs.into_iter().skip(offset).take(limit).collect(), + skipped: offset, + total: total_count, + }) + } + + fn take_txs_according_to_filters( + txs: I, + filters: Option, + ) -> WasmNftCacheResult> + where + I: Iterator, + { + let mut filtered_txs = Vec::new(); + for tx_table in txs { + let tx = tx_details_from_item(tx_table)?; + if let Some(filters) = &filters { + if filters.is_status_match(&tx) && filters.is_date_match(&tx) { + filtered_txs.push(tx); + } + } else { + filtered_txs.push(tx); + } + } + Ok(filtered_txs) + } +} + +impl NftTxHistoryFilters { + fn is_status_match(&self, tx: &NftTransferHistory) -> bool { + (!self.receive && !self.send) + || (self.receive && tx.status == TransferStatus::Receive) + || (self.send && tx.status == TransferStatus::Send) + } + + fn is_date_match(&self, tx: &NftTransferHistory) -> bool { + self.from_date.map_or(true, |from| tx.block_timestamp >= from) + && self.to_date.map_or(true, |to| tx.block_timestamp <= to) + } +} + +#[async_trait] +impl NftListStorageOps for IndexedDbNftStorage { + type Error = WasmNftCacheError; + + async fn init(&self, _chain: &Chain) -> MmResult<(), Self::Error> { Ok(()) } + + async fn is_initialized(&self, _chain: &Chain) -> MmResult { Ok(true) } + + async fn get_nft_list( + &self, + chains: Vec, + max: bool, + limit: usize, + page_number: Option, + ) -> MmResult { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + let mut nfts = Vec::new(); + for chain in chains { + let items = table.get_items("chain", chain.to_string()).await?; + for (_item_id, item) in items.into_iter() { + let nft_detail = nft_details_from_item(item)?; + nfts.push(nft_detail); + } + } + Self::take_nft_according_to_paging_opts(nfts, max, limit, page_number) + } + + async fn add_nfts_to_list(&self, chain: &Chain, nfts: I, last_scanned_block: u64) -> MmResult<(), Self::Error> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send, + { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let nft_table = db_transaction.table::().await?; + let last_scanned_block_table = db_transaction.table::().await?; + for nft in nfts { + let nft_item = NftListTable::from_nft(&nft)?; + nft_table.add_item(&nft_item).await?; + } + let last_scanned_block = LastScannedBlockTable { + chain: chain.to_string(), + last_scanned_block: BeBigUint::from(last_scanned_block), + }; + last_scanned_block_table + .replace_item_by_unique_index("chain", chain.to_string(), &last_scanned_block) + .await?; + Ok(()) + } + + async fn get_nft( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(chain.to_string())? + .with_value(&token_address)? + .with_value(token_id.to_string())?; + + if let Some((_item_id, item)) = table.get_item_by_unique_multi_index(index_keys).await? { + Ok(Some(nft_details_from_item(item)?)) + } else { + Ok(None) + } + } + + async fn remove_nft_from_list( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + scanned_block: u64, + ) -> MmResult { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let nft_table = db_transaction.table::().await?; + let last_scanned_block_table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(chain.to_string())? + .with_value(&token_address)? + .with_value(token_id.to_string())?; + + let last_scanned_block = LastScannedBlockTable { + chain: chain.to_string(), + last_scanned_block: BeBigUint::from(scanned_block), + }; + + let nft_removed = nft_table.delete_item_by_unique_multi_index(index_keys).await?.is_some(); + last_scanned_block_table + .replace_item_by_unique_index("chain", chain.to_string(), &last_scanned_block) + .await?; + if nft_removed { + Ok(RemoveNftResult::NftRemoved) + } else { + Ok(RemoveNftResult::NftDidNotExist) + } + } + + async fn get_nft_amount( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(chain.to_string())? + .with_value(&token_address)? + .with_value(token_id.to_string())?; + + if let Some((_item_id, item)) = table.get_item_by_unique_multi_index(index_keys).await? { + Ok(Some(nft_details_from_item(item)?.common.amount.to_string())) + } else { + Ok(None) + } + } + + async fn refresh_nft_metadata(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(chain.to_string())? + .with_value(eth_addr_to_hex(&nft.common.token_address))? + .with_value(nft.common.token_id.to_string())?; + + let nft_item = NftListTable::from_nft(&nft)?; + table.replace_item_by_unique_multi_index(index_keys, &nft_item).await?; + Ok(()) + } + + async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + get_last_block_from_table(chain, table, NftListTable::CHAIN_BLOCK_NUMBER_INDEX).await + } + + async fn get_last_scanned_block(&self, chain: &Chain) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + if let Some((_item_id, item)) = table.get_item_by_unique_index("chain", chain.to_string()).await? { + let last_scanned_block = item + .last_scanned_block + .to_u64() + .ok_or_else(|| WasmNftCacheError::GetLastNftBlockError("height is too large".to_string()))?; + Ok(Some(last_scanned_block)) + } else { + Ok(None) + } + } + + async fn update_nft_amount(&self, chain: &Chain, nft: Nft, scanned_block: u64) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let nft_table = db_transaction.table::().await?; + let last_scanned_block_table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(chain.to_string())? + .with_value(eth_addr_to_hex(&nft.common.token_address))? + .with_value(nft.common.token_id.to_string())?; + + let nft_item = NftListTable::from_nft(&nft)?; + nft_table + .replace_item_by_unique_multi_index(index_keys, &nft_item) + .await?; + let last_scanned_block = LastScannedBlockTable { + chain: chain.to_string(), + last_scanned_block: BeBigUint::from(scanned_block), + }; + last_scanned_block_table + .replace_item_by_unique_index("chain", chain.to_string(), &last_scanned_block) + .await?; + Ok(()) + } + + async fn update_nft_amount_and_block_number(&self, chain: &Chain, nft: Nft) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let nft_table = db_transaction.table::().await?; + let last_scanned_block_table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(NftListTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(chain.to_string())? + .with_value(eth_addr_to_hex(&nft.common.token_address))? + .with_value(nft.common.token_id.to_string())?; + + let nft_item = NftListTable::from_nft(&nft)?; + nft_table + .replace_item_by_unique_multi_index(index_keys, &nft_item) + .await?; + let last_scanned_block = LastScannedBlockTable { + chain: chain.to_string(), + last_scanned_block: BeBigUint::from(nft.block_number), + }; + last_scanned_block_table + .replace_item_by_unique_index("chain", chain.to_string(), &last_scanned_block) + .await?; + Ok(()) + } +} + +#[async_trait] +impl NftTxHistoryStorageOps for IndexedDbNftStorage { + type Error = WasmNftCacheError; + + async fn init(&self, _chain: &Chain) -> MmResult<(), Self::Error> { Ok(()) } + + async fn is_initialized(&self, _chain: &Chain) -> MmResult { Ok(true) } + + async fn get_tx_history( + &self, + chains: Vec, + max: bool, + limit: usize, + page_number: Option, + filters: Option, + ) -> MmResult { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + let mut txs = Vec::new(); + for chain in chains { + let tx_tables = table + .get_items("chain", chain.to_string()) + .await? + .into_iter() + .map(|(_item_id, tx)| tx); + let filtered = Self::take_txs_according_to_filters(tx_tables, filters)?; + txs.extend(filtered); + } + Self::take_txs_according_to_paging_opts(txs, max, limit, page_number) + } + + async fn add_txs_to_history(&self, _chain: &Chain, txs: I) -> MmResult<(), Self::Error> + where + I: IntoIterator + Send + 'static, + I::IntoIter: Send, + { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + for tx in txs { + let tx_item = NftTxHistoryTable::from_tx_history(&tx)?; + table.add_item(&tx_item).await?; + } + Ok(()) + } + + async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + get_last_block_from_table(chain, table, NftTxHistoryTable::CHAIN_BLOCK_NUMBER_INDEX).await + } + + async fn get_txs_from_block( + &self, + chain: &Chain, + from_block: u64, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + let items = table + .cursor_builder() + .only("chain", chain.to_string()) + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .bound("block_number", BeBigUint::from(from_block), BeBigUint::from(u64::MAX)) + .open_cursor(NftTxHistoryTable::CHAIN_BLOCK_NUMBER_INDEX) + .await + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .collect() + .await + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + + let mut res = Vec::new(); + for (_item_id, item) in items.into_iter() { + let tx = tx_details_from_item(item)?; + res.push(tx); + } + Ok(res) + } + + async fn get_txs_by_token_addr_id( + &self, + chain: &Chain, + token_address: String, + token_id: BigDecimal, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(NftTxHistoryTable::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) + .with_value(chain.to_string())? + .with_value(&token_address)? + .with_value(token_id.to_string())?; + + table + .get_items_by_multi_index(index_keys) + .await? + .into_iter() + .map(|(_item_id, item)| tx_details_from_item(item)) + .collect() + } + + async fn get_tx_by_tx_hash( + &self, + chain: &Chain, + transaction_hash: String, + ) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + let index_keys = MultiIndex::new(NftTxHistoryTable::CHAIN_TX_HASH_INDEX) + .with_value(chain.to_string())? + .with_value(&transaction_hash)?; + + if let Some((_item_id, item)) = table.get_item_by_unique_multi_index(index_keys).await? { + Ok(Some(tx_details_from_item(item)?)) + } else { + Ok(None) + } + } + + async fn update_tx_meta_by_hash(&self, chain: &Chain, tx: NftTransferHistory) -> MmResult<(), Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + + let index_keys = MultiIndex::new(NftTxHistoryTable::CHAIN_TX_HASH_INDEX) + .with_value(chain.to_string())? + .with_value(&tx.common.transaction_hash)?; + + let item = NftTxHistoryTable::from_tx_history(&tx)?; + table.replace_item_by_unique_multi_index(index_keys, &item).await?; + Ok(()) + } + + async fn update_txs_meta_by_token_addr_id(&self, chain: &Chain, tx_meta: TxMeta) -> MmResult<(), Self::Error> { + let txs: Vec = self + .get_txs_by_token_addr_id(chain, tx_meta.token_address, tx_meta.token_id) + .await?; + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + for mut tx in txs { + tx.token_uri = tx_meta.token_uri.clone(); + tx.collection_name = tx_meta.collection_name.clone(); + tx.image_url = tx_meta.image_url.clone(); + tx.token_name = tx_meta.token_name.clone(); + drop_mutability!(tx); + + let index_keys = MultiIndex::new(NftTxHistoryTable::CHAIN_TX_HASH_INDEX) + .with_value(chain.to_string())? + .with_value(&tx.common.transaction_hash)?; + + let item = NftTxHistoryTable::from_tx_history(&tx)?; + table.replace_item_by_unique_multi_index(index_keys, &item).await?; + } + Ok(()) + } + + async fn get_txs_with_empty_meta(&self, chain: &Chain) -> MmResult, Self::Error> { + let locked_db = self.lock_db().await?; + let db_transaction = locked_db.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + let items = table + .cursor_builder() + .only("chain", chain.to_string()) + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .open_cursor("chain") + .await + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .collect() + .await + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + + let mut res = HashSet::new(); + for (_item_id, item) in items.into_iter() { + if item.token_uri.is_none() + && item.collection_name.is_none() + && item.image_url.is_none() + && item.token_name.is_none() + { + res.insert(NftTokenAddrId { + token_address: item.token_address, + token_id: BigDecimal::from_str(&item.token_id).map_err(WasmNftCacheError::ParseBigDecimalError)?, + }); + } + } + Ok(res.into_iter().collect()) + } +} + +/// `get_last_block_from_table` function returns the highest block in the table related to certain blockchain type. +async fn get_last_block_from_table( + chain: &Chain, + table: DbTable<'_, impl TableSignature + BlockNumberTable>, + cursor: &str, +) -> MmResult, WasmNftCacheError> { + let items = table + .cursor_builder() + .only("chain", chain.to_string()) + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + // Sets lower and upper bounds for block_number field + .bound("block_number", BeBigUint::from(0u64), BeBigUint::from(u64::MAX)) + // Opens a cursor by the specified index. + // In get_last_block_from_table case it is CHAIN_BLOCK_NUMBER_INDEX, as we need to search block_number for specific chain. + // Cursor returns values from the lowest to highest key indexes. + .open_cursor(cursor) + .await + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .collect() + .await + .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + + let maybe_item = items + .into_iter() + .last() + .map(|(_item_id, item)| { + item.get_block_number() + .to_u64() + .ok_or_else(|| WasmNftCacheError::GetLastNftBlockError("height is too large".to_string())) + }) + .transpose()?; + Ok(maybe_item) +} + +trait BlockNumberTable { + fn get_block_number(&self) -> &BeBigUint; +} + +impl BlockNumberTable for NftListTable { + fn get_block_number(&self) -> &BeBigUint { &self.block_number } +} + +impl BlockNumberTable for NftTxHistoryTable { + fn get_block_number(&self) -> &BeBigUint { &self.block_number } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct NftListTable { + token_address: String, + token_id: String, + chain: String, + amount: String, + block_number: BeBigUint, + contract_type: ContractType, + details_json: Json, +} + +impl NftListTable { + const CHAIN_TOKEN_ADD_TOKEN_ID_INDEX: &str = "chain_token_add_token_id_index"; + + const CHAIN_BLOCK_NUMBER_INDEX: &str = "chain_block_number_index"; + + fn from_nft(nft: &Nft) -> WasmNftCacheResult { + let details_json = json::to_value(nft).map_to_mm(|e| WasmNftCacheError::ErrorSerializing(e.to_string()))?; + Ok(NftListTable { + token_address: eth_addr_to_hex(&nft.common.token_address), + token_id: nft.common.token_id.to_string(), + chain: nft.chain.to_string(), + amount: nft.common.amount.to_string(), + block_number: BeBigUint::from(nft.block_number), + contract_type: nft.contract_type, + details_json, + }) + } +} + +impl TableSignature for NftListTable { + fn table_name() -> &'static str { "nft_list_cache_table" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if is_initial_upgrade(old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index( + Self::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, + &["chain", "token_address", "token_id"], + true, + )?; + table.create_multi_index(Self::CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; + table.create_index("chain", false)?; + table.create_index("block_number", false)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct NftTxHistoryTable { + transaction_hash: String, + chain: String, + block_number: BeBigUint, + block_timestamp: BeBigUint, + contract_type: ContractType, + token_address: String, + token_id: String, + status: TransferStatus, + amount: String, + token_uri: Option, + collection_name: Option, + image_url: Option, + token_name: Option, + details_json: Json, +} + +impl NftTxHistoryTable { + const CHAIN_TOKEN_ADD_TOKEN_ID_INDEX: &str = "chain_token_add_token_id_index"; + + const CHAIN_TX_HASH_INDEX: &str = "chain_tx_hash_index"; + + const CHAIN_BLOCK_NUMBER_INDEX: &str = "chain_block_number_index"; + + fn from_tx_history(tx: &NftTransferHistory) -> WasmNftCacheResult { + let details_json = json::to_value(tx).map_to_mm(|e| WasmNftCacheError::ErrorSerializing(e.to_string()))?; + Ok(NftTxHistoryTable { + transaction_hash: tx.common.transaction_hash.clone(), + chain: tx.chain.to_string(), + block_number: BeBigUint::from(tx.block_number), + block_timestamp: BeBigUint::from(tx.block_timestamp), + contract_type: tx.contract_type, + token_address: eth_addr_to_hex(&tx.common.token_address), + token_id: tx.common.token_id.to_string(), + status: tx.status, + amount: tx.common.amount.to_string(), + token_uri: tx.token_uri.clone(), + collection_name: tx.collection_name.clone(), + image_url: tx.image_url.clone(), + token_name: tx.token_name.clone(), + details_json, + }) + } +} + +impl TableSignature for NftTxHistoryTable { + fn table_name() -> &'static str { "nft_tx_history_cache_table" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if is_initial_upgrade(old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index( + Self::CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, + &["chain", "token_address", "token_id"], + false, + )?; + table.create_multi_index(Self::CHAIN_TX_HASH_INDEX, &["chain", "transaction_hash"], true)?; + table.create_multi_index(Self::CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; + table.create_index("block_number", false)?; + table.create_index("chain", false)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct LastScannedBlockTable { + chain: String, + last_scanned_block: BeBigUint, +} + +impl TableSignature for LastScannedBlockTable { + fn table_name() -> &'static str { "last_scanned_block_table" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if is_initial_upgrade(old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_index("chain", true)?; + } + Ok(()) + } +} + +fn nft_details_from_item(item: NftListTable) -> WasmNftCacheResult { + json::from_value(item.details_json).map_to_mm(|e| WasmNftCacheError::ErrorDeserializing(e.to_string())) +} + +fn tx_details_from_item(item: NftTxHistoryTable) -> WasmNftCacheResult { + json::from_value(item.details_json).map_to_mm(|e| WasmNftCacheError::ErrorDeserializing(e.to_string())) +} diff --git a/mm2src/coins/rpc_command/get_enabled_coins.rs b/mm2src/coins/rpc_command/get_enabled_coins.rs index 9390dd0fe5..543f6160eb 100644 --- a/mm2src/coins/rpc_command/get_enabled_coins.rs +++ b/mm2src/coins/rpc_command/get_enabled_coins.rs @@ -25,11 +25,11 @@ pub struct GetEnabledCoinsRequest; #[derive(Serialize)] pub struct GetEnabledCoinsResponse { - coins: Vec, + coins: Vec, } #[derive(Serialize)] -pub struct EnabledCoin { +pub struct EnabledCoinV2 { ticker: String, } @@ -42,7 +42,7 @@ pub async fn get_enabled_coins( let coins = coins_map .iter() - .map(|(ticker, _coin)| EnabledCoin { ticker: ticker.clone() }) + .map(|(ticker, _coin)| EnabledCoinV2 { ticker: ticker.clone() }) .collect(); Ok(GetEnabledCoinsResponse { coins }) } diff --git a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs index a490ef5cec..eb5d29430f 100644 --- a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs +++ b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs @@ -3,7 +3,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; use mm2_number::BigDecimal; -use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawResult}; +use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawFee, WithdrawResult}; #[derive(Clone, Deserialize)] pub struct IBCWithdrawRequest { @@ -15,6 +15,7 @@ pub struct IBCWithdrawRequest { #[serde(default)] pub(crate) max: bool, pub(crate) memo: Option, + pub(crate) fee: Option, } pub async fn ibc_withdraw(ctx: MmArc, req: IBCWithdrawRequest) -> WithdrawResult { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 6f09949284..5279e4fbef 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -27,7 +27,7 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; @@ -582,8 +582,10 @@ impl TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; // >> END TX SIMULATION FOR FEE CALCULATION + let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); + let fee_amount_u64 = coin - .calculate_fee_amount_as_u64(msg_transfer.clone(), timeout_height, memo.clone()) + .calculate_fee_amount_as_u64(msg_transfer.clone(), timeout_height, memo.clone(), req.fee) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); @@ -592,7 +594,7 @@ impl TendermintCoin { amount: fee_amount_u64.into(), }; - let fee = Fee::from_amount_and_gas(fee_amount, IBC_GAS_LIMIT_DEFAULT); + let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); let (amount_denom, total_amount) = if req.max { if balance_denom < fee_amount_u64 { @@ -654,7 +656,7 @@ impl TendermintCoin { coin: coin.ticker.clone(), amount: fee_amount_dec, uamount: fee_amount_u64, - gas_limit: IBC_GAS_LIMIT_DEFAULT, + gas_limit, })), coin: coin.ticker.to_string(), internal_id: hash.to_vec().into(), @@ -876,6 +878,7 @@ impl TendermintCoin { msg: Any, timeout_height: u64, memo: String, + withdraw_fee: Option, ) -> MmResult { let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); @@ -922,14 +925,16 @@ impl TendermintCoin { )) })?; - let amount = ((gas.gas_used as f64 * 1.5) * self.gas_price()).ceil(); + let (gas_price, gas_limit) = self.gas_info_for_withdraw(&withdraw_fee, GAS_LIMIT_DEFAULT); + + let amount = ((gas.gas_used as f64 * 1.5) * gas_price).ceil(); let fee_amount = Coin { denom: self.platform_denom().clone(), amount: (amount as u64).into(), }; - Ok(Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT)) + Ok(Fee::from_amount_and_gas(fee_amount, gas_limit)) } #[allow(deprecated)] @@ -938,6 +943,7 @@ impl TendermintCoin { msg: Any, timeout_height: u64, memo: String, + withdraw_fee: Option, ) -> MmResult { let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); @@ -984,7 +990,9 @@ impl TendermintCoin { )) })?; - Ok(((gas.gas_used as f64 * 1.5) * self.gas_price()).ceil() as u64) + let (gas_price, _) = self.gas_info_for_withdraw(&withdraw_fee, 0); + + Ok(((gas.gas_used as f64 * 1.5) * gas_price).ceil() as u64) } pub(super) async fn my_account_info(&self) -> MmResult { @@ -1222,7 +1230,8 @@ impl TendermintCoin { coin.calculate_fee( create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.to_owned() + TX_DEFAULT_MEMO.to_owned(), + None ) .await ); @@ -1276,7 +1285,7 @@ impl TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee = try_tx_s!( - coin.calculate_fee(tx_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned()) + coin.calculate_fee(tx_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), None) .await ); @@ -1518,6 +1527,7 @@ impl TendermintCoin { create_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), + None, ) .await?; @@ -1562,7 +1572,7 @@ impl TendermintCoin { .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let fee_uamount = self - .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned()) + .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), None) .await?; let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, decimals); @@ -1726,6 +1736,17 @@ impl TendermintCoin { unexpected_state => MmError::err(SearchForSwapTxSpendErr::UnexpectedHtlcState(unexpected_state)), } } + + pub(crate) fn gas_info_for_withdraw( + &self, + withdraw_fee: &Option, + fallback_gas_limit: u64, + ) -> (f64, u64) { + match withdraw_fee { + Some(WithdrawFee::CosmosGas { gas_price, gas_limit }) => (*gas_price, *gas_limit), + _ => (self.gas_price(), fallback_gas_limit), + } + } } fn clients_from_urls(rpc_urls: &[String]) -> MmResult, TendermintInitErrorKind> { @@ -1858,8 +1879,10 @@ impl MmCoin for TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; // >> END TX SIMULATION FOR FEE CALCULATION + let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let fee_amount_u64 = coin - .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, memo.clone()) + .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, memo.clone(), req.fee) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); @@ -1868,7 +1891,7 @@ impl MmCoin for TendermintCoin { amount: fee_amount_u64.into(), }; - let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); + let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); let (amount_denom, total_amount) = if req.max { if balance_denom < fee_amount_u64 { @@ -1914,6 +1937,7 @@ impl MmCoin for TendermintCoin { .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let hash = sha256(&tx_bytes); + Ok(TransactionDetails { tx_hash: hex::encode_upper(hash.as_slice()), tx_hex: tx_bytes.into(), @@ -1929,7 +1953,7 @@ impl MmCoin for TendermintCoin { coin: coin.ticker.clone(), amount: fee_amount_dec, uamount: fee_amount_u64, - gas_limit: GAS_LIMIT_DEFAULT, + gas_limit, })), coin: coin.ticker.to_string(), internal_id: hash.to_vec().into(), @@ -2325,7 +2349,8 @@ impl SwapOps for TendermintCoin { coin.calculate_fee( claim_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.to_owned() + TX_DEFAULT_MEMO.to_owned(), + None ) .await ); @@ -2378,6 +2403,7 @@ impl SwapOps for TendermintCoin { claim_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO.into(), + None ) .await ); @@ -2788,6 +2814,7 @@ pub mod tendermint_coin_tests { create_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), + None, ) .await .unwrap() @@ -2832,6 +2859,7 @@ pub mod tendermint_coin_tests { claim_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), + None, ) .await .unwrap() diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index c104fc6301..f4c128114d 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -175,8 +175,10 @@ impl TendermintToken { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); + let fee_amount_u64 = platform - .calculate_fee_amount_as_u64(msg_transfer.clone(), timeout_height, memo.clone()) + .calculate_fee_amount_as_u64(msg_transfer.clone(), timeout_height, memo.clone(), req.fee) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); @@ -194,7 +196,7 @@ impl TendermintToken { amount: fee_amount_u64.into(), }; - let fee = Fee::from_amount_and_gas(fee_amount, IBC_GAS_LIMIT_DEFAULT); + let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); let account_info = platform.my_account_info().await?; let tx_raw = platform @@ -221,7 +223,7 @@ impl TendermintToken { coin: platform.ticker().to_string(), amount: fee_amount_dec, uamount: fee_amount_u64, - gas_limit: IBC_GAS_LIMIT_DEFAULT, + gas_limit, })), coin: token.ticker.clone(), internal_id: hash.to_vec().into(), @@ -649,8 +651,10 @@ impl MmCoin for TendermintToken { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let fee_amount_u64 = platform - .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, memo.clone()) + .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, memo.clone(), req.fee) .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); @@ -668,7 +672,7 @@ impl MmCoin for TendermintToken { amount: fee_amount_u64.into(), }; - let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); + let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); let account_info = platform.my_account_info().await?; let tx_raw = platform @@ -695,7 +699,7 @@ impl MmCoin for TendermintToken { coin: platform.ticker().to_string(), amount: fee_amount_dec, uamount: fee_amount_u64, - gas_limit: GAS_LIMIT_DEFAULT, + gas_limit, })), coin: token.ticker.clone(), internal_id: hash.to_vec().into(), diff --git a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs index b418bba556..49993e4c6a 100644 --- a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use common::{async_blocking, PagingOptionsEnum}; use db_common::sql_build::*; use db_common::sqlite::rusqlite::types::Type; -use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row, NO_PARAMS}; +use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Row}; use db_common::sqlite::{query_single_row, string_from_row, validate_table_name, CHECK_TABLE_EXISTS_SQL}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -402,12 +402,12 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { async_blocking(move || { let conn = selfi.0.lock().unwrap(); - conn.execute(&sql_history, NO_PARAMS).map(|_| ())?; - conn.execute(&sql_addr, NO_PARAMS).map(|_| ())?; - conn.execute(&sql_cache, NO_PARAMS).map(|_| ())?; + conn.execute(&sql_history, []).map(|_| ())?; + conn.execute(&sql_addr, []).map(|_| ())?; + conn.execute(&sql_cache, []).map(|_| ())?; - conn.execute(&sql_history_index, NO_PARAMS).map(|_| ())?; - conn.execute(&sql_addr_index, NO_PARAMS).map(|_| ())?; + conn.execute(&sql_history_index, []).map(|_| ())?; + conn.execute(&sql_addr_index, []).map(|_| ())?; Ok(()) }) .await @@ -466,12 +466,12 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { token_id, tx_json, ]; - sql_transaction.execute(&insert_tx_in_history_sql(&wallet_id)?, ¶ms)?; + sql_transaction.execute(&insert_tx_in_history_sql(&wallet_id)?, params)?; let addresses: FilteringAddresses = tx.from.into_iter().chain(tx.to.into_iter()).collect(); for address in addresses { let params = [internal_id.clone(), address]; - sql_transaction.execute(&insert_tx_address_sql(&wallet_id)?, ¶ms)?; + sql_transaction.execute(&insert_tx_address_sql(&wallet_id)?, params)?; } } sql_transaction.commit()?; @@ -495,9 +495,9 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { let mut conn = selfi.0.lock().unwrap(); let sql_transaction = conn.transaction()?; - sql_transaction.execute(&remove_tx_addr_sql, ¶ms)?; + sql_transaction.execute(&remove_tx_addr_sql, params.clone())?; - let rows_num = sql_transaction.execute(&remove_tx_history_sql, ¶ms)?; + let rows_num = sql_transaction.execute(&remove_tx_history_sql, params)?; let remove_tx_result = if rows_num > 0 { RemoveTxResult::TxRemoved } else { @@ -532,7 +532,7 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { async_blocking(move || { let conn = selfi.0.lock().unwrap(); - query_single_row(&conn, &sql, NO_PARAMS, block_height_from_row).map_to_mm(SqlError::from) + query_single_row(&conn, &sql, [], block_height_from_row).map_to_mm(SqlError::from) }) .await } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index ab7c6ad16e..b089af276d 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -371,7 +371,16 @@ impl BchCoin { params.my_addresses, ) .await?; - let maybe_op_return: Script = tx.outputs[0].script_pubkey.clone().into(); + let maybe_op_return: Script = tx + .outputs + .get(0) + .ok_or(UtxoTxDetailsError::Internal(format!( + "Transaction {} has no outputs", + params.hash + )))? + .script_pubkey + .clone() + .into(); if !(maybe_op_return.is_pay_to_public_key_hash() || maybe_op_return.is_pay_to_public_key() || maybe_op_return.is_pay_to_script_hash()) @@ -1004,7 +1013,7 @@ impl SwapOps for BchCoin { MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) } - fn is_supported_by_watchers(&self) -> bool { true } + fn is_supported_by_watchers(&self) -> bool { std::env::var("USE_WATCHERS").is_ok() } } #[async_trait] diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 4a3220ed17..9640e66184 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -699,7 +699,7 @@ impl SwapOps for QtumCoin { MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) } - fn is_supported_by_watchers(&self) -> bool { true } + fn is_supported_by_watchers(&self) -> bool { std::env::var("USE_WATCHERS").is_ok() } } #[async_trait] diff --git a/mm2src/coins/utxo/utxo_block_header_storage/sql_block_header_storage.rs b/mm2src/coins/utxo/utxo_block_header_storage/sql_block_header_storage.rs index b60802c2a7..183c3abea1 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/sql_block_header_storage.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/sql_block_header_storage.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use chain::BlockHeader; use common::async_blocking; use db_common::{sqlite::rusqlite::Error as SqlError, - sqlite::rusqlite::{Connection, Row, ToSql, NO_PARAMS}, + sqlite::rusqlite::{params_from_iter, Connection, Row, ToSql}, sqlite::string_from_row, sqlite::validate_table_name, sqlite::CHECK_TABLE_EXISTS_SQL}; @@ -110,8 +110,7 @@ fn query_single_row( map_fn: F, ) -> Result, BlockHeaderStorageError> where - P: IntoIterator, - P::Item: ToSql, + P: db_common::sqlite::rusqlite::Params, F: FnOnce(&Row<'_>) -> Result, { db_common::sqlite::query_single_row(conn, query, params, map_fn).map_err(|e| BlockHeaderStorageError::QueryError { @@ -129,12 +128,12 @@ impl BlockHeaderStorageOps for SqliteBlockHeadersStorage { async_blocking(move || { let conn = selfi.conn.lock().unwrap(); - conn.execute(&sql_cache, NO_PARAMS).map(|_| ()).map_err(|e| { - BlockHeaderStorageError::InitializationError { + conn.execute(&sql_cache, []) + .map(|_| ()) + .map_err(|e| BlockHeaderStorageError::InitializationError { coin, reason: e.to_string(), - } - })?; + })?; Ok(()) }) .await @@ -246,7 +245,7 @@ impl BlockHeaderStorageOps for SqliteBlockHeadersStorage { async_blocking(move || { let conn = selfi.conn.lock().unwrap(); - query_single_row(&conn, &sql, NO_PARAMS, |row| row.get::<_, i64>(0)) + query_single_row(&conn, &sql, [], |row| row.get::<_, i64>(0)) }) .await .map_err(|e| BlockHeaderStorageError::GetFromStorageError { @@ -271,7 +270,7 @@ impl BlockHeaderStorageOps for SqliteBlockHeadersStorage { let maybe_header_raw = async_blocking(move || { let conn = selfi.conn.lock().unwrap(); - query_single_row(&conn, &sql, NO_PARAMS, string_from_row) + query_single_row(&conn, &sql, [], string_from_row) }) .await .map_err(|e| BlockHeaderStorageError::GetFromStorageError { @@ -320,7 +319,7 @@ impl BlockHeaderStorageOps for SqliteBlockHeadersStorage { async_blocking(move || { let conn = selfi.conn.lock().unwrap(); - conn.execute(&sql, ¶ms) + conn.execute(&sql, params_from_iter(params.iter())) }) .await .map_err(|err| BlockHeaderStorageError::UnableToDeleteHeaders { @@ -337,7 +336,7 @@ impl BlockHeaderStorageOps for SqliteBlockHeadersStorage { let table_name = get_table_name_and_validate(&self.ticker).unwrap(); let sql = format!("SELECT COUNT(block_height) FROM {table_name};"); let conn = self.conn.lock().unwrap(); - let rows_count: u32 = conn.query_row(&sql, NO_PARAMS, |row| row.get(0)).unwrap(); + let rows_count: u32 = conn.query_row(&sql, [], |row| row.get(0)).unwrap(); if rows_count == 0 { return Ok(()); }; diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index b80e0a98e4..5d359dad9a 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2928,11 +2928,20 @@ where if e.get().should_update_timestamp() || e.get().firo_negative_fee() { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => coin.as_ref().conf.ticker.clone(), "method" => "tx_detail_by_hash"); - if let Ok(tx_details) = coin.tx_details_by_hash(&txid.0, &mut input_transactions).await { - mm_counter!(ctx.metrics, "tx.history.response.count", 1, "coin" => coin.as_ref().conf.ticker.clone(), "method" => "tx_detail_by_hash"); - // replace with new tx details in case we need to update any data - e.insert(tx_details); - updated = true; + match coin.tx_details_by_hash(&txid.0, &mut input_transactions).await { + Ok(tx_details) => { + mm_counter!(ctx.metrics, "tx.history.response.count", 1, "coin" => coin.as_ref().conf.ticker.clone(), "method" => "tx_detail_by_hash"); + // replace with new tx details in case we need to update any data + e.insert(tx_details); + updated = true; + }, + Err(e) => log_tag!( + ctx, + "", + "tx_history", + "coin" => coin.as_ref().conf.ticker; + fmt = "Error {:?} on getting the details of {:?}, skipping the tx", e, txid + ), } } }, @@ -3116,16 +3125,19 @@ pub async fn tx_details_by_hash( let prev_tx = &mut prev_tx.tx; prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - let prev_tx_value = prev_tx.outputs[input.previous_output.index as usize].value; - input_amount += prev_tx_value; - let from: Vec
= try_s!(coin.addresses_from_script( - &prev_tx.outputs[input.previous_output.index as usize] - .script_pubkey - .clone() - .into() - )); + let prev_output_index: usize = try_s!(input.previous_output.index.try_into()); + let prev_tx_output = prev_tx.outputs.get(prev_output_index).ok_or(ERRL!( + "Previous output index is out of bound: coin={}, prev_output_index={}, prev_tx_hash={}, tx_hash={}, tx_hex={:02x}", + ticker, + prev_output_index, + prev_tx_hash, + hash, + verbose_tx.hex, + ))?; + input_amount += prev_tx_output.value; + let from: Vec
= try_s!(coin.addresses_from_script(&prev_tx_output.script_pubkey.clone().into())); if from.contains(my_address) { - spent_by_me += prev_tx_value; + spent_by_me += prev_tx_output.value; } from_addresses.extend(from.into_iter()); } @@ -3548,7 +3560,7 @@ pub fn coin_protocol_info(coin: &T) -> Vec { pub fn is_coin_protocol_supported(coin: &T, info: &Option>) -> bool { match info { - Some(format) => rmp_serde::from_read_ref::<_, UtxoAddressFormat>(format).is_ok(), + Some(format) => rmp_serde::from_slice::(format).is_ok(), None => !coin.addr_format().is_segwit(), } } diff --git a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs index 8e6c8dcdd3..97a637a68c 100644 --- a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs +++ b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs @@ -9,8 +9,8 @@ use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetails UtxoTxHistoryOps}; use crate::utxo::{output_script, RequestTxHistoryResult, UtxoCoinFields, UtxoCommonOps, UtxoHDAccount}; use crate::{big_decimal_from_sat_unsigned, compare_transactions, BalanceResult, CoinWithDerivationMethod, - DerivationMethod, HDAccountAddressId, MarketCoinOps, TransactionDetails, TxFeeDetails, TxIdHeight, - UtxoFeeDetails, UtxoTx}; + DerivationMethod, HDAccountAddressId, MarketCoinOps, NumConversError, TransactionDetails, TxFeeDetails, + TxIdHeight, UtxoFeeDetails, UtxoTx}; use common::jsonrpc_client::JsonRpcErrorType; use crypto::Bip44Chain; use futures::compat::Future01CompatExt; @@ -22,8 +22,9 @@ use mm2_number::BigDecimal; use rpc::v1::types::{TransactionInputEnum, H256 as H256Json}; use serialization::deserialize; use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use std::iter; +use std::num::TryFromIntError; /// [`CoinWithTxHistoryV2::history_wallet_id`] implementation. pub fn history_wallet_id(coin: &UtxoCoinFields) -> WalletId { WalletId::new(coin.conf.ticker.clone()) } @@ -190,13 +191,20 @@ where let prev_tx = coin.tx_from_storage_or_rpc(&prev_tx_hash, params.storage).await?; - let prev_output_index = input.previous_output.index as usize; - let prev_tx_value = prev_tx.outputs[prev_output_index].value; - let prev_script = prev_tx.outputs[prev_output_index].script_pubkey.clone().into(); + let prev_output_index: usize = input.previous_output.index.try_into().map_to_mm(|e: TryFromIntError| { + UtxoTxDetailsError::NumConversionErr(NumConversError::new(e.to_string())) + })?; + let prev_tx_output = prev_tx.outputs.get(prev_output_index).ok_or_else(|| { + UtxoTxDetailsError::Internal(format!( + "Previous output index is out of bound: coin={}, prev_output_index={}, prev_tx_hash={}, tx_hash={}, tx_hex={:02x}", + ticker, prev_output_index, prev_tx_hash, params.hash, verbose_tx.hex + )) + })?; - input_amount += prev_tx_value; - let amount = big_decimal_from_sat_unsigned(prev_tx_value, decimals); + input_amount += prev_tx_output.value; + let amount = big_decimal_from_sat_unsigned(prev_tx_output.value, decimals); + let prev_script = prev_tx_output.script_pubkey.clone().into(); let from: Vec
= coin .addresses_from_script(&prev_script) .map_to_mm(UtxoTxDetailsError::TxAddressDeserializationError)?; diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index b21e33e639..8c03e66a8c 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -464,7 +464,7 @@ impl SwapOps for UtxoStandardCoin { MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) } - fn is_supported_by_watchers(&self) -> bool { true } + fn is_supported_by_watchers(&self) -> bool { std::env::var("USE_WATCHERS").is_ok() } } #[async_trait] diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 350537c7de..4e3f6f2ecb 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1,43 +1,41 @@ use crate::coin_errors::MyAddressError; +#[cfg(not(target_arch = "wasm32"))] use crate::my_tx_history_v2::{MyTxHistoryErrorV2, MyTxHistoryRequestV2, MyTxHistoryResponseV2}; +#[cfg(not(target_arch = "wasm32"))] use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus, WithdrawTaskHandle}; use crate::utxo::rpc_clients::{ElectrumRpcRequest, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; -use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, - UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, - UtxoFieldsWithIguanaSecretBuilder}; -use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat, big_decimal_from_sat_unsigned, - payment_script}; +use crate::utxo::utxo_builder::UtxoCoinBuildError; +use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, + UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script}; use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, AddrFromStrError, Address, BroadcastTxErr, FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, - UtxoCommonOps, UtxoFeeDetails, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, - VerboseTransactionFrom}; + UtxoCommonOps, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, - PaymentInstructionsErr, PrivKeyActivationPolicy, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, - RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, - SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionFut, - TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; + NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + PrivKeyActivationPolicy, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, + RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, SignatureResult, SpendPaymentArgs, + SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionEnum, TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, + WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; use crate::{Transaction, WithdrawError}; use async_trait::async_trait; use bitcrypto::dhash256; use chain::constants::SEQUENCE_FINAL; use chain::{Transaction as UtxoTx, TransactionOutput}; use common::executor::{AbortableSystem, AbortedError}; -use common::{async_blocking, calc_total_pages, log, one_thousand_u32, sha256_digest, PagingOptionsEnum}; +use common::sha256_digest; +use common::{log, one_thousand_u32}; use crypto::privkey::{key_pair_from_secret, secp_privkey_from_hash}; -use crypto::{Bip32DerPathOps, GlobalHDAccountArc, StandardHDPathToCoin}; -use db_common::sqlite::offset_by_id; -use db_common::sqlite::rusqlite::{Error as SqlError, Row, NO_PARAMS}; -use db_common::sqlite::sql_builder::{name, SqlBuilder, SqlName}; +use crypto::StandardHDPathToCoin; +use crypto::{Bip32DerPathOps, GlobalHDAccountArc}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; @@ -48,7 +46,6 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; -use parking_lot::Mutex; use primitives::bytes::Bytes; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; @@ -58,37 +55,54 @@ use std::collections::{HashMap, HashSet}; use std::iter; use std::path::PathBuf; use std::sync::Arc; -use zcash_client_backend::data_api::WalletRead; +#[cfg(target_arch = "wasm32")] +use z_coin_errors::ZCoinBalanceError; +use z_rpc::{SaplingSyncConnector, SaplingSyncGuard}; use zcash_client_backend::encoding::{decode_payment_address, encode_extended_spending_key, encode_payment_address}; -use zcash_client_backend::wallet::{AccountId, SpendableNote}; -use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; -use zcash_client_sqlite::error::SqliteClientError; -use zcash_client_sqlite::wallet::get_balance; -use zcash_client_sqlite::wallet::transact::get_spendable_notes; +use zcash_client_backend::wallet::SpendableNote; use zcash_primitives::consensus::{BlockHeight, NetworkUpgrade, Parameters, H0}; use zcash_primitives::memo::MemoBytes; use zcash_primitives::sapling::keys::OutgoingViewingKey; use zcash_primitives::sapling::note_encryption::try_sapling_output_recovery; -use zcash_primitives::transaction::builder::Builder as ZTxBuilder; use zcash_primitives::transaction::components::{Amount, TxOut}; use zcash_primitives::transaction::Transaction as ZTransaction; use zcash_primitives::zip32::ChildIndex as Zip32Child; -use zcash_primitives::{consensus, constants::mainnet as z_mainnet_constants, sapling::PaymentAddress, +use zcash_primitives::{constants::mainnet as z_mainnet_constants, sapling::PaymentAddress, zip32::ExtendedFullViewingKey, zip32::ExtendedSpendingKey}; -use zcash_proofs::default_params_folder; use zcash_proofs::prover::LocalTxProver; mod z_htlc; use z_htlc::{z_p2sh_spend, z_send_dex_fee, z_send_htlc}; mod z_rpc; +use z_rpc::init_light_client; pub use z_rpc::SyncStatus; -use z_rpc::{init_light_client, init_native_client, SaplingSyncConnector, SaplingSyncGuard, WalletDbShared}; -mod z_coin_errors; -use crate::z_coin::z_rpc::{create_wallet_db, BlockDb}; +cfg_native!( + use crate::{NumConversError, TransactionDetails, TxFeeDetails}; + use crate::utxo::UtxoFeeDetails; + use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat}; + + use common::{async_blocking, calc_total_pages, PagingOptionsEnum}; + use db_common::sqlite::offset_by_id; + use db_common::sqlite::rusqlite::{Error as SqlError, Row}; + use db_common::sqlite::sql_builder::{name, SqlBuilder, SqlName}; + use zcash_client_backend::data_api::WalletRead; + use zcash_client_backend::wallet::{AccountId}; + use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; + use zcash_client_sqlite::wallet::{get_balance}; + use zcash_client_sqlite::wallet::transact::get_spendable_notes; + use zcash_primitives::consensus; + use zcash_primitives::transaction::builder::Builder as ZTxBuilder; + use zcash_proofs::default_params_folder; + use z_rpc::{init_native_client}; +); + +#[allow(unused)] mod z_coin_errors; +use crate::z_coin::storage::{BlockDbImpl, WalletDbShared}; pub use z_coin_errors::*; +pub mod storage; #[cfg(all(test, feature = "zhtlc-native-tests"))] mod z_coin_native_tests; @@ -113,12 +127,14 @@ macro_rules! try_ztx_s { const DEX_FEE_OVK: OutgoingViewingKey = OutgoingViewingKey([7; 32]); const DEX_FEE_Z_ADDR: &str = "zs1rp6426e9r6jkq2nsanl66tkd34enewrmr0uvj0zelhkcwmsy0uvxz2fhm9eu9rl3ukxvgzy2v9f"; -const TRANSACTIONS_TABLE: &str = "transactions"; -const BLOCKS_TABLE: &str = "blocks"; const SAPLING_SPEND_NAME: &str = "sapling-spend.params"; const SAPLING_OUTPUT_NAME: &str = "sapling-output.params"; const SAPLING_SPEND_EXPECTED_HASH: &str = "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"; const SAPLING_OUTPUT_EXPECTED_HASH: &str = "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"; +cfg_native!( + const BLOCKS_TABLE: &str = "blocks"; + const TRANSACTIONS_TABLE: &str = "transactions"; +); #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ZcoinConsensusParams { @@ -177,6 +193,7 @@ impl Parameters for ZcoinConsensusParams { fn b58_script_address_prefix(&self) -> [u8; 2] { self.b58_script_address_prefix } } +#[allow(unused)] pub struct ZCoinFields { dex_fee_addr: PaymentAddress, my_z_addr: PaymentAddress, @@ -216,6 +233,7 @@ pub struct ZOutput { pub memo: Option, } +#[cfg(not(target_arch = "wasm32"))] struct ZCoinSqlTxHistoryItem { tx_hash: Vec, internal_id: i64, @@ -225,6 +243,7 @@ struct ZCoinSqlTxHistoryItem { spent_amount: i64, } +#[cfg(not(target_arch = "wasm32"))] impl ZCoinSqlTxHistoryItem { fn try_from_sql_row(row: &Row<'_>) -> Result { let mut tx_hash: Vec = row.get(0)?; @@ -240,6 +259,7 @@ impl ZCoinSqlTxHistoryItem { } } +#[cfg(not(target_arch = "wasm32"))] struct SqlTxHistoryRes { transactions: Vec, total_tx_count: u32, @@ -317,30 +337,44 @@ impl ZCoin { }) } + #[cfg(not(target_arch = "wasm32"))] async fn my_balance_sat(&self) -> Result> { - let db = self.z_fields.light_wallet_db.clone(); + let wallet_db = self.z_fields.light_wallet_db.clone(); async_blocking(move || { - let balance = get_balance(&db.lock(), AccountId::default())?.into(); + let balance = get_balance(&wallet_db.db.lock(), AccountId::default())?.into(); Ok(balance) }) .await } - async fn get_spendable_notes(&self) -> Result, MmError> { - let db = self.z_fields.light_wallet_db.clone(); + #[cfg(target_arch = "wasm32")] + async fn my_balance_sat(&self) -> Result> { todo!() } + + #[cfg(not(target_arch = "wasm32"))] + async fn get_spendable_notes(&self) -> Result, MmError> { + let wallet_db = self.z_fields.light_wallet_db.clone(); async_blocking(move || { - let guard = db.lock(); - let latest_db_block = match guard.block_height_extrema()? { + let guard = wallet_db.db.lock(); + let latest_db_block = match guard + .block_height_extrema() + .map_err(|err| SpendableNotesError::DBClientError(err.to_string()))? + { Some((_, latest)) => latest, None => return Ok(Vec::new()), }; - get_spendable_notes(&guard, AccountId::default(), latest_db_block).map_err(MmError::new) + get_spendable_notes(&guard, AccountId::default(), latest_db_block) + .map_err(|err| MmError::new(SpendableNotesError::DBClientError(err.to_string()))) }) .await } + #[cfg(target_arch = "wasm32")] + #[allow(unused)] + async fn get_spendable_notes(&self) -> Result, MmError> { todo!() } + /// Returns spendable notes - async fn spendable_notes_ordered(&self) -> Result, MmError> { + #[allow(unused)] + async fn spendable_notes_ordered(&self) -> Result, MmError> { let mut unspents = self.get_spendable_notes().await?; unspents.sort_unstable_by(|a, b| a.note_value.cmp(&b.note_value)); @@ -357,6 +391,7 @@ impl ZCoin { } /// Generates a tx sending outputs from our address + #[cfg(not(target_arch = "wasm32"))] async fn gen_tx( &self, t_outputs: Vec, @@ -371,7 +406,10 @@ impl ZCoin { let total_output = big_decimal_from_sat_unsigned(total_output_sat, self.utxo_arc.decimals); let total_required = &total_output + &tx_fee; - let spendable_notes = self.spendable_notes_ordered().await?; + let spendable_notes = self + .spendable_notes_ordered() + .await + .map_err(|err| GenTxError::SpendableNotesError(err.to_string()))?; let mut total_input_amount = BigDecimal::from(0); let mut change = BigDecimal::from(0); @@ -456,6 +494,15 @@ impl ZCoin { Ok((tx, additional_data, sync_guard)) } + #[cfg(target_arch = "wasm32")] + async fn gen_tx( + &self, + _t_outputs: Vec, + _z_outputs: Vec, + ) -> Result<(ZTransaction, AdditionalTxData, SaplingSyncGuard<'_>), MmError> { + todo!() + } + pub async fn send_outputs( &self, t_outputs: Vec, @@ -474,6 +521,7 @@ impl ZCoin { Ok(tx) } + #[cfg(not(target_arch = "wasm32"))] async fn tx_history_from_sql( &self, limit: usize, @@ -481,14 +529,14 @@ impl ZCoin { ) -> Result> { let wallet_db = self.z_fields.light_wallet_db.clone(); async_blocking(move || { - let db_guard = wallet_db.lock(); + let db_guard = wallet_db.db.lock(); let conn = db_guard.sql_conn(); let total_sql = SqlBuilder::select_from(TRANSACTIONS_TABLE) .field("COUNT(id_tx)") .sql() .expect("valid SQL"); - let total_tx_count = conn.query_row(&total_sql, NO_PARAMS, |row| row.get(0))?; + let total_tx_count = conn.query_row(&total_sql, [], |row| row.get(0))?; let mut sql_builder = SqlBuilder::select_from(name!(TRANSACTIONS_TABLE; "txes")); sql_builder @@ -526,7 +574,7 @@ impl ZCoin { let sql_items = conn .prepare(&sql)? - .query_map(NO_PARAMS, ZCoinSqlTxHistoryItem::try_from_sql_row)? + .query_map([], ZCoinSqlTxHistoryItem::try_from_sql_row)? .collect::, _>>()?; Ok(SqlTxHistoryRes { @@ -538,6 +586,7 @@ impl ZCoin { .await } + #[cfg(not(target_arch = "wasm32"))] async fn z_transactions_from_cache_or_rpc( &self, hashes: HashSet, @@ -553,6 +602,7 @@ impl ZCoin { .map_to_mm(|e| UtxoRpcError::InvalidResponse(e.to_string())) } + #[cfg(not(target_arch = "wasm32"))] fn tx_details_from_sql_item( &self, sql_item: ZCoinSqlTxHistoryItem, @@ -648,6 +698,7 @@ impl ZCoin { }) } + #[cfg(not(target_arch = "wasm32"))] pub async fn tx_history( &self, request: MyTxHistoryRequestV2, @@ -707,6 +758,7 @@ impl AsRef for ZCoin { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "rpc", content = "rpc_data")] pub enum ZcoinRpcMode { + #[cfg(not(target_arch = "wasm32"))] Native, Light { electrum_servers: Vec, @@ -726,6 +778,7 @@ pub struct ZcoinActivationParams { pub scan_interval_ms: u64, } +#[cfg(not(target_arch = "wasm32"))] pub async fn z_coin_from_conf_and_params( ctx: &MmArc, ticker: &str, @@ -749,12 +802,14 @@ pub async fn z_coin_from_conf_and_params( builder.build().await } +#[allow(unused)] fn verify_checksum_zcash_params(spend_path: &PathBuf, output_path: &PathBuf) -> Result { let spend_hash = sha256_digest(spend_path)?; let out_hash = sha256_digest(output_path)?; Ok(spend_hash == SAPLING_SPEND_EXPECTED_HASH && out_hash == SAPLING_OUTPUT_EXPECTED_HASH) } +#[allow(unused)] fn get_spend_output_paths(params_dir: PathBuf) -> Result<(PathBuf, PathBuf), ZCoinBuildError> { if !params_dir.exists() { return Err(ZCoinBuildError::ZCashParamsNotFound); @@ -826,42 +881,19 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { .expect("DEX_FEE_Z_ADDR is a valid z-address") .expect("DEX_FEE_Z_ADDR is a valid z-address"); - let params_dir = match &self.z_coin_params.zcash_params_path { - None => default_params_folder().or_mm_err(|| ZCoinBuildError::ZCashParamsNotFound)?, - Some(file_path) => PathBuf::from(file_path), - }; - - let z_tx_prover = async_blocking(move || { - let (spend_path, output_path) = get_spend_output_paths(params_dir)?; - let verification_successful = verify_checksum_zcash_params(&spend_path, &output_path)?; - if verification_successful { - Ok(LocalTxProver::new(&spend_path, &output_path)) - } else { - MmError::err(ZCoinBuildError::SaplingParamsInvalidChecksum) - } - }) - .await?; - + let z_tx_prover = self.z_tx_prover().await?; let my_z_addr_encoded = encode_payment_address( self.protocol_info.consensus_params.hrp_sapling_payment_address(), &my_z_addr, ); - let evk = ExtendedFullViewingKey::from(&z_spending_key); - let cache_db_path = self.db_dir_path.join(format!("{}_cache.db", self.ticker)); - let wallet_db_path = self.db_dir_path.join(format!("{}_wallet.db", self.ticker)); - let blocks_db = - async_blocking(|| BlockDb::for_path(cache_db_path).map_to_mm(ZcoinClientInitError::BlocksDbInitFailure)) - .await?; - let wallet_db = create_wallet_db( - wallet_db_path, - self.protocol_info.consensus_params.clone(), - self.protocol_info.check_point_block.clone(), - evk, - ) - .await?; - let wallet_db = Arc::new(Mutex::new(wallet_db)); + let blocks_db = self.blocks_db().await?; + let wallet_db = WalletDbShared::new(&self) + .await + .map_err(|err| ZCoinBuildError::ZcashDBError(err.to_string()))?; + let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { + #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => { let native_client = self.native_client()?; init_native_client( @@ -890,7 +922,6 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { .await? }, }; - let z_fields = ZCoinFields { dex_fee_addr, my_z_addr, @@ -925,6 +956,7 @@ impl<'a> ZCoinBuilder<'a> { protocol_info: ZcoinProtocolInfo, ) -> ZCoinBuilder<'a> { let utxo_mode = match &z_coin_params.mode { + #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => UtxoRpcMode::Native, ZcoinRpcMode::Light { electrum_servers, .. } => UtxoRpcMode::Electrum { servers: electrum_servers.clone(), @@ -954,6 +986,37 @@ impl<'a> ZCoinBuilder<'a> { protocol_info, } } + + async fn blocks_db(&self) -> Result> { + let cache_db_path = self.db_dir_path.join(format!("{}_cache.db", self.ticker)); + let ctx = self.ctx.clone(); + let ticker = self.ticker.to_string(); + BlockDbImpl::new(ctx, ticker, cache_db_path) + .map_err(|err| MmError::new(ZcoinClientInitError::ZcashDBError(err.to_string()))) + .await + } + + #[cfg(not(target_arch = "wasm32"))] + async fn z_tx_prover(&self) -> Result> { + let params_dir = match &self.z_coin_params.zcash_params_path { + None => default_params_folder().or_mm_err(|| ZCoinBuildError::ZCashParamsNotFound)?, + Some(file_path) => PathBuf::from(file_path), + }; + + async_blocking(move || { + let (spend_path, output_path) = get_spend_output_paths(params_dir)?; + let verification_successful = verify_checksum_zcash_params(&spend_path, &output_path)?; + if verification_successful { + Ok(LocalTxProver::new(&spend_path, &output_path)) + } else { + MmError::err(ZCoinBuildError::SaplingParamsInvalidChecksum) + } + }) + .await + } + + #[cfg(target_arch = "wasm32")] + async fn z_tx_prover(&self) -> Result> { todo!() } } /// Initialize `ZCoin` with a forced `z_spending_key`. @@ -1491,18 +1554,11 @@ impl MakerSwapTakerCoin for ZCoin { #[async_trait] impl WatcherOps for ZCoin { - fn create_maker_payment_spend_preimage( - &self, - _maker_payment_tx: &[u8], - _time_lock: u32, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_unique_data: &[u8], - ) -> TransactionFut { + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { unimplemented!(); } - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -1518,7 +1574,14 @@ impl WatcherOps for ZCoin { unimplemented!(); } - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u32, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { unimplemented!(); } @@ -1834,6 +1897,7 @@ impl UtxoCommonOps for ZCoin { } } +#[cfg(not(target_arch = "wasm32"))] #[async_trait] impl InitWithdrawCoin for ZCoin { async fn init_withdraw( diff --git a/mm2src/coins/z_coin/storage.rs b/mm2src/coins/z_coin/storage.rs new file mode 100644 index 0000000000..548ea0303f --- /dev/null +++ b/mm2src/coins/z_coin/storage.rs @@ -0,0 +1,5 @@ +pub mod blockdb; +pub use blockdb::*; + +pub mod walletdb; +pub use walletdb::*; diff --git a/mm2src/coins/z_coin/storage/blockdb/block_idb.rs b/mm2src/coins/z_coin/storage/blockdb/block_idb.rs new file mode 100644 index 0000000000..e70cfce122 --- /dev/null +++ b/mm2src/coins/z_coin/storage/blockdb/block_idb.rs @@ -0,0 +1,53 @@ +use async_trait::async_trait; +use mm2_db::indexed_db::{BeBigUint, DbIdentifier, DbInstance, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, + OnUpgradeResult, TableSignature}; + +const DB_NAME: &str = "z_compactblocks_cache"; +const DB_VERSION: u32 = 1; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BlockDbTable { + height: BeBigUint, + data: Vec, + ticker: String, +} + +impl BlockDbTable { + pub const TICKER_HEIGHT_INDEX: &str = "block_height_ticker_index"; +} + +impl TableSignature for BlockDbTable { + fn table_name() -> &'static str { "compactblocks" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(Self::TICKER_HEIGHT_INDEX, &["ticker", "height"], true)?; + table.create_index("ticker", false)?; + } + Ok(()) + } +} + +pub struct BlockDbInner { + pub inner: IndexedDb, +} + +impl BlockDbInner { + pub fn _get_inner(&self) -> &IndexedDb { &self.inner } +} + +#[async_trait] +impl DbInstance for BlockDbInner { + fn db_name() -> &'static str { DB_NAME } + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .build() + .await?; + + Ok(Self { inner }) + } +} diff --git a/mm2src/coins/z_coin/storage/blockdb/mod.rs b/mm2src/coins/z_coin/storage/blockdb/mod.rs new file mode 100644 index 0000000000..3067162008 --- /dev/null +++ b/mm2src/coins/z_coin/storage/blockdb/mod.rs @@ -0,0 +1,202 @@ +#[cfg(target_arch = "wasm32")] pub(crate) mod block_idb; + +use mm2_core::mm_ctx::MmArc; +use std::path::Path; +use zcash_client_backend::data_api::BlockSource; +use zcash_client_backend::proto::compact_formats::CompactBlock; +use zcash_primitives::consensus::BlockHeight; + +cfg_native!( + use db_common::sqlite::rusqlite::{params, Connection}; + use db_common::sqlite::{query_single_row, run_optimization_pragmas}; + use protobuf::Message; + use mm2_err_handle::prelude::*; + use std::sync::{Arc, Mutex}; + use zcash_client_sqlite::error::{SqliteClientError as ZcashClientError, SqliteClientError}; + use zcash_client_sqlite::NoteId; + use zcash_client_backend::data_api::error::Error as ChainError; + + struct CompactBlockRow { + height: BlockHeight, + data: Vec, + } +); + +#[derive(Debug, Display)] +pub enum BlockDbError { + #[cfg(not(target_arch = "wasm32"))] + SqliteError(SqliteClientError), + #[cfg(target_arch = "wasm32")] + IndexedDBError(String), + CorruptedData(String), +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for BlockDbError { + fn from(value: SqliteClientError) -> Self { Self::SqliteError(value) } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From> for BlockDbError { + fn from(value: ChainError) -> Self { Self::SqliteError(SqliteClientError::from(value)) } +} + +/// A wrapper for the db connection to the block cache database. +pub struct BlockDbImpl { + #[cfg(not(target_arch = "wasm32"))] + pub db: Arc>, + #[cfg(target_arch = "wasm32")] + pub db: SharedDb, + #[allow(unused)] + ticker: String, +} + +#[cfg(not(target_arch = "wasm32"))] +impl BlockDbImpl { + pub async fn new(_ctx: MmArc, ticker: String, path: impl AsRef) -> MmResult { + let conn = Connection::open(path).map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err)))?; + run_optimization_pragmas(&conn).map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err)))?; + conn.execute( + "CREATE TABLE IF NOT EXISTS compactblocks ( + height INTEGER PRIMARY KEY, + data BLOB NOT NULL + )", + [], + ) + .map_to_mm(|err| BlockDbError::SqliteError(SqliteClientError::from(err)))?; + + Ok(Self { + db: Arc::new(Mutex::new(conn)), + ticker, + }) + } + + pub(crate) fn get_latest_block(&self) -> Result { + Ok(query_single_row( + &self.db.lock().unwrap(), + "SELECT height FROM compactblocks ORDER BY height DESC LIMIT 1", + [], + |row| row.get(0), + )? + .unwrap_or(0)) + } + + pub(crate) fn insert_block(&self, height: u32, cb_bytes: Vec) -> Result { + self.db + .lock() + .unwrap() + .prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)") + .map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err)))? + .execute(params![height, cb_bytes]) + .map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err))) + } + + pub(crate) fn rewind_to_height(&self, height: u32) -> Result { + self.db + .lock() + .unwrap() + .execute("DELETE from compactblocks WHERE height > ?1", [height]) + .map_err(|err| BlockDbError::SqliteError(SqliteClientError::from(err))) + } + + fn with_blocks( + &self, + from_height: BlockHeight, + limit: Option, + mut with_row: F, + ) -> Result<(), SqliteClientError> + where + F: FnMut(CompactBlock) -> Result<(), SqliteClientError>, + { + // Fetch the CompactBlocks we need to scan + let stmt_blocks = self.db.lock().unwrap(); + let mut stmt_blocks = stmt_blocks.prepare( + "SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC \ + LIMIT ?", + )?; + + let rows = stmt_blocks.query_map( + params![u32::from(from_height), limit.unwrap_or(u32::max_value()),], + |row| { + Ok(CompactBlockRow { + height: BlockHeight::from_u32(row.get(0)?), + data: row.get(1)?, + }) + }, + )?; + + for row_result in rows { + let cbr = row_result?; + let block = CompactBlock::parse_from_bytes(&cbr.data).map_err(ChainError::from)?; + + if block.height() != cbr.height { + return Err(SqliteClientError::CorruptedData(format!( + "Block height {} did not match row's height field value {}", + block.height(), + cbr.height + ))); + } + + with_row(block)?; + } + + Ok(()) + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl BlockSource for BlockDbImpl { + type Error = SqliteClientError; + + fn with_blocks(&self, from_height: BlockHeight, limit: Option, with_row: F) -> Result<(), Self::Error> + where + F: FnMut(CompactBlock) -> Result<(), Self::Error>, + { + self.with_blocks(from_height, limit, with_row) + } +} + +cfg_wasm32!( + use crate::z_coin::storage::blockdb::block_idb::BlockDbInner; + use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; + use mm2_err_handle::prelude::*; + + pub type BlockDbRes = MmResult; + pub type BlockDbInnerLocked<'a> = DbLocked<'a, BlockDbInner>; + + impl BlockDbImpl { + pub async fn new(ctx: MmArc, ticker: String, _path: impl AsRef) -> Result { + Ok(Self { + db: ConstructibleDb::new(&ctx).into_shared(), + ticker, + }) + } + + #[allow(unused)] + async fn lock_db(&self) -> BlockDbRes> { + self.db + .get_or_initialize() + .await + .mm_err(|err| BlockDbError::IndexedDBError(err.to_string())) + } + + pub fn get_latest_block(&self) -> Result { todo!() } + + pub fn insert_block(&self, _height: u32, _cb_bytes: Vec) -> Result { todo!() } + + pub fn rewind_to_height(&self, _height: u32) -> Result { todo!() } + + pub fn with_blocks(&self, _from_height: BlockHeight, _limit: Option, mut _with_row: F) -> Result<(), + BlockDbError> + where F: FnMut(CompactBlock) -> Result<(), BlockDbError> + { todo!() } + } + + impl BlockSource for BlockDbImpl { + type Error = BlockDbError; + fn with_blocks(&self, _from_height: BlockHeight, _limit: Option, _with_row: F) -> Result<(), + Self::Error> + where F: FnMut(CompactBlock) -> Result<(), Self::Error>, + { todo!() } + } +); diff --git a/mm2src/coins/z_coin/storage/walletdb/mod.rs b/mm2src/coins/z_coin/storage/walletdb/mod.rs new file mode 100644 index 0000000000..c7773df4ab --- /dev/null +++ b/mm2src/coins/z_coin/storage/walletdb/mod.rs @@ -0,0 +1,87 @@ +use crate::z_coin::{ZCoinBuilder, ZcoinClientInitError}; +use mm2_err_handle::prelude::*; + +cfg_native!( + use crate::z_coin::{extended_spending_key_from_protocol_info_and_policy, ZcoinConsensusParams}; + use crate::z_coin::z_rpc::create_wallet_db; + + use parking_lot::Mutex; + use std::sync::Arc; + use zcash_client_sqlite::WalletDb; + use zcash_primitives::zip32::ExtendedFullViewingKey; +); + +cfg_wasm32!( + mod wallet_idb; + use wallet_idb::WalletDbInner; +); + +#[derive(Debug, Display)] +pub enum WalletDbError { + ZcoinClientInitError(ZcoinClientInitError), + ZCoinBuildError(String), + IndexedDBError(String), +} + +#[derive(Clone)] +pub struct WalletDbShared { + #[cfg(not(target_arch = "wasm32"))] + pub db: Arc>>, + #[cfg(target_arch = "wasm32")] + pub db: SharedDb, + #[allow(unused)] + ticker: String, +} + +#[cfg(not(target_arch = "wasm32"))] +impl<'a> WalletDbShared { + pub async fn new(zcoin_builder: &ZCoinBuilder<'a>) -> MmResult { + let z_spending_key = match zcoin_builder.z_spending_key { + Some(ref z_spending_key) => z_spending_key.clone(), + None => extended_spending_key_from_protocol_info_and_policy( + &zcoin_builder.protocol_info, + &zcoin_builder.priv_key_policy, + ) + .map_err(|err| WalletDbError::ZCoinBuildError(err.to_string()))?, + }; + let wallet_db = create_wallet_db( + zcoin_builder + .db_dir_path + .join(format!("{}_wallet.db", zcoin_builder.ticker)), + zcoin_builder.protocol_info.consensus_params.clone(), + zcoin_builder.protocol_info.check_point_block.clone(), + ExtendedFullViewingKey::from(&z_spending_key), + ) + .await + .map_err(|err| MmError::new(WalletDbError::ZcoinClientInitError(err.into_inner())))?; + + Ok(Self { + db: Arc::new(Mutex::new(wallet_db)), + ticker: zcoin_builder.ticker.to_string(), + }) + } +} + +cfg_wasm32!( + use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; + + pub type WalletDbRes = MmResult; + pub type WalletDbInnerLocked<'a> = DbLocked<'a, WalletDbInner>; + + impl<'a> WalletDbShared { + pub async fn new(zcoin_builder: &ZCoinBuilder<'a>) -> MmResult { + Ok(Self { + db: ConstructibleDb::new(zcoin_builder.ctx).into_shared(), + ticker: zcoin_builder.ticker.to_string(), + }) + } + + #[allow(unused)] + async fn lock_db(&self) -> WalletDbRes> { + self.db + .get_or_initialize() + .await + .mm_err(|err| WalletDbError::IndexedDBError(err.to_string())) + } + } +); diff --git a/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs b/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs new file mode 100644 index 0000000000..129097dbe6 --- /dev/null +++ b/mm2src/coins/z_coin/storage/walletdb/wallet_idb.rs @@ -0,0 +1,243 @@ +use async_trait::async_trait; +use mm2_db::indexed_db::{BeBigUint, DbIdentifier, DbInstance, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, + OnUpgradeResult, TableSignature}; + +const DB_NAME: &str = "wallet_db_cache"; +const DB_VERSION: u32 = 1; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WalletDbAccountsTable { + account: BeBigUint, + extfvk: String, + address: String, + ticker: String, +} + +impl WalletDbAccountsTable { + /// A **unique** index that consists of the following properties: + /// * ticker + /// * account + pub const TICKER_ACCOUNT_INDEX: &str = "ticker_account_index"; +} + +impl TableSignature for WalletDbAccountsTable { + fn table_name() -> &'static str { "walletdb_accounts" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(Self::TICKER_ACCOUNT_INDEX, &["ticker", "account"], true)?; + table.create_index("ticker", false)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WalletDbBlocksTable { + height: BeBigUint, + hash: String, + time: BeBigUint, + sapling_tree: String, + ticker: String, +} + +impl WalletDbBlocksTable { + /// A **unique** index that consists of the following properties: + /// * ticker + /// * height + pub const TICKER_HEIGHT_INDEX: &str = "ticker_height_index"; + /// A **unique** index that consists of the following properties: + /// * ticker + /// * hash + pub const TICKER_HASH_INDEX: &str = "ticker_hash_index"; +} + +impl TableSignature for WalletDbBlocksTable { + fn table_name() -> &'static str { "walletdb_blocks" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(Self::TICKER_HEIGHT_INDEX, &["ticker", "height"], true)?; + table.create_multi_index(Self::TICKER_HASH_INDEX, &["ticker", "hash"], true)?; + table.create_index("ticker", false)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WalletDbTransactionsTable { + id_tx: BeBigUint, + txid: String, // unique + created: String, + block: BeBigUint, + tx_index: BeBigUint, + expiry_height: BeBigUint, + raw: String, + ticker: String, +} + +impl WalletDbTransactionsTable { + /// A **unique** index that consists of the following properties: + /// * ticker + /// * id_tx + /// * txid + pub const TICKER_ID_TX_INDEX: &'static str = "ticker_id_tx_index"; +} + +impl TableSignature for WalletDbTransactionsTable { + fn table_name() -> &'static str { "walletdb_transactions" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(Self::TICKER_ID_TX_INDEX, &["ticker", "id_tx", "txid"], true)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WalletDbReceivedNotesTable { + id_note: BeBigUint, + tx: BeBigUint, + output_index: BeBigUint, + account: BeBigUint, + diversifier: String, + value: BeBigUint, + rcm: String, + nf: String, // unique + is_change: BeBigUint, + memo: String, + spent: BeBigUint, + ticker: String, +} + +impl WalletDbReceivedNotesTable { + /// A **unique** index that consists of the following properties: + /// * ticker + /// * note_id + /// * nf + pub const TICKER_NOTES_ID_NF_INDEX: &'static str = "ticker_note_id_nf_index"; + /// A **unique** index that consists of the following properties: + /// * ticker + /// * tx + /// * output_index + pub const TICKER_NOTES_TX_OUTPUT_INDEX: &'static str = "ticker_notes_tx_output_index"; +} + +impl TableSignature for WalletDbReceivedNotesTable { + fn table_name() -> &'static str { "walletdb_received_notes" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(Self::TICKER_NOTES_ID_NF_INDEX, &["ticker", "id_note", "nf"], true)?; + table.create_multi_index( + Self::TICKER_NOTES_TX_OUTPUT_INDEX, + &["ticker", "tx", "output_index"], + true, + )?; + table.create_index("ticker", false)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WalletDbSaplingWitnessesTable { + id_witness: BeBigUint, + note: BeBigUint, + block: BeBigUint, + witness: String, + ticker: String, +} + +impl WalletDbSaplingWitnessesTable { + /// A **unique** index that consists of the following properties: + /// * ticker + /// * note + /// * block + pub const TICKER_NOTE_BLOCK_INDEX: &'static str = "ticker_note_block_index"; + /// A **unique** index that consists of the following properties: + /// * ticker + /// * id_witness + pub const TICKER_ID_WITNESS_INDEX: &'static str = "ticker_id_witness_index"; +} + +impl TableSignature for WalletDbSaplingWitnessesTable { + fn table_name() -> &'static str { "walletdb_sapling_witness" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(Self::TICKER_NOTE_BLOCK_INDEX, &["ticker", "note", "block"], true)?; + table.create_multi_index(Self::TICKER_ID_WITNESS_INDEX, &["ticker", "id_witness"], true)?; + table.create_index("ticker", false)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WalletDbSentNotesTable { + id_note: BeBigUint, + tx: BeBigUint, + output_index: BeBigUint, + from_account: BeBigUint, + address: String, + value: BeBigUint, + memo: String, + ticker: String, +} + +impl WalletDbSentNotesTable { + /// A **unique** index that consists of the following properties: + /// * ticker + /// * tx + /// * output_index + pub const TICKER_TX_OUTPUT_INDEX: &'static str = "ticker_tx_output_index"; +} + +impl TableSignature for WalletDbSentNotesTable { + fn table_name() -> &'static str { "walletdb_sent_notes" } + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(Self::TICKER_TX_OUTPUT_INDEX, &["ticker", "tx", "output_index"], true)?; + table.create_index("ticker", false)?; + } + Ok(()) + } +} + +pub struct WalletDbInner { + pub inner: IndexedDb, +} + +impl WalletDbInner { + pub fn _get_inner(&self) -> &IndexedDb { &self.inner } +} + +#[async_trait] +impl DbInstance for WalletDbInner { + fn db_name() -> &'static str { DB_NAME } + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .with_table::() + .with_table::() + .with_table::() + .with_table::() + .with_table::() + .build() + .await?; + + Ok(Self { inner }) + } +} diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index 8cb6da1359..b8e3875e81 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -1,41 +1,47 @@ use crate::my_tx_history_v2::MyTxHistoryErrorV2; use crate::utxo::rpc_clients::UtxoRpcError; use crate::utxo::utxo_builder::UtxoCoinBuildError; +use crate::z_coin::storage::WalletDbError; +use crate::NumConversError; +use crate::PrivKeyPolicyNotAllowed; use crate::WithdrawError; -use crate::{NumConversError, PrivKeyPolicyNotAllowed}; + use common::jsonrpc_client::JsonRpcError; +#[cfg(not(target_arch = "wasm32"))] use db_common::sqlite::rusqlite::Error as SqliteError; -use db_common::sqlite::rusqlite::Error as SqlError; use derive_more::Display; use http::uri::InvalidUri; use mm2_number::BigDecimal; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +#[cfg(not(target_arch = "wasm32"))] use zcash_client_sqlite::error::SqliteClientError; -use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; use zcash_primitives::transaction::builder::Error as ZTxBuilderError; #[derive(Debug, Display)] #[non_exhaustive] pub enum UpdateBlocksCacheErr { + #[cfg(not(target_arch = "wasm32"))] GrpcError(tonic::Status), - BlocksDbError(SqliteError), - ZcashSqliteError(ZcashClientError), UtxoRpcError(UtxoRpcError), InternalError(String), JsonRpcError(JsonRpcError), GetLiveLightClientError(String), + ZcashDBError(String), } +#[cfg(not(target_arch = "wasm32"))] impl From for UpdateBlocksCacheErr { fn from(err: tonic::Status) -> Self { UpdateBlocksCacheErr::GrpcError(err) } } +#[cfg(not(target_arch = "wasm32"))] impl From for UpdateBlocksCacheErr { - fn from(err: SqliteError) -> Self { UpdateBlocksCacheErr::BlocksDbError(err) } + fn from(err: SqliteError) -> Self { UpdateBlocksCacheErr::ZcashDBError(err.to_string()) } } -impl From for UpdateBlocksCacheErr { - fn from(err: ZcashClientError) -> Self { UpdateBlocksCacheErr::ZcashSqliteError(err) } +#[cfg(not(target_arch = "wasm32"))] +impl From for UpdateBlocksCacheErr { + fn from(err: SqliteClientError) -> Self { UpdateBlocksCacheErr::ZcashDBError(err.to_string()) } } impl From for UpdateBlocksCacheErr { @@ -49,29 +55,26 @@ impl From for UpdateBlocksCacheErr { #[derive(Debug, Display)] #[non_exhaustive] pub enum ZcoinClientInitError { - BlocksDbInitFailure(SqliteError), - WalletDbInitFailure(SqliteError), - ZcashSqliteError(ZcashClientError), + ZcashDBError(String), EmptyLightwalletdUris, #[display(fmt = "Fail to init clients while iterating lightwalletd urls {:?}", _0)] UrlIterFailure(Vec), } -impl From for ZcoinClientInitError { - fn from(err: ZcashClientError) -> Self { ZcoinClientInitError::ZcashSqliteError(err) } +#[cfg(not(target_arch = "wasm32"))] +impl From for ZcoinClientInitError { + fn from(err: SqliteClientError) -> Self { ZcoinClientInitError::ZcashDBError(err.to_string()) } } #[derive(Debug, Display)] pub enum UrlIterError { InvalidUri(InvalidUri), + #[cfg(not(target_arch = "wasm32"))] TlsConfigFailure(tonic::transport::Error), + #[cfg(not(target_arch = "wasm32"))] ConnectionFailure(tonic::transport::Error), } -#[derive(Debug, Display)] -#[display(fmt = "Blockchain scan process stopped")] -pub struct BlockchainScanStopped {} - #[derive(Debug, Display)] pub enum GenTxError { DecryptedOutputNotFound, @@ -98,8 +101,9 @@ pub enum GenTxError { err: std::io::Error, }, BlockchainScanStopped, - LightClientErr(SqliteClientError), + LightClientErr(String), FailedToCreateNote, + SpendableNotesError(String), } impl From for GenTxError { @@ -118,8 +122,9 @@ impl From for GenTxError { fn from(err: ZTxBuilderError) -> GenTxError { GenTxError::TxBuilderError(err) } } +#[cfg(not(target_arch = "wasm32"))] impl From for GenTxError { - fn from(err: SqliteClientError) -> Self { GenTxError::LightClientErr(err) } + fn from(err: SqliteClientError) -> Self { GenTxError::LightClientErr(err.to_string()) } } impl From for WithdrawError { @@ -144,11 +149,16 @@ impl From for WithdrawError { | GenTxError::TxReadError { .. } | GenTxError::BlockchainScanStopped | GenTxError::LightClientErr(_) + | GenTxError::SpendableNotesError(_) | GenTxError::FailedToCreateNote => WithdrawError::InternalError(gen_tx.to_string()), } } } +#[derive(Debug, Display)] +#[display(fmt = "Blockchain scan process stopped")] +pub struct BlockchainScanStopped {} + impl From for GenTxError { #[inline] fn from(_: BlockchainScanStopped) -> Self { GenTxError::BlockchainScanStopped } @@ -185,18 +195,19 @@ pub enum GetUnspentWitnessErr { EmptyDbResult, TreeOrWitnessAppendFailed, OutputCmuNotFoundInCache, - Sql(SqliteError), + ZcashDBError(String), } +#[cfg(not(target_arch = "wasm32"))] impl From for GetUnspentWitnessErr { - fn from(err: SqliteError) -> GetUnspentWitnessErr { GetUnspentWitnessErr::Sql(err) } + fn from(err: SqliteError) -> GetUnspentWitnessErr { GetUnspentWitnessErr::ZcashDBError(err.to_string()) } } #[derive(Debug, Display)] pub enum ZCoinBuildError { UtxoBuilderError(UtxoCoinBuildError), GetAddressError, - SqliteError(SqliteError), + ZcashDBError(String), Rpc(UtxoRpcError), #[display(fmt = "Sapling cache DB does not exist at {}. Please download it.", path)] SaplingCacheDbDoesNotExist { @@ -209,35 +220,39 @@ pub enum ZCoinBuildError { SaplingParamsInvalidChecksum, } +#[cfg(not(target_arch = "wasm32"))] impl From for ZCoinBuildError { - fn from(err: SqliteError) -> ZCoinBuildError { ZCoinBuildError::SqliteError(err) } + fn from(err: SqliteError) -> ZCoinBuildError { ZCoinBuildError::ZcashDBError(err.to_string()) } } impl From for ZCoinBuildError { fn from(err: UtxoRpcError) -> ZCoinBuildError { ZCoinBuildError::Rpc(err) } } -impl From for ZCoinBuildError { - fn from(err: UtxoCoinBuildError) -> Self { ZCoinBuildError::UtxoBuilderError(err) } -} - impl From for ZCoinBuildError { fn from(err: std::io::Error) -> ZCoinBuildError { ZCoinBuildError::Io(err) } } +impl From for ZCoinBuildError { + fn from(err: UtxoCoinBuildError) -> Self { ZCoinBuildError::UtxoBuilderError(err) } +} + impl From for ZCoinBuildError { fn from(err: ZcoinClientInitError) -> Self { ZCoinBuildError::RpcClientInitErr(err) } } +#[cfg(not(target_arch = "wasm32"))] pub(super) enum SqlTxHistoryError { - Sql(SqlError), + Sql(SqliteError), FromIdDoesNotExist(i64), } -impl From for SqlTxHistoryError { - fn from(err: SqlError) -> Self { SqlTxHistoryError::Sql(err) } +#[cfg(not(target_arch = "wasm32"))] +impl From for SqlTxHistoryError { + fn from(err: SqliteError) -> Self { SqlTxHistoryError::Sql(err) } } +#[cfg(not(target_arch = "wasm32"))] impl From for MyTxHistoryErrorV2 { fn from(err: SqlTxHistoryError) -> Self { match err { @@ -256,3 +271,11 @@ impl From for MyTxHistoryErrorV2 { MyTxHistoryErrorV2::RpcError(format!("No info about transaction {:02x}", err.0)) } } + +#[derive(Debug, Display)] +pub enum SpendableNotesError { + DBClientError(String), +} + +#[derive(Debug, Display)] +pub enum ZCoinBalanceError {} diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index 4914c0002d..edde4bbc37 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -9,24 +9,33 @@ use super::ZCoin; use crate::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcError}; use crate::utxo::utxo_common::payment_script; use crate::utxo::{sat_from_big_decimal, UtxoAddressFormat}; -use crate::z_coin::{SendOutputsErr, ZOutput, DEX_FEE_OVK}; -use crate::{NumConversError, PrivKeyPolicyNotAllowed, TransactionEnum}; +use crate::z_coin::SendOutputsErr; +use crate::z_coin::{ZOutput, DEX_FEE_OVK}; +use crate::NumConversError; +use crate::{PrivKeyPolicyNotAllowed, TransactionEnum}; use bitcrypto::dhash160; -use common::async_blocking; use derive_more::Display; use futures::compat::Future01CompatExt; -use keys::{Address, KeyPair, Public}; +use keys::Address; +use keys::{KeyPair, Public}; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; -use script::{Builder as ScriptBuilder, Opcode, Script}; -use secp256k1::SecretKey; -use zcash_primitives::consensus; +use script::Script; +use script::{Builder as ScriptBuilder, Opcode}; use zcash_primitives::legacy::Script as ZCashScript; use zcash_primitives::memo::MemoBytes; -use zcash_primitives::transaction::builder::{Builder as ZTxBuilder, Error as ZTxBuilderError}; -use zcash_primitives::transaction::components::{Amount, OutPoint as ZCashOutpoint, TxOut}; +use zcash_primitives::transaction::builder::Error as ZTxBuilderError; +use zcash_primitives::transaction::components::{Amount, TxOut}; use zcash_primitives::transaction::Transaction as ZTransaction; +cfg_native!( + use common::async_blocking; + use secp256k1::SecretKey; + use zcash_primitives::consensus; + use zcash_primitives::transaction::builder::Builder as ZTxBuilder; + use zcash_primitives::transaction::components::OutPoint as ZCashOutpoint; +); + /// Sends HTLC output from the coin's my_z_addr pub async fn z_send_htlc( coin: &ZCoin, @@ -93,7 +102,7 @@ pub async fn z_send_dex_fee( } #[derive(Debug, Display)] -#[allow(clippy::large_enum_variant, clippy::upper_case_acronyms)] +#[allow(clippy::large_enum_variant, clippy::upper_case_acronyms, unused)] pub enum ZP2SHSpendError { ZTxBuilderError(ZTxBuilderError), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), @@ -130,6 +139,7 @@ impl ZP2SHSpendError { } /// Spends P2SH output 0 to the coin's my_z_addr +#[cfg(not(target_arch = "wasm32"))] pub async fn z_p2sh_spend( coin: &ZCoin, p2sh_tx: ZTransaction, @@ -181,3 +191,16 @@ pub async fn z_p2sh_spend( .map(|_| zcash_tx.clone()) .mm_err(|e| ZP2SHSpendError::TxRecoverable(zcash_tx.into(), e.to_string())) } + +#[cfg(target_arch = "wasm32")] +pub async fn z_p2sh_spend( + _coin: &ZCoin, + _p2sh_tx: ZTransaction, + _tx_locktime: u32, + _input_sequence: u32, + _redeem_script: Script, + _script_data: Script, + _htlc_keypair: &KeyPair, +) -> Result> { + todo!() +} diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index 66a5cfff1b..129abab700 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -1,59 +1,67 @@ -use super::{z_coin_errors::*, CheckPointBlockInfo, ZcoinConsensusParams}; -use crate::utxo::rpc_clients::{NativeClient, UtxoRpcClientOps, NO_TX_ERROR_CODE}; +use super::{z_coin_errors::*, ZcoinConsensusParams}; +use crate::utxo::rpc_clients::NativeClient; +use crate::z_coin::storage::{BlockDbImpl, WalletDbShared}; use async_trait::async_trait; -use common::executor::{spawn_abortable, AbortOnDropHandle, Timer}; -use common::log::{debug, error, info, LogOnError}; -use common::{async_blocking, Future01CompatExt}; -use db_common::sqlite::rusqlite::{params, Connection, Error as SqliteError, NO_PARAMS}; -use db_common::sqlite::{query_single_row, run_optimization_pragmas}; -use futures::channel::mpsc::{channel, Receiver as AsyncReceiver, Sender as AsyncSender}; +use common::executor::{spawn_abortable, AbortOnDropHandle}; +use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender}; use futures::channel::oneshot::{channel as oneshot_channel, Sender as OneshotSender}; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::StreamExt; -use group::GroupEncoding; -use http::Uri; use mm2_err_handle::prelude::*; use parking_lot::Mutex; -use prost::Message; -use protobuf::Message as ProtobufMessage; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::str::FromStr; use std::sync::Arc; -use std::time::Duration; -use tokio::task::block_in_place; -use tonic::transport::{Channel, ClientTlsConfig}; -use zcash_client_backend::data_api::chain::{scan_cached_blocks, validate_chain}; -use zcash_client_backend::data_api::error::Error as ChainError; -use zcash_client_backend::data_api::{BlockSource, WalletRead, WalletWrite}; -use zcash_client_backend::proto::compact_formats::CompactBlock; -use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; -use zcash_client_sqlite::wallet::init::{init_accounts_table, init_blocks_table, init_wallet_db}; -use zcash_client_sqlite::WalletDb; -use zcash_primitives::block::BlockHash; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::TxId; -use zcash_primitives::zip32::ExtendedFullViewingKey; -mod z_coin_grpc { - tonic::include_proto!("cash.z.wallet.sdk.rpc"); -} -use crate::{RpcCommonOps, ZTransaction}; -use rpc::v1::types::H256 as H256Json; -use z_coin_grpc::compact_tx_streamer_client::CompactTxStreamerClient; -use z_coin_grpc::{BlockId, BlockRange, ChainSpec, CompactBlock as TonicCompactBlock, +cfg_native!( + use super::CheckPointBlockInfo; + use crate::{RpcCommonOps, ZTransaction}; + use crate::utxo::rpc_clients::{UtxoRpcClientOps, NO_TX_ERROR_CODE}; + use crate::z_coin::storage::BlockDbError; + + use db_common::sqlite::rusqlite::Connection; + use db_common::sqlite::{query_single_row, run_optimization_pragmas}; + use common::async_blocking; + use common::executor::Timer; + use common::log::{debug, error, info, LogOnError}; + use common::Future01CompatExt; + use futures::channel::mpsc::channel; + use group::GroupEncoding; + use http::Uri; + use prost::Message; + use rpc::v1::types::H256 as H256Json; + use std::path::PathBuf; + use std::pin::Pin; + use std::str::FromStr; + use std::time::Duration; + use tokio::task::block_in_place; + use tonic::transport::{Channel, ClientTlsConfig}; + use zcash_client_backend::data_api::{WalletRead, WalletWrite}; + use zcash_client_backend::data_api::chain::{scan_cached_blocks, validate_chain}; + use zcash_client_backend::data_api::error::Error as ChainError; + use zcash_primitives::block::BlockHash; + use zcash_primitives::zip32::ExtendedFullViewingKey; + use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; + use zcash_client_sqlite::wallet::init::{init_accounts_table, init_blocks_table, init_wallet_db}; + use zcash_client_sqlite::WalletDb; + + mod z_coin_grpc { + tonic::include_proto!("cash.z.wallet.sdk.rpc"); + } + + use z_coin_grpc::compact_tx_streamer_client::CompactTxStreamerClient; + use z_coin_grpc::{BlockId, BlockRange, ChainSpec, CompactBlock as TonicCompactBlock, CompactOutput as TonicCompactOutput, CompactSpend as TonicCompactSpend, CompactTx as TonicCompactTx, TxFilter}; +); -pub type WalletDbShared = Arc>>; - -struct CompactBlockRow { - height: BlockHeight, - data: Vec, -} - +#[cfg(not(target_arch = "wasm32"))] pub type OnCompactBlockFn<'a> = dyn FnMut(TonicCompactBlock) -> Result<(), MmError> + Send + 'a; +#[cfg(target_arch = "wasm32")] +#[allow(unused)] +pub type OnCompactBlockFn<'a> = dyn FnMut(String) -> Result<(), MmError> + Send + 'a; + #[async_trait] pub trait ZRpcOps { async fn get_block_height(&mut self) -> Result>; @@ -68,10 +76,12 @@ pub trait ZRpcOps { async fn check_tx_existence(&mut self, tx_id: TxId) -> bool; } +#[cfg(not(target_arch = "wasm32"))] struct LightRpcClient { rpc_clients: AsyncMutex>>, } +#[cfg(not(target_arch = "wasm32"))] #[async_trait] impl RpcCommonOps for LightRpcClient { type RpcClient = CompactTxStreamerClient; @@ -93,6 +103,7 @@ impl RpcCommonOps for LightRpcClient { } } +#[cfg(not(target_arch = "wasm32"))] #[async_trait] impl ZRpcOps for LightRpcClient { async fn get_block_height(&mut self) -> Result> { @@ -167,6 +178,7 @@ impl ZRpcOps for LightRpcClient { } } +#[cfg(not(target_arch = "wasm32"))] #[async_trait] impl ZRpcOps for NativeClient { async fn get_block_height(&mut self) -> Result> { @@ -271,106 +283,7 @@ impl ZRpcOps for NativeClient { } } -/// A wrapper for the SQLite connection to the block cache database. -pub struct BlockDb(Connection); - -impl BlockDb { - /// Opens a connection to the wallet database stored at the specified path. - pub fn for_path>(path: P) -> Result { - let conn = Connection::open(path)?; - run_optimization_pragmas(&conn)?; - conn.execute( - "CREATE TABLE IF NOT EXISTS compactblocks ( - height INTEGER PRIMARY KEY, - data BLOB NOT NULL - )", - NO_PARAMS, - )?; - Ok(BlockDb(conn)) - } - - fn with_blocks( - &self, - from_height: BlockHeight, - limit: Option, - mut with_row: F, - ) -> Result<(), ZcashClientError> - where - F: FnMut(CompactBlock) -> Result<(), ZcashClientError>, - { - // Fetch the CompactBlocks we need to scan - let mut stmt_blocks = self - .0 - .prepare("SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC LIMIT ?")?; - - let rows = stmt_blocks.query_map( - params![u32::from(from_height), limit.unwrap_or(u32::max_value()),], - |row| { - Ok(CompactBlockRow { - height: BlockHeight::from_u32(row.get(0)?), - data: row.get(1)?, - }) - }, - )?; - - for row_result in rows { - let cbr = row_result?; - let block = CompactBlock::parse_from_bytes(&cbr.data) - .map_err(zcash_client_backend::data_api::error::Error::from)?; - - if block.height() != cbr.height { - return Err(ZcashClientError::CorruptedData(format!( - "Block height {} did not match row's height field value {}", - block.height(), - cbr.height - ))); - } - - with_row(block)?; - } - - Ok(()) - } - - fn get_latest_block(&self) -> Result> { - Ok(query_single_row( - &self.0, - "SELECT height FROM compactblocks ORDER BY height DESC LIMIT 1", - NO_PARAMS, - |row| row.get(0), - )? - .unwrap_or(0)) - } - - fn insert_block( - &self, - height: u32, - cb_bytes: Vec, - ) -> Result> { - self.0 - .prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)")? - .execute(params![height, cb_bytes]) - .map_err(MmError::new) - } - - fn rewind_to_height(&self, height: u32) -> Result> { - self.0 - .execute("DELETE from compactblocks WHERE height > ?1", [height]) - .map_err(MmError::new) - } -} - -impl BlockSource for BlockDb { - type Error = ZcashClientError; - - fn with_blocks(&self, from_height: BlockHeight, limit: Option, with_row: F) -> Result<(), Self::Error> - where - F: FnMut(CompactBlock) -> Result<(), Self::Error>, - { - self.with_blocks(from_height, limit, with_row) - } -} - +#[cfg(not(target_arch = "wasm32"))] pub async fn create_wallet_db( wallet_db_path: PathBuf, consensus_params: ZcoinConsensusParams, @@ -380,9 +293,10 @@ pub async fn create_wallet_db( async_blocking({ move || -> Result, MmError> { let db = WalletDb::for_path(wallet_db_path, consensus_params) - .map_to_mm(ZcoinClientInitError::WalletDbInitFailure)?; - run_optimization_pragmas(db.sql_conn()).map_to_mm(ZcoinClientInitError::WalletDbInitFailure)?; - init_wallet_db(&db).map_to_mm(ZcoinClientInitError::WalletDbInitFailure)?; + .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + run_optimization_pragmas(db.sql_conn()) + .map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; + init_wallet_db(&db).map_to_mm(|err| ZcoinClientInitError::ZcashDBError(err.to_string()))?; if db.get_extended_full_viewing_keys()?.is_empty() { init_accounts_table(&db, &[evk])?; if let Some(check_point) = check_point_block { @@ -401,10 +315,11 @@ pub async fn create_wallet_db( .await } +#[cfg(not(target_arch = "wasm32"))] pub(super) async fn init_light_client( coin: String, lightwalletd_urls: Vec, - blocks_db: BlockDb, + blocks_db: BlockDbImpl, wallet_db: WalletDbShared, consensus_params: ZcoinConsensusParams, scan_blocks_per_iteration: u32, @@ -472,10 +387,25 @@ pub(super) async fn init_light_client( )) } +#[cfg(target_arch = "wasm32")] +#[allow(unused)] +pub(super) async fn init_light_client( + _coin: String, + _lightwalletd_urls: Vec, + _blocks_db: BlockDbImpl, + _wallet_db: WalletDbShared, + _consensus_params: ZcoinConsensusParams, + _scan_blocks_per_iteration: u32, + _scan_interval_ms: u64, +) -> Result<(AsyncMutex, WalletDbShared), MmError> { + todo!() +} + +#[cfg(not(target_arch = "wasm32"))] pub(super) async fn init_native_client( coin: String, native_client: NativeClient, - blocks_db: BlockDb, + blocks_db: BlockDbImpl, wallet_db: WalletDbShared, consensus_params: ZcoinConsensusParams, scan_blocks_per_iteration: u32, @@ -504,6 +434,19 @@ pub(super) async fn init_native_client( )) } +#[cfg(target_arch = "wasm32")] +pub(super) async fn _init_native_client( + _coin: String, + _native_client: NativeClient, + _blocks_db: BlockDbImpl, + _consensus_params: ZcoinConsensusParams, + _scan_blocks_per_iteration: u32, + _scan_interval_ms: u64, +) -> Result<(AsyncMutex, String), MmError> { + todo!() +} + +#[cfg(not(target_arch = "wasm32"))] fn is_tx_imported(conn: &Connection, tx_id: TxId) -> bool { const QUERY: &str = "SELECT id_tx FROM transactions WHERE txid = ?1;"; match query_single_row(conn, QUERY, [tx_id.0.to_vec()], |row| row.get::<_, i64>(0)) { @@ -512,6 +455,10 @@ fn is_tx_imported(conn: &Connection, tx_id: TxId) -> bool { } } +#[cfg(target_arch = "wasm32")] +#[allow(unused)] +fn is_tx_imported(_conn: String, _tx_id: TxId) -> bool { todo!() } + pub struct SaplingSyncRespawnGuard { pub(super) sync_handle: Option<(SaplingSyncLoopHandle, Box)>, pub(super) abort_handle: Arc>, @@ -525,6 +472,7 @@ impl Drop for SaplingSyncRespawnGuard { } } +#[allow(unused)] impl SaplingSyncRespawnGuard { pub(super) fn watch_for_tx(&mut self, tx_id: TxId) { if let Some(ref mut handle) = self.sync_handle { @@ -553,10 +501,11 @@ pub enum SyncStatus { }, } +#[allow(unused)] pub struct SaplingSyncLoopHandle { coin: String, current_block: BlockHeight, - blocks_db: BlockDb, + blocks_db: BlockDbImpl, wallet_db: WalletDbShared, consensus_params: ZcoinConsensusParams, /// Notifies about sync status without stopping the loop, e.g. on coin activation @@ -569,6 +518,7 @@ pub struct SaplingSyncLoopHandle { scan_interval_ms: u64, } +#[cfg(not(target_arch = "wasm32"))] impl SaplingSyncLoopHandle { fn notify_blocks_cache_status(&mut self, current_scanned_block: u64, latest_block: u64) { self.sync_status_notifier @@ -608,7 +558,11 @@ impl SaplingSyncLoopHandle { ) -> Result<(), MmError> { let current_block = rpc.get_block_height().await?; let current_block_in_db = block_in_place(|| self.blocks_db.get_latest_block())?; - let extrema = block_in_place(|| self.wallet_db.lock().block_height_extrema())?; + let wallet_db = self.wallet_db.clone(); + let extrema = block_in_place(|| { + let conn = wallet_db.db.lock(); + conn.block_height_extrema() + })?; let mut from_block = self .consensus_params .sapling_activation_height @@ -619,7 +573,8 @@ impl SaplingSyncLoopHandle { } if current_block >= from_block { rpc.scan_blocks(from_block, current_block, &mut |block: TonicCompactBlock| { - block_in_place(|| self.blocks_db.insert_block(block.height as u32, block.encode_to_vec()))?; + block_in_place(|| self.blocks_db.insert_block(block.height as u32, block.encode_to_vec())) + .map_err(|err| UpdateBlocksCacheErr::ZcashDBError(err.to_string()))?; self.notify_blocks_cache_status(block.height, current_block); Ok(()) }) @@ -631,10 +586,10 @@ impl SaplingSyncLoopHandle { /// Scans cached blocks, validates the chain and updates WalletDb. /// For more notes on the process, check https://github.com/zcash/librustzcash/blob/master/zcash_client_backend/src/data_api/chain.rs#L2 - fn scan_blocks(&mut self) -> Result<(), MmError> { + fn scan_blocks(&mut self) -> Result<(), MmError> { // required to avoid immutable borrow of self let wallet_db_arc = self.wallet_db.clone(); - let wallet_guard = wallet_db_arc.lock(); + let wallet_guard = wallet_db_arc.db.lock(); let mut wallet_ops = wallet_guard.get_update_ops().expect("get_update_ops always returns Ok"); if let Err(e) = validate_chain( @@ -652,7 +607,7 @@ impl SaplingSyncLoopHandle { wallet_ops.rewind_to_height(rewind_height)?; self.blocks_db.rewind_to_height(rewind_height.into())?; }, - e => return MmError::err(e), + e => return MmError::err(BlockDbError::SqliteError(e)), } } @@ -691,6 +646,31 @@ impl SaplingSyncLoopHandle { } } +#[cfg(target_arch = "wasm32")] +#[allow(unused)] +impl SaplingSyncLoopHandle { + fn notify_blocks_cache_status(&mut self, _current_scanned_block: u64, _latest_block: u64) { todo!() } + + fn notify_building_wallet_db(&mut self, _current_scanned_block: u64, _latest_block: u64) { todo!() } + + fn notify_on_error(&mut self, _error: String) { todo!() } + + fn notify_sync_finished(&mut self) { todo!() } + + async fn update_blocks_cache( + &mut self, + _rpc: &mut (dyn ZRpcOps + Send), + ) -> Result<(), MmError> { + todo!() + } + + /// Scans cached blocks, validates the chain and updates WalletDb. + /// For more notes on the process, check https://github.com/zcash/librustzcash/blob/master/zcash_client_backend/src/data_api/chain.rs#L2 + fn scan_blocks(&mut self) -> Result<(), MmError> { todo!() } + + async fn check_watch_for_tx_existence(&mut self, _rpc: &mut (dyn ZRpcOps + Send)) { todo!() } +} + /// For more info on shielded light client protocol, please check the https://zips.z.cash/zip-0307 /// /// It's important to note that unlike standard UTXOs, shielded outputs are not spendable until the transaction is confirmed. @@ -712,6 +692,7 @@ impl SaplingSyncLoopHandle { /// 6. Once the transaction is generated and sent, `SaplingSyncRespawnGuard::watch_for_tx` is called to update `SaplingSyncLoopHandle` state. /// 7. Once the loop is respawned, it will check that broadcast tx is imported (or not available anymore) before stopping in favor of /// next wait_for_gen_tx_blockchain_sync call. +#[cfg(not(target_arch = "wasm32"))] async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle, mut client: Box) { info!( "(Re)starting light_wallet_db_sync_loop for {}, blocks per iteration {}, interval in ms {}", @@ -738,7 +719,7 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle, mut c sync_handle.check_watch_for_tx_existence(client.as_mut()).await; if let Some(tx_id) = sync_handle.watch_for_tx { - if !block_in_place(|| is_tx_imported(sync_handle.wallet_db.lock().sql_conn(), tx_id)) { + if !block_in_place(|| is_tx_imported(sync_handle.wallet_db.db.lock().sql_conn(), tx_id)) { info!("Tx {} is not imported yet", tx_id); Timer::sleep(10.).await; continue; @@ -760,6 +741,11 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle, mut c } } +#[cfg(target_arch = "wasm32")] +async fn light_wallet_db_sync_loop(mut _sync_handle: SaplingSyncLoopHandle, mut _client: Box) { + todo!() +} + type SyncWatcher = AsyncReceiver; type NewTxNotifier = AsyncSender)>>; @@ -770,6 +756,7 @@ pub(super) struct SaplingSyncConnector { } impl SaplingSyncConnector { + #[allow(unused)] #[inline] pub(super) fn new_mutex_wrapped( simple_sync_watcher: SyncWatcher, diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index 7247743631..47608b2b02 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -19,7 +19,7 @@ backtrace = "0.3" bytes = "1.1" cfg-if = "1.0" crossbeam = "0.8" -env_logger = "0.9.0" +env_logger = "0.9.3" derive_more = "0.99" fnv = "1.0.6" futures01 = { version = "0.1", package = "futures" } @@ -48,7 +48,6 @@ instant = { version = "0.1.12" } [target.'cfg(target_arch = "wasm32")'.dependencies] chrono = { version = "0.4", features = ["wasmbind"] } -getrandom = { version = "0.2.9", features = ["js"] } # see https://docs.rs/getrandom/0.2.0/getrandom/#webassembly-support gstuff = { version = "0.7", features = ["nightly"] } instant = { version = "0.1.12", features = ["wasm-bindgen"] } js-sys = "0.3.27" @@ -63,7 +62,7 @@ web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomExcepti anyhow = "1.0" chrono = "0.4" gstuff = { version = "0.7", features = ["nightly"] } -hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } +hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features hyper-rustls = { version = "0.23", default-features = false, features = ["http1", "http2", "webpki-tokio"] } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 8670eb3d87..653ad11353 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -109,7 +109,7 @@ macro_rules! some_or_return_ok_none { #[macro_use] pub mod jsonrpc_client; #[macro_use] -pub mod fmt; +pub mod write_safe; #[macro_use] pub mod log; @@ -132,7 +132,9 @@ pub mod wio; #[cfg(target_arch = "wasm32")] pub use wasm::*; use backtrace::SymbolName; -use chrono::Utc; +use chrono::format::ParseError; +use chrono::{DateTime, Utc}; +use derive_more::Display; pub use futures::compat::Future01CompatExt; use futures01::{future, Future}; use http::header::CONTENT_TYPE; @@ -151,7 +153,7 @@ use std::future::Future as Future03; use std::io::{BufReader, Read, Write}; use std::iter::Peekable; use std::mem::{forget, zeroed}; -use std::num::NonZeroUsize; +use std::num::{NonZeroUsize, TryFromIntError}; use std::ops::{Add, Deref, Div, RangeInclusive}; use std::os::raw::c_void; use std::panic::{set_hook, PanicInfo}; @@ -1014,3 +1016,32 @@ pub fn sha256_digest(path: &PathBuf) -> Result { }; Ok(digest) } + +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] +pub enum ParseRfc3339Err { + #[display( + fmt = "Error parsing datetime to timestamp. Expected format 'YYYY-MM-DDTHH:MM:SS.sssZ', got: {}", + _0 + )] + ParseTimestampError(String), + #[display(fmt = "Error while converting types: {}", _0)] + TryFromIntError(String), +} + +impl From for ParseRfc3339Err { + fn from(e: ParseError) -> Self { ParseRfc3339Err::ParseTimestampError(e.to_string()) } +} + +impl From for ParseRfc3339Err { + fn from(e: TryFromIntError) -> Self { ParseRfc3339Err::TryFromIntError(e.to_string()) } +} + +pub fn parse_rfc3339_to_timestamp(date_str: &str) -> Result { + let date: DateTime = date_str.parse()?; + Ok(date.timestamp().try_into()?) +} + +/// `is_initial_upgrade` function checks if the database is being upgraded from version 0 to 1. +/// This function returns a boolean indicating whether the database is being upgraded from version 0 to 1. +#[cfg(target_arch = "wasm32")] +pub fn is_initial_upgrade(old_version: u32, new_version: u32) -> bool { old_version == 0 && new_version == 1 } diff --git a/mm2src/common/fmt.rs b/mm2src/common/write_safe/fmt.rs similarity index 100% rename from mm2src/common/fmt.rs rename to mm2src/common/write_safe/fmt.rs diff --git a/mm2src/common/write_safe/io.rs b/mm2src/common/write_safe/io.rs new file mode 100644 index 0000000000..42982c3322 --- /dev/null +++ b/mm2src/common/write_safe/io.rs @@ -0,0 +1,30 @@ +use std::cell::RefMut; +use std::fmt; +use std::io::Write; +use std::ops::DerefMut; + +#[macro_export] +macro_rules! write_safe_io { + ($dst:expr, $($arg:tt)*) => { + $dst.write_safe(format_args!($($arg)*)) + } +} +#[macro_export] +macro_rules! writeln_safe_io { + ($dst:expr, $($arg:tt)*) => {{ + write_safe_io!($dst, $($arg)*); + write_safe_io!($dst, "\n"); + }}; +} + +pub trait WriteSafeIO { + fn write_safe<'a>(&mut self, args: fmt::Arguments<'_>) + where + Self: DerefMut, + { + let writer = self.deref_mut(); + Write::write_fmt(writer, args).expect("`write_fmt` should never fail for `WriteSafeIO` types") + } +} + +impl WriteSafeIO for RefMut<'_, &'_ mut dyn Write> {} diff --git a/mm2src/common/write_safe/mod.rs b/mm2src/common/write_safe/mod.rs new file mode 100644 index 0000000000..fa4a84f4f4 --- /dev/null +++ b/mm2src/common/write_safe/mod.rs @@ -0,0 +1,4 @@ +#[macro_use] +pub mod fmt; +#[macro_use] +pub mod io; diff --git a/mm2src/db_common/Cargo.toml b/mm2src/db_common/Cargo.toml index 3977b92329..7a469bca71 100644 --- a/mm2src/db_common/Cargo.toml +++ b/mm2src/db_common/Cargo.toml @@ -13,5 +13,5 @@ log = "0.4.17" uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -rusqlite = { version = "0.24.2", features = ["bundled"] } +rusqlite = { version = "0.28", features = ["bundled"] } sql-builder = "3.1.1" diff --git a/mm2src/db_common/src/sql_constraint.rs b/mm2src/db_common/src/sql_constraint.rs index ed4d44eafe..fa1592eba6 100644 --- a/mm2src/db_common/src/sql_constraint.rs +++ b/mm2src/db_common/src/sql_constraint.rs @@ -1,5 +1,5 @@ use crate::sqlite::StringError; -use common::fmt::WriteJoin; +use common::write_safe::fmt::WriteJoin; use rusqlite::{Error as SqlError, Result as SqlResult}; use std::collections::BTreeMap; use std::fmt; diff --git a/mm2src/db_common/src/sql_create.rs b/mm2src/db_common/src/sql_create.rs index f4fe557c69..771232dfd8 100644 --- a/mm2src/db_common/src/sql_create.rs +++ b/mm2src/db_common/src/sql_create.rs @@ -1,9 +1,9 @@ use crate::sql_constraint::SqlConstraint; use crate::sql_value::{FromQuoted, SqlValue}; use crate::sqlite::StringError; -use common::fmt::{WriteSafe, WriteSafeJoin}; -use common::write_safe; -use rusqlite::{Connection, Error as SqlError, Result as SqlResult, NO_PARAMS}; +use common::{write_safe, + write_safe::fmt::{WriteSafe, WriteSafeJoin}}; +use rusqlite::{Connection, Error as SqlError, Result as SqlResult}; use std::fmt; pub enum SqlType { @@ -144,7 +144,7 @@ impl<'a> SqlCreateTable<'a> { } pub fn create(self) -> SqlResult<()> { - self.conn.execute(&self.sql()?, NO_PARAMS)?; + self.conn.execute(&self.sql()?, [])?; Ok(()) } diff --git a/mm2src/db_common/src/sql_delete.rs b/mm2src/db_common/src/sql_delete.rs index fe6436fd61..eac48b9e77 100644 --- a/mm2src/db_common/src/sql_delete.rs +++ b/mm2src/db_common/src/sql_delete.rs @@ -1,7 +1,7 @@ use crate::sql_condition::SqlCondition; use crate::sqlite::{validate_table_name, OwnedSqlParams, SqlParamsBuilder}; use common::log::debug; -use rusqlite::{Connection, Error as SqlError, Result as SqlResult}; +use rusqlite::{params_from_iter, Connection, Error as SqlError, Result as SqlResult}; use sql_builder::SqlBuilder; /// A `DELETE` SQL request builder. @@ -36,7 +36,8 @@ impl<'a> SqlDelete<'a> { let params = self.params(); debug!("Trying to execute SQL query {} with params {:?}", sql, params); - self.conn.execute(&sql, params) + let params = params.clone().into_boxed_slice(); + self.conn.execute(&sql, params_from_iter(params.iter())) } /// Generates a string SQL request. @@ -68,7 +69,6 @@ impl<'a> SqlCondition for SqlDelete<'a> { #[cfg(test)] mod tests { use super::*; - use rusqlite::NO_PARAMS; const CREATE_TX_HISTORY_TABLE: &str = "CREATE TABLE tx_history ( tx_hash VARCHAR(255) NOT NULL UNIQUE, @@ -77,7 +77,7 @@ mod tests { description TEXT );"; - fn init_table_for_test(conn: &Connection) { conn.execute(CREATE_TX_HISTORY_TABLE, NO_PARAMS).unwrap(); } + fn init_table_for_test(conn: &Connection) { conn.execute(CREATE_TX_HISTORY_TABLE, []).unwrap(); } #[test] fn test_delete_all_sql() { diff --git a/mm2src/db_common/src/sql_insert.rs b/mm2src/db_common/src/sql_insert.rs index b5aa5f461b..bf6d77a5ca 100644 --- a/mm2src/db_common/src/sql_insert.rs +++ b/mm2src/db_common/src/sql_insert.rs @@ -1,9 +1,9 @@ use crate::sql_value::{FromQuoted, SqlValueOptional, SqlValueToString}; use crate::sqlite::{OwnedSqlParam, OwnedSqlParams, SqlParamsBuilder, ToValidSqlIdent}; -use common::fmt::{WriteSafe, WriteSafeJoin}; use common::write_safe; +use common::write_safe::fmt::{WriteSafe, WriteSafeJoin}; use log::debug; -use rusqlite::{Connection, Result as SqlResult}; +use rusqlite::{params_from_iter, Connection, Result as SqlResult}; use std::fmt; enum InsertMode { @@ -112,7 +112,7 @@ impl<'a> SqlInsert<'a> { debug!("Trying to execute SQL query {} with params {:?}", sql, self.params()); let mut stmt = self.conn.prepare(&sql)?; - stmt.execute(self.params()) + stmt.execute(params_from_iter(self.params().iter())) } /// Returns the reference to the specified SQL parameters. @@ -146,7 +146,6 @@ impl<'a> SqlInsert<'a> { #[cfg(test)] mod tests { use super::*; - use rusqlite::NO_PARAMS; const CREATE_TX_HISTORY_TABLE: &str = "CREATE TABLE tx_history ( tx_hash VARCHAR(255) NOT NULL UNIQUE, @@ -170,7 +169,7 @@ mod tests { fn select_from_tx_history(conn: &Connection) -> Vec { let mut stmt = conn.prepare(SELECT_FROM_TX_HISTORY).unwrap(); - stmt.query_map(NO_PARAMS, |row| { + stmt.query_map([], |row| { Ok(TxHistoryItem { tx_hash: row.get(0)?, description: row.get(1)?, @@ -186,7 +185,7 @@ mod tests { fn select_from_id_table(conn: &Connection) -> Vec { let mut stmt = conn.prepare(SELECT_FROM_ID_TABLE).unwrap(); - stmt.query_map(NO_PARAMS, |row| row.get(0)) + stmt.query_map([], |row| row.get(0)) .unwrap() .collect::>() .unwrap() @@ -195,7 +194,7 @@ mod tests { #[test] fn test_sql_insert() { let conn = Connection::open_in_memory().unwrap(); - conn.execute(CREATE_TX_HISTORY_TABLE, NO_PARAMS).unwrap(); + conn.execute(CREATE_TX_HISTORY_TABLE, []).unwrap(); let mut insert = SqlInsert::new(&conn, "tx_history"); insert @@ -244,7 +243,7 @@ mod tests { #[test] fn test_sql_insert_nulls() { let conn = Connection::open_in_memory().unwrap(); - conn.execute(CREATE_TX_HISTORY_TABLE, NO_PARAMS).unwrap(); + conn.execute(CREATE_TX_HISTORY_TABLE, []).unwrap(); let description: Option<&'static str> = None; let tx_hex: Option> = None; @@ -285,7 +284,7 @@ mod tests { #[test] fn test_sql_insert_one_column() { let conn = Connection::open_in_memory().unwrap(); - conn.execute(CREATE_TX_HISTORY_TABLE, NO_PARAMS).unwrap(); + conn.execute(CREATE_TX_HISTORY_TABLE, []).unwrap(); let mut insert = SqlInsert::new(&conn, "tx_history"); insert.column_quoted("tx_hash", "tx_hash_1").unwrap(); @@ -310,7 +309,7 @@ mod tests { #[test] fn test_sql_create_no_columns() { let conn = Connection::open_in_memory().unwrap(); - conn.execute(CREATE_ID_TABLE, NO_PARAMS).unwrap(); + conn.execute(CREATE_ID_TABLE, []).unwrap(); let insert = SqlInsert::new(&conn, "id"); diff --git a/mm2src/db_common/src/sql_query.rs b/mm2src/db_common/src/sql_query.rs index 4c7b6eb362..170939c18a 100644 --- a/mm2src/db_common/src/sql_query.rs +++ b/mm2src/db_common/src/sql_query.rs @@ -3,7 +3,7 @@ use crate::sql_value::{SqlValue, SqlValueToString}; use crate::sqlite::{query_single_row, validate_ident, validate_table_name, OwnedSqlParam, OwnedSqlParams, SqlParamsBuilder, StringError, ToValidSqlIdent, ToValidSqlTable}; use log::debug; -use rusqlite::{Connection, Error as SqlError, Result as SqlResult, Row}; +use rusqlite::{params_from_iter, Connection, Error as SqlError, Result as SqlResult, Row}; use sql_builder::SqlBuilder; /// A `SELECT` SQL query builder. @@ -266,7 +266,9 @@ impl<'a> SqlQuery<'a> { debug!("Trying to execute SQL query {} with params {:?}", sql, self.params()); let mut stmt = self.conn.prepare(&sql)?; - let items = stmt.query_map(self.params(), f)?.collect::>>()?; + let items = stmt + .query_map(params_from_iter(self.params().iter()), f)? + .collect::>>()?; // Otherwise, we'll get the compile error: // `stmt` does not live long enough Ok(items) @@ -284,7 +286,7 @@ impl<'a> SqlQuery<'a> { .sql() .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))?; debug!("Trying to execute SQL query {} with params {:?}", sql, self.params()); - query_single_row(self.conn, &sql, self.params(), f) + query_single_row(self.conn, &sql, params_from_iter(self.params().iter()), f) } /// Applies [`SqlQuery::ordering`] to [`SqlQuery::sql_builder`]. @@ -316,6 +318,23 @@ impl<'a> SqlQuery<'a> { .field(format!("ROW_NUMBER() OVER (ORDER BY {}) AS {}", order_by, alias)); Ok(self) } + + /// Count all rows + pub fn count_all(&mut self) -> SqlResult<&mut Self> { + self.sql_builder.count("*"); + Ok(self) + } + + /// Select from union tables + pub fn select_from_union_alias(conn: &'a Connection, union_sql: &str, alias: &'static str) -> SqlResult { + validate_table_name(alias)?; + Ok(SqlQuery { + conn, + sql_builder: SqlBuilder::select_from(format!("({}) AS {}", union_sql, alias)), + params: SqlParamsBuilder::default(), + ordering: Vec::default(), + }) + } } /// `SqlCondition` implements the following methods by default: @@ -357,7 +376,6 @@ impl SqlOrdering { mod tests { use super::*; use crate::sql_insert::SqlInsert; - use rusqlite::NO_PARAMS; const CREATE_TX_HISTORY_TABLE: &str = "CREATE TABLE tx_history ( tx_hash VARCHAR(255) NOT NULL UNIQUE, @@ -371,8 +389,8 @@ mod tests { );"; fn init_table_for_test(conn: &Connection) { - conn.execute(CREATE_TX_HISTORY_TABLE, NO_PARAMS).unwrap(); - conn.execute(CREATE_TX_ADDRESS_TABLE, NO_PARAMS).unwrap(); + conn.execute(CREATE_TX_HISTORY_TABLE, []).unwrap(); + conn.execute(CREATE_TX_ADDRESS_TABLE, []).unwrap(); let history_items = vec![ ("tx_hash_1", 699545, 23, Some(0.5)), diff --git a/mm2src/db_common/src/sql_update.rs b/mm2src/db_common/src/sql_update.rs index 7c38ec6ea4..2d4dbca6f6 100644 --- a/mm2src/db_common/src/sql_update.rs +++ b/mm2src/db_common/src/sql_update.rs @@ -2,7 +2,7 @@ use crate::sql_condition::SqlCondition; use crate::sql_value::{FromQuoted, SqlValueOptional, SqlValueToString}; use crate::sqlite::{validate_table_name, OwnedSqlParam, OwnedSqlParams, SqlParamsBuilder, ToValidSqlIdent}; use common::log::debug; -use rusqlite::{Connection, Error as SqlError, Result as SqlResult}; +use rusqlite::{params_from_iter, Connection, Error as SqlError, Result as SqlResult}; use sql_builder::SqlBuilder; /// An `UPDATE` SQL request builder. @@ -91,7 +91,7 @@ impl<'a> SqlUpdate<'a> { let params = self.params(); debug!("Trying to execute SQL query {} with params {:?}", sql, params); - self.conn.execute(&sql, params) + self.conn.execute(&sql, params_from_iter(params.iter())) } } @@ -115,7 +115,6 @@ impl<'a> SqlCondition for SqlUpdate<'a> { #[cfg(test)] mod tests { use super::*; - use rusqlite::NO_PARAMS; const CREATE_TX_HISTORY_TABLE: &str = "CREATE TABLE tx_history ( tx_hash VARCHAR(255) NOT NULL UNIQUE, @@ -124,7 +123,7 @@ mod tests { kmd_rewards REAL );"; - fn init_table_for_test(conn: &Connection) { conn.execute(CREATE_TX_HISTORY_TABLE, NO_PARAMS).unwrap(); } + fn init_table_for_test(conn: &Connection) { conn.execute(CREATE_TX_HISTORY_TABLE, []).unwrap(); } #[test] fn test_update_all_records() { diff --git a/mm2src/db_common/src/sqlite.rs b/mm2src/db_common/src/sqlite.rs index 4c3f2a610b..5da327c0fd 100644 --- a/mm2src/db_common/src/sqlite.rs +++ b/mm2src/db_common/src/sqlite.rs @@ -1,9 +1,11 @@ +#![allow(deprecated)] // TODO: remove this once rusqlite is >= 0.29 + pub use rusqlite; pub use sql_builder; use log::debug; use rusqlite::types::{FromSql, Type as SqlType, Value}; -use rusqlite::{Connection, Error as SqlError, Result as SqlResult, Row, ToSql, NO_PARAMS}; +use rusqlite::{Connection, Error as SqlError, Result as SqlResult, Row, ToSql}; use sql_builder::SqlBuilder; use std::error::Error as StdError; use std::fmt; @@ -50,8 +52,7 @@ pub fn string_from_row(row: &Row<'_>) -> Result { row.get(0) } pub fn query_single_row(conn: &Connection, query: &str, params: P, map_fn: F) -> Result, SqlError> where - P: IntoIterator, - P::Item: ToSql, + P: rusqlite::Params, F: FnOnce(&Row<'_>) -> Result, { let maybe_result = conn.query_row(query, params, map_fn); @@ -145,7 +146,7 @@ pub fn offset_by_id

( where_id: &str, ) -> SqlResult> where - P: IntoIterator + fmt::Debug, + P: IntoIterator + fmt::Debug + rusqlite::Params, P::Item: ToSql, { let row_number = format!("ROW_NUMBER() OVER (ORDER BY {}) AS row", order_by); @@ -215,10 +216,10 @@ where /// be safe to use, while giving great speed boost. /// With these, Mac and Linux have comparable SQLite performance. pub fn run_optimization_pragmas(conn: &Connection) -> Result<(), SqlError> { - conn.query_row("pragma journal_mode = WAL;", NO_PARAMS, |row| row.get::<_, String>(0))?; - conn.execute("pragma synchronous = normal;", NO_PARAMS)?; - conn.execute("pragma temp_store = memory;", NO_PARAMS)?; - conn.execute("pragma foreign_keys = ON;", NO_PARAMS)?; + conn.query_row("pragma journal_mode = WAL;", [], |row| row.get::<_, String>(0))?; + conn.execute("pragma synchronous = normal;", [])?; + conn.execute("pragma temp_store = memory;", [])?; + conn.execute("pragma foreign_keys = ON;", [])?; Ok(()) } diff --git a/mm2src/gossipsub/Cargo.toml b/mm2src/gossipsub/Cargo.toml index 3bb1530199..bc4c37e8cb 100644 --- a/mm2src/gossipsub/Cargo.toml +++ b/mm2src/gossipsub/Cargo.toml @@ -32,7 +32,7 @@ wasm-timer = "0.2.4" [dev-dependencies] async-std = "1.6.2" -env_logger = "0.9.0" +env_logger = "0.9.3" libp2p-plaintext = { git = "https://github.com/libp2p/rust-libp2p.git", tag ="v0.45.1" } libp2p-yamux = { git = "https://github.com/libp2p/rust-libp2p.git", tag ="v0.45.1" } quickcheck= { version = "0.9.2", default-features = false } diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index d208cb134a..e708e460aa 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "mm2_bin_lib" -version = "1.0.5-beta" +version = "1.0.6-beta" authors = ["James Lee", "Artem Pikulin", "Artem Grinblat", "Omar S.", "Onur Ozkan", "Alina Sharon", "Caglar Kaya", "Cipi", "Sergey Boiko", "Samuel Onoja", "Roman Sztergbaum", "Kadan Stadelmann "] edition = "2018" default-run = "mm2" @@ -49,7 +49,7 @@ gstuff = { version = "0.7", features = ["nightly"] } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = { version = "0.3.27" } -mm2_rpc = { path = "../mm2_rpc" } +mm2_rpc = { path = "../mm2_rpc", features=["rpc_facilities"] } serde = "1.0" wasm-bindgen = "0.2.86" wasm-bindgen-futures = { version = "0.4.1" } diff --git a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs index ad133685c3..4cafd6509e 100644 --- a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs @@ -16,7 +16,8 @@ use super::*; use common::log::{register_callback, LogLevel, WasmCallback}; use common::{console_err, console_info, deserialize_from_js, executor, serialize_to_js, set_panic_hook}; use enum_primitive_derive::Primitive; -use mm2_main::mm2::{LpMainParams, MmVersionResult}; +use mm2_main::mm2::LpMainParams; +use mm2_rpc::data::legacy::MmVersionResponse; use mm2_rpc::wasm_rpc::WasmRpcResponse; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -240,8 +241,11 @@ pub async fn mm2_rpc(payload: JsValue) -> Result { /// ``` #[wasm_bindgen] pub fn mm2_version() -> JsValue { - serialize_to_js(&MmVersionResult::new(MM_VERSION.into(), MM_DATETIME.into())) - .expect("expected serialization to succeed") + serialize_to_js(&MmVersionResponse { + result: MM_VERSION.into(), + datetime: MM_DATETIME.into(), + }) + .expect("expected serialization to succeed") } /// Stops the MarketMaker2 instance. diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index 6d967b852b..b8e34b34e4 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -26,7 +26,8 @@ uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] gstuff = { version = "0.7", features = ["nightly"] } -mm2_rpc = { path = "../mm2_rpc" } +mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +futures-rustls = { version = "0.21.1" } gstuff = { version = "0.7", features = ["nightly"] } diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 4b61c68724..3717b76606 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -26,10 +26,12 @@ cfg_wasm32! { cfg_native! { use db_common::sqlite::rusqlite::Connection; + use futures_rustls::webpki::DNSNameRef; use mm2_metrics::prometheus; use mm2_metrics::MmMetricsError; use std::net::{IpAddr, SocketAddr, AddrParseError}; use std::path::{Path, PathBuf}; + use std::str::FromStr; use std::sync::MutexGuard; } @@ -119,6 +121,8 @@ pub struct MmCtx { pub graceful_shutdown_registry: graceful_shutdown::GracefulShutdownRegistry, #[cfg(target_arch = "wasm32")] pub db_namespace: DbNamespaceId, + /// The context belonging to the `nft` mod: `NftCtx`. + pub nft_ctx: Mutex>>, } impl MmCtx { @@ -160,6 +164,7 @@ impl MmCtx { graceful_shutdown_registry: graceful_shutdown::GracefulShutdownRegistry::default(), #[cfg(target_arch = "wasm32")] db_namespace: DbNamespaceId::Main, + nft_ctx: Mutex::new(None), } } @@ -197,6 +202,52 @@ impl MmCtx { Ok(SocketAddr::new(ip, port as u16)) } + /// Whether to use HTTPS for RPC server or not. + #[cfg(not(target_arch = "wasm32"))] + pub fn is_https(&self) -> bool { self.conf["https"].as_bool().unwrap_or(false) } + + /// SANs for self-signed certificate generation. + #[cfg(not(target_arch = "wasm32"))] + pub fn alt_names(&self) -> Result, String> { + // Helper function to validate `alt_names` entries + fn validate_alt_name(name: &str) -> Result<(), String> { + // Check if it is a valid IP address + if let Ok(ip) = IpAddr::from_str(name) { + if ip.is_unspecified() { + return ERR!("IP address {} must be specified", ip); + } + return Ok(()); + } + + // Check if it is a valid DNS name + if DNSNameRef::try_from_ascii_str(name).is_ok() { + return Ok(()); + } + + ERR!( + "`alt_names` contains {} which is neither a valid IP address nor a valid DNS name", + name + ) + } + + if self.conf["alt_names"].is_null() { + // Default SANs + return Ok(vec!["localhost".to_string(), "127.0.0.1".to_string()]); + } + + json::from_value(self.conf["alt_names"].clone()) + .map_err(|e| format!("`alt_names` is not a valid JSON array of strings: {}", e)) + .and_then(|names: Vec| { + if names.is_empty() { + return ERR!("alt_names is empty"); + } + for name in &names { + try_s!(validate_alt_name(name)); + } + Ok(names) + }) + } + /// MM database path. /// Defaults to a relative "DB". /// @@ -221,7 +272,10 @@ impl MmCtx { pub fn is_watcher(&self) -> bool { self.conf["is_watcher"].as_bool().unwrap_or_default() } - pub fn use_watchers(&self) -> bool { self.conf["use_watchers"].as_bool().unwrap_or_default() } + pub fn use_watchers(&self) -> bool { + std::env::var("USE_WATCHERS").is_ok() + //self.conf["use_watchers"].as_bool().unwrap_or(true) + } pub fn netid(&self) -> u16 { let netid = self.conf["netid"].as_u64().unwrap_or(0); diff --git a/mm2src/mm2_libp2p/Cargo.toml b/mm2src/mm2_libp2p/Cargo.toml index d8667db241..3a0f283e36 100644 --- a/mm2src/mm2_libp2p/Cargo.toml +++ b/mm2src/mm2_libp2p/Cargo.toml @@ -33,11 +33,10 @@ tokio = { version = "1.20", features = ["rt-multi-thread", "macros"] } libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", tag = "v0.45.1", default-features = false, features = ["dns-tokio", "floodsub", "mplex", "noise", "ping", "request-response", "secp256k1", "tcp-tokio", "websocket"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2.9", features = ["js"] } # see https://docs.rs/getrandom/0.2.0/getrandom/#webassembly-support libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", tag = "v0.45.1", default-features = false, features = ["floodsub", "mplex", "noise", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket"] } wasm-bindgen-futures = "0.4.21" [dev-dependencies] async-std = { version = "1.6.2", features = ["unstable"] } -env_logger = "0.9.0" +env_logger = "0.9.3" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_libp2p/src/atomicdex_behaviour.rs b/mm2src/mm2_libp2p/src/atomicdex_behaviour.rs index da2b051bdd..56dc93cbb5 100644 --- a/mm2src/mm2_libp2p/src/atomicdex_behaviour.rs +++ b/mm2src/mm2_libp2p/src/atomicdex_behaviour.rs @@ -424,7 +424,7 @@ impl NetworkBehaviourEventProcess for AtomicDexBehaviour { if let FloodsubEvent::Message(message) = &event { for topic in &message.topics { if topic == &FloodsubTopic::new(PEERS_TOPIC) { - let addresses: PeerAddresses = match rmp_serde::from_read_ref(&message.data) { + let addresses: PeerAddresses = match rmp_serde::from_slice(&message.data) { Ok(a) => a, Err(_) => return, }; diff --git a/mm2src/mm2_libp2p/src/lib.rs b/mm2src/mm2_libp2p/src/lib.rs index a99841329e..ae554ca311 100644 --- a/mm2src/mm2_libp2p/src/lib.rs +++ b/mm2src/mm2_libp2p/src/lib.rs @@ -53,8 +53,9 @@ pub fn encode_message(message: &T) -> Result, rmp_serde::e rmp_serde::to_vec(message) } +#[inline] pub fn decode_message<'de, T: de::Deserialize<'de>>(bytes: &'de [u8]) -> Result { - rmp_serde::from_read_ref(bytes) + rmp_serde::from_slice(bytes) } #[derive(Deserialize, Serialize)] diff --git a/mm2src/mm2_libp2p/src/peers_exchange.rs b/mm2src/mm2_libp2p/src/peers_exchange.rs index 2bb1fbc7db..1721f73f8d 100644 --- a/mm2src/mm2_libp2p/src/peers_exchange.rs +++ b/mm2src/mm2_libp2p/src/peers_exchange.rs @@ -362,7 +362,7 @@ mod tests { fn test_peer_id_serde() { let peer_id = PeerIdSerde(PeerId::random()); let serialized = rmp_serde::to_vec(&peer_id).unwrap(); - let deserialized: PeerIdSerde = rmp_serde::from_read_ref(&serialized).unwrap(); + let deserialized: PeerIdSerde = rmp_serde::from_slice(&serialized).unwrap(); assert_eq!(peer_id.0, deserialized.0); } diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 42b5216333..f1258d1827 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -25,7 +25,7 @@ default = [] async-std = { version = "1.5", features = ["unstable"] } async-trait = "0.1" bitcrypto = { path = "../mm2_bitcoin/crypto" } -blake2 = "0.10" +blake2 = "0.10.6" bytes = "0.4" chain = { path = "../mm2_bitcoin/chain" } cfg-if = "1.0" @@ -44,12 +44,12 @@ enum-primitive-derive = "0.2" futures01 = { version = "0.1", package = "futures" } futures = { version = "0.3.1", package = "futures", features = ["compat", "async-await"] } gstuff = { version = "0.7", features = ["nightly"] } -mm2_gui_storage = { path = "../mm2_gui_storage" } hash256-std-hasher = "0.15.2" hash-db = "0.15.2" hex = "0.4.2" http = "0.2" hw_common = { path = "../hw_common" } +instant = { version = "0.1.12" } itertools = "0.10" keys = { path = "../mm2_bitcoin/keys" } lazy_static = "1.4" @@ -57,12 +57,13 @@ lazy_static = "1.4" libc = "0.2" mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } +mm2_gui_storage = { path = "../mm2_gui_storage" } mm2_io = { path = "../mm2_io" } mm2-libp2p = { path = "../mm2_libp2p" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } -mm2_rpc = { path = "../mm2_rpc" } +mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} num-traits = "0.2" parity-util-mem = "0.11" parking_lot = { version = "0.12.0", features = ["nightly"] } @@ -90,7 +91,6 @@ sp-trie = { version = "6.0", default-features = false } trie-db = { version = "0.23.1", default-features = false } trie-root = "0.16.0" uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } -instant = { version = "0.1.12" } [target.'cfg(target_arch = "wasm32")'.dependencies] instant = { version = "0.1.12", features = ["wasm-bindgen"] } @@ -105,7 +105,10 @@ web-sys = { version = "0.3.55", features = ["console"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs = { version = "1" } futures-rustls = { version = "0.21.1" } -hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } +hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } +rcgen = "0.10" +rustls = { version = "0.20", default-features = false } +rustls-pemfile = "1.0.2" tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } [target.'cfg(windows)'.dependencies] diff --git a/mm2src/mm2_main/src/database.rs b/mm2src/mm2_main/src/database.rs index 3a6f1f3add..47b1241cb3 100644 --- a/mm2src/mm2_main/src/database.rs +++ b/mm2src/mm2_main/src/database.rs @@ -9,7 +9,7 @@ pub mod my_orders; use crate::CREATE_MY_SWAPS_TABLE; use common::log::{debug, error, info}; use db_common::sqlite::run_optimization_pragmas; -use db_common::sqlite::rusqlite::{Result as SqlResult, NO_PARAMS}; +use db_common::sqlite::rusqlite::{params_from_iter, Result as SqlResult}; use mm2_core::mm_ctx::MmArc; use my_swaps::fill_my_swaps_from_json_statements; @@ -19,7 +19,7 @@ const SELECT_MIGRATION: &str = "SELECT * FROM migration ORDER BY current_migrati fn get_current_migration(ctx: &MmArc) -> SqlResult { let conn = ctx.sqlite_connection(); - conn.query_row(SELECT_MIGRATION, NO_PARAMS, |row| row.get(0)) + conn.query_row(SELECT_MIGRATION, [], |row| row.get(0)) } pub async fn init_and_migrate_db(ctx: &MmArc) -> SqlResult<()> { @@ -124,10 +124,10 @@ pub async fn migrate_sqlite_database(ctx: &MmArc, mut current_migration: i64) -> let transaction = conn.unchecked_transaction()?; for (statement, params) in statements_with_params { debug!("Executing SQL statement {:?} with params {:?}", statement, params); - transaction.execute(statement, params)?; + transaction.execute(statement, params_from_iter(params.iter()))?; } current_migration += 1; - transaction.execute("INSERT INTO migration (current_migration) VALUES (?1);", &[ + transaction.execute("INSERT INTO migration (current_migration) VALUES (?1);", [ current_migration, ])?; transaction.commit()?; diff --git a/mm2src/mm2_main/src/database/my_orders.rs b/mm2src/mm2_main/src/database/my_orders.rs index 776dca8487..898d1f1620 100644 --- a/mm2src/mm2_main/src/database/my_orders.rs +++ b/mm2src/mm2_main/src/database/my_orders.rs @@ -1,12 +1,14 @@ -use crate::mm2::lp_ordermatch::{FilteringOrder, MakerOrder, MyOrdersFilter, RecentOrdersSelectResult, TakerAction, - TakerOrder}; +#![allow(deprecated)] // TODO: remove this once rusqlite is >= 0.29 + +use crate::mm2::lp_ordermatch::{FilteringOrder, MakerOrder, MyOrdersFilter, RecentOrdersSelectResult, TakerOrder}; /// This module contains code to work with my_orders table in MM2 SQLite DB use common::log::debug; use common::{now_ms, PagingOptions}; use db_common::sqlite::offset_by_uuid; -use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Result as SqlResult, ToSql}; +use db_common::sqlite::rusqlite::{params_from_iter, Connection, Error as SqlError, Result as SqlResult, ToSql}; use db_common::sqlite::sql_builder::SqlBuilder; use mm2_core::mm_ctx::MmArc; +use mm2_rpc::data::legacy::TakerAction; use std::convert::TryInto; use std::string::ParseError; use uuid::Uuid; @@ -55,7 +57,8 @@ pub fn insert_maker_order(ctx: &MmArc, uuid: Uuid, order: &MakerOrder) -> SqlRes "Created".to_string(), ]; let conn = ctx.sqlite_connection(); - conn.execute(INSERT_MY_ORDER, ¶ms).map(|_| ()) + conn.execute(INSERT_MY_ORDER, params_from_iter(params.iter())) + .map(|_| ()) } pub fn insert_taker_order(ctx: &MmArc, uuid: Uuid, order: &TakerOrder) -> SqlResult<()> { @@ -79,7 +82,8 @@ pub fn insert_taker_order(ctx: &MmArc, uuid: Uuid, order: &TakerOrder) -> SqlRes "Created".to_string(), ]; let conn = ctx.sqlite_connection(); - conn.execute(INSERT_MY_ORDER, ¶ms).map(|_| ()) + conn.execute(INSERT_MY_ORDER, params_from_iter(params.iter())) + .map(|_| ()) } pub fn update_maker_order(ctx: &MmArc, uuid: Uuid, order: &MakerOrder) -> SqlResult<()> { @@ -92,7 +96,8 @@ pub fn update_maker_order(ctx: &MmArc, uuid: Uuid, order: &MakerOrder) -> SqlRes "Updated".to_string(), ]; let conn = ctx.sqlite_connection(); - conn.execute(UPDATE_MY_ORDER, ¶ms).map(|_| ()) + conn.execute(UPDATE_MY_ORDER, params_from_iter(params.iter())) + .map(|_| ()) } pub fn update_was_taker(ctx: &MmArc, uuid: Uuid) -> SqlResult<()> { @@ -104,14 +109,16 @@ pub fn update_was_taker(ctx: &MmArc, uuid: Uuid) -> SqlResult<()> { 1.to_string(), ]; let conn = ctx.sqlite_connection(); - conn.execute(UPDATE_WAS_TAKER, ¶ms).map(|_| ()) + conn.execute(UPDATE_WAS_TAKER, params_from_iter(params.iter())) + .map(|_| ()) } pub fn update_order_status(ctx: &MmArc, uuid: Uuid, status: String) -> SqlResult<()> { debug!("Updating order {} in the SQLite database", uuid); let params = vec![uuid.to_string(), now_ms().to_string(), status]; let conn = ctx.sqlite_connection(); - conn.execute(UPDATE_ORDER_STATUS, ¶ms).map(|_| ()) + conn.execute(UPDATE_ORDER_STATUS, params_from_iter(params.iter())) + .map(|_| ()) } /// Adds where clauses determined by MyOrdersFilter @@ -277,5 +284,7 @@ pub fn select_orders_by_filter( pub fn select_status_by_uuid(conn: &Connection, uuid: &Uuid) -> Result { let params = vec![uuid.to_string()]; - conn.query_row(SELECT_STATUS_BY_UUID, ¶ms, |row| row.get::<_, String>(0)) + conn.query_row(SELECT_STATUS_BY_UUID, params_from_iter(params.iter()), |row| { + row.get::<_, String>(0) + }) } diff --git a/mm2src/mm2_main/src/database/my_swaps.rs b/mm2src/mm2_main/src/database/my_swaps.rs index 835de9f240..ab5a08b84b 100644 --- a/mm2src/mm2_main/src/database/my_swaps.rs +++ b/mm2src/mm2_main/src/database/my_swaps.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] // TODO: remove this once rusqlite is >= 0.29 + /// This module contains code to work with my_swaps table in MM2 SQLite DB use crate::mm2::lp_swap::{MyRecentSwapsUuids, MySwapsFilter, SavedSwap, SavedSwapIo}; use common::log::debug; @@ -31,7 +33,7 @@ pub fn insert_new_swap(ctx: &MmArc, my_coin: &str, other_coin: &str, uuid: &str, debug!("Inserting new swap {} to the SQLite database", uuid); let conn = ctx.sqlite_connection(); let params = [my_coin, other_coin, uuid, started_at]; - conn.execute(INSERT_MY_SWAP, ¶ms).map(|_| ()) + conn.execute(INSERT_MY_SWAP, params).map(|_| ()) } /// Returns SQL statements to initially fill my_swaps table using existing DB with JSON files diff --git a/mm2src/mm2_main/src/database/stats_nodes.rs b/mm2src/mm2_main/src/database/stats_nodes.rs index b4f896015f..7a6330c24a 100644 --- a/mm2src/mm2_main/src/database/stats_nodes.rs +++ b/mm2src/mm2_main/src/database/stats_nodes.rs @@ -1,7 +1,7 @@ /// This module contains code to work with nodes table for stats collection in MM2 SQLite DB use crate::mm2::lp_stats::{NodeInfo, NodeVersionStat}; use common::log::debug; -use db_common::sqlite::rusqlite::{Error as SqlError, Result as SqlResult, NO_PARAMS}; +use db_common::sqlite::rusqlite::{params_from_iter, Error as SqlError, Result as SqlResult}; use mm2_core::mm_ctx::MmArc; use std::collections::hash_map::HashMap; @@ -38,21 +38,21 @@ pub fn insert_node_info(ctx: &MmArc, node_info: &NodeInfo) -> SqlResult<()> { node_info.peer_id.clone(), ]; let conn = ctx.sqlite_connection(); - conn.execute(INSERT_NODE, ¶ms).map(|_| ()) + conn.execute(INSERT_NODE, params_from_iter(params.iter())).map(|_| ()) } pub fn delete_node_info(ctx: &MmArc, name: String) -> SqlResult<()> { debug!("Deleting info about node {} from the SQLite database", name); let params = vec![name]; let conn = ctx.sqlite_connection(); - conn.execute(DELETE_NODE, ¶ms).map(|_| ()) + conn.execute(DELETE_NODE, params_from_iter(params.iter())).map(|_| ()) } pub fn select_peers_addresses(ctx: &MmArc) -> SqlResult, SqlError> { let conn = ctx.sqlite_connection(); let mut stmt = conn.prepare(SELECT_PEERS_ADDRESSES)?; let peers_addresses = stmt - .query_map(NO_PARAMS, |row| Ok((row.get(0)?, row.get(1)?)))? + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? .collect::>>()?; Ok(peers_addresses) @@ -62,7 +62,7 @@ pub fn select_peers_names(ctx: &MmArc) -> SqlResult, Sql let conn = ctx.sqlite_connection(); let mut stmt = conn.prepare(SELECT_PEERS_NAMES)?; let peers_names = stmt - .query_map(NO_PARAMS, |row| Ok((row.get(0)?, row.get(1)?)))? + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? .collect::>>(); peers_names @@ -80,5 +80,5 @@ pub fn insert_node_version_stat(ctx: &MmArc, node_version_stat: NodeVersionStat) node_version_stat.error.unwrap_or_default(), ]; let conn = ctx.sqlite_connection(); - conn.execute(INSERT_STAT, ¶ms).map(|_| ()) + conn.execute(INSERT_STAT, params_from_iter(params.iter())).map(|_| ()) } diff --git a/mm2src/mm2_main/src/database/stats_swaps.rs b/mm2src/mm2_main/src/database/stats_swaps.rs index 9ee2599aba..eb9c5e0fd1 100644 --- a/mm2src/mm2_main/src/database/stats_swaps.rs +++ b/mm2src/mm2_main/src/database/stats_swaps.rs @@ -1,7 +1,9 @@ +#![allow(deprecated)] // TODO: remove this once rusqlite is >= 0.29 + use crate::mm2::lp_swap::{MakerSavedSwap, SavedSwap, SavedSwapIo, TakerSavedSwap}; use common::log::{debug, error}; use db_common::{owned_named_params, - sqlite::{rusqlite::{Connection, OptionalExtension}, + sqlite::{rusqlite::{params_from_iter, Connection, OptionalExtension}, AsSqlNamedParams, OwnedSqlNamedParams}}; use mm2_core::mm_ctx::MmArc; use std::collections::HashSet; @@ -291,7 +293,9 @@ fn insert_stats_taker_swap_sql_init(swap: &TakerSavedSwap) -> Option<(&'static s pub fn add_swap_to_index(conn: &Connection, swap: &SavedSwap) { let params = vec![swap.uuid().to_string()]; - let query_row = conn.query_row(SELECT_ID_BY_UUID, ¶ms, |row| row.get::<_, i64>(0)); + let query_row = conn.query_row(SELECT_ID_BY_UUID, params_from_iter(params.iter()), |row| { + row.get::<_, i64>(0) + }); match query_row.optional() { // swap is not indexed yet, go ahead Ok(None) => (), diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index d79b2b75cc..d11a47a2ca 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -61,7 +61,7 @@ cfg_native! { #[path = "lp_init/init_metamask.rs"] pub mod init_metamask; -const NETID_7777_SEEDNODES: [&str; 3] = ["seed1.defimania.live", "seed2.defimania.live", "seed3.defimania.live"]; +const NETID_7777_SEEDNODES: [&str; 3] = ["seed1.komodo.earth", "seed2.komodo.earth", "seed3.komodo.earth"]; pub type P2PResult = Result>; pub type MmInitResult = Result>; diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index b1ff74d6b6..df54276698 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -31,7 +31,7 @@ use common::executor::{simple_map::AbortableSimpleMap, AbortSettings, AbortableS SpawnFuture, Timer}; use common::log::{error, warn, LogOnError}; use common::time_cache::TimeCache; -use common::{bits256, log, new_uuid, now_ms, now_sec, now_sec_i64}; +use common::{bits256, log, new_uuid, now_ms, now_sec}; use crypto::privkey::SerializableSecp256k1Keypair; use crypto::{CryptoCtx, CryptoCtxError}; use derive_more::Display; @@ -45,8 +45,13 @@ use mm2_err_handle::prelude::*; use mm2_libp2p::{decode_signed, encode_and_sign, encode_message, pub_sub_topic, TopicHash, TopicPrefix, TOPIC_SEPARATOR}; use mm2_metrics::mm_gauge; -use mm2_number::{construct_detailed, BigDecimal, BigRational, Fraction, MmNumber, MmNumberMultiRepr}; +use mm2_number::{BigDecimal, BigRational, MmNumber, MmNumberMultiRepr}; +use mm2_rpc::data::legacy::{MatchBy, Mm2RpcResult, OrderConfirmationsSettings, OrderType, RpcOrderbookEntry, + SellBuyRequest, SellBuyResponse, TakerAction, TakerRequestForRpc}; #[cfg(test)] use mocktopus::macros::*; +use my_orders_storage::{delete_my_maker_order, delete_my_taker_order, save_maker_order_on_update, + save_my_new_maker_order, save_my_new_taker_order, MyActiveOrders, MyOrdersFilteringHistory, + MyOrdersHistory, MyOrdersStorage}; use num_traits::identities::Zero; use parking_lot::Mutex as PaMutex; use rpc::v1::types::H256 as H256Json; @@ -72,9 +77,6 @@ use crate::mm2::lp_swap::{calc_max_maker_vol, check_balance_for_maker_swap, chec RunTakerSwapInput, SwapConfirmationsSettings, TakerSwap}; pub use best_orders::{best_orders_rpc, best_orders_rpc_v2}; -use my_orders_storage::{delete_my_maker_order, delete_my_taker_order, save_maker_order_on_update, - save_my_new_maker_order, save_my_new_taker_order, MyActiveOrders, MyOrdersFilteringHistory, - MyOrdersHistory, MyOrdersStorage}; pub use orderbook_depth::orderbook_depth_rpc; pub use orderbook_rpc::{orderbook_rpc, orderbook_rpc_v2}; @@ -211,7 +213,7 @@ impl From<(new_protocol::MakerOrderCreated, String)> for OrderbookItem { } pub fn addr_format_from_protocol_info(protocol_info: &[u8]) -> AddressFormat { - match rmp_serde::from_read_ref::<_, AddressFormat>(protocol_info) { + match rmp_serde::from_slice::(protocol_info) { Ok(format) => format, Err(_) => AddressFormat::Standard, } @@ -359,12 +361,12 @@ fn process_maker_order_updated( // Ok(()) // } -// ZHTLC protocol coin uses random keypair to sign P2P messages per every order. -// So, each ZHTLC order has unique «pubkey» field that doesn’t match node persistent pubkey derived from passphrase. +// Some coins, for example ZHTLC, have privacy features like random keypair to sign P2P messages per every order. +// So, each order of such coin has unique «pubkey» field that doesn’t match node persistent pubkey derived from passphrase. // We can compare pubkeys from maker_orders and from asks or bids, to find our order. #[inline(always)] -fn is_my_order(my_orders_pubkeys: &HashSet, my_pub: &Option, order_pubkey: &str) -> bool { - my_pub.as_deref() == Some(order_pubkey) || my_orders_pubkeys.contains(order_pubkey) +fn is_my_order(order_pubkey: &str, my_pub: &Option, my_p2p_pubkeys: &HashSet) -> bool { + my_pub.as_deref() == Some(order_pubkey) || my_p2p_pubkeys.contains(order_pubkey) } /// Request best asks and bids for the given `base` and `rel` coins from relays. @@ -395,11 +397,7 @@ async fn request_and_fill_orderbook(ctx: &MmArc, base: &str, rel: &str) -> Resul let ordermatch_ctx = OrdermatchContext::from_ctx(ctx).unwrap(); let mut orderbook = ordermatch_ctx.orderbook.lock(); - let my_pubsecp = match CryptoCtx::from_ctx(ctx).discard_mm_trace() { - Ok(crypto_ctx) => Some(crypto_ctx.mm2_internal_pubkey_hex()), - Err(CryptoCtxError::NotInitialized) => None, - Err(other) => return ERR!("{}", other), - }; + let my_pubsecp = mm2_internal_pubkey_hex(ctx, String::from).map_err(MmError::into_inner)?; let alb_pair = alb_ordered_pair(base, rel); for (pubkey, GetOrderbookPubkeyItem { orders, .. }) in pubkey_orders { @@ -411,7 +409,7 @@ async fn request_and_fill_orderbook(ctx: &MmArc, base: &str, rel: &str) -> Resul }, }; - if is_my_order(&orderbook.my_p2p_pubkeys, &my_pubsecp, &pubkey) { + if is_my_order(&pubkey, &my_pubsecp, &orderbook.my_p2p_pubkeys) { continue; } @@ -473,6 +471,18 @@ fn delete_my_order(ctx: &MmArc, uuid: Uuid, p2p_privkey: Option(ctx: &MmArc, err_construct: F) -> MmResult, E> +where + E: NotMmError, + F: Fn(String) -> E, +{ + match CryptoCtx::from_ctx(ctx).split_mm() { + Ok(crypto_ctx) => Ok(Some(CryptoCtx::mm2_internal_pubkey_hex(crypto_ctx.as_ref()))), + Err((CryptoCtxError::NotInitialized, _)) => Ok(None), + Err((CryptoCtxError::Internal(error), trace)) => MmError::err_with_trace(err_construct(error), trace), + } +} + fn remove_pubkey_pair_orders(orderbook: &mut Orderbook, pubkey: &str, alb_pair: &str) { let pubkey_state = match orderbook.pubkeys_state.get_mut(pubkey) { Some(state) => state, @@ -738,8 +748,8 @@ fn get_pubkeys_orders(orderbook: &Orderbook, base: String, rel: String) -> GetPu }; let uuids = uuids_by_pubkey.entry(order.pubkey.clone()).or_insert_with(Vec::new); protocol_infos.insert(order.uuid, order.base_rel_proto_info()); - if let Some(info) = order.conf_settings { - conf_infos.insert(order.uuid, info); + if let Some(ref info) = order.conf_settings { + conf_infos.insert(order.uuid, info.clone()); } uuids.push((*uuid, order.clone().into())) } @@ -945,8 +955,8 @@ fn process_sync_pubkey_orderbook_state( base: o.base_protocol_info.clone(), rel: o.rel_protocol_info.clone(), }); - if let Some(info) = o.conf_settings { - conf_infos.insert(o.uuid, info); + if let Some(ref info) = o.conf_settings { + conf_infos.insert(o.uuid, info.clone()); } }, None => { @@ -1032,7 +1042,7 @@ fn maker_order_created_p2p_notify( price: order.price.to_ratio(), max_volume: order.available_amount().to_ratio(), min_volume: order.min_base_vol.to_ratio(), - conf_settings: order.conf_settings.unwrap(), + conf_settings: order.conf_settings.clone().unwrap(), created_at: now_sec(), timestamp: now_sec(), pair_trie_root: H64::default(), @@ -1154,32 +1164,6 @@ impl BalanceTradeFeeUpdatedHandler for BalanceUpdateOrdermatchHandler { } } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum TakerAction { - Buy, - Sell, -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[cfg_attr(test, derive(Default))] -pub struct OrderConfirmationsSettings { - pub base_confs: u64, - pub base_nota: bool, - pub rel_confs: u64, - pub rel_nota: bool, -} - -impl OrderConfirmationsSettings { - pub fn reversed(&self) -> OrderConfirmationsSettings { - OrderConfirmationsSettings { - base_confs: self.rel_confs, - base_nota: self.rel_nota, - rel_confs: self.base_confs, - rel_nota: self.base_nota, - } - } -} - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct TakerRequest { pub base: String, @@ -1572,29 +1556,6 @@ impl<'a> TakerOrderBuilder<'a> { } } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(tag = "type", content = "data")] -pub enum MatchBy { - Any, - Orders(HashSet), - Pubkeys(HashSet), -} - -impl Default for MatchBy { - fn default() -> Self { MatchBy::Any } -} - -#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(tag = "type", content = "data")] -enum OrderType { - FillOrKill, - GoodTillCancelled, -} - -impl Default for OrderType { - fn default() -> Self { OrderType::GoodTillCancelled } -} - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct TakerOrder { pub created_at: u64, @@ -2953,7 +2914,7 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO let my_persistent_pub = compressed_pub_key_from_priv_raw(raw_priv.as_slice(), ChecksumType::DSHA256).unwrap(); let my_conf_settings = choose_maker_confs_and_notas( - maker_order.conf_settings, + maker_order.conf_settings.clone(), &maker_match.request, &maker_coin, &taker_coin, @@ -3437,8 +3398,12 @@ async fn process_maker_reserved(ctx: MmArc, from_pubkey: H256Json, reserved_msg: reserved_messages.sort_unstable_by_key(|r| r.price()); for reserved_msg in reserved_messages { - let my_conf_settings = - choose_maker_confs_and_notas(reserved_msg.conf_settings, &my_order.request, &base_coin, &rel_coin); + let my_conf_settings = choose_maker_confs_and_notas( + reserved_msg.conf_settings.clone(), + &my_order.request, + &base_coin, + &rel_coin, + ); let other_conf_settings = choose_taker_confs_and_notas(&my_order.request, &reserved_msg.conf_settings, &base_coin, &rel_coin); let atomic_locktime_v = AtomicLocktimeVersion::V2 { @@ -3564,7 +3529,7 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: }; let my_conf_settings = - choose_maker_confs_and_notas(order.conf_settings, &taker_request, &base_coin, &rel_coin); + choose_maker_confs_and_notas(order.conf_settings.clone(), &taker_request, &base_coin, &rel_coin); let other_conf_settings = choose_taker_confs_and_notas(&taker_request, &order.conf_settings, &base_coin, &rel_coin); let atomic_locktime_v = AtomicLocktimeVersion::V2 { @@ -3602,7 +3567,7 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: rel: order.rel_orderbook_ticker().to_owned(), taker_order_uuid: taker_request.uuid, maker_order_uuid: *uuid, - conf_settings: order.conf_settings.or_else(|| { + conf_settings: order.conf_settings.clone().or_else(|| { Some(OrderConfirmationsSettings { base_confs: base_coin.required_confirmations(), base_nota: base_coin.requires_notarization(), @@ -3704,39 +3669,8 @@ async fn process_taker_connect(ctx: MmArc, sender_pubkey: H256Json, connect_msg: } } -#[derive(Deserialize, Debug)] -pub struct AutoBuyInput { - base: String, - rel: String, - price: MmNumber, - volume: MmNumber, - timeout: Option, - /// Not used. Deprecated. - #[allow(dead_code)] - duration: Option, - // TODO: remove this field on API refactoring, method should be separated from params - method: String, - #[allow(dead_code)] - gui: Option, - #[serde(rename = "destpubkey")] - #[serde(default)] - #[allow(dead_code)] - dest_pub_key: H256Json, - #[serde(default)] - match_by: MatchBy, - #[serde(default)] - order_type: OrderType, - base_confs: Option, - base_nota: Option, - rel_confs: Option, - rel_nota: Option, - min_volume: Option, - #[serde(default = "get_true")] - save_in_history: bool, -} - pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { - let input: AutoBuyInput = try_s!(json::from_value(req)); + let input: SellBuyRequest = try_s!(json::from_value(req)); if input.base == input.rel { return ERR!("Base and rel must be different coins"); } @@ -3763,12 +3697,12 @@ pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { ) .await ); - let res = try_s!(lp_auto_buy(&ctx, &base_coin, &rel_coin, input).await).into_bytes(); + let res = try_s!(lp_auto_buy(&ctx, &base_coin, &rel_coin, input).await); Ok(try_s!(Response::builder().body(res))) } pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { - let input: AutoBuyInput = try_s!(json::from_value(req)); + let input: SellBuyRequest = try_s!(json::from_value(req)); if input.base == input.rel { return ERR!("Base and rel must be different coins"); } @@ -3794,7 +3728,7 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { ) .await ); - let res = try_s!(lp_auto_buy(&ctx, &base_coin, &rel_coin, input).await).into_bytes(); + let res = try_s!(lp_auto_buy(&ctx, &base_coin, &rel_coin, input).await); Ok(try_s!(Response::builder().body(res))) } @@ -3817,63 +3751,33 @@ struct TakerMatch { last_updated: u64, } -impl<'a> From<&'a TakerRequest> for TakerRequestForRpc<'a> { - fn from(request: &'a TakerRequest) -> TakerRequestForRpc<'a> { +impl<'a> From<&'a TakerRequest> for TakerRequestForRpc { + fn from(request: &'a TakerRequest) -> TakerRequestForRpc { TakerRequestForRpc { - base: &request.base, - rel: &request.rel, + base: request.base.clone(), + rel: request.rel.clone(), base_amount: request.base_amount.to_decimal(), base_amount_rat: request.base_amount.to_ratio(), rel_amount: request.rel_amount.to_decimal(), rel_amount_rat: request.rel_amount.to_ratio(), - action: &request.action, - uuid: &request.uuid, + action: request.action.clone(), + uuid: request.uuid, method: "request".to_string(), - sender_pubkey: &request.sender_pubkey, - dest_pub_key: &request.dest_pub_key, - match_by: &request.match_by, - conf_settings: &request.conf_settings, + sender_pubkey: request.sender_pubkey, + dest_pub_key: request.dest_pub_key, + match_by: request.match_by.clone(), + conf_settings: request.conf_settings.clone(), } } } -construct_detailed!(DetailedMinVolume, min_volume); - -#[derive(Serialize)] -struct LpautobuyResult<'a> { - #[serde(flatten)] - request: TakerRequestForRpc<'a>, - order_type: &'a OrderType, - #[serde(flatten)] - min_volume: DetailedMinVolume, - base_orderbook_ticker: &'a Option, - rel_orderbook_ticker: &'a Option, -} - -#[derive(Clone, Debug, Serialize)] -pub struct TakerRequestForRpc<'a> { - base: &'a str, - rel: &'a str, - base_amount: BigDecimal, - base_amount_rat: BigRational, - rel_amount: BigDecimal, - rel_amount_rat: BigRational, - action: &'a TakerAction, - uuid: &'a Uuid, - method: String, - sender_pubkey: &'a H256Json, - dest_pub_key: &'a H256Json, - match_by: &'a MatchBy, - conf_settings: &'a Option, -} - #[allow(clippy::needless_borrow)] pub async fn lp_auto_buy( ctx: &MmArc, base_coin: &MmCoinEnum, rel_coin: &MmCoinEnum, - input: AutoBuyInput, -) -> Result { + input: SellBuyRequest, +) -> Result, String> { if input.price < MmNumber::from(BigRational::new(1.into(), 100_000_000.into())) { return ERR!("Price is too low, minimum is 0.00000001"); } @@ -3927,19 +3831,20 @@ pub async fn lp_auto_buy( order.p2p_keypair(), ); - let result = json!({ "result": LpautobuyResult { + let res = try_s!(json::to_vec(&Mm2RpcResult::new(SellBuyResponse { request: (&order.request).into(), - order_type: &order.order_type, + order_type: order.order_type.clone(), min_volume: order.min_volume.clone().into(), - base_orderbook_ticker: &order.base_orderbook_ticker, - rel_orderbook_ticker: &order.rel_orderbook_ticker, - } }); + base_orderbook_ticker: order.base_orderbook_ticker.clone(), + rel_orderbook_ticker: order.rel_orderbook_ticker.clone(), + }))); save_my_new_taker_order(ctx.clone(), &order) .await .map_err(|e| ERRL!("{}", e))?; my_taker_orders.insert(order.request.uuid, order); - Ok(result.to_string()) + + Ok(res) } /// Orderbook Item P2P message @@ -3956,6 +3861,16 @@ struct OrderbookP2PItem { created_at: u64, } +macro_rules! try_get_age_or_default { + ($created_at: expr) => {{ + let now = now_sec(); + now.checked_sub($created_at).unwrap_or_else(|| { + warn!("now - created_at: ({} - {}) caused a u64 underflow", now, $created_at); + Default::default() + }) + }}; +} + impl OrderbookP2PItem { fn as_rpc_best_orders_buy( &self, @@ -3985,8 +3900,7 @@ impl OrderbookP2PItem { min_volume_rat: min_vol_mm.to_ratio(), min_volume_fraction: min_vol_mm.to_fraction(), pubkey: self.pubkey.clone(), - age: now_sec_i64(), - zcredits: 0, + age: try_get_age_or_default!(self.created_at), uuid: self.uuid, is_mine, base_max_volume, @@ -4051,8 +3965,7 @@ impl OrderbookP2PItem { min_volume_rat: min_vol_mm.to_ratio(), min_volume_fraction: min_vol_mm.to_fraction(), pubkey: self.pubkey.clone(), - age: now_sec_i64(), - zcredits: 0, + age: try_get_age_or_default!(self.created_at), uuid: self.uuid, is_mine, base_max_volume, @@ -4213,15 +4126,14 @@ impl OrderbookItem { min_volume_rat: min_vol_mm.to_ratio(), min_volume_fraction: min_vol_mm.to_fraction(), pubkey: self.pubkey.clone(), - age: now_sec_i64(), - zcredits: 0, + age: try_get_age_or_default!(self.created_at), uuid: self.uuid, is_mine, base_max_volume, base_min_volume, rel_max_volume, rel_min_volume, - conf_settings: self.conf_settings, + conf_settings: self.conf_settings.clone(), } } @@ -4234,7 +4146,7 @@ impl OrderbookItem { let base_min_volume = (&min_vol_mm / &price_mm).into(); let rel_max_volume = max_vol_mm.clone().into(); let rel_min_volume = min_vol_mm.clone().into(); - let conf_settings = self.conf_settings.map(|conf| conf.reversed()); + let conf_settings = self.conf_settings.as_ref().map(|conf| conf.reversed()); RpcOrderbookEntry { coin: self.base.clone(), @@ -4249,8 +4161,7 @@ impl OrderbookItem { min_volume_rat: min_vol_mm.to_ratio(), min_volume_fraction: min_vol_mm.to_fraction(), pubkey: self.pubkey.clone(), - age: now_sec_i64(), - zcredits: 0, + age: try_get_age_or_default!(self.created_at), uuid: self.uuid, is_mine, base_max_volume, @@ -4277,7 +4188,7 @@ impl OrderbookItem { is_mine, base_max_volume: max_vol_mm.into(), base_min_volume: min_vol_mm.into(), - conf_settings: self.conf_settings, + conf_settings: self.conf_settings.clone(), } } @@ -4286,7 +4197,7 @@ impl OrderbookItem { let max_vol_mm = MmNumber::from(self.max_volume.clone()); let min_vol_mm = MmNumber::from(self.min_volume.clone()); - let conf_settings = self.conf_settings.map(|conf| conf.reversed()); + let conf_settings = self.conf_settings.as_ref().map(|conf| conf.reversed()); RpcOrderbookEntryV2 { coin: self.base.clone(), @@ -4474,7 +4385,7 @@ impl<'a> From<&'a MakerReserved> for MakerReservedForRpc<'a> { #[derive(Debug, Serialize)] struct MakerMatchForRpc<'a> { - request: TakerRequestForRpc<'a>, + request: TakerRequestForRpc, reserved: MakerReservedForRpc<'a>, connect: Option>, connected: Option>, @@ -4682,7 +4593,7 @@ pub async fn set_price(ctx: MmArc, req: Json) -> Result>, Strin let req: SetPriceReq = try_s!(json::from_value(req)); let maker_order = create_maker_order(&ctx, req).await?; let rpc_result = MakerOrderForRpc::from(&maker_order); - let res = try_s!(json::to_vec(&json!({ "result": rpc_result }))); + let res = try_s!(json::to_vec(&Mm2RpcResult::new(rpc_result))); Ok(try_s!(Response::builder().body(res))) } @@ -4741,7 +4652,7 @@ pub async fn update_maker_order(ctx: &MmArc, req: MakerOrderUpdateReq) -> Result _ => return ERR!("Base coin {} and/or rel coin {} are not activated", base, rel), }; - let original_conf_settings = order_before_update.conf_settings.unwrap(); + let original_conf_settings = order_before_update.conf_settings.clone().unwrap(); let updated_conf_settings = OrderConfirmationsSettings { base_confs: req.base_confs.unwrap_or(original_conf_settings.base_confs), base_nota: req.base_nota.unwrap_or(original_conf_settings.base_nota), @@ -4853,7 +4764,7 @@ pub async fn update_maker_order_rpc(ctx: MmArc, req: Json) -> Result From<&'a TakerMatch> for TakerMatchForRpc<'a> { reserved: (&taker_match.reserved).into(), connect: (&taker_match.connect).into(), connected: taker_match.connected.as_ref().map(|connected| connected.into()), - last_updated: 0, + last_updated: taker_match.last_updated, } } } @@ -5242,7 +5153,7 @@ impl<'a> From<&'a TakerMatch> for TakerMatchForRpc<'a> { #[derive(Serialize)] struct TakerOrderForRpc<'a> { created_at: u64, - request: TakerRequestForRpc<'a>, + request: TakerRequestForRpc, matches: HashMap>, order_type: &'a OrderType, cancellable: bool, @@ -5269,6 +5180,7 @@ impl<'a> From<&'a TakerOrder> for TakerOrderForRpc<'a> { } } +#[allow(clippy::large_enum_variant)] #[derive(Serialize)] #[serde(tag = "type", content = "order")] enum OrderForRpc<'a> { @@ -5559,7 +5471,7 @@ pub async fn cancel_all_orders_rpc(ctx: MmArc, req: Json) -> Result, -} - #[derive(Clone, Debug, Serialize)] pub struct RpcOrderbookEntryV2 { coin: String, @@ -5667,7 +5543,7 @@ fn choose_maker_confs_and_notas( }); let (maker_coin_confs, maker_coin_nota, taker_coin_confs, taker_coin_nota) = match taker_req.conf_settings { - Some(taker_settings) => match taker_req.action { + Some(ref taker_settings) => match taker_req.action { TakerAction::Sell => { let maker_coin_confs = if taker_settings.rel_confs < maker_settings.base_confs { taker_settings.rel_confs @@ -5729,7 +5605,7 @@ fn choose_taker_confs_and_notas( ) -> SwapConfirmationsSettings { let (mut taker_coin_confs, mut taker_coin_nota, maker_coin_confs, maker_coin_nota) = match taker_req.action { TakerAction::Buy => match taker_req.conf_settings { - Some(s) => (s.rel_confs, s.rel_nota, s.base_confs, s.base_nota), + Some(ref s) => (s.rel_confs, s.rel_nota, s.base_confs, s.base_nota), None => ( taker_coin.required_confirmations(), taker_coin.requires_notarization(), @@ -5738,7 +5614,7 @@ fn choose_taker_confs_and_notas( ), }, TakerAction::Sell => match taker_req.conf_settings { - Some(s) => (s.base_confs, s.base_nota, s.rel_confs, s.rel_nota), + Some(ref s) => (s.base_confs, s.base_nota, s.rel_confs, s.rel_nota), None => ( taker_coin.required_confirmations(), taker_coin.requires_notarization(), @@ -5849,12 +5725,13 @@ fn orderbook_address( CoinProtocol::SOLANA | CoinProtocol::SPLTOKEN { .. } => { MmError::err(OrderbookAddrErr::CoinIsNotSupported(coin.to_owned())) }, + CoinProtocol::ZHTLC { .. } => Ok(OrderbookAddress::Shielded), #[cfg(not(target_arch = "wasm32"))] // Todo: Shielded address is used for lightning for now, the lightning node public key can be used for the orderbook entry pubkey // Todo: instead of the platform coin pubkey which is used right now. But lightning payments are supposed to be private, // Todo: so maybe we should hide the node address in the orderbook, only the sending node and the receiving node should know about a payment, // Todo: a routing node will know about a payment it routed but not the sender or the receiver. This will require using a new keypair for every order/swap // Todo: similar to how it's done for zcoin. - CoinProtocol::ZHTLC { .. } | CoinProtocol::LIGHTNING { .. } => Ok(OrderbookAddress::Shielded), + CoinProtocol::LIGHTNING { .. } => Ok(OrderbookAddress::Shielded), } } diff --git a/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs b/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs index 965fb743c2..7e20ed46bc 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs @@ -1,7 +1,3 @@ -use super::{addr_format_from_protocol_info, BaseRelProtocolInfo, OrderConfirmationsSettings, - OrderbookP2PItemWithProof, OrdermatchContext, OrdermatchRequest}; -use crate::mm2::lp_network::{request_any_relay, P2PRequest}; -use crate::mm2::lp_ordermatch::{orderbook_address, RpcOrderbookEntryV2}; use coins::{address_by_coin_conf_and_pubkey_str, coin_conf, is_wallet_only_conf, is_wallet_only_ticker}; use common::{log, HttpStatusCode}; use derive_more::Display; @@ -9,11 +5,16 @@ use http::{Response, StatusCode}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigRational, MmNumber}; +use mm2_rpc::data::legacy::OrderConfirmationsSettings; use num_traits::Zero; use serde_json::{self as json, Value as Json}; use std::collections::{HashMap, HashSet}; use uuid::Uuid; +use super::{addr_format_from_protocol_info, is_my_order, mm2_internal_pubkey_hex, orderbook_address, + BaseRelProtocolInfo, OrderbookP2PItemWithProof, OrdermatchContext, OrdermatchRequest, RpcOrderbookEntryV2}; +use crate::mm2::lp_network::{request_any_relay, P2PRequest}; + #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum BestOrdersAction { @@ -51,6 +52,8 @@ pub struct BestOrdersRequestV2 { coin: String, action: BestOrdersAction, request_by: RequestBestOrdersBy, + #[serde(default)] + exclude_mine: bool, } pub fn process_best_orders_p2p_request( @@ -103,8 +106,8 @@ pub fn process_best_orders_p2p_request( }; let order_w_proof = orderbook.orderbook_item_with_proof(o.clone()); protocol_infos.insert(order_w_proof.order.uuid, order_w_proof.order.base_rel_proto_info()); - if let Some(info) = order_w_proof.order.conf_settings { - conf_infos.insert(order_w_proof.order.uuid, info); + if let Some(ref info) = order_w_proof.order.conf_settings { + conf_infos.insert(order_w_proof.order.uuid, info.clone()); } best_orders.push(order_w_proof.into()); @@ -175,8 +178,8 @@ pub fn process_best_orders_p2p_request_by_number( Some(o) => { let order_w_proof = orderbook.orderbook_item_with_proof(o.clone()); protocol_infos.insert(order_w_proof.order.uuid, order_w_proof.order.base_rel_proto_info()); - if let Some(info) = order_w_proof.order.conf_settings { - conf_infos.insert(order_w_proof.order.uuid, info); + if let Some(ref info) = order_w_proof.order.conf_settings { + conf_infos.insert(order_w_proof.order.uuid, info.clone()); } best_orders.push(order_w_proof.into()); }, @@ -223,6 +226,8 @@ pub async fn best_orders_rpc(ctx: MmArc, req: Json) -> Result>, let mut response = HashMap::new(); if let Some((p2p_response, peer_id)) = best_orders_res { log::debug!("Got best orders {:?} from peer {}", p2p_response, peer_id); + let my_pubsecp = mm2_internal_pubkey_hex(&ctx, String::from).map_err(MmError::into_inner)?; + let my_p2p_pubkeys = ordermatch_ctx.orderbook.lock().my_p2p_pubkeys.clone(); for (coin, orders_w_proofs) in p2p_response.orders { let coin_conf = coin_conf(&ctx, &coin); if coin_conf.is_null() { @@ -256,9 +261,10 @@ pub async fn best_orders_rpc(ctx: MmArc, req: Json) -> Result>, }, }; let conf_settings = p2p_response.conf_infos.get(&order.uuid); + let is_mine = is_my_order(&order.pubkey, &my_pubsecp, &my_p2p_pubkeys); let entry = match req.action { - BestOrdersAction::Buy => order.as_rpc_best_orders_buy(address, conf_settings, false), - BestOrdersAction::Sell => order.as_rpc_best_orders_sell(address, conf_settings, false), + BestOrdersAction::Buy => order.as_rpc_best_orders_buy(address, conf_settings, is_mine), + BestOrdersAction::Sell => order.as_rpc_best_orders_sell(address, conf_settings, is_mine), }; if let Some(original_tickers) = ordermatch_ctx.original_tickers.get(&coin) { for ticker in original_tickers { @@ -286,13 +292,14 @@ pub async fn best_orders_rpc(ctx: MmArc, req: Json) -> Result>, pub enum BestOrdersRpcError { CoinIsWalletOnly(String), P2PError(String), + CtxError(String), } impl HttpStatusCode for BestOrdersRpcError { fn status_code(&self) -> StatusCode { match self { BestOrdersRpcError::CoinIsWalletOnly(_) => StatusCode::BAD_REQUEST, - BestOrdersRpcError::P2PError(_) => StatusCode::INTERNAL_SERVER_ERROR, + BestOrdersRpcError::P2PError(_) | BestOrdersRpcError::CtxError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -330,6 +337,9 @@ pub async fn best_orders_rpc_v2( let mut orders = HashMap::new(); if let Some((p2p_response, peer_id)) = best_orders_res { log::debug!("Got best orders {:?} from peer {}", p2p_response, peer_id); + let my_pubsecp = mm2_internal_pubkey_hex(&ctx, BestOrdersRpcError::CtxError)?; + let my_p2p_pubkeys = ordermatch_ctx.orderbook.lock().my_p2p_pubkeys.clone(); + for (coin, orders_w_proofs) in p2p_response.orders { let coin_conf = coin_conf(&ctx, &coin); if coin_conf.is_null() { @@ -345,6 +355,10 @@ pub async fn best_orders_rpc_v2( } for order_w_proof in orders_w_proofs { let order = order_w_proof.order; + let is_mine = is_my_order(&order.pubkey, &my_pubsecp, &my_p2p_pubkeys); + if req.exclude_mine && is_mine { + continue; + } let empty_proto_info = BaseRelProtocolInfo::default(); let proto_infos = p2p_response .protocol_infos @@ -363,8 +377,8 @@ pub async fn best_orders_rpc_v2( }; let conf_settings = p2p_response.conf_infos.get(&order.uuid); let entry = match req.action { - BestOrdersAction::Buy => order.as_rpc_best_orders_buy_v2(address, conf_settings, false), - BestOrdersAction::Sell => order.as_rpc_best_orders_sell_v2(address, conf_settings, false), + BestOrdersAction::Buy => order.as_rpc_best_orders_buy_v2(address, conf_settings, is_mine), + BestOrdersAction::Sell => order.as_rpc_best_orders_sell_v2(address, conf_settings, is_mine), }; if let Some(original_tickers) = ordermatch_ctx.original_tickers.get(&coin) { for ticker in original_tickers { @@ -454,18 +468,18 @@ mod best_orders_test { orders: HashMap::from_iter(std::iter::once(("RICK".into(), v1_orders))), }; - let v1_serialized = rmp_serde::to_vec(&v1).unwrap(); + let v1_serialized = rmp_serde::to_vec_named(&v1).unwrap(); - let mut new: BestOrdersP2PRes = rmp_serde::from_read_ref(&v1_serialized).unwrap(); + let mut new: BestOrdersP2PRes = rmp_serde::from_slice(&v1_serialized).unwrap(); new.protocol_infos.insert(new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], }); new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings::default()); - let new_serialized = rmp_serde::to_vec(&new).unwrap(); + let new_serialized = rmp_serde::to_vec_named(&new).unwrap(); - let v1_from_new: BestOrdersResV1 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + let v1_from_new: BestOrdersResV1 = rmp_serde::from_slice(&new_serialized).unwrap(); assert_eq!(v1, v1_from_new); #[derive(Debug, Deserialize, PartialEq, Serialize)] @@ -491,14 +505,14 @@ mod best_orders_test { }))), }; - let v2_serialized = rmp_serde::to_vec(&v2).unwrap(); + let v2_serialized = rmp_serde::to_vec_named(&v2).unwrap(); - let mut new: BestOrdersP2PRes = rmp_serde::from_read_ref(&v2_serialized).unwrap(); + let mut new: BestOrdersP2PRes = rmp_serde::from_slice(&v2_serialized).unwrap(); new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings::default()); - let new_serialized = rmp_serde::to_vec(&new).unwrap(); + let new_serialized = rmp_serde::to_vec_named(&new).unwrap(); - let v2_from_new: BestOrdersResV2 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + let v2_from_new: BestOrdersResV2 = rmp_serde::from_slice(&new_serialized).unwrap(); assert_eq!(v2, v2_from_new); } } diff --git a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs index 1fbbba5ca6..38a144952f 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs @@ -353,8 +353,9 @@ mod wasm_impl { use crate::mm2::lp_ordermatch::ordermatch_wasm_db::{DbTransactionError, InitDbError, MyActiveMakerOrdersTable, MyActiveTakerOrdersTable, MyFilteringHistoryOrdersTable, MyHistoryOrdersTable}; - use crate::mm2::lp_ordermatch::{OrdermatchContext, TakerAction}; + use crate::mm2::lp_ordermatch::OrdermatchContext; use common::log::warn; + use mm2_rpc::data::legacy::TakerAction; use num_traits::ToPrimitive; use std::sync::Arc; @@ -694,12 +695,13 @@ mod tests { use super::wasm_impl::{maker_order_to_filtering_history_item, taker_order_to_filtering_history_item}; use super::*; use crate::mm2::lp_ordermatch::ordermatch_wasm_db::{ItemId, MyFilteringHistoryOrdersTable}; - use crate::mm2::lp_ordermatch::{MatchBy, OrderType, OrdermatchContext, TakerAction, TakerRequest}; + use crate::mm2::lp_ordermatch::{OrdermatchContext, TakerRequest}; use common::{new_uuid, now_ms}; use futures::compat::Future01CompatExt; use itertools::Itertools; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_db::indexed_db::TableSignature; + use mm2_rpc::data::legacy::{MatchBy, OrderType, TakerAction}; use std::collections::HashMap; use wasm_bindgen_test::*; diff --git a/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs b/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs index 3505c59501..d00cd50050 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs @@ -1,11 +1,12 @@ -use super::{MatchBy as SuperMatchBy, TakerAction}; -use crate::mm2::lp_ordermatch::{AlbOrderedOrderbookPair, OrderConfirmationsSettings, H64}; use common::now_sec; use compact_uuid::CompactUuid; use mm2_number::{BigRational, MmNumber}; +use mm2_rpc::data::legacy::{MatchBy as SuperMatchBy, OrderConfirmationsSettings, TakerAction}; use std::collections::{HashMap, HashSet}; use uuid::Uuid; +use crate::mm2::lp_ordermatch::{AlbOrderedOrderbookPair, H64}; + #[derive(Debug, Deserialize, Serialize)] #[allow(clippy::large_enum_variant)] pub enum OrdermatchMessage { @@ -108,7 +109,7 @@ mod compact_uuid { } } -#[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct MakerOrderCreated { pub uuid: CompactUuid, pub base: String, @@ -236,7 +237,7 @@ impl MakerOrderUpdated { pub fn new_conf_settings(&self) -> Option { match self { MakerOrderUpdated::V1(_) => None, - MakerOrderUpdated::V2(v2) => v2.conf_settings, + MakerOrderUpdated::V2(v2) => v2.conf_settings.clone(), } } @@ -330,9 +331,9 @@ mod new_protocol_tests { pair_trie_root: H64::default(), }); - let serialized = rmp_serde::to_vec(&v1).unwrap(); + let serialized = rmp_serde::to_vec_named(&v1).unwrap(); - let deserialized: MakerOrderUpdated = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: MakerOrderUpdated = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, expected); @@ -344,7 +345,7 @@ mod new_protocol_tests { new_min_volume: Some(BigRational::from_integer(1.into())), timestamp, pair_trie_root: H64::default(), - conf_settings, + conf_settings: conf_settings.clone(), }); let expected = MakerOrderUpdatedV1 { @@ -356,9 +357,9 @@ mod new_protocol_tests { pair_trie_root: H64::default(), }; - let serialized = rmp_serde::to_vec(&v2).unwrap(); + let serialized = rmp_serde::to_vec_named(&v2).unwrap(); - let deserialized: MakerOrderUpdatedV1 = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: MakerOrderUpdatedV1 = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, expected); @@ -375,7 +376,7 @@ mod new_protocol_tests { let serialized = rmp_serde::to_vec(&v2).unwrap(); - let deserialized: MakerOrderUpdated = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: MakerOrderUpdated = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, v2); } @@ -411,14 +412,14 @@ mod new_protocol_tests { pair_trie_root: H64::default(), }; - let old_serialized = rmp_serde::to_vec(&old_msg).unwrap(); + let old_serialized = rmp_serde::to_vec_named(&old_msg).unwrap(); - let mut new: MakerOrderCreated = rmp_serde::from_read_ref(&old_serialized).unwrap(); + let mut new: MakerOrderCreated = rmp_serde::from_slice(&old_serialized).unwrap(); new.base_protocol_info = vec![1, 2, 3]; new.rel_protocol_info = vec![1, 2, 3, 4]; - let new_serialized = rmp_serde::to_vec(&new).unwrap(); - let _old_from_new: MakerOrderCreatedV1 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + let new_serialized = rmp_serde::to_vec_named(&new).unwrap(); + let _old_from_new: MakerOrderCreatedV1 = rmp_serde::from_slice(&new_serialized).unwrap(); } } diff --git a/mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs b/mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs index 56ad5cdb85..f50f7f2f3b 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/orderbook_rpc.rs @@ -1,39 +1,17 @@ -use super::{addr_format_from_protocol_info, is_my_order, orderbook_address, subscribe_to_orderbook_topic, - OrdermatchContext, RpcOrderbookEntry, RpcOrderbookEntryV2}; use coins::{address_by_coin_conf_and_pubkey_str, coin_conf, is_wallet_only_conf}; use common::log::warn; use common::{now_sec, HttpStatusCode}; -use crypto::{CryptoCtx, CryptoCtxError}; use derive_more::Display; use http::{Response, StatusCode}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_number::{construct_detailed, BigRational, MmNumber, MmNumberMultiRepr}; +use mm2_number::{BigRational, MmNumber, MmNumberMultiRepr}; +use mm2_rpc::data::legacy::{AggregatedOrderbookEntry, OrderbookRequest, OrderbookResponse, RpcOrderbookEntry}; use num_traits::Zero; use serde_json::{self as json, Value as Json}; -#[derive(Deserialize)] -pub struct OrderbookReq { - base: String, - rel: String, -} - -construct_detailed!(TotalAsksBaseVol, total_asks_base_vol); -construct_detailed!(TotalAsksRelVol, total_asks_rel_vol); -construct_detailed!(TotalBidsBaseVol, total_bids_base_vol); -construct_detailed!(TotalBidsRelVol, total_bids_rel_vol); -construct_detailed!(AggregatedBaseVol, base_max_volume_aggr); -construct_detailed!(AggregatedRelVol, rel_max_volume_aggr); - -#[derive(Debug, Serialize)] -pub struct AggregatedOrderbookEntry { - #[serde(flatten)] - entry: RpcOrderbookEntry, - #[serde(flatten)] - base_max_volume_aggr: AggregatedBaseVol, - #[serde(flatten)] - rel_max_volume_aggr: AggregatedRelVol, -} +use super::{addr_format_from_protocol_info, is_my_order, mm2_internal_pubkey_hex, orderbook_address, + subscribe_to_orderbook_topic, OrdermatchContext, RpcOrderbookEntryV2}; #[derive(Debug, Serialize)] pub struct AggregatedOrderbookEntryV2 { @@ -43,32 +21,6 @@ pub struct AggregatedOrderbookEntryV2 { rel_max_volume_aggr: MmNumberMultiRepr, } -#[derive(Debug, Serialize)] -pub struct OrderbookResponse { - #[serde(rename = "askdepth")] - ask_depth: u32, - asks: Vec, - base: String, - #[serde(rename = "biddepth")] - bid_depth: u32, - bids: Vec, - netid: u16, - #[serde(rename = "numasks")] - num_asks: usize, - #[serde(rename = "numbids")] - num_bids: usize, - rel: String, - timestamp: u64, - #[serde(flatten)] - total_asks_base: TotalAsksBaseVol, - #[serde(flatten)] - total_asks_rel: TotalAsksRelVol, - #[serde(flatten)] - total_bids_base: TotalBidsBaseVol, - #[serde(flatten)] - total_bids_rel: TotalBidsRelVol, -} - fn build_aggregated_entries(entries: Vec) -> (Vec, MmNumber, MmNumber) { let mut total_base = BigRational::zero(); let mut total_rel = BigRational::zero(); @@ -108,7 +60,7 @@ fn build_aggregated_entries_v2( } pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, String> { - let req: OrderbookReq = try_s!(json::from_value(req)); + let req: OrderbookRequest = try_s!(json::from_value(req)); if req.base == req.rel { return ERR!("Base and rel must be different coins"); } @@ -136,13 +88,11 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S try_s!(subscribe_to_orderbook_topic(&ctx, &base_ticker, &rel_ticker, request_orderbook).await); - let my_pubsecp = match CryptoCtx::from_ctx(&ctx).discard_mm_trace() { - Ok(crypto_ctx) => Some(crypto_ctx.mm2_internal_pubkey_hex()), - Err(CryptoCtxError::NotInitialized) => None, - Err(other) => return ERR!("{}", other), - }; + let my_pubsecp = mm2_internal_pubkey_hex(&ctx, String::from).map_err(MmError::into_inner)?; let orderbook = ordermatch_ctx.orderbook.lock(); + let my_p2p_pubkeys = &orderbook.my_p2p_pubkeys; + let mut asks = match orderbook.unordered.get(&(base_ticker.clone(), rel_ticker.clone())) { Some(uuids) => { let mut orderbook_entries = Vec::new(); @@ -159,7 +109,7 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S &ask.pubkey, address_format, )); - let is_mine = is_my_order(&orderbook.my_p2p_pubkeys, &my_pubsecp, &ask.pubkey); + let is_mine = is_my_order(&ask.pubkey, &my_pubsecp, my_p2p_pubkeys); orderbook_entries.push(ask.as_rpc_entry_ask(address, is_mine)); } orderbook_entries @@ -186,7 +136,7 @@ pub async fn orderbook_rpc(ctx: MmArc, req: Json) -> Result>, S &bid.pubkey, address_format, )); - let is_mine = is_my_order(&orderbook.my_p2p_pubkeys, &my_pubsecp, &bid.pubkey); + let is_mine = is_my_order(&bid.pubkey, &my_pubsecp, my_p2p_pubkeys); orderbook_entries.push(bid.as_rpc_entry_bid(address, is_mine)); } orderbook_entries @@ -284,7 +234,7 @@ impl From for OrderbookRpcError { pub async fn orderbook_rpc_v2( ctx: MmArc, - req: OrderbookReq, + req: OrderbookRequest, ) -> Result> { if req.base == req.rel { return MmError::err(OrderbookRpcError::BaseRelSame); @@ -305,15 +255,9 @@ pub async fn orderbook_rpc_v2( .await .map_to_mm(OrderbookRpcError::P2PSubscribeError)?; + let my_pubsecp = mm2_internal_pubkey_hex(&ctx, OrderbookRpcError::Internal)?; let orderbook = ordermatch_ctx.orderbook.lock(); - - let my_pubsecp = match CryptoCtx::from_ctx(&ctx).split_mm() { - Ok(crypto_ctx) => Some(crypto_ctx.mm2_internal_pubkey_hex()), - Err((CryptoCtxError::NotInitialized, _trace)) => None, - Err((CryptoCtxError::Internal(e), trace)) => { - return MmError::err_with_trace(OrderbookRpcError::Internal(e), trace) - }, - }; + let my_p2p_pubkeys = &orderbook.my_p2p_pubkeys; let mut asks = match orderbook.unordered.get(&(base_ticker.clone(), rel_ticker.clone())) { Some(uuids) => { @@ -334,7 +278,7 @@ pub async fn orderbook_rpc_v2( continue; }, }; - let is_mine = is_my_order(&orderbook.my_p2p_pubkeys, &my_pubsecp, &ask.pubkey); + let is_mine = is_my_order(&ask.pubkey, &my_pubsecp, my_p2p_pubkeys); orderbook_entries.push(ask.as_rpc_v2_entry_ask(address, is_mine)); } orderbook_entries @@ -364,7 +308,7 @@ pub async fn orderbook_rpc_v2( continue; }, }; - let is_mine = is_my_order(&orderbook.my_p2p_pubkeys, &my_pubsecp, &bid.pubkey); + let is_mine = is_my_order(&bid.pubkey, &my_pubsecp, my_p2p_pubkeys); orderbook_entries.push(bid.as_rpc_v2_entry_bid(address, is_mine)); } orderbook_entries diff --git a/mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs b/mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs index 6ff4319ed1..bec6262a7a 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/simple_market_maker.rs @@ -23,7 +23,7 @@ use std::collections::{HashMap, HashSet}; use uuid::Uuid; // !< constants -pub const KMD_PRICE_ENDPOINT: &str = "https://prices.komodo.live:1313/api/v2/tickers"; +pub const KMD_PRICE_ENDPOINT: &str = "https://prices.komodo.earth:1313/api/v2/tickers"; pub const BOT_DEFAULT_REFRESH_RATE: f64 = 30.0; pub const PRECISION_FOR_NOTIFICATION: u64 = 8; const LATEST_SWAPS_LIMIT: usize = 1000; diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 9d725d0021..f2d6b91e4b 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -119,7 +119,7 @@ pub use pubkey_banning::{ban_pubkey_rpc, is_pubkey_banned, list_banned_pubkeys_r pub use recreate_swap_data::recreate_swap_data; pub use saved_swap::{SavedSwap, SavedSwapError, SavedSwapIo, SavedSwapResult}; pub use swap_watcher::{process_watcher_msg, watcher_topic, TakerSwapWatcherData, MAKER_PAYMENT_SPEND_FOUND_LOG, - MAKER_PAYMENT_SPEND_SENT_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, TAKER_SWAP_ENTRY_TIMEOUT, + MAKER_PAYMENT_SPEND_SENT_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, TAKER_SWAP_ENTRY_TIMEOUT_SEC, WATCHER_PREFIX}; use taker_swap::TakerSwapEvent; pub use taker_swap::{calc_max_taker_vol, check_balance_for_taker_swap, max_taker_vol, max_taker_vol_from_available, @@ -202,17 +202,35 @@ pub fn p2p_private_and_peer_id_to_broadcast(ctx: &MmArc, p2p_privkey: Option<&Ke /// Spawns the loop that broadcasts message every `interval` seconds returning the AbortOnDropHandle /// to stop it -pub fn broadcast_swap_message_every( +pub fn broadcast_swap_msg_every( ctx: MmArc, topic: String, msg: T, - interval: f64, + interval_sec: f64, p2p_privkey: Option, ) -> AbortOnDropHandle { let fut = async move { loop { broadcast_swap_message(&ctx, topic.clone(), msg.clone(), &p2p_privkey); - Timer::sleep(interval).await; + Timer::sleep(interval_sec).await; + } + }; + spawn_abortable(fut) +} + +/// Spawns the loop that broadcasts message every `interval` seconds returning the AbortOnDropHandle +/// to stop it. This function waits for interval seconds first before starting the broadcast. +pub fn broadcast_swap_msg_every_delayed( + ctx: MmArc, + topic: String, + msg: T, + interval_sec: f64, + p2p_privkey: Option, +) -> AbortOnDropHandle { + let fut = async move { + loop { + Timer::sleep(interval_sec).await; + broadcast_swap_message(&ctx, topic.clone(), msg.clone(), &p2p_privkey); } }; spawn_abortable(fut) @@ -370,7 +388,7 @@ pub fn wait_for_maker_payment_conf_until(swap_started_at: u64, locktime: u64) -> const _SWAP_DEFAULT_NUM_CONFIRMS: u32 = 1; const _SWAP_DEFAULT_MAX_CONFIRMS: u32 = 6; /// MM2 checks that swap payment is confirmed every WAIT_CONFIRM_INTERVAL seconds -const WAIT_CONFIRM_INTERVAL: u64 = 15; +const WAIT_CONFIRM_INTERVAL_SEC: u64 = 15; #[derive(Debug, PartialEq, Serialize)] pub enum RecoveredSwapAction { @@ -437,7 +455,9 @@ impl SwapsContext { running_swaps: Mutex::new(vec![]), banned_pubkeys: Mutex::new(HashMap::new()), swap_msgs: Mutex::new(HashMap::new()), - taker_swap_watchers: PaMutex::new(DuplicateCache::new(Duration::from_secs(TAKER_SWAP_ENTRY_TIMEOUT))), + taker_swap_watchers: PaMutex::new(DuplicateCache::new(Duration::from_secs( + TAKER_SWAP_ENTRY_TIMEOUT_SEC, + ))), #[cfg(target_arch = "wasm32")] swap_db: ConstructibleDb::new(ctx), }) @@ -1628,9 +1648,9 @@ mod lp_swap_tests { persistent_pubkey: vec![1; 33], }); - let serialized = rmp_serde::to_vec(&v1).unwrap(); + let serialized = rmp_serde::to_vec_named(&v1).unwrap(); - let deserialized: NegotiationDataMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: NegotiationDataMsg = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, expected); @@ -1651,9 +1671,9 @@ mod lp_swap_tests { persistent_pubkey: vec![1; 33], }; - let serialized = rmp_serde::to_vec(&v2).unwrap(); + let serialized = rmp_serde::to_vec_named(&v2).unwrap(); - let deserialized: NegotiationDataV1 = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: NegotiationDataV1 = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, expected); @@ -1669,7 +1689,7 @@ mod lp_swap_tests { let serialized = rmp_serde::to_vec(&v2).unwrap(); - let deserialized: NegotiationDataMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: NegotiationDataMsg = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, v2); @@ -1686,7 +1706,7 @@ mod lp_swap_tests { // v3 must be deserialized to v3, backward compatibility is not required let serialized = rmp_serde::to_vec(&v3).unwrap(); - let deserialized: NegotiationDataMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: NegotiationDataMsg = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, v3); } @@ -1710,9 +1730,9 @@ mod lp_swap_tests { let expected = SwapMsg::MakerPayment(SwapTxDataMsg::Regular(MSG_DATA_INSTRUCTIONS.to_vec())); - let serialized = rmp_serde::to_vec(&old).unwrap(); + let serialized = rmp_serde::to_vec_named(&old).unwrap(); - let deserialized: SwapMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: SwapMsg = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, expected); @@ -1721,18 +1741,18 @@ mod lp_swap_tests { let expected = old; - let serialized = rmp_serde::to_vec(&v1).unwrap(); + let serialized = rmp_serde::to_vec_named(&v1).unwrap(); - let deserialized: SwapMsgOld = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: SwapMsgOld = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, expected); // PaymentDataMsg::Regular should be deserialized to PaymentDataMsg::Regular let v1 = SwapMsg::MakerPayment(SwapTxDataMsg::Regular(MSG_DATA_INSTRUCTIONS.to_vec())); - let serialized = rmp_serde::to_vec(&v1).unwrap(); + let serialized = rmp_serde::to_vec_named(&v1).unwrap(); - let deserialized: SwapMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: SwapMsg = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, v1); @@ -1742,9 +1762,9 @@ mod lp_swap_tests { next_step_instructions: MSG_DATA_INSTRUCTIONS.to_vec(), })); - let serialized = rmp_serde::to_vec(&v2).unwrap(); + let serialized = rmp_serde::to_vec_named(&v2).unwrap(); - let deserialized: SwapMsg = rmp_serde::from_read_ref(serialized.as_slice()).unwrap(); + let deserialized: SwapMsg = rmp_serde::from_slice(serialized.as_slice()).unwrap(); assert_eq!(deserialized, v2); @@ -1754,10 +1774,9 @@ mod lp_swap_tests { next_step_instructions: MSG_DATA_INSTRUCTIONS.to_vec(), })); - let serialized = rmp_serde::to_vec(&v2).unwrap(); + let serialized = rmp_serde::to_vec_named(&v2).unwrap(); - let deserialized: Result = - rmp_serde::from_read_ref(serialized.as_slice()); + let deserialized: Result = rmp_serde::from_slice(serialized.as_slice()); assert!(deserialized.is_err()); } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 9821173a7b..bfd69634ab 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -3,16 +3,16 @@ use super::check_balance::{check_base_coin_balance_for_swap, check_my_coin_balan use super::pubkey_banning::ban_pubkey_on_failed_swap; use super::swap_lock::{SwapLock, SwapLockOps}; use super::trade_preimage::{TradePreimageRequest, TradePreimageRpcError, TradePreimageRpcResult}; -use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_message_every, +use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_every, check_other_coin_balance_for_swap, detect_secret_hash_algo, dex_fee_amount_from_taker_coin, get_locked_amount, recv_swap_msg, swap_topic, taker_payment_spend_deadline, tx_helper_topic, wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, - SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; + SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL_SEC}; use crate::mm2::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::mm2::lp_network::subscribe_to_topic; -use crate::mm2::lp_ordermatch::{MakerOrderBuilder, OrderConfirmationsSettings}; +use crate::mm2::lp_ordermatch::MakerOrderBuilder; use crate::mm2::lp_swap::{broadcast_swap_message, taker_payment_spend_duration}; use coins::lp_price::fetch_swap_coins_price; use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, @@ -29,6 +29,7 @@ use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; +use mm2_rpc::data::legacy::OrderConfirmationsSettings; use parking_lot::Mutex as PaMutex; use primitives::hash::{H256, H264}; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; @@ -575,7 +576,7 @@ impl MakerSwap { }; // This will be done during order match - self.w().watcher_reward = false; + self.w().watcher_reward = std::env::var("USE_WATCHER_REWARD").is_ok(); Ok((Some(MakerSwapCommand::Negotiate), vec![MakerSwapEvent::Started(data)])) } @@ -584,21 +585,21 @@ impl MakerSwap { let negotiation_data = self.get_my_negotiation_data(); let maker_negotiation_data = SwapMsg::Negotiation(negotiation_data); - const NEGOTIATION_TIMEOUT: u64 = 90; + const NEGOTIATION_TIMEOUT_SEC: u64 = 90; debug!("Sending maker negotiation data {:?}", maker_negotiation_data); - let send_abort_handle = broadcast_swap_message_every( + let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), swap_topic(&self.uuid), maker_negotiation_data, - NEGOTIATION_TIMEOUT as f64 / 6., + NEGOTIATION_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); let recv_fut = recv_swap_msg( self.ctx.clone(), |store| store.negotiation_reply.take(), &self.uuid, - NEGOTIATION_TIMEOUT, + NEGOTIATION_TIMEOUT_SEC, ); let taker_data = match recv_fut.await { Ok(d) => d, @@ -688,13 +689,13 @@ impl MakerSwap { } async fn wait_taker_fee(&self) -> Result<(Option, Vec), String> { - const TAKER_FEE_RECV_TIMEOUT: u64 = 600; + const TAKER_FEE_RECV_TIMEOUT_SEC: u64 = 600; let negotiated = SwapMsg::Negotiated(true); - let send_abort_handle = broadcast_swap_message_every( + let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), swap_topic(&self.uuid), negotiated, - TAKER_FEE_RECV_TIMEOUT as f64 / 6., + TAKER_FEE_RECV_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); @@ -702,7 +703,7 @@ impl MakerSwap { self.ctx.clone(), |store| store.taker_fee.take(), &self.uuid, - TAKER_FEE_RECV_TIMEOUT, + TAKER_FEE_RECV_TIMEOUT_SEC, ); let payload = match recv_fut.await { Ok(d) => d, @@ -888,6 +889,7 @@ impl MakerSwap { } async fn wait_for_taker_payment(&self) -> Result<(Option, Vec), String> { + const PAYMENT_MSG_INTERVAL_SEC: f64 = 600.; let payment_data_msg = match self.get_my_payment_data().await { Ok(data) => data, Err(e) => { @@ -900,8 +902,13 @@ impl MakerSwap { }, }; let msg = SwapMsg::MakerPayment(payment_data_msg); - let abort_send_handle = - broadcast_swap_message_every(self.ctx.clone(), swap_topic(&self.uuid), msg, 600., self.p2p_privkey); + let abort_send_handle = broadcast_swap_msg_every( + self.ctx.clone(), + swap_topic(&self.uuid), + msg, + PAYMENT_MSG_INTERVAL_SEC, + self.p2p_privkey, + ); let maker_payment_wait_confirm = wait_for_maker_payment_conf_until(self.r().data.started_at, self.r().data.lock_duration); @@ -910,7 +917,7 @@ impl MakerSwap { confirmations: self.r().data.maker_payment_confirmations, requires_nota: self.r().data.maker_payment_requires_nota.unwrap_or(false), wait_until: maker_payment_wait_confirm, - check_every: WAIT_CONFIRM_INTERVAL, + check_every: WAIT_CONFIRM_INTERVAL_SEC, }; let f = self.maker_coin.wait_for_confirmations(confirm_maker_payment_input); @@ -984,7 +991,7 @@ impl MakerSwap { confirmations, requires_nota: self.r().data.taker_payment_requires_nota.unwrap_or(false), wait_until: wait_taker_payment, - check_every: WAIT_CONFIRM_INTERVAL, + check_every: WAIT_CONFIRM_INTERVAL_SEC, }; let wait_f = self .taker_coin @@ -1140,7 +1147,7 @@ impl MakerSwap { confirmations, requires_nota, wait_until: self.wait_refund_until(), - check_every: WAIT_CONFIRM_INTERVAL, + check_every: WAIT_CONFIRM_INTERVAL_SEC, }; let wait_fut = self.taker_coin.wait_for_confirmations(confirm_taker_payment_input); if let Err(err) = wait_fut.compat().await { diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index bc2695332a..1e6bcdf3f2 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -1,5 +1,5 @@ use super::{broadcast_p2p_tx_msg, get_payment_locktime, lp_coinfind, taker_payment_spend_deadline, tx_helper_topic, - H256Json, SwapsContext, WAIT_CONFIRM_INTERVAL}; + H256Json, SwapsContext, WAIT_CONFIRM_INTERVAL_SEC}; use crate::mm2::lp_network::{P2PRequestError, P2PRequestResult}; use crate::mm2::MmError; use async_trait::async_trait; @@ -22,7 +22,7 @@ use uuid::Uuid; pub const WATCHER_PREFIX: TopicPrefix = "swpwtchr"; const TAKER_SWAP_CONFIRMATIONS: u64 = 1; -pub const TAKER_SWAP_ENTRY_TIMEOUT: u64 = 21600; +pub const TAKER_SWAP_ENTRY_TIMEOUT_SEC: u64 = 21600; pub const MAKER_PAYMENT_SPEND_SENT_LOG: &str = "Maker payment spend sent"; pub const MAKER_PAYMENT_SPEND_FOUND_LOG: &str = "Maker payment spend found by watcher"; @@ -224,7 +224,7 @@ impl State for ValidateTakerPayment { confirmations, requires_nota: watcher_ctx.data.taker_payment_requires_nota.unwrap_or(false), wait_until: taker_payment_spend_deadline, - check_every: WAIT_CONFIRM_INTERVAL, + check_every: WAIT_CONFIRM_INTERVAL_SEC, }; let wait_fut = watcher_ctx diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 660629749a..1be5345b2d 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -4,16 +4,16 @@ use super::pubkey_banning::ban_pubkey_on_failed_swap; use super::swap_lock::{SwapLock, SwapLockOps}; use super::swap_watcher::{watcher_topic, SwapWatcherMsg}; use super::trade_preimage::{TradePreimageRequest, TradePreimageRpcError, TradePreimageRpcResult}; -use super::{broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_message_every, +use super::{broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_msg_every, check_other_coin_balance_for_swap, dex_fee_amount_from_taker_coin, dex_fee_rate, dex_fee_threshold, get_locked_amount, recv_swap_msg, swap_topic, wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, - SwapTxDataMsg, SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; + SwapTxDataMsg, SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL_SEC}; use crate::mm2::lp_network::subscribe_to_topic; -use crate::mm2::lp_ordermatch::{MatchBy, OrderConfirmationsSettings, TakerAction, TakerOrderBuilder}; -use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, tx_helper_topic, wait_for_maker_payment_conf_duration, - TakerSwapWatcherData}; +use crate::mm2::lp_ordermatch::TakerOrderBuilder; +use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, broadcast_swap_msg_every_delayed, tx_helper_topic, + wait_for_maker_payment_conf_duration, TakerSwapWatcherData}; use coins::lp_price::fetch_swap_coins_price; use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, @@ -29,6 +29,7 @@ use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; +use mm2_rpc::data::legacy::{MatchBy, OrderConfirmationsSettings, TakerAction}; use parking_lot::Mutex as PaMutex; use primitives::hash::H264; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; @@ -113,14 +114,18 @@ async fn save_my_taker_swap_event(ctx: &MmArc, swap: &TakerSwap, event: TakerSav gui: ctx.gui().map(|g| g.to_owned()), mm_version: Some(ctx.mm_version.to_owned()), events: vec![], - success_events: match ctx.use_watchers() { - true => TAKER_USING_WATCHERS_SUCCESS_EVENTS + success_events: if ctx.use_watchers() + && swap.taker_coin.is_supported_by_watchers() + && swap.maker_coin.is_supported_by_watchers() + { + TAKER_USING_WATCHERS_SUCCESS_EVENTS .iter() - .map(|event| event.to_string()) - .collect(), - false => TAKER_SUCCESS_EVENTS.iter().map(|event| event.to_string()).collect(), + .map(<&str>::to_string) + .collect() + } else { + TAKER_SUCCESS_EVENTS.iter().map(<&str>::to_string).collect() }, - error_events: TAKER_ERROR_EVENTS.iter().map(|event| event.to_string()).collect(), + error_events: TAKER_ERROR_EVENTS.iter().map(<&str>::to_string).collect(), }), Err(e) => return ERR!("{}", e), }; @@ -319,7 +324,7 @@ impl TakerSavedSwap { Some(event) => match &event.event { TakerSwapEvent::Negotiated(neg) => { let Some(key) = neg.maker_coin_htlc_pubkey else { - return ERR!("maker's pubkey is empty"); + return ERR!("maker's pubkey is empty"); }; key.to_string() }, @@ -1070,19 +1075,19 @@ impl TakerSwap { }; // This will be done during order match - self.w().watcher_reward = false; + self.w().watcher_reward = std::env::var("USE_WATCHER_REWARD").is_ok(); Ok((Some(TakerSwapCommand::Negotiate), vec![TakerSwapEvent::Started(data)])) } async fn negotiate(&self) -> Result<(Option, Vec), String> { - const NEGOTIATE_TIMEOUT: u64 = 90; + const NEGOTIATE_TIMEOUT_SEC: u64 = 90; let recv_fut = recv_swap_msg( self.ctx.clone(), |store| store.negotiation.take(), &self.uuid, - NEGOTIATE_TIMEOUT, + NEGOTIATE_TIMEOUT_SEC, ); let maker_data = match recv_fut.await { Ok(d) => d, @@ -1182,18 +1187,18 @@ impl TakerSwap { let taker_data = SwapMsg::NegotiationReply(my_negotiation_data); debug!("Sending taker negotiation data {:?}", taker_data); - let send_abort_handle = broadcast_swap_message_every( + let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), swap_topic(&self.uuid), taker_data, - NEGOTIATE_TIMEOUT as f64 / 6., + NEGOTIATE_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); let recv_fut = recv_swap_msg( self.ctx.clone(), |store| store.negotiated.take(), &self.uuid, - NEGOTIATE_TIMEOUT, + NEGOTIATE_TIMEOUT_SEC, ); let negotiated = match recv_fut.await { Ok(d) => d, @@ -1264,7 +1269,7 @@ impl TakerSwap { } async fn wait_for_maker_payment(&self) -> Result<(Option, Vec), String> { - const MAKER_PAYMENT_WAIT_TIMEOUT: u64 = 600; + const MAKER_PAYMENT_WAIT_TIMEOUT_SEC: u64 = 600; let payment_data_msg = match self.get_taker_fee_data().await { Ok(data) => data, @@ -1276,11 +1281,11 @@ impl TakerSwap { }; let msg = SwapMsg::TakerFee(payment_data_msg); - let abort_send_handle = broadcast_swap_message_every( + let abort_send_handle = broadcast_swap_msg_every( self.ctx.clone(), swap_topic(&self.uuid), msg, - MAKER_PAYMENT_WAIT_TIMEOUT as f64 / 6., + MAKER_PAYMENT_WAIT_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); @@ -1288,7 +1293,7 @@ impl TakerSwap { self.ctx.clone(), |store| store.maker_payment.take(), &self.uuid, - MAKER_PAYMENT_WAIT_TIMEOUT, + MAKER_PAYMENT_WAIT_TIMEOUT_SEC, ); let payload = match recv_fut.await { Ok(p) => p, @@ -1356,7 +1361,7 @@ impl TakerSwap { confirmations, requires_nota: self.r().data.maker_payment_requires_nota.unwrap_or(false), wait_until: self.r().data.maker_payment_wait, - check_every: WAIT_CONFIRM_INTERVAL, + check_every: WAIT_CONFIRM_INTERVAL_SEC, }; let f = self.maker_coin.wait_for_confirmations(confirm_maker_payment_input); @@ -1606,7 +1611,7 @@ impl TakerSwap { } async fn wait_for_taker_payment_spend(&self) -> Result<(Option, Vec), String> { - const BROADCAST_SWAP_MESSAGE_INTERVAL: f64 = 600.; + const BROADCAST_MSG_INTERVAL_SEC: f64 = 600.; let tx_hex = self.r().taker_payment.as_ref().unwrap().tx_hex.0.clone(); let mut watcher_broadcast_abort_handle = None; @@ -1627,11 +1632,11 @@ impl TakerSwap { ); let swpmsg_watcher = SwapWatcherMsg::TakerSwapWatcherMsg(watcher_data); let htlc_keypair = self.taker_coin.derive_htlc_key_pair(&self.unique_swap_data()); - watcher_broadcast_abort_handle = Some(broadcast_swap_message_every( + watcher_broadcast_abort_handle = Some(broadcast_swap_msg_every_delayed( self.ctx.clone(), watcher_topic(&self.r().data.taker_coin), swpmsg_watcher, - BROADCAST_SWAP_MESSAGE_INTERVAL, + BROADCAST_MSG_INTERVAL_SEC, Some(htlc_keypair), )); } @@ -1639,11 +1644,11 @@ impl TakerSwap { // Todo: taker_payment should be a message on lightning network not a swap message let msg = SwapMsg::TakerPayment(tx_hex); - let send_abort_handle = broadcast_swap_message_every( + let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), swap_topic(&self.uuid), msg, - BROADCAST_SWAP_MESSAGE_INTERVAL, + BROADCAST_MSG_INTERVAL_SEC, self.p2p_privkey, ); @@ -1652,7 +1657,7 @@ impl TakerSwap { confirmations: self.r().data.taker_payment_confirmations, requires_nota: self.r().data.taker_payment_requires_nota.unwrap_or(false), wait_until: self.r().data.taker_payment_lock, - check_every: WAIT_CONFIRM_INTERVAL, + check_every: WAIT_CONFIRM_INTERVAL_SEC, }; let wait_f = self .taker_coin diff --git a/mm2src/mm2_main/src/lp_swap/trade_preimage.rs b/mm2src/mm2_main/src/lp_swap/trade_preimage.rs index 7355f99b7e..7f64b052db 100644 --- a/mm2src/mm2_main/src/lp_swap/trade_preimage.rs +++ b/mm2src/mm2_main/src/lp_swap/trade_preimage.rs @@ -1,6 +1,6 @@ use super::check_balance::CheckBalanceError; use super::{maker_swap_trade_preimage, taker_swap_trade_preimage, MakerTradePreimage, TakerTradePreimage}; -use crate::mm2::lp_ordermatch::{MakerOrderBuildError, TakerAction, TakerOrderBuildError}; +use crate::mm2::lp_ordermatch::{MakerOrderBuildError, TakerOrderBuildError}; use coins::{is_wallet_only_ticker, lp_coinfind_or_err, BalanceError, CoinFindError, TradeFee, TradePreimageError}; use common::HttpStatusCode; use crypto::CryptoCtxError; @@ -9,6 +9,7 @@ use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{construct_detailed, BigDecimal, MmNumber}; +use mm2_rpc::data::legacy::TakerAction; use std::collections::HashMap; construct_detailed!(DetailedAmount, amount); diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index c40f9741c3..e343f24ba9 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -70,18 +70,6 @@ pub const PASSWORD_MAXIMUM_CONSECUTIVE_CHARACTERS: usize = 3; #[cfg(feature = "custom-swap-locktime")] const CUSTOM_PAYMENT_LOCKTIME_DEFAULT: u64 = 900; -#[derive(Serialize)] -pub struct MmVersionResult { - result: String, - datetime: String, -} - -impl MmVersionResult { - pub const fn new(result: String, datetime: String) -> MmVersionResult { MmVersionResult { result, datetime } } - - pub fn to_json(&self) -> Json { json::to_value(self).expect("expected valid JSON object") } -} - pub struct LpMainParams { conf: Json, filter: Option, @@ -304,7 +292,10 @@ pub fn get_mm2config(first_arg: Option<&str>) -> Result { let mut conf: Json = match json::from_str(conf) { Ok(json) => json, - Err(err) => return ERR!("Couldn't parse.({}).{}", conf, err), + // Syntax or io errors may include the conf string in the error message so we don't want to take risks and show these errors internals in the log. + // If new variants are added to the Error enum, there can be a risk of exposing the conf string in the error message when updating serde_json so + // I think it's better to not include the serde_json::error::Error at all in the returned error message rather than selectively excluding certain variants. + Err(_) => return ERR!("Couldn't parse mm2 config to JSON format!"), }; if conf["coins"].is_null() { diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 9fc2c99ce6..6f2af8a757 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -1828,8 +1828,8 @@ fn test_request_and_fill_orderbook() { let orders = orders .into_iter() .map(|(uuid, order)| { - if let Some(conf_settings) = order.conf_settings { - conf_infos.insert(uuid, conf_settings); + if let Some(ref conf_settings) = order.conf_settings { + conf_infos.insert(uuid, conf_settings.clone()); } (uuid, order.into()) }) @@ -2985,9 +2985,9 @@ fn check_get_orderbook_p2p_res_serde() { pubkey_orders: HashMap::from_iter(std::iter::once(("pubkey".into(), item))), }; - let v1_serialized = rmp_serde::to_vec(&v1).unwrap(); + let v1_serialized = rmp_serde::to_vec_named(&v1).unwrap(); - let mut new: GetOrderbookRes = rmp_serde::from_read_ref(&v1_serialized).unwrap(); + let mut new: GetOrderbookRes = rmp_serde::from_slice(&v1_serialized).unwrap(); new.protocol_infos.insert(new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], @@ -2999,9 +2999,9 @@ fn check_get_orderbook_p2p_res_serde() { rel_nota: true, }); - let new_serialized = rmp_serde::to_vec(&new).unwrap(); + let new_serialized = rmp_serde::to_vec_named(&new).unwrap(); - let v1_from_new: GetOrderbookResV1 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + let v1_from_new: GetOrderbookResV1 = rmp_serde::from_slice(&new_serialized).unwrap(); assert_eq!(v1, v1_from_new); #[derive(Debug, Deserialize, PartialEq, Serialize)] @@ -3026,9 +3026,9 @@ fn check_get_orderbook_p2p_res_serde() { }))), }; - let v2_serialized = rmp_serde::to_vec(&v2).unwrap(); + let v2_serialized = rmp_serde::to_vec_named(&v2).unwrap(); - let mut new: GetOrderbookRes = rmp_serde::from_read_ref(&v2_serialized).unwrap(); + let mut new: GetOrderbookRes = rmp_serde::from_slice(&v2_serialized).unwrap(); new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings { base_confs: 6, base_nota: false, @@ -3036,9 +3036,9 @@ fn check_get_orderbook_p2p_res_serde() { rel_nota: true, }); - let new_serialized = rmp_serde::to_vec(&new).unwrap(); + let new_serialized = rmp_serde::to_vec_named(&new).unwrap(); - let v2_from_new: GetOrderbookResV2 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + let v2_from_new: GetOrderbookResV2 = rmp_serde::from_slice(&new_serialized).unwrap(); assert_eq!(v2, v2_from_new); } @@ -3094,9 +3094,9 @@ fn check_sync_pubkey_state_p2p_res_serde() { ))), }; - let v1_serialized = rmp_serde::to_vec(&v1).unwrap(); + let v1_serialized = rmp_serde::to_vec_named(&v1).unwrap(); - let mut new: SyncPubkeyOrderbookStateRes = rmp_serde::from_read_ref(&v1_serialized).unwrap(); + let mut new: SyncPubkeyOrderbookStateRes = rmp_serde::from_slice(&v1_serialized).unwrap(); new.protocol_infos.insert(new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], @@ -3108,9 +3108,9 @@ fn check_sync_pubkey_state_p2p_res_serde() { rel_nota: true, }); - let new_serialized = rmp_serde::to_vec(&new).unwrap(); + let new_serialized = rmp_serde::to_vec_named(&new).unwrap(); - let _v1_from_new: SyncPubkeyOrderbookStateResV1 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + let _v1_from_new: SyncPubkeyOrderbookStateResV1 = rmp_serde::from_slice(&new_serialized).unwrap(); #[derive(Debug, Deserialize, Serialize)] struct SyncPubkeyOrderbookStateResV2 { @@ -3133,9 +3133,9 @@ fn check_sync_pubkey_state_p2p_res_serde() { }))), }; - let v2_serialized = rmp_serde::to_vec(&v2).unwrap(); + let v2_serialized = rmp_serde::to_vec_named(&v2).unwrap(); - let mut new: SyncPubkeyOrderbookStateRes = rmp_serde::from_read_ref(&v2_serialized).unwrap(); + let mut new: SyncPubkeyOrderbookStateRes = rmp_serde::from_slice(&v2_serialized).unwrap(); new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings { base_confs: 6, base_nota: false, @@ -3143,9 +3143,9 @@ fn check_sync_pubkey_state_p2p_res_serde() { rel_nota: true, }); - let new_serialized = rmp_serde::to_vec(&new).unwrap(); + let new_serialized = rmp_serde::to_vec_named(&new).unwrap(); - let _v2_from_new: SyncPubkeyOrderbookStateResV2 = rmp_serde::from_read_ref(&new_serialized).unwrap(); + let _v2_from_new: SyncPubkeyOrderbookStateResV2 = rmp_serde::from_slice(&new_serialized).unwrap(); } #[test] diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index c0c338dab6..8ca80d5274 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -21,7 +21,7 @@ // use crate::mm2::rpc::rate_limiter::RateLimitError; -use common::log::error; +use common::log::{error, info}; use common::{err_to_rpc_json_string, err_tp_rpc_json, HttpStatusCode, APPLICATION_JSON}; use derive_more::Display; use futures::future::{join_all, FutureExt}; @@ -303,10 +303,47 @@ async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Resp #[cfg(not(target_arch = "wasm32"))] pub extern "C" fn spawn_rpc(ctx_h: u32) { + use common::now_sec; use common::wio::CORE; - use hyper::server::conn::AddrStream; + use hyper::server::conn::{AddrIncoming, AddrStream}; use hyper::service::{make_service_fn, service_fn}; + use mm2_net::native_tls::{TlsAcceptor, TlsStream}; + use rcgen::{generate_simple_self_signed, RcgenError}; + use rustls::{Certificate, PrivateKey}; + use rustls_pemfile as pemfile; use std::convert::Infallible; + use std::env; + use std::fs::File; + use std::io::{self, BufReader}; + + // Reads a certificate and a key from the specified files. + fn read_certificate_and_key( + cert_file: &File, + cert_key_path: &str, + ) -> Result<(Vec, PrivateKey), io::Error> { + let cert_file = &mut BufReader::new(cert_file); + let cert_chain = pemfile::certs(cert_file)?.into_iter().map(Certificate).collect(); + let key_file = &mut BufReader::new(File::open(cert_key_path)?); + let key = pemfile::read_all(key_file)? + .into_iter() + .find_map(|item| match item { + pemfile::Item::RSAKey(key) | pemfile::Item::PKCS8Key(key) | pemfile::Item::ECKey(key) => Some(key), + _ => None, + }) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "No private key found"))?; + Ok((cert_chain, PrivateKey(key))) + } + + // Generates a self-signed certificate + fn generate_self_signed_cert(subject_alt_names: Vec) -> Result<(Vec, PrivateKey), RcgenError> { + // Generate the certificate + let cert = generate_simple_self_signed(subject_alt_names)?; + let cert_der = cert.serialize_der()?; + let privkey = PrivateKey(cert.serialize_private_key_der()); + let cert = Certificate(cert_der); + let cert_chain = vec![cert]; + Ok((cert_chain, privkey)) + } // NB: We need to manually handle the incoming connections in order to get the remote IP address, // cf. https://github.com/hyperium/hyper/issues/1410#issuecomment-419510220. @@ -314,62 +351,126 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { // then we might want to refactor into starting it ideomatically in order to benefit from a more graceful shutdown, // cf. https://github.com/hyperium/hyper/pull/1640. + let make_svc_fut = move |remote_addr: SocketAddr| async move { + Ok::<_, Infallible>(service_fn(move |req: Request| async move { + let res = rpc_service(req, ctx_h, remote_addr).await; + Ok::<_, Infallible>(res) + })) + }; + + //The `make_svc` macro creates a `make_service_fn` for a specified socket type. + // `$socket_type`: The socket type with a `remote_addr` method that returns a `SocketAddr`. + macro_rules! make_svc { + ($socket_type:ty) => { + make_service_fn(move |socket: &$socket_type| { + let remote_addr = socket.remote_addr(); + make_svc_fut(remote_addr) + }) + }; + } + + // The `get_shutdown_future` macro registers a graceful shutdown listener by calling the `register_listener` + // method of `GracefulShutdownRegistry`. + // If the `register_listener` method fails, it implies that the application is already in a shutdown state. + // In this case, the macro logs an error and immediately returns. + macro_rules! get_shutdown_future { + ($ctx:expr) => { + match $ctx.graceful_shutdown_registry.register_listener() { + Ok(shutdown_fut) => shutdown_fut, + Err(e) => { + error!("MmCtx seems to be stopped already: {e}"); + return; + }, + } + }; + } + + // Macro for spawning a server with error handling and logging + macro_rules! spawn_server { + ($server:expr, $ctx:expr, $ip:expr, $port:expr) => { + { + let server = $server.then(|r| { + if let Err(err) = r { + error!("{}", err); + }; + futures::future::ready(()) + }); + + // As it's said in the [issue](https://github.com/hyperium/tonic/issues/330): + // + // Aborting the server future will forcefully cancel all connections and not perform a proper drain/shutdown. + // While using the special shutdown methods on the server will allow hyper to gracefully drain all connections + // and gracefully close connections. + common::executor::spawn({ + log_tag!( + $ctx, + "😉"; + fmt = ">>>>>>>>>> DEX stats {}:{} DEX stats API enabled at unixtime.{} <<<<<<<<<", + $ip, + $port, + now_sec() + ); + let _ = $ctx.rpc_started.pin(true); + server + }); + } + }; + } + let ctx = MmArc::from_ffi_handle(ctx_h).expect("No context"); - let rpc_ip_port = ctx.rpc_ip_port().unwrap(); + let rpc_ip_port = ctx + .rpc_ip_port() + .unwrap_or_else(|err| panic!("Invalid RPC port: {}", err)); // By entering the context, we tie `tokio::spawn` to this executor. let _runtime_guard = CORE.0.enter(); - let server = Server::try_bind(&rpc_ip_port).unwrap_or_else(|_| panic!("Can't bind on {}", rpc_ip_port)); - let make_svc = make_service_fn(move |socket: &AddrStream| { - let remote_addr = socket.remote_addr(); - async move { - Ok::<_, Infallible>(service_fn(move |req: Request| async move { - let res = rpc_service(req, ctx_h, remote_addr).await; - Ok::<_, Infallible>(res) - })) - } - }); - - let shutdown_fut = match ctx.graceful_shutdown_registry.register_listener() { - Ok(shutdown_fut) => shutdown_fut, - Err(e) => { - error!("MmCtx seems to be stopped already: {e}"); - return; - }, - }; + if ctx.is_https() { + let cert_path = env::var("MM_CERT_PATH").unwrap_or_else(|_| "cert.pem".to_string()); + let (cert_chain, privkey) = match File::open(cert_path.clone()) { + Ok(cert_file) => { + let cert_key_path = env::var("MM_CERT_KEY_PATH").unwrap_or_else(|_| "key.pem".to_string()); + read_certificate_and_key(&cert_file, &cert_key_path) + .unwrap_or_else(|err| panic!("Can't read certificate and/or key from {:?}: {}", cert_path, err)) + }, + Err(ref err) if err.kind() == io::ErrorKind::NotFound => { + info!( + "No certificate found at {:?}, generating a self-signed certificate", + cert_path + ); + let subject_alt_names = ctx + .alt_names() + .unwrap_or_else(|err| panic!("Invalid `alt_names` config: {}", err)); + generate_self_signed_cert(subject_alt_names) + .unwrap_or_else(|err| panic!("Can't generate self-signed certificate: {}", err)) + }, + Err(err) => panic!("Can't open {:?}: {}", cert_path, err), + }; - let server = server - .http1_half_close(false) - .serve(make_svc) - .with_graceful_shutdown(shutdown_fut); + // Create a TcpListener + let incoming = + AddrIncoming::bind(&rpc_ip_port).unwrap_or_else(|err| panic!("Can't bind on {}: {}", rpc_ip_port, err)); + let acceptor = TlsAcceptor::builder() + .with_single_cert(cert_chain, privkey) + .unwrap_or_else(|err| panic!("Can't set certificate for TlsAcceptor: {}", err)) + .with_all_versions_alpn() + .with_incoming(incoming); + + let server = Server::builder(acceptor) + .http1_half_close(false) + .serve(make_svc!(TlsStream)) + .with_graceful_shutdown(get_shutdown_future!(ctx)); + + spawn_server!(server, ctx, rpc_ip_port.ip(), rpc_ip_port.port()); + } else { + let server = Server::try_bind(&rpc_ip_port) + .unwrap_or_else(|err| panic!("Can't bind on {}: {}", rpc_ip_port, err)) + .http1_half_close(false) + .serve(make_svc!(AddrStream)) + .with_graceful_shutdown(get_shutdown_future!(ctx)); - let server = server.then(|r| { - if let Err(err) = r { - error!("{}", err); - }; - futures::future::ready(()) - }); - - let rpc_ip_port = ctx.rpc_ip_port().unwrap(); - - // As it's said in the [issue](https://github.com/hyperium/tonic/issues/330): - // - // Aborting the server future will forcefully cancel all connections and not perform a proper drain/shutdown. - // While using the special shutdown methods on the server will allow hyper to gracefully drain all connections - // and gracefully close connections. - common::executor::spawn({ - log_tag!( - ctx, - "😉"; - fmt = ">>>>>>>>>> DEX stats {}:{} DEX stats API enabled at unixtime.{} <<<<<<<<<", - rpc_ip_port.ip(), - rpc_ip_port.port(), - gstuff::now_ms() / 1000 - ); - let _ = ctx.rpc_started.pin(true); - server - }); + spawn_server!(server, ctx, rpc_ip_port.ip(), rpc_ip_port.port()); + } } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 48eec45170..5645b9a68f 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -49,7 +49,7 @@ use http::Response; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_rpc::mm_protocol::{MmRpcBuilder, MmRpcRequest, MmRpcVersion}; -use nft::{get_nft_list, get_nft_metadata, get_nft_transfers, withdraw_nft}; +use nft::{get_nft_list, get_nft_metadata, get_nft_transfers, refresh_nft_metadata, update_nft, withdraw_nft}; use serde::de::DeserializeOwned; use serde_json::{self as json, Value as Json}; use std::net::SocketAddr; @@ -181,6 +181,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, my_tx_history_v2_rpc).await, "orderbook" => handle_mmrpc(ctx, request, orderbook_rpc_v2).await, "recreate_swap_data" => handle_mmrpc(ctx, request, recreate_swap_data).await, + "refresh_nft_metadata" => handle_mmrpc(ctx, request, refresh_nft_metadata).await, "remove_delegation" => handle_mmrpc(ctx, request, remove_delegation).await, "remove_node_from_version_stat" => handle_mmrpc(ctx, request, remove_node_from_version_stat).await, "sign_message" => handle_mmrpc(ctx, request, sign_message).await, @@ -190,6 +191,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, stop_version_stat_collection).await, "trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await, "trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await, + "update_nft" => handle_mmrpc(ctx, request, update_nft).await, "update_version_stat_collection" => handle_mmrpc(ctx, request, update_version_stat_collection).await, "verify_message" => handle_mmrpc(ctx, request, verify_message).await, "withdraw" => handle_mmrpc(ctx, request, withdraw).await, diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs index b003eded3a..7b5d68b8d0 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs @@ -27,7 +27,8 @@ use futures::compat::Future01CompatExt; use http::Response; use mm2_core::mm_ctx::MmArc; use mm2_metrics::MetricsOps; -use mm2_number::{construct_detailed, BigDecimal}; +use mm2_number::construct_detailed; +use mm2_rpc::data::legacy::{BalanceResponse, CoinInitResponse, Mm2RpcResult, MmVersionResponse, Status}; use serde_json::{self as json, Value as Json}; use std::borrow::Cow; use std::collections::HashSet; @@ -38,7 +39,6 @@ use crate::mm2::lp_dispatcher::{dispatch_lp_event, StopCtxEvent}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::{cancel_orders_by, get_matching_orders, CancelBy}; use crate::mm2::lp_swap::{active_swaps_using_coins, tx_helper_topic, watcher_topic}; -use crate::mm2::MmVersionResult; const INTERNAL_SERVER_ERROR_CODE: u16 = 500; const RESPONSE_OK_STATUS_CODE: u16 = 200; @@ -138,30 +138,17 @@ pub async fn disable_coin(ctx: MmArc, req: Json) -> Result>, St response(&ticker, cancelled_orders, false) } -#[derive(Serialize)] -struct CoinInitResponse<'a> { - result: &'a str, - address: String, - balance: BigDecimal, - unspendable_balance: BigDecimal, - coin: &'a str, - required_confirmations: u64, - requires_notarization: bool, - #[serde(skip_serializing_if = "Option::is_none")] - mature_confirmations: Option, -} - /// Enable a coin in the Electrum mode. pub async fn electrum(ctx: MmArc, req: Json) -> Result>, String> { let ticker = try_s!(req["coin"].as_str().ok_or("No 'coin' field")).to_owned(); let coin: MmCoinEnum = try_s!(lp_coininit(&ctx, &ticker, &req).await); let balance = try_s!(coin.my_balance().compat().await); let res = CoinInitResponse { - result: "success", + result: "success".into(), address: try_s!(coin.my_address()), balance: balance.spendable, unspendable_balance: balance.unspendable, - coin: coin.ticker(), + coin: coin.ticker().into(), required_confirmations: coin.required_confirmations(), requires_notarization: coin.requires_notarization(), mature_confirmations: coin.mature_confirmations(), @@ -176,11 +163,11 @@ pub async fn enable(ctx: MmArc, req: Json) -> Result>, String> let coin: MmCoinEnum = try_s!(lp_coininit(&ctx, &ticker, &req).await); let balance = try_s!(coin.my_balance().compat().await); let res = CoinInitResponse { - result: "success", + result: "success".to_string(), address: try_s!(coin.my_address()), balance: balance.spendable, unspendable_balance: balance.unspendable, - coin: coin.ticker(), + coin: coin.ticker().to_string(), required_confirmations: coin.required_confirmations(), requires_notarization: coin.requires_notarization(), mature_confirmations: coin.mature_confirmations(), @@ -248,13 +235,13 @@ pub async fn my_balance(ctx: MmArc, req: Json) -> Result>, Stri Err(err) => return ERR!("!lp_coinfind({}): {}", ticker, err), }; let my_balance = try_s!(coin.my_balance().compat().await); - let res = json!({ - "coin": ticker, - "balance": my_balance.spendable, - "unspendable_balance": my_balance.unspendable, - "address": try_s!(coin.my_address()), - }); - let res = try_s!(json::to_vec(&res)); + + let res = try_s!(json::to_vec(&BalanceResponse { + coin: ticker, + balance: my_balance.spendable, + unspendable_balance: my_balance.unspendable, + address: try_s!(coin.my_address()) + })); Ok(try_s!(Response::builder().body(res))) } @@ -274,10 +261,7 @@ pub async fn stop(ctx: MmArc) -> Result>, String> { // and it may lead to an unexpected behaviour. common::executor::spawn(fut); - let res = json!({ - "result": "success" - }); - let res = try_s!(json::to_vec(&res)); + let res = try_s!(json::to_vec(&Mm2RpcResult::new(Status::Success))); Ok(try_s!(Response::builder().body(res))) } @@ -312,12 +296,13 @@ pub async fn sim_panic(req: Json) -> Result>, String> { } pub fn version(ctx: MmArc) -> HyRes { - rpc_response( - RESPONSE_OK_STATUS_CODE, - MmVersionResult::new(ctx.mm_version.clone(), ctx.datetime.clone()) - .to_json() - .to_string(), - ) + match json::to_vec(&MmVersionResponse { + result: ctx.mm_version.clone(), + datetime: ctx.datetime.clone(), + }) { + Ok(response) => rpc_response(RESPONSE_OK_STATUS_CODE, response), + Err(err) => rpc_err_response(INTERNAL_SERVER_ERROR_CODE, ERRL!("{}", err).as_str()), + } } pub async fn get_peers_info(ctx: MmArc) -> Result>, String> { diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index 19a85033ea..6a29a497c2 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -2,12 +2,12 @@ use crate::mm2::lp_init; use common::executor::{spawn, Timer}; use common::log::wasm_log::register_wasm_log; use mm2_core::mm_ctx::MmArc; +use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::electrums::{morty_electrums, rick_electrums}; use mm2_test_helpers::for_tests::{check_recent_swaps, enable_electrum_json, morty_conf, rick_conf, start_swaps, test_qrc20_history_impl, wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, MORTY, RICK}; use mm2_test_helpers::get_passphrase; -use mm2_test_helpers::structs::OrderbookResponse; use serde_json::json; use wasm_bindgen_test::wasm_bindgen_test; 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 8755c407d9..10f90abdb8 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs @@ -2,9 +2,10 @@ use crate::generate_utxo_coin_with_random_privkey; use crate::integration_tests_common::enable_native; use common::block_on; use mm2_number::BigDecimal; +use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::for_tests::{mm_dump, MarketMakerIt}; use mm2_test_helpers::structs::{BestOrdersResponse, BestOrdersV2Response, BuyOrSellRpcResult, MyOrdersRpcResult, - OrderbookDepthResponse, OrderbookResponse, RpcV2Response, SetPriceResponse}; + OrderbookDepthResponse, RpcV2Response, SetPriceResponse}; use serde_json::Value as Json; use std::thread; use std::time::Duration; @@ -816,7 +817,7 @@ fn test_zombie_order_after_balance_reduce_and_mm_restart() { 1, "MYCOIN/MYCOIN1 orderbook must have exactly 1 asks" ); - assert_eq!(orderbook.asks[0].max_volume, new_expected_vol); + assert_eq!(orderbook.asks[0].entry.max_volume, new_expected_vol); log!("Get my orders"); let rc = block_on(mm_maker.rpc(&json! ({ @@ -879,7 +880,7 @@ fn test_zombie_order_after_balance_reduce_and_mm_restart() { 1, "MYCOIN/MYCOIN1 orderbook must have exactly 1 asks" ); - assert_eq!(orderbook.asks[0].max_volume, new_expected_vol); + assert_eq!(orderbook.asks[0].entry.max_volume, new_expected_vol); log!("Get my orders"); let rc = block_on(mm_maker_dup.rpc(&json! ({ diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index 49157b79e8..6c5db29d00 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -1,10 +1,12 @@ pub use common::{block_on, now_ms, now_sec, wait_until_ms, wait_until_sec}; 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, check_stats_swap_status, - enable_native_bch, eth_sepolia_conf, jst_sepolia_conf, mm_dump, MarketMakerIt, - MAKER_ERROR_EVENTS, MAKER_SUCCESS_EVENTS, TAKER_ERROR_EVENTS, - TAKER_SUCCESS_EVENTS}; -use mm2_test_helpers::for_tests::{ETH_SEPOLIA_NODE, ETH_SEPOLIA_SWAP_CONTRACT, ETH_SEPOLIA_TOKEN_CONTRACT}; + enable_native, enable_native_bch, eth_jst_testnet_conf, eth_sepolia_conf, + eth_testnet_conf, jst_sepolia_conf, mm_dump, MarketMakerIt, ETH_DEV_NODES, + ETH_DEV_SWAP_CONTRACT, ETH_DEV_TOKEN_CONTRACT, MAKER_ERROR_EVENTS, + MAKER_SUCCESS_EVENTS, TAKER_ERROR_EVENTS, TAKER_SUCCESS_EVENTS}; + pub use secp256k1::{PublicKey, SecretKey}; pub use std::env; pub use std::thread; @@ -31,8 +33,7 @@ use http::StatusCode; use keys::{Address, AddressHashEnum, KeyPair, NetworkPrefix as CashAddrPrefix}; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_number::BigDecimal; -use mm2_test_helpers::for_tests::{enable_native, eth_testnet_conf, ETH_DEV_NODES, ETH_DEV_SWAP_CONTRACT}; -use mm2_test_helpers::structs::{MyBalanceResponse, TransactionDetails}; +use mm2_test_helpers::structs::TransactionDetails; use primitives::hash::{H160, H256}; use script::Builder; use secp256k1::Secp256k1; @@ -165,19 +166,19 @@ pub fn _fill_eth(to_addr: &str) { } // Generates an ethereum coin in the sepolia network with the given seed -pub fn generate_eth_coin_with_seed(seed: &str) -> EthCoin { +pub fn _generate_eth_coin_with_seed(seed: &str) -> EthCoin { let req = json!({ "method": "enable", "coin": "ETH", - "urls": ETH_SEPOLIA_NODE, - "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, + "urls": ETH_DEV_NODES, + "swap_contract_address": ETH_DEV_SWAP_CONTRACT, }); let keypair = key_pair_from_seed(seed).unwrap(); let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(keypair.private().secret); block_on(eth_coin_from_conf_and_request( &MM_CTX, "ETH", - ð_sepolia_conf(), + ð_testnet_conf(), &req, CoinProtocol::ETH, priv_key_policy, @@ -189,8 +190,8 @@ pub fn generate_jst_with_seed(seed: &str) -> EthCoin { let req = json!({ "method": "enable", "coin": "JST", - "urls": ETH_SEPOLIA_NODE, - "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, + "urls": ETH_DEV_NODES, + "swap_contract_address": ETH_DEV_SWAP_CONTRACT, }); let keypair = key_pair_from_seed(seed).unwrap(); @@ -198,11 +199,11 @@ pub fn generate_jst_with_seed(seed: &str) -> EthCoin { block_on(eth_coin_from_conf_and_request( &MM_CTX, "JST", - &jst_sepolia_conf(), + ð_jst_testnet_conf(), &req, CoinProtocol::ERC20 { platform: "ETH".into(), - contract_address: String::from(ETH_SEPOLIA_TOKEN_CONTRACT), + contract_address: String::from(ETH_DEV_TOKEN_CONTRACT), }, priv_key_policy, )) @@ -894,8 +895,6 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { block_on(check_my_swap_status( &mm_alice, &uuid, - &TAKER_SUCCESS_EVENTS, - &TAKER_ERROR_EVENTS, "2".parse().unwrap(), "2".parse().unwrap(), )); @@ -904,8 +903,6 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { block_on(check_my_swap_status( &mm_bob, &uuid, - &MAKER_SUCCESS_EVENTS, - &MAKER_ERROR_EVENTS, "2".parse().unwrap(), "2".parse().unwrap(), )); @@ -914,20 +911,10 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { thread::sleep(Duration::from_secs(3)); log!("Checking alice status.."); - block_on(check_stats_swap_status( - &mm_alice, - &uuid, - &MAKER_SUCCESS_EVENTS, - &TAKER_SUCCESS_EVENTS, - )); + block_on(check_stats_swap_status(&mm_alice, &uuid)); log!("Checking bob status.."); - block_on(check_stats_swap_status( - &mm_bob, - &uuid, - &MAKER_SUCCESS_EVENTS, - &TAKER_SUCCESS_EVENTS, - )); + block_on(check_stats_swap_status(&mm_bob, &uuid)); log!("Checking alice recent swaps.."); block_on(check_recent_swaps(&mm_alice, 1)); @@ -986,7 +973,7 @@ pub fn _solana_supplied_node() -> MarketMakerIt { .unwrap() } -pub fn get_balance(mm: &MarketMakerIt, coin: &str) -> MyBalanceResponse { +pub fn get_balance(mm: &MarketMakerIt, coin: &str) -> BalanceResponse { let rc = block_on(mm.rpc(&json!({ "userpass": mm.userpass, "method": "my_balance", diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 68219bfaf2..b5c5526436 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -19,8 +19,8 @@ use http::StatusCode; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_main::mm2::lp_swap::{dex_fee_amount, max_taker_vol_from_available}; use mm2_number::BigDecimal; -use mm2_test_helpers::structs::{trade_preimage_error, EnableElectrumResponse, OrderbookResponse, RpcErrorResponse, - RpcSuccessResponse, TransactionDetails}; +use mm2_rpc::data::legacy::{CoinInitResponse, OrderbookResponse}; +use mm2_test_helpers::structs::{trade_preimage_error, RpcErrorResponse, RpcSuccessResponse, TransactionDetails}; use rand6::Rng; use serde_json::{self as json, Value as Json}; use std::convert::TryFrom; @@ -1704,7 +1704,7 @@ fn segwit_address_in_the_orderbook() { block_on(mm.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); let enable_qtum_res = block_on(enable_native_segwit(&mm, "QTUM")); - let enable_qtum_res: EnableElectrumResponse = json::from_value(enable_qtum_res).unwrap(); + let enable_qtum_res: CoinInitResponse = json::from_value(enable_qtum_res).unwrap(); let segwit_addr = enable_qtum_res.address; fill_address(&coin, &segwit_addr, 1000.into(), 30); @@ -1732,8 +1732,8 @@ fn segwit_address_in_the_orderbook() { assert!(orderbook.0.is_success(), "!orderbook: {}", rc.1); let orderbook: OrderbookResponse = json::from_str(&orderbook.1).unwrap(); - assert_eq!(orderbook.asks[0].coin, "QTUM"); - assert_eq!(orderbook.asks[0].address, segwit_addr); + assert_eq!(orderbook.asks[0].entry.coin, "QTUM"); + assert_eq!(orderbook.asks[0].entry.address, segwit_addr); block_on(mm.stop()).unwrap(); } diff --git a/mm2src/mm2_main/tests/docker_tests/slp_tests.rs b/mm2src/mm2_main/tests/docker_tests/slp_tests.rs index 0ca520e014..b1fe7b6b9f 100644 --- a/mm2src/mm2_main/tests/docker_tests/slp_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/slp_tests.rs @@ -2,9 +2,10 @@ use crate::docker_tests::docker_tests_common::*; use crate::integration_tests_common::enable_native; use http::StatusCode; use mm2_number::BigDecimal; +use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::for_tests::{assert_coin_not_found_on_balance, disable_coin, enable_bch_with_tokens, enable_slp, my_balance, UtxoRpcMode}; -use mm2_test_helpers::structs::{EnableBchWithTokensResponse, EnableElectrumResponse, EnableSlpResponse, RpcV2Response}; +use mm2_test_helpers::structs::{EnableBchWithTokensResponse, EnableSlpResponse, RpcV2Response}; use serde_json::{self as json, json, Value as Json}; use std::collections::HashSet; use std::time::Duration; @@ -56,7 +57,7 @@ fn test_bch_and_slp_balance() { let mm = slp_supplied_node(); let enable_bch = block_on(enable_native_bch(&mm, "FORSLP", &[])); - let enable_bch: EnableElectrumResponse = json::from_value(enable_bch).unwrap(); + let enable_bch: CoinInitResponse = json::from_value(enable_bch).unwrap(); let expected_spendable = BigDecimal::from(1000); assert_eq!(expected_spendable, enable_bch.balance); @@ -89,7 +90,7 @@ fn test_bch_and_slp_balance_enable_slp_v2() { let mm = slp_supplied_node(); let enable_bch = block_on(enable_native_bch(&mm, "FORSLP", &[])); - let enable_bch: EnableElectrumResponse = json::from_value(enable_bch).unwrap(); + let enable_bch: CoinInitResponse = json::from_value(enable_bch).unwrap(); let expected_spendable = BigDecimal::from(1000); assert_eq!(expected_spendable, enable_bch.balance); diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index ee71fa9033..264bbc4f9b 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -1,9 +1,9 @@ -use crate::docker_tests::docker_tests_common::{eth_distributor, generate_eth_coin_with_seed, generate_jst_with_seed}; +use crate::docker_tests::docker_tests_common::{eth_distributor, generate_jst_with_seed}; 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::utxo::{dhash160, UtxoCommonOps}; -use coins::{ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, +use coins::{ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, RewardTarget, SearchForSwapTxSpendInput, SendPaymentArgs, SwapOps, WatcherOps, 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, @@ -16,17 +16,19 @@ use mm2_main::mm2::lp_swap::{dex_fee_amount, dex_fee_amount_from_taker_coin, dex MAKER_PAYMENT_SPEND_SENT_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, eth_sepolia_conf, jst_sepolia_conf, mm_dump, my_balance, +use mm2_test_helpers::for_tests::{enable_eth_coin, eth_jst_testnet_conf, eth_testnet_conf, mm_dump, my_balance, mycoin1_conf, mycoin_conf, start_swaps, MarketMakerIt, Mm2TestConf, - DEFAULT_RPC_PASSWORD, ETH_SEPOLIA_NODE, ETH_SEPOLIA_SWAP_CONTRACT}; + DEFAULT_RPC_PASSWORD, ETH_DEV_NODES, ETH_DEV_SWAP_CONTRACT}; +use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::WatcherConf; -use num_traits::Zero; +use num_traits::{One, Zero}; +use primitives::hash::H256; use std::str::FromStr; use std::thread; use std::time::Duration; use uuid::Uuid; -#[derive(Debug)] +#[derive(Debug, Clone)] struct BalanceResult { alice_acoin_balance_before: BigDecimal, alice_acoin_balance_middle: BigDecimal, @@ -58,9 +60,9 @@ fn enable_eth(mm_node: &MarketMakerIt, coin: &str) { dbg!(block_on(enable_eth_coin( mm_node, coin, - ETH_SEPOLIA_NODE, - ETH_SEPOLIA_SWAP_CONTRACT, - Some(ETH_SEPOLIA_SWAP_CONTRACT), + ETH_DEV_NODES, + ETH_DEV_SWAP_CONTRACT, + Some(ETH_DEV_SWAP_CONTRACT), true ))); } @@ -72,6 +74,7 @@ enum SwapFlow { TakerSpendsMakerPayment, } +#[allow(clippy::too_many_arguments)] fn start_swaps_and_get_balances( a_coin: &'static str, b_coin: &'static str, @@ -80,22 +83,18 @@ fn start_swaps_and_get_balances( volume: f64, envs: &[(&str, &str)], swap_flow: SwapFlow, + alice_privkey: &str, + bob_privkey: &str, + watcher_privkey: &str, ) -> BalanceResult { let coins = json!([ - eth_sepolia_conf(), - jst_sepolia_conf(), + eth_testnet_conf(), + eth_jst_testnet_conf(), mycoin_conf(1000), mycoin1_conf(1000) ]); - let alice_passphrase = if (a_coin == "MYCOIN" || a_coin == "MYCOIN1") && (b_coin == "MYCOIN" || b_coin == "MYCOIN1") - { - format!("0x{}", hex::encode(random_secp256k1_secret())) - } else { - String::from("spice describe gravity federal thank unfair blast come canal monkey style afraid") - }; - - let alice_conf = Mm2TestConf::seednode_using_watchers(&alice_passphrase, &coins); + let alice_conf = Mm2TestConf::seednode(&format!("0x{}", alice_privkey), &coins); let mut mm_alice = block_on(MarketMakerIt::start_with_envs( alice_conf.conf.clone(), alice_conf.rpc_password.clone(), @@ -106,13 +105,7 @@ fn start_swaps_and_get_balances( let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - let bob_passphrase = if (a_coin == "MYCOIN" || a_coin == "MYCOIN1") && b_coin == "MYCOIN" || b_coin == "MYCOIN1" { - format!("0x{}", hex::encode(random_secp256k1_secret())) - } else { - String::from("also shoot benefit prefer juice shell elder veteran woman mimic image kidney") - }; - - let bob_conf = Mm2TestConf::light_node_using_watchers(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); + let bob_conf = Mm2TestConf::light_node(&format!("0x{}", bob_privkey), &coins, &[&mm_alice.ip.to_string()]); let mut mm_bob = block_on(MarketMakerIt::start_with_envs( bob_conf.conf.clone(), bob_conf.rpc_password, @@ -123,13 +116,10 @@ fn start_swaps_and_get_balances( let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); - let bob_keypair = key_pair_from_seed(&bob_passphrase).unwrap(); - let alice_keypair = key_pair_from_seed(&alice_passphrase).unwrap(); - - generate_utxo_coin_with_privkey("MYCOIN", 100.into(), bob_keypair.private().secret); - generate_utxo_coin_with_privkey("MYCOIN", 100.into(), alice_keypair.private().secret); - generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), bob_keypair.private().secret); - generate_utxo_coin_with_privkey("MYCOIN1", 100.into(), alice_keypair.private().secret); + 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 => ( @@ -161,10 +151,13 @@ fn start_swaps_and_get_balances( ), }; - let watcher_passphrase = - String::from("also shoot benefit prefer juice shell thank unfair canal monkey style afraid"); - let watcher_conf = - Mm2TestConf::watcher_light_node(&watcher_passphrase, &coins, &[&mm_alice.ip.to_string()], watcher_conf).conf; + let watcher_conf = Mm2TestConf::watcher_light_node( + &format!("0x{}", watcher_privkey), + &coins, + &[&mm_alice.ip.to_string()], + watcher_conf, + ) + .conf; let mut mm_watcher = block_on(MarketMakerIt::start_with_envs( watcher_conf, @@ -209,18 +202,18 @@ fn start_swaps_and_get_balances( )); if matches!(swap_flow, SwapFlow::WatcherRefundsTakerPayment) { - block_on(mm_bob.wait_for_log(100., |log| log.contains(MAKER_PAYMENT_SENT_LOG))).unwrap(); + 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(100., |log| log.contains("Taker payment confirmed"))).unwrap(); + block_on(mm_alice.wait_for_log(120., |log| log.contains("Taker payment confirmed"))).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(100., |log| log.contains(watcher_log_to_wait))).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(); @@ -263,14 +256,21 @@ fn start_swaps_and_get_balances( #[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., - &[], + &[("USE_WATCHERS", "")], SwapFlow::WatcherSpendsMakerPayment, + &alice_privkey, + &bob_privkey, + &watcher_privkey, ); let acoin_volume = BigDecimal::from_str("50").unwrap(); @@ -295,16 +295,22 @@ fn test_watcher_spends_maker_payment_utxo_utxo() { } #[test] -#[ignore] fn test_watcher_spends_maker_payment_utxo_eth() { + let alice_privkey = "0af1b1a4cdfbec12c9014e2422c8819e02e5d0f6539f8bf15190d3ea592e4f14"; + let bob_privkey = "3245331f141578d8c4604639deb1e6f38f107a65642525ef32387325a079a463"; + let watcher_privkey = "9d1d86be257b3bd2504757689d0da24dd052fdff0641be073f1ea8aa5cccf597"; + let balances = start_swaps_and_get_balances( "ETH", "MYCOIN", 0.01, 0.01, 1., - &[], + &[("USE_WATCHERS", ""), ("USE_WATCHER_REWARD", "")], SwapFlow::WatcherSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -322,16 +328,26 @@ fn test_watcher_spends_maker_payment_utxo_eth() { } #[test] -#[ignore] fn test_watcher_spends_maker_payment_eth_utxo() { + let alice_privkey = "0591b2acbe4798c6156a26bc8106c36d6fc09a85c9e02710eec32c1b41f047ec"; + let bob_privkey = "b6e59dee1112486573989f07d480691ca7e3eab81b499fe801d94b65ea1f1341"; + let watcher_privkey = "dc8ad0723a6a2c02d3239e8b009d4de6f3f0ad8b9bc51838cbed41edb378dd86"; + let balances = start_swaps_and_get_balances( "MYCOIN", "ETH", 100., 100., 0.01, - &[("TEST_COIN_PRICE", "0.01")], + &[ + ("USE_WATCHERS", ""), + ("TEST_COIN_PRICE", "0.01"), + ("USE_WATCHER_REWARD", ""), + ], SwapFlow::WatcherSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); let eth_volume = BigDecimal::from_str("0.01").unwrap(); @@ -362,16 +378,26 @@ fn test_watcher_spends_maker_payment_eth_utxo() { } #[test] -#[ignore] fn test_watcher_spends_maker_payment_eth_erc20() { + let alice_privkey = "92ee1f48f07dcaab03ff3d5077211912fdf2229bb401e7a969f73fc2c3d4fe3f"; + let bob_privkey = "59e8c09c3aace4eb9301b2f70547fc0936be2bc662b9c0a7a625b5e8929491c7"; + let watcher_privkey = "e0915d112440fdc58405faace4626a983bb3fd8cb51f0e5a7ed8565b552b5751"; + let balances = start_swaps_and_get_balances( "JST", "ETH", 100., 100., 0.01, - &[("TEST_COIN_PRICE", "0.01")], + &[ + ("USE_WATCHERS", ""), + ("TEST_COIN_PRICE", "0.01"), + ("USE_WATCHER_REWARD", ""), + ], SwapFlow::WatcherSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); let eth_volume = BigDecimal::from_str("0.01").unwrap(); @@ -389,9 +415,23 @@ fn test_watcher_spends_maker_payment_eth_erc20() { } #[test] -#[ignore] fn test_watcher_spends_maker_payment_erc20_eth() { - let balances = start_swaps_and_get_balances("ETH", "JST", 0.01, 0.01, 1., &[], SwapFlow::WatcherSpendsMakerPayment); + let alice_privkey = "2fd8d83e3b9799fa0a02cdaf6776dd36eee3243a62d399a54dc9a68f5e77b27c"; + let bob_privkey = "6425a922265573100165b60ff380fba5035c7406169087a43aefdee66aceccc1"; + let watcher_privkey = "b9b5fa738dcf7c99073b0f7d518a50b72139a7636ba3488766944fd3dc4df646"; + + let balances = start_swaps_and_get_balances( + "ETH", + "JST", + 0.01, + 0.01, + 1., + &[("USE_WATCHERS", ""), ("USE_WATCHER_REWARD", "")], + SwapFlow::WatcherSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, + ); let jst_volume = BigDecimal::from_str("1").unwrap(); let eth_volume = BigDecimal::from_str("0.01").unwrap(); @@ -408,16 +448,26 @@ fn test_watcher_spends_maker_payment_erc20_eth() { } #[test] -#[ignore] fn test_watcher_spends_maker_payment_utxo_erc20() { + let alice_privkey = "e4fc65b69c323312ee3ba46406671bc9f2d524190621d82eeb51452701cfe43b"; + let bob_privkey = "721fc6b7f56495f7f721e1e11cddcaf593351264705c4044e83656f06eb595ef"; + let watcher_privkey = "a1f1c2666be032492a3cb772abc8a2845adfd6dca299fbed13416ccc6feb57ee"; + let balances = start_swaps_and_get_balances( "JST", "MYCOIN", 1., 1., 1., - &[("TEST_COIN_PRICE", "0.01")], + &[ + ("USE_WATCHERS", ""), + ("TEST_COIN_PRICE", "0.01"), + ("USE_WATCHER_REWARD", ""), + ], SwapFlow::WatcherSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -435,16 +485,26 @@ fn test_watcher_spends_maker_payment_utxo_erc20() { } #[test] -#[ignore] fn test_watcher_spends_maker_payment_erc20_utxo() { + let alice_privkey = "5c9fbc69376c3ee6bb56d8d2b715f24b3bb92ccd47e93332d4d94899aa9fc7ae"; + let bob_privkey = "ccc24b9653087d939949d513756cefe1eff657de4c5bf34febc97843a6b26782"; + let watcher_privkey = "a1f1c2666be032492a3cb772abc8a2845adfd6dca299fbed13416ccc6feb57ee"; + let balances = start_swaps_and_get_balances( "MYCOIN", "JST", 1., 1., 1., - &[("TEST_COIN_PRICE", "0.01")], + &[ + ("USE_WATCHERS", ""), + ("TEST_COIN_PRICE", "0.01"), + ("USE_WATCHER_REWARD", ""), + ], SwapFlow::WatcherSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -463,7 +523,8 @@ fn test_watcher_spends_maker_payment_erc20_utxo() { - mycoin_volume.clone() - dex_fee.with_scale(8); - let bob_jst_reward_sent = balances.bob_bcoin_balance_before - jst_volume.clone() - balances.bob_bcoin_balance_after; + let bob_jst_reward_sent = + balances.bob_bcoin_balance_before - jst_volume.clone() - balances.bob_bcoin_balance_after.clone(); assert_eq!( balances.alice_bcoin_balance_after, @@ -481,14 +542,21 @@ 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., - &[("USE_TEST_LOCKTIME", "")], + &[("USE_WATCHERS", ""), ("USE_TEST_LOCKTIME", "")], SwapFlow::WatcherRefundsTakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); assert_eq!( @@ -499,16 +567,26 @@ fn test_watcher_refunds_taker_payment_utxo() { } #[test] -#[ignore] fn test_watcher_refunds_taker_payment_eth() { + let alice_privkey = "0816c0558b934fafa845946bdd2b3163fe6b928e6160ea9aa10a8bea221e3813"; + let bob_privkey = "e5cb76954c5160d7df5bfa5798540d3583c73c9daa46903b98abb9eed2edecc6"; + let watcher_privkey = "ccd7f2c0da8f6428b60b42a27c0e37af59abd42251773156f4f59c5d16855f8c"; + let balances = start_swaps_and_get_balances( "ETH", "JST", 0.01, 0.01, 1., - &[("USE_TEST_LOCKTIME", "")], + &[ + ("USE_WATCHERS", ""), + ("USE_TEST_LOCKTIME", ""), + ("USE_WATCHER_REWARD", ""), + ], SwapFlow::WatcherRefundsTakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); assert_eq!( balances.alice_acoin_balance_after.with_scale(2), @@ -519,16 +597,27 @@ fn test_watcher_refunds_taker_payment_eth() { } #[test] -#[ignore] fn test_watcher_refunds_taker_payment_erc20() { + let alice_privkey = "82c1bb28bb13488f901eff67f886e9895c4dfa28e3e24f1ed7873a73231c9492"; + let bob_privkey = "9a4721db00336ea0d8b7a373cdbdefc321285e7959fff8aea493af6f485b683f"; + let watcher_privkey = "8fdf25f087140b2797deb2a1d3ce66bd59e2449cc805b99958b3bfa8cd621eb8"; + let balances = start_swaps_and_get_balances( "JST", "ETH", 100., 100., 0.01, - &[("USE_TEST_LOCKTIME", ""), ("TEST_COIN_PRICE", "0.01")], + &[ + ("USE_WATCHERS", ""), + ("USE_TEST_LOCKTIME", ""), + ("TEST_COIN_PRICE", "0.01"), + ("USE_WATCHER_REWARD", ""), + ], SwapFlow::WatcherRefundsTakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); let jst_volume = BigDecimal::from_str("1").unwrap(); @@ -542,6 +631,10 @@ fn test_watcher_refunds_taker_payment_erc20() { #[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", @@ -550,37 +643,49 @@ fn test_watcher_waits_for_taker_utxo() { 2., &[], SwapFlow::TakerSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); } #[test] -#[ignore] fn test_watcher_waits_for_taker_eth() { + let alice_privkey = "814ea055c807c1ff2d49c81abfc3434fa0d10a427369b1f8d60fc78ab1da7d16"; + let bob_privkey = "36533ec51a61f4b32856c8ce2ee811a263c625ae26e45ee68e6d28b65c8f9298"; + let watcher_privkey = "baa1c83a0993ba96f88ffc943919991792ce9e2498fc41f42b38030915d58f9f"; + start_swaps_and_get_balances( "JST", "ETH", 100., 100., 0.01, - &[("TEST_COIN_PRICE", "0.01")], + &[ + ("USE_WATCHERS", ""), + ("TEST_COIN_PRICE", "0.01"), + ("USE_WATCHER_REWARD", ""), + ], SwapFlow::TakerSpendsMakerPayment, + alice_privkey, + bob_privkey, + watcher_privkey, ); } #[test] -#[ignore] fn test_two_watchers_spend_maker_payment_eth_erc20() { - let coins = json!([eth_sepolia_conf(), jst_sepolia_conf()]); + let coins = json!([eth_testnet_conf(), eth_jst_testnet_conf()]); let alice_passphrase = String::from("spice describe gravity federal blast come thank unfair canal monkey style afraid"); - let alice_conf = Mm2TestConf::seednode_using_watchers(&alice_passphrase, &coins); + let alice_conf = Mm2TestConf::seednode(&alice_passphrase, &coins); let mut mm_alice = MarketMakerIt::start(alice_conf.conf.clone(), 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()); let bob_passphrase = String::from("also shoot benefit prefer juice shell elder veteran woman mimic image kidney"); - let bob_conf = Mm2TestConf::light_node_using_watchers(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); + let bob_conf = Mm2TestConf::light_node(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); 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()); @@ -785,7 +890,6 @@ fn test_watcher_validate_taker_fee_utxo() { } #[test] -#[ignore] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 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(); @@ -794,7 +898,7 @@ fn test_watcher_validate_taker_fee_eth() { let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); let taker_pubkey = taker_keypair.public(); - let taker_amount = MmNumber::from((10, 1)); + let taker_amount = MmNumber::from((1, 1)); let fee_amount = dex_fee_amount_from_taker_coin(&MmCoinEnum::EthCoin(taker_coin.clone()), "ETH", &taker_amount); let taker_fee = taker_coin .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount.into(), Uuid::new_v4().as_bytes()) @@ -888,17 +992,16 @@ fn test_watcher_validate_taker_fee_eth() { } #[test] -#[ignore] 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 seed = String::from("spice describe gravity federal thank unfair blast come canal monkey style afraid"); + let seed = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); let taker_coin = generate_jst_with_seed(&seed); let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); let taker_pubkey = taker_keypair.public(); - let taker_amount = MmNumber::from((10, 1)); + let taker_amount = MmNumber::from((1, 1)); let fee_amount = dex_fee_amount_from_taker_coin(&MmCoinEnum::EthCoin(taker_coin.clone()), "ETH", &taker_amount); let taker_fee = taker_coin .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount.into(), Uuid::new_v4().as_bytes()) @@ -1203,16 +1306,15 @@ fn test_watcher_validate_taker_payment_utxo() { } #[test] -#[ignore] fn test_watcher_validate_taker_payment_eth() { let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let seed = String::from("spice describe gravity federal thank unfair blast come canal monkey style afraid"); - let taker_coin = generate_eth_coin_with_seed(&seed); + let taker_coin = eth_distributor(); let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); let taker_pub = taker_keypair.public(); - let maker_keypair = key_pair_from_secret(random_secp256k1_secret().as_slice()).unwrap(); + let maker_seed = get_passphrase!(".env.client", "BOB_PASSPHRASE").unwrap(); + let maker_keypair = key_pair_from_seed(&maker_seed).unwrap(); let maker_pub = maker_keypair.public(); let time_lock_duration = get_payment_locktime(); @@ -1447,16 +1549,16 @@ fn test_watcher_validate_taker_payment_eth() { } #[test] -#[ignore] fn test_watcher_validate_taker_payment_erc20() { let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run - let seed = String::from("spice describe gravity federal thank unfair blast come canal monkey style afraid"); + let seed = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); let taker_coin = generate_jst_with_seed(&seed); let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); let taker_pub = taker_keypair.public(); - let maker_keypair = key_pair_from_secret(random_secp256k1_secret().as_slice()).unwrap(); + let maker_seed = get_passphrase!(".env.client", "BOB_PASSPHRASE").unwrap(); + let maker_keypair = key_pair_from_seed(&maker_seed).unwrap(); let maker_pub = maker_keypair.public(); let time_lock_duration = get_payment_locktime(); @@ -1465,13 +1567,13 @@ fn test_watcher_validate_taker_payment_erc20() { let secret_hash = dhash160(&MakerSwap::generate_secret().unwrap()); - let taker_amount = BigDecimal::from(10); - let maker_amount = BigDecimal::from(10); + let taker_amount = BigDecimal::from_str("0.01").unwrap(); + let maker_amount = BigDecimal::from_str("0.01").unwrap(); let watcher_reward = Some( block_on(taker_coin.get_taker_watcher_reward( &MmCoinEnum::from(taker_coin.clone()), - Some(taker_amount), + Some(taker_amount.clone()), Some(maker_amount), None, wait_for_confirmation_until, @@ -1485,7 +1587,7 @@ fn test_watcher_validate_taker_payment_erc20() { time_lock, other_pubkey: maker_pub, secret_hash: secret_hash.as_slice(), - amount: BigDecimal::from(10), + amount: taker_amount.clone(), swap_contract_address: &taker_coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, @@ -1551,7 +1653,7 @@ fn test_watcher_validate_taker_payment_erc20() { time_lock, other_pubkey: maker_pub, secret_hash: secret_hash.as_slice(), - amount: BigDecimal::from(10), + amount: taker_amount.clone(), swap_contract_address: &Some("9130b257d37a52e52f21054c4da3450c72f595ce".into()), swap_unique_data: &[], payment_instructions: &None, @@ -1621,7 +1723,7 @@ fn test_watcher_validate_taker_payment_erc20() { time_lock, other_pubkey: maker_pub, secret_hash: wrong_secret_hash.as_slice(), - amount: BigDecimal::from(10), + amount: taker_amount, swap_contract_address: &taker_coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, @@ -1765,3 +1867,86 @@ fn test_send_taker_payment_refund_preimage_utxo() { .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 + let (_ctx, utxo_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let eth_coin = eth_distributor(); + + let watcher_reward = + block_on(eth_coin.get_taker_watcher_reward(&MmCoinEnum::EthCoin(eth_coin.clone()), None, None, None, timeout)) + .unwrap(); + assert!(!watcher_reward.is_exact_amount); + assert!(matches!(watcher_reward.reward_target, RewardTarget::Contract)); + assert!(!watcher_reward.send_contract_reward_on_spend); + + let watcher_reward = block_on(eth_coin.get_taker_watcher_reward( + &MmCoinEnum::EthCoin(eth_coin.clone()), + None, + None, + Some(BigDecimal::one()), + timeout, + )) + .unwrap(); + assert!(watcher_reward.is_exact_amount); + assert!(matches!(watcher_reward.reward_target, RewardTarget::Contract)); + assert!(!watcher_reward.send_contract_reward_on_spend); + assert_eq!(watcher_reward.amount, BigDecimal::one()); + + let watcher_reward = block_on(eth_coin.get_taker_watcher_reward( + &MmCoinEnum::UtxoCoin(utxo_coin.clone()), + None, + None, + None, + timeout, + )) + .unwrap(); + assert!(!watcher_reward.is_exact_amount); + assert!(matches!(watcher_reward.reward_target, RewardTarget::PaymentSender)); + assert!(!watcher_reward.send_contract_reward_on_spend); + + let watcher_reward = + block_on(eth_coin.get_maker_watcher_reward(&MmCoinEnum::EthCoin(eth_coin.clone()), None, timeout)) + .unwrap() + .unwrap(); + assert!(!watcher_reward.is_exact_amount); + assert!(matches!(watcher_reward.reward_target, RewardTarget::None)); + assert!(watcher_reward.send_contract_reward_on_spend); + + let watcher_reward = block_on(eth_coin.get_maker_watcher_reward( + &MmCoinEnum::EthCoin(eth_coin.clone()), + Some(BigDecimal::one()), + timeout, + )) + .unwrap() + .unwrap(); + assert!(watcher_reward.is_exact_amount); + assert!(matches!(watcher_reward.reward_target, RewardTarget::None)); + assert!(watcher_reward.send_contract_reward_on_spend); + assert_eq!(watcher_reward.amount, BigDecimal::one()); + + let watcher_reward = + block_on(eth_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoin(utxo_coin.clone()), None, timeout)) + .unwrap() + .unwrap(); + assert!(!watcher_reward.is_exact_amount); + assert!(matches!(watcher_reward.reward_target, RewardTarget::PaymentSpender)); + assert!(!watcher_reward.send_contract_reward_on_spend); + + let watcher_reward = block_on(utxo_coin.get_taker_watcher_reward( + &MmCoinEnum::EthCoin(eth_coin), + Some(BigDecimal::from_str("0.01").unwrap()), + Some(BigDecimal::from_str("1").unwrap()), + None, + timeout, + )) + .unwrap(); + assert!(!watcher_reward.is_exact_amount); + assert!(matches!(watcher_reward.reward_target, RewardTarget::PaymentReceiver)); + assert!(!watcher_reward.send_contract_reward_on_spend); + + let watcher_reward = + block_on(utxo_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoin(utxo_coin.clone()), None, timeout)).unwrap(); + assert!(matches!(watcher_reward, None)); +} 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 42447ae35b..0886e3e83b 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,8 +1,8 @@ use crate::generate_utxo_coin_with_random_privkey; use crate::integration_tests_common::enable_native; use common::block_on; -use mm2_main::mm2::lp_ordermatch::OrderConfirmationsSettings; use mm2_main::mm2::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 std::thread; diff --git a/mm2src/mm2_main/tests/integration_tests_common/mod.rs b/mm2src/mm2_main/tests/integration_tests_common/mod.rs index 5dc484a89e..75d95e8af5 100644 --- a/mm2src/mm2_main/tests/integration_tests_common/mod.rs +++ b/mm2src/mm2_main/tests/integration_tests_common/mod.rs @@ -3,11 +3,12 @@ use common::log::LogLevel; use common::{block_on, log, now_ms, wait_until_ms}; use crypto::privkey::key_pair_from_seed; use mm2_main::mm2::{lp_main, LpMainParams}; +use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::electrums::{morty_electrums, rick_electrums}; use mm2_test_helpers::for_tests::{enable_native as enable_native_impl, init_utxo_electrum, init_utxo_status, init_z_coin_light, init_z_coin_status, MarketMakerIt}; -use mm2_test_helpers::structs::{CoinActivationResult, EnableElectrumResponse, InitTaskResult, InitUtxoStatus, - InitZcoinStatus, RpcV2Response, UtxoStandardActivationResult}; +use mm2_test_helpers::structs::{CoinActivationResult, InitTaskResult, InitUtxoStatus, InitZcoinStatus, RpcV2Response, + UtxoStandardActivationResult}; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::env::var; @@ -32,12 +33,7 @@ pub fn test_mm_start_impl() { } /// Ideally, this function should be replaced everywhere with `enable_electrum_json`. -pub async fn enable_electrum( - mm: &MarketMakerIt, - coin: &str, - tx_history: bool, - urls: &[&str], -) -> EnableElectrumResponse { +pub async fn enable_electrum(mm: &MarketMakerIt, coin: &str, tx_history: bool, urls: &[&str]) -> CoinInitResponse { use mm2_test_helpers::for_tests::enable_electrum as enable_electrum_impl; let value = enable_electrum_impl(mm, coin, tx_history, urls).await; @@ -49,19 +45,19 @@ pub async fn enable_electrum_json( coin: &str, tx_history: bool, servers: Vec, -) -> EnableElectrumResponse { +) -> CoinInitResponse { use mm2_test_helpers::for_tests::enable_electrum_json as enable_electrum_impl; let value = enable_electrum_impl(mm, coin, tx_history, servers).await; json::from_value(value).unwrap() } -pub async fn enable_native(mm: &MarketMakerIt, coin: &str, urls: &[&str]) -> EnableElectrumResponse { +pub async fn enable_native(mm: &MarketMakerIt, coin: &str, urls: &[&str]) -> CoinInitResponse { let value = enable_native_impl(mm, coin, urls).await; json::from_value(value).unwrap() } -pub async fn enable_coins_rick_morty_electrum(mm: &MarketMakerIt) -> HashMap<&'static str, EnableElectrumResponse> { +pub async fn enable_coins_rick_morty_electrum(mm: &MarketMakerIt) -> HashMap<&'static str, CoinInitResponse> { let mut replies = HashMap::new(); replies.insert("RICK", enable_electrum_json(mm, "RICK", false, rick_electrums()).await); replies.insert( @@ -126,7 +122,7 @@ pub async fn enable_utxo_v2_electrum( pub async fn enable_coins_eth_electrum( mm: &MarketMakerIt, eth_urls: &[&str], -) -> HashMap<&'static str, EnableElectrumResponse> { +) -> HashMap<&'static str, CoinInitResponse> { let mut replies = HashMap::new(); replies.insert("RICK", enable_electrum_json(mm, "RICK", false, rick_electrums()).await); replies.insert( @@ -138,7 +134,7 @@ pub async fn enable_coins_eth_electrum( replies } -pub fn addr_from_enable<'a>(enable_response: &'a HashMap<&str, EnableElectrumResponse>, coin: &str) -> &'a str { +pub fn addr_from_enable<'a>(enable_response: &'a HashMap<&str, CoinInitResponse>, coin: &str) -> &'a str { &enable_response.get(coin).unwrap().address } diff --git a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs index 3fbd815ae6..41a510c63c 100644 --- a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs @@ -2,22 +2,23 @@ use crate::integration_tests_common::*; use common::{block_on, log}; use http::StatusCode; use mm2_number::BigDecimal; +use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::for_tests::ETH_DEV_NODES; use mm2_test_helpers::for_tests::{best_orders_v2, best_orders_v2_by_number, eth_jst_testnet_conf, eth_testnet_conf, get_passphrase, morty_conf, rick_conf, tbtc_conf, tbtc_segwit_conf, MarketMakerIt, Mm2TestConf, RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS}; -use mm2_test_helpers::structs::{BestOrdersResponse, BestOrdersV2Response, EnableElectrumResponse, RpcV2Response, - SetPriceResponse}; +use mm2_test_helpers::structs::{BestOrdersResponse, SetPriceResponse}; use serde_json::{self as json, json}; +use std::collections::BTreeSet; use std::env::{self}; use std::thread; use std::time::Duration; +use uuid::Uuid; #[test] #[cfg(not(target_arch = "wasm32"))] fn test_best_orders() { let bob_passphrase = get_passphrase(&".env.seed", "BOB_PASSPHRASE").unwrap(); - let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf(), eth_jst_testnet_conf()]); // start bob and immediately place the orders @@ -259,18 +260,16 @@ fn test_best_orders_v2_by_number() { })) .unwrap(); - let rc = block_on(best_orders_v2_by_number(&mm_alice, "RICK", "buy", 1)); - log!("rc {:?}", rc); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2_by_number(&mm_alice, "RICK", "buy", 1, false)); + log!("response {response:?}"); let best_morty_orders = response.result.orders.get("MORTY").unwrap(); log!("Best MORTY orders when buy RICK {:?}", [best_morty_orders]); assert_eq!(1, best_morty_orders.len()); let expected_price: BigDecimal = "0.7".parse().unwrap(); assert_eq!(expected_price, best_morty_orders[0].price.decimal); - let rc = block_on(best_orders_v2_by_number(&mm_alice, "RICK", "buy", 2)); - log!("rc {:?}", rc); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2_by_number(&mm_alice, "RICK", "buy", 2, false)); + log!("response {response:?}"); let best_morty_orders = response.result.orders.get("MORTY").unwrap(); log!("Best MORTY orders when buy RICK {:?}", [best_morty_orders]); assert_eq!(2, best_morty_orders.len()); @@ -279,9 +278,8 @@ fn test_best_orders_v2_by_number() { let expected_price: BigDecimal = "0.8".parse().unwrap(); assert_eq!(expected_price, best_morty_orders[1].price.decimal); - let rc = block_on(best_orders_v2_by_number(&mm_alice, "RICK", "sell", 1)); - log!("rc {:?}", rc); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2_by_number(&mm_alice, "RICK", "sell", 1, false)); + log!("response {response:?}"); let expected_price: BigDecimal = "1.25".parse().unwrap(); let best_morty_orders = response.result.orders.get("MORTY").unwrap(); log!("Best MORTY orders when sell RICK {:?}", [best_morty_orders]); @@ -292,9 +290,8 @@ fn test_best_orders_v2_by_number() { assert_eq!(1, best_eth_orders.len()); assert_eq!(expected_price, best_eth_orders[0].price.decimal); - let rc = block_on(best_orders_v2_by_number(&mm_alice, "ETH", "sell", 1)); - log!("rc {:?}", rc); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2_by_number(&mm_alice, "ETH", "sell", 1, false)); + log!("response {response:?}"); let best_rick_orders = response.result.orders.get("RICK").unwrap(); log!("Best RICK orders when sell ETH {:?}", [best_rick_orders]); assert_eq!(1, best_rick_orders.len()); @@ -386,9 +383,8 @@ fn test_best_orders_v2_by_volume() { })) .unwrap(); - let rc = block_on(best_orders_v2(&mm_alice, "RICK", "buy", "1.7")); - log!("rc {:?}", rc); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2(&mm_alice, "RICK", "buy", "1.7")); + log!("response {response:?}"); // MORTY let best_morty_orders = response.result.orders.get("MORTY").unwrap(); log!("Best MORTY orders when buy RICK {:?}", [best_morty_orders]); @@ -402,9 +398,8 @@ fn test_best_orders_v2_by_volume() { log!("Best ETH orders when buy RICK {:?}", [best_eth_orders]); assert_eq!(expected_price, best_eth_orders[0].price.decimal); - let rc = block_on(best_orders_v2(&mm_alice, "RICK", "sell", "0.1")); - log!("rc {:?}", rc); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2(&mm_alice, "RICK", "sell", "0.1")); + log!("response {response:?}"); let expected_price: BigDecimal = "1.25".parse().unwrap(); let best_morty_orders = response.result.orders.get("MORTY").unwrap(); log!("Best MORTY orders when sell RICK {:?}", [best_morty_orders]); @@ -414,9 +409,8 @@ fn test_best_orders_v2_by_volume() { log!("Best ETH orders when sell RICK {:?}", [best_morty_orders]); assert_eq!(expected_price, best_eth_orders[0].price.decimal); - let rc = block_on(best_orders_v2(&mm_alice, "ETH", "sell", "0.1")); - log!("rc {:?}", rc); - let response: RpcV2Response = json::from_value(rc).unwrap(); + 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_morty_orders = response.result.orders.get("MORTY").unwrap(); log!("Best MORTY orders when sell ETH {:?}", [best_morty_orders]); @@ -428,6 +422,104 @@ fn test_best_orders_v2_by_volume() { block_on(mm_alice.stop()).unwrap(); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_best_orders_v2_exclude_mine() { + let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf(), eth_jst_testnet_conf()]); + let bob_passphrase = get_passphrase(&".env.seed", "BOB_PASSPHRASE").unwrap(); + let mm_bob = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9998, + "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": "pass", + "i_am_seed": true, + }), + "pass".into(), + None, + ) + .unwrap(); + + let _ = block_on(enable_coins_eth_electrum(&mm_bob, ETH_DEV_NODES)); + let bob_orders = [ + ("RICK", "MORTY", "0.9", "0.9", None), + ("RICK", "MORTY", "0.8", "0.9", None), + ]; + + let mut bob_order_ids = BTreeSet::::new(); + for (base, rel, price, volume, min_volume) in bob_orders.iter() { + let (status, data, _headers) = 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(); + let result: SetPriceResponse = json::from_str(&data).unwrap(); + bob_order_ids.insert(result.result.uuid); + assert!(status.is_success(), "!setprice: {}", data); + } + + let alice_passphrase = get_passphrase(&".env.seed", "ALICE_PASSPHRASE").unwrap(); + let mm_alice = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9998, + "myipaddr": env::var ("ALICE_TRADE_IP") .ok(), + "rpcip": env::var ("ALICE_TRADE_IP") .ok(), + "passphrase": alice_passphrase, + "coins": coins, + "seednodes": [mm_bob.ip.to_string()], + "rpc_password": "pass", + }), + "pass".into(), + None, + ) + .unwrap(); + + let _ = block_on(enable_coins_eth_electrum(&mm_alice, ETH_DEV_NODES)); + let alice_orders = [("RICK", "MORTY", "0.85", "1", None)]; + let mut alice_order_ids = BTreeSet::::new(); + for (base, rel, price, volume, min_volume) in alice_orders.iter() { + let (status, data, _headers) = block_on(mm_alice.rpc(&json! ({ + "userpass": mm_alice.userpass, + "method": "setprice", + "base": base, + "rel": rel, + "price": price, + "volume": volume, + "min_volume": min_volume.unwrap_or("0.00777"), + "cancel_previous": false, + }))) + .unwrap(); + let result: SetPriceResponse = json::from_str(&data).unwrap(); + alice_order_ids.insert(result.result.uuid); + assert!(status.is_success(), "!setprice: {}", data); + } + let response = block_on(best_orders_v2_by_number(&mm_alice, "RICK", "buy", 100, false)); + log!("all orders response: {response:?}"); + assert_eq!(response.result.orders.get("MORTY").unwrap().len(), 3); + + let response = block_on(best_orders_v2_by_number(&mm_alice, "RICK", "buy", 100, true)); + log!("alice orders response: {response:?}"); + assert_eq!(response.result.orders.get("MORTY").unwrap().len(), 2); + for order in response.result.orders.get("MORTY").unwrap() { + assert!(bob_order_ids.remove(&order.uuid)); + } + assert!(bob_order_ids.is_empty()); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_best_orders_no_duplicates_after_update() { @@ -738,7 +830,7 @@ fn test_best_orders_address_and_confirmations() { electrum.1 ); log!("enable tBTC: {:?}", electrum); - let enable_tbtc_res: EnableElectrumResponse = json::from_str(&electrum.1).unwrap(); + let enable_tbtc_res: CoinInitResponse = json::from_str(&electrum.1).unwrap(); let tbtc_segwit_address = enable_tbtc_res.address; let electrum = block_on(mm_bob.rpc(&json!({ @@ -756,7 +848,7 @@ fn test_best_orders_address_and_confirmations() { electrum.1 ); log!("enable RICK: {:?}", electrum); - let enable_rick_res: EnableElectrumResponse = json::from_str(&electrum.1).unwrap(); + let enable_rick_res: CoinInitResponse = json::from_str(&electrum.1).unwrap(); let rick_address = enable_rick_res.address; // issue sell request on Bob side by setting base/rel price @@ -821,10 +913,10 @@ fn test_best_orders_address_and_confirmations() { assert_eq!(1, best_orders.len()); assert_eq!(best_orders[0].coin, "RICK"); assert_eq!(best_orders[0].address, rick_address); - assert_eq!(best_orders[0].base_confs, 5); - assert!(!best_orders[0].base_nota); - assert_eq!(best_orders[0].rel_confs, 10); - assert!(best_orders[0].rel_nota); + assert_eq!(best_orders[0].conf_settings.as_ref().unwrap().base_confs, 5); + assert!(!best_orders[0].conf_settings.as_ref().unwrap().base_nota); + assert_eq!(best_orders[0].conf_settings.as_ref().unwrap().rel_confs, 10); + assert!(best_orders[0].conf_settings.as_ref().unwrap().rel_nota); let rc = block_on(mm_alice.rpc(&json! ({ "userpass": mm_alice.userpass, @@ -840,10 +932,10 @@ fn test_best_orders_address_and_confirmations() { assert_eq!(1, best_orders.len()); assert_eq!(best_orders[0].coin, "tBTC"); assert_eq!(best_orders[0].address, tbtc_segwit_address); - assert_eq!(best_orders[0].base_confs, 10); - assert!(best_orders[0].base_nota); - assert_eq!(best_orders[0].rel_confs, 5); - assert!(!best_orders[0].rel_nota); + assert_eq!(best_orders[0].conf_settings.as_ref().unwrap().base_confs, 10); + assert!(best_orders[0].conf_settings.as_ref().unwrap().base_nota); + assert_eq!(best_orders[0].conf_settings.as_ref().unwrap().rel_confs, 5); + assert!(!best_orders[0].conf_settings.as_ref().unwrap().rel_nota); // checking buy and sell best_orders against ("RICK", "tBTC", "0.7", "0.0002", Some("0.00015")) let rc = block_on(mm_alice.rpc(&json! ({ @@ -860,10 +952,10 @@ fn test_best_orders_address_and_confirmations() { assert_eq!(1, best_orders.len()); assert_eq!(best_orders[0].coin, "tBTC"); assert_eq!(best_orders[0].address, tbtc_segwit_address); - assert_eq!(best_orders[0].base_confs, 10); - assert!(best_orders[0].base_nota); - assert_eq!(best_orders[0].rel_confs, 5); - assert!(!best_orders[0].rel_nota); + assert_eq!(best_orders[0].conf_settings.as_ref().unwrap().base_confs, 10); + assert!(best_orders[0].conf_settings.as_ref().unwrap().base_nota); + assert_eq!(best_orders[0].conf_settings.as_ref().unwrap().rel_confs, 5); + assert!(!best_orders[0].conf_settings.as_ref().unwrap().rel_nota); let rc = block_on(mm_alice.rpc(&json! ({ "userpass": mm_alice.userpass, @@ -879,10 +971,10 @@ fn test_best_orders_address_and_confirmations() { assert_eq!(1, best_orders.len()); assert_eq!(best_orders[0].coin, "RICK"); assert_eq!(best_orders[0].address, rick_address); - assert_eq!(best_orders[0].base_confs, 5); - assert!(!best_orders[0].base_nota); - assert_eq!(best_orders[0].rel_confs, 10); - assert!(best_orders[0].rel_nota); + assert_eq!(best_orders[0].conf_settings.as_ref().unwrap().base_confs, 5); + assert!(!best_orders[0].conf_settings.as_ref().unwrap().base_nota); + assert_eq!(best_orders[0].conf_settings.as_ref().unwrap().rel_confs, 10); + assert!(best_orders[0].conf_settings.as_ref().unwrap().rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -993,26 +1085,22 @@ fn best_orders_must_return_duplicate_for_orderbook_tickers() { assert_eq!(best_orders.len(), 1); assert_eq!(best_orders[0].coin, "tBTC-Segwit"); - let rc = block_on(best_orders_v2(&mm_alice, "tBTC-Segwit", "buy", "0.0002")); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2(&mm_alice, "tBTC-Segwit", "buy", "0.0002")); let best_orders = response.result.orders.get("RICK").unwrap(); assert_eq!(best_orders.len(), 1); - let rc = block_on(best_orders_v2(&mm_alice, "tBTC-Segwit", "sell", "0.0002")); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2(&mm_alice, "tBTC-Segwit", "sell", "0.0002")); let best_orders = response.result.orders.get("RICK").unwrap(); assert_eq!(best_orders.len(), 1); - let rc = block_on(best_orders_v2(&mm_alice, "RICK", "buy", "0.0002")); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2(&mm_alice, "RICK", "buy", "0.0002")); let best_orders = response.result.orders.get("tBTC").unwrap(); assert_eq!(best_orders.len(), 1); let best_orders = response.result.orders.get("tBTC-Segwit").unwrap(); assert_eq!(best_orders.len(), 1); assert_eq!(best_orders[0].coin, "tBTC-Segwit"); - let rc = block_on(best_orders_v2(&mm_alice, "RICK", "sell", "0.0002")); - let response: RpcV2Response = json::from_value(rc).unwrap(); + let response = block_on(best_orders_v2(&mm_alice, "RICK", "sell", "0.0002")); let best_orders = response.result.orders.get("tBTC").unwrap(); assert_eq!(best_orders.len(), 1); let best_orders = response.result.orders.get("tBTC-Segwit").unwrap(); @@ -1096,7 +1184,6 @@ fn zhtlc_best_orders() { log!("Alice log path: {}", mm_alice.log_path.display()); let best_orders = block_on(best_orders_v2(&mm_alice, "RICK", "sell", "1")); - let best_orders: RpcV2Response = json::from_value(best_orders).unwrap(); let zombie_best_orders = best_orders.result.orders.get("ZOMBIE").unwrap(); assert_eq!(1, zombie_best_orders.len()); @@ -1106,7 +1193,6 @@ fn zhtlc_best_orders() { .unwrap(); let best_orders = block_on(best_orders_v2(&mm_alice, "ZOMBIE", "buy", "1")); - let best_orders: RpcV2Response = json::from_value(best_orders).unwrap(); let rick_best_orders = best_orders.result.orders.get("RICK").unwrap(); assert_eq!(1, rick_best_orders.len()); diff --git a/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs index 779a04962c..9bbc37f056 100644 --- a/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs +++ b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs @@ -2,11 +2,10 @@ use crate::integration_tests_common::enable_electrum; use common::executor::Timer; use common::{block_on, log}; use mm2_number::BigDecimal; +use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, check_stats_swap_status, enable_eth_coin, enable_tendermint, iris_nimda_testnet_conf, iris_testnet_conf, rick_conf, tbnb_conf, - usdc_ibc_iris_testnet_conf, MarketMakerIt, MAKER_ERROR_EVENTS, MAKER_SUCCESS_EVENTS, - RICK_ELECTRUM_ADDRS, TAKER_ERROR_EVENTS, TAKER_SUCCESS_EVENTS}; -use mm2_test_helpers::structs::OrderbookResponse; + usdc_ibc_iris_testnet_conf, MarketMakerIt, RICK_ELECTRUM_ADDRS}; use serde_json::{json, Value as Json}; use std::convert::TryFrom; use std::env; @@ -219,8 +218,6 @@ pub async fn trade_base_rel_iris( check_my_swap_status( &mm_alice, uuid, - &TAKER_SUCCESS_EVENTS, - &TAKER_ERROR_EVENTS, BigDecimal::try_from(volume).unwrap(), BigDecimal::try_from(volume).unwrap(), ) @@ -231,8 +228,6 @@ pub async fn trade_base_rel_iris( check_my_swap_status( &mm_bob, uuid, - &MAKER_SUCCESS_EVENTS, - &MAKER_ERROR_EVENTS, BigDecimal::try_from(volume).unwrap(), BigDecimal::try_from(volume).unwrap(), ) @@ -244,10 +239,10 @@ pub async fn trade_base_rel_iris( for uuid in uuids.iter() { log!("Checking alice status.."); - check_stats_swap_status(&mm_alice, uuid, &MAKER_SUCCESS_EVENTS, &TAKER_SUCCESS_EVENTS).await; + check_stats_swap_status(&mm_alice, uuid).await; log!("Checking bob status.."); - check_stats_swap_status(&mm_bob, uuid, &MAKER_SUCCESS_EVENTS, &TAKER_SUCCESS_EVENTS).await; + check_stats_swap_status(&mm_bob, uuid).await; } log!("Checking alice recent swaps.."); 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 65a4723bc6..02e0b53b04 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -8,7 +8,10 @@ use http::{HeaderMap, StatusCode}; use mm2_main::mm2::lp_ordermatch::MIN_ORDER_KEEP_ALIVE_INTERVAL; use mm2_metrics::{MetricType, MetricsJson}; use mm2_number::{BigDecimal, BigRational, Fraction, MmNumber}; +use mm2_rpc::data::legacy::{CoinInitResponse, MmVersionResponse, OrderbookResponse}; use mm2_test_helpers::electrums::*; +#[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] +use mm2_test_helpers::for_tests::check_stats_swap_status; use mm2_test_helpers::for_tests::{btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, check_recent_swaps, enable_eth_coin, enable_qrc20, eth_jst_testnet_conf, eth_testnet_conf, find_metrics_in_json, from_env_file, get_shared_db_id, mm_spat, @@ -19,8 +22,6 @@ use mm2_test_helpers::for_tests::{btc_segwit_conf, btc_with_spv_conf, btc_with_s MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, ETH_DEV_NODES, ETH_DEV_SWAP_CONTRACT, ETH_DEV_TOKEN_CONTRACT, ETH_MAINNET_NODE, ETH_MAINNET_SWAP_CONTRACT, MORTY, QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS}; -#[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] -use mm2_test_helpers::for_tests::{check_stats_swap_status, MAKER_SUCCESS_EVENTS, TAKER_SUCCESS_EVENTS}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::*; @@ -81,7 +82,7 @@ fn test_rpc() { .unwrap(); assert_eq!(version.0, StatusCode::OK); assert_eq!((version.2)[ACCESS_CONTROL_ALLOW_ORIGIN], "http://localhost:4000"); - let _version: MmVersion = json::from_str(&version.1).unwrap(); + let _version: MmVersionResponse = json::from_str(&version.1).unwrap(); let help = block_on(mm.rpc(&json! ({ "userpass": mm.userpass, @@ -821,10 +822,10 @@ async fn trade_base_rel_electrum( #[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] for uuid in uuids.iter() { log!("Checking alice status.."); - check_stats_swap_status(&mm_alice, uuid, &MAKER_SUCCESS_EVENTS, &TAKER_SUCCESS_EVENTS).await; + check_stats_swap_status(&mm_alice, uuid).await; log!("Checking bob status.."); - check_stats_swap_status(&mm_bob, uuid, &MAKER_SUCCESS_EVENTS, &TAKER_SUCCESS_EVENTS).await; + check_stats_swap_status(&mm_bob, uuid).await; } log!("Checking alice recent swaps.."); @@ -898,7 +899,7 @@ fn withdraw_and_send( mm: &MarketMakerIt, coin: &str, to: &str, - enable_res: &HashMap<&'static str, EnableElectrumResponse>, + enable_res: &HashMap<&'static str, CoinInitResponse>, expected_bal_change: &str, amount: f64, ) { @@ -1005,7 +1006,6 @@ fn test_withdraw_and_send() { "-0.00101", 0.001, ); - // dev chain gas price is 0 so ETH expected balance change doesn't include the fee withdraw_and_send( &mm_alice, "ETH", @@ -1014,6 +1014,8 @@ fn test_withdraw_and_send() { "-0.001", 0.001, ); + log!("Wait for the ETH payment to be sent"); + thread::sleep(Duration::from_secs(15)); withdraw_and_send( &mm_alice, "JST", @@ -1165,8 +1167,7 @@ fn test_tbtc_withdraw_to_cashaddresses_should_fail() { ); log!("enable_coins (alice): {:?}", electrum); - let electrum_response: EnableElectrumResponse = - json::from_str(&electrum.1).expect("Expected 'EnableElectrumResponse'"); + let electrum_response: CoinInitResponse = json::from_str(&electrum.1).expect("Expected 'CoinInitResponse'"); let mut enable_res = HashMap::new(); enable_res.insert("tBTC", electrum_response); @@ -3841,8 +3842,7 @@ fn test_validateaddress_segwit() { ); log!("enable_coins (alice): {:?}", electrum); - let electrum_response: EnableElectrumResponse = - json::from_str(&electrum.1).expect("Expected 'EnableElectrumResponse'"); + let electrum_response: CoinInitResponse = json::from_str(&electrum.1).expect("Expected 'CoinInitResponse'"); let mut enable_res = HashMap::new(); enable_res.insert("tBTC", electrum_response); @@ -4900,7 +4900,7 @@ fn test_my_orders_after_matched() { fn test_sell_conf_settings() { let bob_passphrase = get_passphrase(&".env.client", "BOB_PASSPHRASE").unwrap(); - let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf(), + let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf(), {"coin":"JST","name":"jst","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address": ETH_DEV_TOKEN_CONTRACT}},"required_confirmations":2},]); let mm_bob = MarketMakerIt::start( @@ -4970,7 +4970,7 @@ fn test_sell_conf_settings() { fn test_set_price_conf_settings() { let bob_passphrase = get_passphrase(&".env.client", "BOB_PASSPHRASE").unwrap(); - let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf(), + let coins = json!([rick_conf(), morty_conf(), eth_testnet_conf(), {"coin":"JST","name":"jst","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address": ETH_DEV_TOKEN_CONTRACT}},"required_confirmations":2},]); let mm_bob = MarketMakerIt::start( @@ -6378,20 +6378,32 @@ fn test_conf_settings_in_orderbook() { 1, "Alice RICK/MORTY orderbook must have exactly 1 ask" ); - assert_eq!(alice_orderbook.asks[0].base_confs, 10); - assert!(alice_orderbook.asks[0].base_nota); - assert_eq!(alice_orderbook.asks[0].rel_confs, 5); - assert!(!alice_orderbook.asks[0].rel_nota); + assert_eq!( + alice_orderbook.asks[0].entry.conf_settings.as_ref().unwrap().base_confs, + 10 + ); + assert!(alice_orderbook.asks[0].entry.conf_settings.as_ref().unwrap().base_nota); + assert_eq!( + alice_orderbook.asks[0].entry.conf_settings.as_ref().unwrap().rel_confs, + 5 + ); + assert!(!alice_orderbook.asks[0].entry.conf_settings.as_ref().unwrap().rel_nota); assert_eq!( alice_orderbook.bids.len(), 1, "Alice RICK/MORTY orderbook must have exactly 1 bid" ); - assert_eq!(alice_orderbook.bids[0].base_confs, 10); - assert!(alice_orderbook.bids[0].base_nota); - assert_eq!(alice_orderbook.bids[0].rel_confs, 5); - assert!(!alice_orderbook.bids[0].rel_nota); + assert_eq!( + alice_orderbook.bids[0].entry.conf_settings.as_ref().unwrap().base_confs, + 10 + ); + assert!(alice_orderbook.bids[0].entry.conf_settings.as_ref().unwrap().base_nota); + assert_eq!( + alice_orderbook.bids[0].entry.conf_settings.as_ref().unwrap().rel_confs, + 5 + ); + assert!(!alice_orderbook.bids[0].entry.conf_settings.as_ref().unwrap().rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -6517,12 +6529,18 @@ fn alice_can_see_confs_in_orderbook_after_sync() { let bob_order_in_orderbook = alice_orderbook .asks .iter() - .find(|entry| entry.pubkey == bob_pubkey) + .find(|entry| entry.entry.pubkey == bob_pubkey) .unwrap(); - assert_eq!(bob_order_in_orderbook.base_confs, 10); - assert!(bob_order_in_orderbook.base_nota); - assert_eq!(bob_order_in_orderbook.rel_confs, 5); - assert!(!bob_order_in_orderbook.rel_nota); + assert_eq!( + bob_order_in_orderbook.entry.conf_settings.as_ref().unwrap().base_confs, + 10 + ); + assert!(bob_order_in_orderbook.entry.conf_settings.as_ref().unwrap().base_nota); + assert_eq!( + bob_order_in_orderbook.entry.conf_settings.as_ref().unwrap().rel_confs, + 5 + ); + assert!(!bob_order_in_orderbook.entry.conf_settings.as_ref().unwrap().rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); 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 c1ca2864fa..8accb1beba 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -4,13 +4,13 @@ use common::{block_on, log}; use http::StatusCode; use mm2_main::mm2::lp_ordermatch::MIN_ORDER_KEEP_ALIVE_INTERVAL; use mm2_number::{BigDecimal, BigRational, MmNumber}; +use mm2_rpc::data::legacy::{AggregatedOrderbookEntry, CoinInitResponse, OrderbookResponse}; use mm2_test_helpers::electrums::rick_electrums; use mm2_test_helpers::for_tests::{eth_jst_testnet_conf, eth_testnet_conf, get_passphrase, morty_conf, orderbook_v2, rick_conf, zombie_conf, MarketMakerIt, Mm2TestConf, ETH_DEV_NODES, RICK, ZOMBIE_ELECTRUMS, ZOMBIE_LIGHTWALLETD_URLS, ZOMBIE_TICKER}; use mm2_test_helpers::get_passphrase; -use mm2_test_helpers::structs::{EnableElectrumResponse, GetPublicKeyResult, OrderbookEntryAggregate, - OrderbookResponse, OrderbookV2Response, RpcV2Response, SetPriceResponse}; +use mm2_test_helpers::structs::{GetPublicKeyResult, OrderbookV2Response, RpcV2Response, SetPriceResponse}; use serde_json::{self as json, json, Value as Json}; use std::env; use std::str::FromStr; @@ -75,7 +75,10 @@ fn alice_can_see_the_active_order_after_connection() { let bob_orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("Bob orderbook {:?}", bob_orderbook); assert!(!bob_orderbook.asks.is_empty(), "Bob RICK/MORTY asks are empty"); - assert_eq!(BigDecimal::from_str("0.9").unwrap(), bob_orderbook.asks[0].max_volume); + assert_eq!( + BigDecimal::from_str("0.9").unwrap(), + bob_orderbook.asks[0].entry.max_volume + ); // start eve and immediately place the order let mm_eve = MarketMakerIt::start( @@ -277,7 +280,7 @@ fn alice_can_see_the_active_order_after_orderbook_sync_segwit() { electrum.1 ); log!("enable tBTC: {:?}", electrum); - let enable_tbtc_res: EnableElectrumResponse = json::from_str(&electrum.1).unwrap(); + let enable_tbtc_res: CoinInitResponse = json::from_str(&electrum.1).unwrap(); let tbtc_segwit_address = enable_tbtc_res.address; let electrum = block_on(mm_bob.rpc(&json!({ @@ -420,8 +423,8 @@ fn alice_can_see_the_active_order_after_orderbook_sync_segwit() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); let response: OrderbookResponse = json::from_str(&rc.1).unwrap(); - assert_eq!(response.asks[0].address, tbtc_segwit_address); - assert_eq!(response.bids[0].address, rick_address); + assert_eq!(response.asks[0].entry.address, tbtc_segwit_address); + assert_eq!(response.bids[0].entry.address, rick_address); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -477,7 +480,7 @@ fn test_orderbook_segwit() { electrum.1 ); log!("enable tBTC: {:?}", electrum); - let enable_tbtc_res: EnableElectrumResponse = json::from_str(&electrum.1).unwrap(); + let enable_tbtc_res: CoinInitResponse = json::from_str(&electrum.1).unwrap(); let tbtc_segwit_address = enable_tbtc_res.address; let electrum = block_on(mm_bob.rpc(&json!({ @@ -555,8 +558,8 @@ fn test_orderbook_segwit() { .unwrap(); assert!(rc.0.is_success(), "!orderbook: {}", rc.1); let response: OrderbookResponse = json::from_str(&rc.1).unwrap(); - assert_eq!(response.asks[0].address, tbtc_segwit_address); - assert_eq!(response.bids[0].address, rick_address); + assert_eq!(response.asks[0].entry.address, tbtc_segwit_address); + assert_eq!(response.bids[0].entry.address, rick_address); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -701,20 +704,32 @@ fn test_conf_settings_in_orderbook() { 1, "Alice RICK/MORTY orderbook must have exactly 1 ask" ); - assert_eq!(alice_orderbook.asks[0].base_confs, 10); - assert!(alice_orderbook.asks[0].base_nota); - assert_eq!(alice_orderbook.asks[0].rel_confs, 5); - assert!(!alice_orderbook.asks[0].rel_nota); + assert_eq!( + alice_orderbook.asks[0].entry.conf_settings.as_ref().unwrap().base_confs, + 10 + ); + assert!(alice_orderbook.asks[0].entry.conf_settings.as_ref().unwrap().base_nota); + assert_eq!( + alice_orderbook.asks[0].entry.conf_settings.as_ref().unwrap().rel_confs, + 5 + ); + assert!(!alice_orderbook.asks[0].entry.conf_settings.as_ref().unwrap().rel_nota); assert_eq!( alice_orderbook.bids.len(), 1, "Alice RICK/MORTY orderbook must have exactly 1 bid" ); - assert_eq!(alice_orderbook.bids[0].base_confs, 10); - assert!(alice_orderbook.bids[0].base_nota); - assert_eq!(alice_orderbook.bids[0].rel_confs, 5); - assert!(!alice_orderbook.bids[0].rel_nota); + assert_eq!( + alice_orderbook.bids[0].entry.conf_settings.as_ref().unwrap().base_confs, + 10 + ); + assert!(alice_orderbook.bids[0].entry.conf_settings.as_ref().unwrap().base_nota); + assert_eq!( + alice_orderbook.bids[0].entry.conf_settings.as_ref().unwrap().rel_confs, + 5 + ); + assert!(!alice_orderbook.bids[0].entry.conf_settings.as_ref().unwrap().rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -839,12 +854,18 @@ fn alice_can_see_confs_in_orderbook_after_sync() { let bob_order_in_orderbook = alice_orderbook .asks .iter() - .find(|entry| entry.pubkey == bob_pubkey) + .find(|entry| entry.entry.pubkey == bob_pubkey) .unwrap(); - assert_eq!(bob_order_in_orderbook.base_confs, 10); - assert!(bob_order_in_orderbook.base_nota); - assert_eq!(bob_order_in_orderbook.rel_confs, 5); - assert!(!bob_order_in_orderbook.rel_nota); + assert_eq!( + bob_order_in_orderbook.entry.conf_settings.as_ref().unwrap().base_confs, + 10 + ); + assert!(bob_order_in_orderbook.entry.conf_settings.as_ref().unwrap().base_nota); + assert_eq!( + bob_order_in_orderbook.entry.conf_settings.as_ref().unwrap().rel_confs, + 5 + ); + assert!(!bob_order_in_orderbook.entry.conf_settings.as_ref().unwrap().rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -924,31 +945,43 @@ fn orderbook_extended_data() { let orderbook: OrderbookResponse = json::from_str(&rc.1).unwrap(); log!("orderbook {:?}", rc.1); let expected_total_asks_base_vol = MmNumber::from("2.7"); - assert_eq!(expected_total_asks_base_vol.to_decimal(), orderbook.total_asks_base_vol); + assert_eq!( + expected_total_asks_base_vol.to_decimal(), + orderbook.total_asks_base.total_asks_base_vol + ); let expected_total_bids_base_vol = MmNumber::from("1.62"); - assert_eq!(expected_total_bids_base_vol.to_decimal(), orderbook.total_bids_base_vol); + assert_eq!( + expected_total_bids_base_vol.to_decimal(), + orderbook.total_bids_base.total_bids_base_vol + ); let expected_total_asks_rel_vol = MmNumber::from("2.16"); - assert_eq!(expected_total_asks_rel_vol.to_decimal(), orderbook.total_asks_rel_vol); + assert_eq!( + expected_total_asks_rel_vol.to_decimal(), + orderbook.total_asks_rel.total_asks_rel_vol + ); let expected_total_bids_rel_vol = MmNumber::from("1.8"); - assert_eq!(expected_total_bids_rel_vol.to_decimal(), orderbook.total_bids_rel_vol); + assert_eq!( + expected_total_bids_rel_vol.to_decimal(), + orderbook.total_bids_rel.total_bids_rel_vol + ); fn check_price_and_vol_aggr( - order: &OrderbookEntryAggregate, + order: &AggregatedOrderbookEntry, price: &'static str, base_aggr: &'static str, rel_aggr: &'static str, ) { let price = MmNumber::from(price); - assert_eq!(price.to_decimal(), order.price); + assert_eq!(price.to_decimal(), order.entry.price); let base_aggr = MmNumber::from(base_aggr); - assert_eq!(base_aggr.to_decimal(), order.base_max_volume_aggr); + assert_eq!(base_aggr.to_decimal(), order.base_max_volume_aggr.base_max_volume_aggr); let rel_aggr = MmNumber::from(rel_aggr); - assert_eq!(rel_aggr.to_decimal(), order.rel_max_volume_aggr); + assert_eq!(rel_aggr.to_decimal(), order.rel_max_volume_aggr.rel_max_volume_aggr); } check_price_and_vol_aggr(&orderbook.asks[0], "0.9", "2.7", "2.16"); @@ -1026,11 +1059,17 @@ fn orderbook_should_display_base_rel_volumes() { log!("orderbook {:?}", orderbook); assert_eq!(orderbook.asks.len(), 1, "RICK/MORTY orderbook must have exactly 1 ask"); let min_volume = BigRational::new(1.into(), 10000.into()); - assert_eq!(volume, orderbook.asks[0].base_max_volume_rat); - assert_eq!(min_volume, orderbook.asks[0].base_min_volume_rat); + assert_eq!(volume, orderbook.asks[0].entry.base_max_volume.base_max_volume_rat); + assert_eq!(min_volume, orderbook.asks[0].entry.base_min_volume.base_min_volume_rat); - assert_eq!(&volume * &price, orderbook.asks[0].rel_max_volume_rat); - assert_eq!(&min_volume * &price, orderbook.asks[0].rel_min_volume_rat); + assert_eq!( + &volume * &price, + orderbook.asks[0].entry.rel_max_volume.rel_max_volume_rat + ); + assert_eq!( + &min_volume * &price, + orderbook.asks[0].entry.rel_min_volume.rel_min_volume_rat + ); log!("Get MORTY/RICK orderbook"); let rc = block_on(mm.rpc(&json!({ @@ -1046,11 +1085,17 @@ fn orderbook_should_display_base_rel_volumes() { log!("orderbook {:?}", orderbook); assert_eq!(orderbook.bids.len(), 1, "MORTY/RICK orderbook must have exactly 1 bid"); let min_volume = BigRational::new(1.into(), 10000.into()); - assert_eq!(volume, orderbook.bids[0].rel_max_volume_rat); - assert_eq!(min_volume, orderbook.bids[0].rel_min_volume_rat); + assert_eq!(volume, orderbook.bids[0].entry.rel_max_volume.rel_max_volume_rat); + assert_eq!(min_volume, orderbook.bids[0].entry.rel_min_volume.rel_min_volume_rat); - assert_eq!(&volume * &price, orderbook.bids[0].base_max_volume_rat); - assert_eq!(&min_volume * &price, orderbook.bids[0].base_min_volume_rat); + assert_eq!( + &volume * &price, + orderbook.bids[0].entry.base_max_volume.base_max_volume_rat + ); + assert_eq!( + &min_volume * &price, + orderbook.bids[0].entry.base_min_volume.base_min_volume_rat + ); } #[test] diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_ibc_asset_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_ibc_asset_tests.rs index 4cd0712aad..6bebbec41a 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_ibc_asset_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_ibc_asset_tests.rs @@ -1,10 +1,11 @@ use common::block_on; use mm2_number::BigDecimal; +use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::for_tests::{enable_tendermint, enable_tendermint_without_balance, iris_testnet_conf, my_balance, orderbook, orderbook_v2, set_price, usdc_ibc_iris_testnet_conf, MarketMakerIt, Mm2TestConf}; -use mm2_test_helpers::structs::{OrderbookAddress, OrderbookResponse, OrderbookV2Response, RpcV2Response, - TendermintActivationResult}; +use mm2_test_helpers::structs::{OrderbookAddress, OrderbookV2Response, RpcV2Response, TendermintActivationResult}; + use serde_json::{self, json}; use std::collections::HashSet; use std::iter::FromIterator; @@ -56,10 +57,10 @@ fn test_iris_with_usdc_activation_balance_orderbook() { let orderbook: OrderbookResponse = serde_json::from_value(orderbook).unwrap(); let first_ask = orderbook.asks.first().unwrap(); - assert_eq!(first_ask.address, expected_address); + assert_eq!(first_ask.entry.address, expected_address); let first_bid = orderbook.bids.first().unwrap(); - assert_eq!(first_bid.address, expected_address); + assert_eq!(first_bid.entry.address, expected_address); let orderbook_v2 = block_on(orderbook_v2(&mm, USDC_IBC_TICKER, IRIS_TICKER)); let orderbook_v2: RpcV2Response = serde_json::from_value(orderbook_v2).unwrap(); diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs index 13e24c7521..7b5e33fe8b 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs @@ -142,6 +142,41 @@ fn test_tendermint_withdraw() { println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); } +#[test] +fn test_custom_gas_limit_on_tendermint_withdraw() { + let coins = json!([atom_testnet_conf()]); + + let conf = Mm2TestConf::seednode(ATOM_TEST_WITHDRAW_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint( + &mm, + ATOM_TICKER, + &[], + ATOM_TENDERMINT_RPC_URLS, + false, + )); + println!("Activation {}", json::to_string(&activation_res).unwrap()); + + let request = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "withdraw", + "coin": ATOM_TICKER, + "to": "cosmos1svaw0aqc4584x825ju7ua03g5xtxwd0ahl86hz", + "amount": "0.1", + "fee": { + "type": "CosmosGas", + "gas_limit": 150000, + "gas_price": 0.1 + } + }))) + .unwrap(); + assert_eq!(request.0, common::StatusCode::OK, "'withdraw' failed: {}", request.1); + let tx_details: TransactionDetails = json::from_str(&request.1).unwrap(); + + assert_eq!(tx_details.fee_details["gas_limit"], 150000); +} + #[test] fn test_tendermint_ibc_withdraw() { const IBC_SOURCE_CHANNEL: &str = "channel-81"; diff --git a/mm2src/mm2_metrics/Cargo.toml b/mm2src/mm2_metrics/Cargo.toml index 0aa55edcba..75735228b5 100644 --- a/mm2src/mm2_metrics/Cargo.toml +++ b/mm2src/mm2_metrics/Cargo.toml @@ -12,16 +12,16 @@ common = { path = "../common" } derive_more = "0.99" futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } itertools = "0.10" -metrics = { version = "0.19" } -metrics-util = { version = "0.13" } +metrics = { version = "0.21" } +metrics-util = { version = "0.15" } mm2_err_handle = { path = "../mm2_err_handle" } serde = "1" serde_derive = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } +hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features hyper-rustls = { version = "0.23", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -metrics-exporter-prometheus = "0.10.0" +metrics-exporter-prometheus = "0.12.1" diff --git a/mm2src/mm2_metrics/src/recorder.rs b/mm2src/mm2_metrics/src/recorder.rs index a19ca982b8..45d55a67ac 100644 --- a/mm2src/mm2_metrics/src/recorder.rs +++ b/mm2src/mm2_metrics/src/recorder.rs @@ -161,15 +161,15 @@ fn key_value_to_snapshot_entry(metrics: &mut HashMap, _description: &'static str) { + fn describe_counter(&self, _key: KeyName, _unit: Option, _description: metrics::SharedString) { // mm2_metrics doesn't use this method } - fn describe_gauge(&self, _key_name: KeyName, _unit: Option, _description: &'static str) { + fn describe_gauge(&self, _key: KeyName, _unit: Option, _description: metrics::SharedString) { // mm2_metrics doesn't use this method } - fn describe_histogram(&self, _key_name: KeyName, _unit: Option, _description: &'static str) { + fn describe_histogram(&self, _key: KeyName, _unit: Option, _description: metrics::SharedString) { // mm2_metrics doesn't use this method } diff --git a/mm2src/mm2_net/Cargo.toml b/mm2src/mm2_net/Cargo.toml index 0d7ec59456..e567546f6c 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -32,5 +32,9 @@ web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomExcepti js-sys = "0.3.27" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } +futures-util = { version = "0.3" } +hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } gstuff = { version = "0.7", features = ["nightly"] } +rustls = { version = "0.20", default-features = false } +tokio = { version = "1.20" } +tokio-rustls = { version = "0.23", default-features = false } diff --git a/mm2src/mm2_net/src/lib.rs b/mm2src/mm2_net/src/lib.rs index 30a951a0fb..99935bd25b 100644 --- a/mm2src/mm2_net/src/lib.rs +++ b/mm2src/mm2_net/src/lib.rs @@ -3,5 +3,6 @@ pub mod transport; #[cfg(not(target_arch = "wasm32"))] pub mod ip_addr; #[cfg(not(target_arch = "wasm32"))] pub mod native_http; +#[cfg(not(target_arch = "wasm32"))] pub mod native_tls; #[cfg(target_arch = "wasm32")] pub mod wasm_http; #[cfg(target_arch = "wasm32")] pub mod wasm_ws; diff --git a/mm2src/mm2_net/src/native_tls/README.md b/mm2src/mm2_net/src/native_tls/README.md new file mode 100644 index 0000000000..b717a85f37 --- /dev/null +++ b/mm2src/mm2_net/src/native_tls/README.md @@ -0,0 +1,13 @@ +# HTTPS Support with TLSAcceptor and Builder + +This mod provides HTTPS support for [hyper](https://github.com/hyperium/hyper) using [rustls](https://github.com/rustls/rustls). The code in this mod is a port of the [acceptor](https://github.com/rustls/hyper-rustls/tree/286e1fa57ff5cac99994fab355f91c3454d6d83d/src/acceptor) module and the [acceptor.rs](https://github.com/rustls/hyper-rustls/blob/286e1fa57ff5cac99994fab355f91c3454d6d83d/src/acceptor.rs) file from the [hyper-rustls](https://github.com/rustls/hyper-rustls) repository at revision [286e1fa57ff5cac99994fab355f91c3454d6d83d](https://github.com/rustls/hyper-rustls/tree/286e1fa57ff5cac99994fab355f91c3454d6d83d). +> **Note:** Please be aware that the acceptor module was not available in the latest version of [hyper-rustls](https://docs.rs/hyper-rustls/0.24.0/hyper_rustls/index.html) at the time of writing this, the latest version was 0.24.0 at this time. + +## Compatibility + +The ported mod is compatible with hyper 0.14 and rustls 0.20. + +## Purpose + +The purpose of porting these files is to enable retrieving the remote address from the incoming connection and to expose the `TlsStream` type. +> **Note:** The following commit [fe6cd24](https://github.com/KomodoPlatform/komodo-defi-framework/commit/fe6cd24c760e4aad760201723b7c3b846309254d) show the changes applied to the ported code. \ No newline at end of file diff --git a/mm2src/mm2_net/src/native_tls/acceptor.rs b/mm2src/mm2_net/src/native_tls/acceptor.rs new file mode 100644 index 0000000000..cd5c86db84 --- /dev/null +++ b/mm2src/mm2_net/src/native_tls/acceptor.rs @@ -0,0 +1,123 @@ +use crate::native_tls::builder::{AcceptorBuilder, WantsTlsConfig}; +use core::task::{Context, Poll}; +use futures_util::ready; +use hyper::server::{accept::Accept, + conn::{AddrIncoming, AddrStream}}; +use rustls::ServerConfig; +use std::future::Future; +use std::io; +use std::net::SocketAddr; +use std::pin::Pin; +use std::sync::Arc; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +enum State { + Handshaking(tokio_rustls::Accept), + Streaming(tokio_rustls::server::TlsStream), +} + +// tokio_rustls::server::TlsStream doesn't expose constructor methods, +// so we have to TlsAcceptor::accept and handshake to have access to it +// TlsStream implements AsyncRead/AsyncWrite by handshaking with tokio_rustls::Accept first +pub struct TlsStream { + state: State, + remote_addr: SocketAddr, +} + +impl TlsStream { + fn new(stream: AddrStream, config: Arc) -> TlsStream { + let remote_addr = stream.remote_addr(); + let accept = tokio_rustls::TlsAcceptor::from(config).accept(stream); + TlsStream { + state: State::Handshaking(accept), + remote_addr, + } + } + + #[inline] + pub fn remote_addr(&self) -> SocketAddr { self.remote_addr } +} + +impl AsyncRead for TlsStream { + fn poll_read(self: Pin<&mut Self>, cx: &mut Context, buf: &mut ReadBuf) -> Poll> { + let pin = self.get_mut(); + match pin.state { + State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { + Ok(mut stream) => { + let result = Pin::new(&mut stream).poll_read(cx, buf); + pin.state = State::Streaming(stream); + result + }, + Err(err) => Poll::Ready(Err(err)), + }, + State::Streaming(ref mut stream) => Pin::new(stream).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for TlsStream { + fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + let pin = self.get_mut(); + match pin.state { + State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { + Ok(mut stream) => { + let result = Pin::new(&mut stream).poll_write(cx, buf); + pin.state = State::Streaming(stream); + result + }, + Err(err) => Poll::Ready(Err(err)), + }, + State::Streaming(ref mut stream) => Pin::new(stream).poll_write(cx, buf), + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.state { + State::Handshaking(_) => Poll::Ready(Ok(())), + State::Streaming(ref mut stream) => Pin::new(stream).poll_flush(cx), + } + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.state { + State::Handshaking(_) => Poll::Ready(Ok(())), + State::Streaming(ref mut stream) => Pin::new(stream).poll_shutdown(cx), + } + } +} + +/// A TLS acceptor that can be used with hyper servers. +pub struct TlsAcceptor { + pub(crate) config: Arc, + pub(crate) incoming: AddrIncoming, +} + +/// An Acceptor for the `https` scheme. +impl TlsAcceptor { + /// Provides a builder for a `TlsAcceptor`. + pub fn builder() -> AcceptorBuilder { AcceptorBuilder::new() } + /// Creates a new `TlsAcceptor` from a `ServerConfig` and an `AddrIncoming`. + pub fn new(config: Arc, incoming: AddrIncoming) -> TlsAcceptor { TlsAcceptor { config, incoming } } +} + +impl From<(C, I)> for TlsAcceptor +where + C: Into>, + I: Into, +{ + fn from((config, incoming): (C, I)) -> TlsAcceptor { TlsAcceptor::new(config.into(), incoming.into()) } +} + +impl Accept for TlsAcceptor { + type Conn = TlsStream; + type Error = io::Error; + + fn poll_accept(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>> { + let pin = self.get_mut(); + match ready!(Pin::new(&mut pin.incoming).poll_accept(cx)) { + Some(Ok(sock)) => Poll::Ready(Some(Ok(TlsStream::new(sock, pin.config.clone())))), + Some(Err(e)) => Poll::Ready(Some(Err(e))), + None => Poll::Ready(None), + } + } +} diff --git a/mm2src/mm2_net/src/native_tls/builder.rs b/mm2src/mm2_net/src/native_tls/builder.rs new file mode 100644 index 0000000000..415a998b45 --- /dev/null +++ b/mm2src/mm2_net/src/native_tls/builder.rs @@ -0,0 +1,92 @@ +use crate::native_tls::TlsAcceptor; +use hyper::server::conn::AddrIncoming; +use rustls::ServerConfig; +use std::sync::Arc; + +/// Builder for [`TlsAcceptor`] +pub struct AcceptorBuilder(State); + +/// State of a builder that needs a TLS client config next +pub struct WantsTlsConfig(()); + +impl AcceptorBuilder { + #[inline] + /// Creates a new [`AcceptorBuilder`] + pub fn new() -> Self { Self(WantsTlsConfig(())) } + + #[inline] + /// Passes a rustls [`ServerConfig`] to configure the TLS connection + pub fn with_tls_config(self, config: ServerConfig) -> AcceptorBuilder { + AcceptorBuilder(WantsAlpn(config)) + } + + /// Use rustls [defaults][with_safe_defaults] without [client authentication][with_no_client_auth] + /// + /// [with_safe_defaults]: rustls::ConfigBuilder::with_safe_defaults + /// [with_no_client_auth]: rustls::ConfigBuilder::with_no_client_auth + pub fn with_single_cert( + self, + cert_chain: Vec, + key_der: rustls::PrivateKey, + ) -> Result, rustls::Error> { + Ok(AcceptorBuilder(WantsAlpn( + ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, key_der)?, + ))) + } +} + +impl Default for AcceptorBuilder { + fn default() -> Self { Self::new() } +} + +/// State of a builder that needs a incoming address next +pub struct WantsAlpn(ServerConfig); + +impl AcceptorBuilder { + /// Configure ALPN accept protocols in order + pub fn with_alpn_protocols(mut self, alpn_protocols: Vec>) -> AcceptorBuilder { + self.0 .0.alpn_protocols = alpn_protocols; + AcceptorBuilder(WantsIncoming(self.0 .0)) + } + + /// Configure ALPN to accept HTTP/2 + pub fn with_http2_alpn(mut self) -> AcceptorBuilder { + self.0 .0.alpn_protocols = vec![b"h2".to_vec()]; + AcceptorBuilder(WantsIncoming(self.0 .0)) + } + + /// Configure ALPN to accept HTTP/1.0 + pub fn with_http10_alpn(mut self) -> AcceptorBuilder { + self.0 .0.alpn_protocols = vec![b"http/1.0".to_vec()]; + AcceptorBuilder(WantsIncoming(self.0 .0)) + } + + /// Configure ALPN to accept HTTP/1.1 + pub fn with_http11_alpn(mut self) -> AcceptorBuilder { + self.0 .0.alpn_protocols = vec![b"http/1.1".to_vec()]; + AcceptorBuilder(WantsIncoming(self.0 .0)) + } + + /// Configure ALPN to accept HTTP/2, HTTP/1.1, HTTP/1.0 in that order. + pub fn with_all_versions_alpn(mut self) -> AcceptorBuilder { + self.0 .0.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()]; + AcceptorBuilder(WantsIncoming(self.0 .0)) + } +} + +/// State of a builder that needs a incoming address next +pub struct WantsIncoming(ServerConfig); + +impl AcceptorBuilder { + /// Passes a [`AddrIncoming`] to configure the TLS connection and + /// creates the [`TlsAcceptor`] + pub fn with_incoming(self, incoming: impl Into) -> TlsAcceptor { + TlsAcceptor { + config: Arc::new(self.0 .0), + incoming: incoming.into(), + } + } +} diff --git a/mm2src/mm2_net/src/native_tls/mod.rs b/mm2src/mm2_net/src/native_tls/mod.rs new file mode 100644 index 0000000000..46970f19c1 --- /dev/null +++ b/mm2src/mm2_net/src/native_tls/mod.rs @@ -0,0 +1,4 @@ +mod acceptor; +pub use acceptor::{TlsAcceptor, TlsStream}; + +mod builder; diff --git a/mm2src/mm2_number/src/mm_number.rs b/mm2src/mm2_number/src/mm_number.rs index 3823186a39..3934f02391 100644 --- a/mm2src/mm2_number/src/mm_number.rs +++ b/mm2src/mm2_number/src/mm_number.rs @@ -33,11 +33,11 @@ use std::str::FromStr; macro_rules! construct_detailed { ($name: ident, $base_field: ident) => { $crate::paste! { - #[derive(Clone, Debug, Serialize)] + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct $name { - $base_field: $crate::BigDecimal, - [<$base_field _fraction>]: $crate::Fraction, - [<$base_field _rat>]: $crate::BigRational, + pub $base_field: $crate::BigDecimal, + pub [<$base_field _fraction>]: $crate::Fraction, + pub [<$base_field _rat>]: $crate::BigRational, } impl From<$crate::MmNumber> for $name { diff --git a/mm2src/mm2_rpc/Cargo.toml b/mm2src/mm2_rpc/Cargo.toml index 5f6fc9d0b1..6f84bb201a 100644 --- a/mm2src/mm2_rpc/Cargo.toml +++ b/mm2src/mm2_rpc/Cargo.toml @@ -8,17 +8,19 @@ doctest = false [dependencies] common = { path = "../common" } -mm2_err_handle = { path = "../mm2_err_handle" } derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -http = "0.2" +futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"], optional = true } +gstuff = { version = "0.7", features = ["nightly"], optional = true} +http = {version = "0.2", optional = true} +mm2_err_handle = { path = "../mm2_err_handle", optional = true } +mm2_number = { path = "../mm2_number" } +rpc = { path = "../mm2_bitcoin/rpc" } serde = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -ser_error = { path = "../derives/ser_error" } -ser_error_derive = { path = "../derives/ser_error_derive" } +ser_error = { path = "../derives/ser_error", optional = true} +ser_error_derive = { path = "../derives/ser_error_derive", optional=true } +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } -[target.'cfg(target_arch = "wasm32")'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } +[features] +default = [] +rpc_facilities = ["futures", "gstuff", "http", "mm2_err_handle", "ser_error", "ser_error_derive"] diff --git a/mm2src/mm2_rpc/src/data/legacy.rs b/mm2src/mm2_rpc/src/data/legacy.rs new file mode 100644 index 0000000000..562f8c37a7 --- /dev/null +++ b/mm2src/mm2_rpc/src/data/legacy.rs @@ -0,0 +1,257 @@ +use common::serde_derive::{Deserialize, Serialize}; +use derive_more::Display; +use mm2_number::{construct_detailed, BigDecimal, BigRational, Fraction, MmNumber}; +use rpc::v1::types::H256 as H256Json; +use std::collections::HashSet; +use std::ops::Deref; +use uuid::Uuid; + +#[derive(Serialize, Deserialize)] +pub struct Mm2RpcResult { + pub result: T, +} + +impl Mm2RpcResult { + pub fn new(result: T) -> Self { Self { result } } +} + +impl Deref for Mm2RpcResult { + type Target = T; + fn deref(&self) -> &Self::Target { &self.result } +} + +#[derive(Serialize, Deserialize)] +pub struct BalanceResponse { + pub coin: String, + pub balance: BigDecimal, + pub unspendable_balance: BigDecimal, + pub address: String, +} + +#[derive(Serialize, Deserialize)] +pub struct OrderbookRequest { + pub base: String, + pub rel: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OrderbookResponse { + #[serde(rename = "askdepth")] + pub ask_depth: u32, + pub asks: Vec, + pub base: String, + #[serde(rename = "biddepth")] + pub bid_depth: u32, + pub bids: Vec, + pub netid: u16, + #[serde(rename = "numasks")] + pub num_asks: usize, + #[serde(rename = "numbids")] + pub num_bids: usize, + pub rel: String, + pub timestamp: u64, + #[serde(flatten)] + pub total_asks_base: TotalAsksBaseVol, + #[serde(flatten)] + pub total_asks_rel: TotalAsksRelVol, + #[serde(flatten)] + pub total_bids_base: TotalBidsBaseVol, + #[serde(flatten)] + pub total_bids_rel: TotalBidsRelVol, +} + +construct_detailed!(TotalAsksBaseVol, total_asks_base_vol); +construct_detailed!(TotalAsksRelVol, total_asks_rel_vol); +construct_detailed!(TotalBidsBaseVol, total_bids_base_vol); +construct_detailed!(TotalBidsRelVol, total_bids_rel_vol); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RpcOrderbookEntry { + pub coin: String, + pub address: String, + pub price: BigDecimal, + pub price_rat: BigRational, + pub price_fraction: Fraction, + #[serde(rename = "maxvolume")] + pub max_volume: BigDecimal, + pub max_volume_rat: BigRational, + pub max_volume_fraction: Fraction, + pub min_volume: BigDecimal, + pub min_volume_rat: BigRational, + pub min_volume_fraction: Fraction, + pub pubkey: String, + pub age: u64, + pub uuid: Uuid, + pub is_mine: bool, + #[serde(flatten)] + pub base_max_volume: DetailedBaseMaxVolume, + #[serde(flatten)] + pub base_min_volume: DetailedBaseMinVolume, + #[serde(flatten)] + pub rel_max_volume: DetailedRelMaxVolume, + #[serde(flatten)] + pub rel_min_volume: DetailedRelMinVolume, + #[serde(flatten)] + pub conf_settings: Option, +} + +construct_detailed!(DetailedBaseMaxVolume, base_max_volume); +construct_detailed!(DetailedBaseMinVolume, base_min_volume); +construct_detailed!(DetailedRelMaxVolume, rel_max_volume); +construct_detailed!(DetailedRelMinVolume, rel_min_volume); + +#[derive(Debug, Serialize, Deserialize)] +pub struct AggregatedOrderbookEntry { + #[serde(flatten)] + pub entry: RpcOrderbookEntry, + #[serde(flatten)] + pub base_max_volume_aggr: AggregatedBaseVol, + #[serde(flatten)] + pub rel_max_volume_aggr: AggregatedRelVol, +} + +construct_detailed!(AggregatedBaseVol, base_max_volume_aggr); +construct_detailed!(AggregatedRelVol, rel_max_volume_aggr); + +#[derive(Deserialize, Serialize, Debug)] +pub struct SellBuyRequest { + pub base: String, + pub rel: String, + pub price: MmNumber, + pub volume: MmNumber, + pub timeout: Option, + /// Not used. Deprecated. + #[allow(dead_code)] + pub duration: Option, + pub method: String, + #[allow(dead_code)] + pub gui: Option, + #[serde(rename = "destpubkey")] + #[serde(default)] + #[allow(dead_code)] + pub dest_pub_key: H256Json, + #[serde(default)] + pub match_by: MatchBy, + #[serde(default)] + pub order_type: OrderType, + pub base_confs: Option, + pub base_nota: Option, + pub rel_confs: Option, + pub rel_nota: Option, + pub min_volume: Option, + #[serde(default = "get_true")] + pub save_in_history: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct SellBuyResponse { + #[serde(flatten)] + pub request: TakerRequestForRpc, + pub order_type: OrderType, + #[serde(flatten)] + pub min_volume: DetailedMinVolume, + pub base_orderbook_ticker: Option, + pub rel_orderbook_ticker: Option, +} + +construct_detailed!(DetailedMinVolume, min_volume); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TakerRequestForRpc { + pub base: String, + pub rel: String, + pub base_amount: BigDecimal, + pub base_amount_rat: BigRational, + pub rel_amount: BigDecimal, + pub rel_amount_rat: BigRational, + pub action: TakerAction, + pub uuid: Uuid, + pub method: String, + pub sender_pubkey: H256Json, + pub dest_pub_key: H256Json, + pub match_by: MatchBy, + pub conf_settings: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum TakerAction { + Buy, + Sell, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub enum OrderType { + FillOrKill, + GoodTillCancelled, +} + +impl Default for OrderType { + fn default() -> Self { OrderType::GoodTillCancelled } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub enum MatchBy { + Any, + Orders(HashSet), + Pubkeys(HashSet), +} + +impl Default for MatchBy { + fn default() -> Self { MatchBy::Any } +} + +#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct OrderConfirmationsSettings { + pub base_confs: u64, + pub base_nota: bool, + pub rel_confs: u64, + pub rel_nota: bool, +} + +impl OrderConfirmationsSettings { + pub fn reversed(&self) -> OrderConfirmationsSettings { + OrderConfirmationsSettings { + base_confs: self.rel_confs, + base_nota: self.rel_nota, + rel_confs: self.base_confs, + rel_nota: self.base_nota, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CoinInitResponse { + pub result: String, + pub address: String, + pub balance: BigDecimal, + pub unspendable_balance: BigDecimal, + pub coin: String, + pub required_confirmations: u64, + pub requires_notarization: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub mature_confirmations: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct EnabledCoin { + pub ticker: String, + pub address: String, +} + +pub type GetEnabledResponse = Vec; + +#[derive(Serialize, Deserialize, Display)] +#[serde(rename_all = "lowercase")] +pub enum Status { + Success, +} + +#[derive(Serialize, Deserialize)] +pub struct MmVersionResponse { + pub result: String, + pub datetime: String, +} + +fn get_true() -> bool { true } diff --git a/mm2src/mm2_rpc/src/data/mod.rs b/mm2src/mm2_rpc/src/data/mod.rs new file mode 100644 index 0000000000..d0b036677b --- /dev/null +++ b/mm2src/mm2_rpc/src/data/mod.rs @@ -0,0 +1 @@ +pub mod legacy; diff --git a/mm2src/mm2_rpc/src/lib.rs b/mm2src/mm2_rpc/src/lib.rs index 4d9a07b918..40ba522fc1 100644 --- a/mm2src/mm2_rpc/src/lib.rs +++ b/mm2src/mm2_rpc/src/lib.rs @@ -1,2 +1,4 @@ -pub mod mm_protocol; -#[cfg(target_arch = "wasm32")] pub mod wasm_rpc; +pub mod data; +#[cfg(feature = "rpc_facilities")] pub mod mm_protocol; +#[cfg(all(feature = "rpc_facilities", target_arch = "wasm32"))] +pub mod wasm_rpc; diff --git a/mm2src/mm2_test_helpers/Cargo.toml b/mm2src/mm2_test_helpers/Cargo.toml index 286c9cc3bf..d6a7332de9 100644 --- a/mm2src/mm2_test_helpers/Cargo.toml +++ b/mm2src/mm2_test_helpers/Cargo.toml @@ -20,6 +20,7 @@ mm2_io = { path = "../mm2_io" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } +mm2_rpc = { path = "../mm2_rpc" } rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } regex = "1" rpc = { path = "../mm2_bitcoin/rpc" } diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index d0749cc919..b83211556a 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -14,6 +14,7 @@ use lazy_static::lazy_static; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_metrics::{MetricType, MetricsJson}; use mm2_number::BigDecimal; +use mm2_rpc::data::legacy::BalanceResponse; use rand::Rng; use serde::{Deserialize, Serialize}; use serde_json::{self as json, json, Value as Json}; @@ -92,6 +93,21 @@ pub const TAKER_SUCCESS_EVENTS: [&str; 11] = [ "Finished", ]; +pub const TAKER_USING_WATCHERS_SUCCESS_EVENTS: [&str; 12] = [ + "Started", + "Negotiated", + "TakerFeeSent", + "TakerPaymentInstructionsReceived", + "MakerPaymentReceived", + "MakerPaymentWaitConfirmStarted", + "MakerPaymentValidatedAndConfirmed", + "TakerPaymentSent", + "WatcherMessageSent", + "TakerPaymentSpent", + "MakerPaymentSpent", + "Finished", +]; + pub const TAKER_ERROR_EVENTS: [&str; 15] = [ "StartFailed", "NegotiateFailed", @@ -175,21 +191,6 @@ impl Mm2TestConf { } } - pub fn seednode_using_watchers(passphrase: &str, coins: &Json) -> Self { - Mm2TestConf { - conf: json!({ - "gui": "nogui", - "netid": 9998, - "passphrase": passphrase, - "coins": coins, - "rpc_password": DEFAULT_RPC_PASSWORD, - "i_am_seed": true, - "use_watchers": true, - }), - rpc_password: DEFAULT_RPC_PASSWORD.into(), - } - } - pub fn seednode_with_hd_account(passphrase: &str, hd_account_id: u32, coins: &Json) -> Self { Mm2TestConf { conf: json!({ @@ -219,21 +220,6 @@ impl Mm2TestConf { } } - pub fn light_node_using_watchers(passphrase: &str, coins: &Json, seednodes: &[&str]) -> Self { - Mm2TestConf { - conf: json!({ - "gui": "nogui", - "netid": 9998, - "passphrase": passphrase, - "coins": coins, - "rpc_password": DEFAULT_RPC_PASSWORD, - "seednodes": seednodes, - "use_watchers": true - }), - rpc_password: DEFAULT_RPC_PASSWORD.into(), - } - } - pub fn watcher_light_node(passphrase: &str, coins: &Json, seednodes: &[&str], conf: WatcherConf) -> Self { Mm2TestConf { conf: json!({ @@ -1618,7 +1604,6 @@ pub async fn enable_eth_coin( "method": "enable", "coin": coin, "urls": urls, - "chain_id": 5, "swap_contract_address": swap_contract_address, "fallback_swap_contract": fallback_swap_contract, "mm2": 1, @@ -2000,17 +1985,22 @@ pub async fn wait_for_swap_negotiation_failure(mm: &MarketMakerIt, swap: &str, u } /// Helper function requesting my swap status and checking it's events -pub async fn check_my_swap_status( - mm: &MarketMakerIt, - uuid: &str, - expected_success_events: &[&str], - expected_error_events: &[&str], - maker_amount: BigDecimal, - taker_amount: BigDecimal, -) { +pub async fn check_my_swap_status(mm: &MarketMakerIt, uuid: &str, maker_amount: BigDecimal, taker_amount: BigDecimal) { let status_response = my_swap_status(mm, uuid).await; + let swap_type = status_response["result"]["type"].as_str().unwrap(); + let success_events: Vec = json::from_value(status_response["result"]["success_events"].clone()).unwrap(); - assert_eq!(expected_success_events, success_events.as_slice()); + if swap_type == "Taker" { + assert!(success_events == TAKER_SUCCESS_EVENTS || success_events == TAKER_USING_WATCHERS_SUCCESS_EVENTS); + } else { + assert_eq!(success_events, MAKER_SUCCESS_EVENTS) + } + + let expected_error_events = if swap_type == "Taker" { + TAKER_ERROR_EVENTS.to_vec() + } else { + MAKER_ERROR_EVENTS.to_vec() + }; let error_events: Vec = json::from_value(status_response["result"]["error_events"].clone()).unwrap(); assert_eq!(expected_error_events, error_events.as_slice()); @@ -2021,7 +2011,7 @@ pub async fn check_my_swap_status( assert_eq!(taker_amount, actual_taker_amount); 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_success_events, actual_events.as_slice()); + assert_eq!(success_events, actual_events.as_slice()); } pub async fn check_my_swap_status_amounts( @@ -2039,12 +2029,7 @@ pub async fn check_my_swap_status_amounts( assert_eq!(taker_amount, actual_taker_amount); } -pub async fn check_stats_swap_status( - mm: &MarketMakerIt, - uuid: &str, - maker_expected_events: &[&str], - taker_expected_events: &[&str], -) { +pub async fn check_stats_swap_status(mm: &MarketMakerIt, uuid: &str) { let response = mm .rpc(&json!({ "method": "stats_swap_status", @@ -2066,8 +2051,12 @@ pub async fn check_stats_swap_status( .iter() .map(|item| item["event"]["type"].as_str().unwrap()); let taker_actual_events: Vec<&str> = taker_actual_events.collect(); - assert_eq!(maker_expected_events, maker_actual_events.as_slice()); - assert_eq!(taker_expected_events, taker_actual_events.as_slice()); + + assert_eq!(maker_actual_events.as_slice(), MAKER_SUCCESS_EVENTS); + assert!( + taker_actual_events.as_slice() == TAKER_SUCCESS_EVENTS + || taker_actual_events.as_slice() == TAKER_USING_WATCHERS_SUCCESS_EVENTS + ); } pub async fn check_recent_swaps(mm: &MarketMakerIt, expected_len: usize) { @@ -2147,7 +2136,12 @@ pub async fn orderbook_v2(mm: &MarketMakerIt, base: &str, rel: &str) -> Json { json::from_str(&request.1).unwrap() } -pub async fn best_orders_v2(mm: &MarketMakerIt, coin: &str, action: &str, volume: &str) -> Json { +pub async fn best_orders_v2( + mm: &MarketMakerIt, + coin: &str, + action: &str, + volume: &str, +) -> RpcV2Response { let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -2168,7 +2162,13 @@ pub async fn best_orders_v2(mm: &MarketMakerIt, coin: &str, action: &str, volume json::from_str(&request.1).unwrap() } -pub async fn best_orders_v2_by_number(mm: &MarketMakerIt, coin: &str, action: &str, number: usize) -> Json { +pub async fn best_orders_v2_by_number( + mm: &MarketMakerIt, + coin: &str, + action: &str, + number: usize, + exclude_mine: bool, +) -> RpcV2Response { let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -2180,7 +2180,8 @@ pub async fn best_orders_v2_by_number(mm: &MarketMakerIt, coin: &str, action: &s "request_by": { "type": "number", "value": number, - } + }, + "exclude_mine": exclude_mine } })) .await @@ -2410,7 +2411,7 @@ pub async fn send_raw_transaction(mm: &MarketMakerIt, coin: &str, tx: &str) -> J json::from_str(&request.1).unwrap() } -pub async fn my_balance(mm: &MarketMakerIt, coin: &str) -> MyBalanceResponse { +pub async fn my_balance(mm: &MarketMakerIt, coin: &str) -> BalanceResponse { let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -2805,8 +2806,6 @@ pub async fn wait_for_swaps_finish_and_check_status( check_my_swap_status( taker, uuid.as_ref(), - &TAKER_SUCCESS_EVENTS, - &TAKER_ERROR_EVENTS, BigDecimal::try_from(volume).unwrap(), BigDecimal::try_from(volume * maker_price).unwrap(), ) @@ -2816,8 +2815,6 @@ pub async fn wait_for_swaps_finish_and_check_status( check_my_swap_status( maker, uuid.as_ref(), - &MAKER_SUCCESS_EVENTS, - &MAKER_ERROR_EVENTS, BigDecimal::try_from(volume).unwrap(), BigDecimal::try_from(volume * maker_price).unwrap(), ) diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index 7cfd148174..81fe48e5b1 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -6,6 +6,7 @@ use http::{HeaderMap, StatusCode}; use mm2_number::{BigDecimal, BigRational, Fraction, MmNumber}; +use mm2_rpc::data::legacy::{MatchBy, OrderConfirmationsSettings, OrderType, RpcOrderbookEntry, TakerAction}; use rpc::v1::types::H256 as H256Json; use serde::de::DeserializeOwned; use serde_json::Value as Json; @@ -77,23 +78,6 @@ impl RpcResponse { } } -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(tag = "type", content = "data")] -pub enum OrderType { - FillOrKill, - GoodTillCancelled, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OrderConfirmationsSettings { - pub base_confs: u64, - pub base_nota: bool, - pub rel_confs: u64, - pub rel_nota: bool, -} - #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct HistoricalOrder { @@ -104,22 +88,6 @@ struct HistoricalOrder { conf_settings: Option, } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub enum TakerAction { - Buy, - Sell, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -#[serde(tag = "type", content = "data")] -pub enum MatchBy { - Any, - Orders(HashSet), - Pubkeys(HashSet), -} - #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct BuyOrSellRpcRes { @@ -300,126 +268,13 @@ pub struct MyOrdersRpcResult { pub result: MyOrdersRpc, } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OrderbookEntry { - pub coin: String, - pub address: String, - pub price: BigDecimal, - pub price_rat: BigRational, - pub price_fraction: Fraction, - #[serde(rename = "maxvolume")] - pub max_volume: BigDecimal, - pub max_volume_rat: BigRational, - pub max_volume_fraction: Fraction, - pub base_max_volume: BigDecimal, - pub base_max_volume_rat: BigRational, - pub base_max_volume_fraction: Fraction, - pub base_min_volume: BigDecimal, - pub base_min_volume_rat: BigRational, - pub base_min_volume_fraction: Fraction, - pub rel_max_volume: BigDecimal, - pub rel_max_volume_rat: BigRational, - pub rel_max_volume_fraction: Fraction, - pub rel_min_volume: BigDecimal, - pub rel_min_volume_rat: BigRational, - pub rel_min_volume_fraction: Fraction, - pub min_volume: BigDecimal, - pub min_volume_rat: BigRational, - pub min_volume_fraction: Fraction, - pub pubkey: String, - pub age: i64, - pub zcredits: u64, - pub uuid: Uuid, - pub is_mine: bool, - pub base_confs: u64, - pub base_nota: bool, - pub rel_confs: u64, - pub rel_nota: bool, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OrderbookEntryAggregate { - pub coin: String, - pub address: String, - pub price: BigDecimal, - pub price_rat: BigRational, - pub price_fraction: Fraction, - #[serde(rename = "maxvolume")] - pub max_volume: BigDecimal, - pub max_volume_rat: BigRational, - pub max_volume_fraction: Fraction, - pub base_max_volume: BigDecimal, - pub base_max_volume_rat: BigRational, - pub base_max_volume_fraction: Fraction, - pub base_min_volume: BigDecimal, - pub base_min_volume_rat: BigRational, - pub base_min_volume_fraction: Fraction, - pub rel_max_volume: BigDecimal, - pub rel_max_volume_rat: BigRational, - pub rel_max_volume_fraction: Fraction, - pub rel_min_volume: BigDecimal, - pub rel_min_volume_rat: BigRational, - pub rel_min_volume_fraction: Fraction, - pub min_volume: BigDecimal, - pub min_volume_rat: BigRational, - pub min_volume_fraction: Fraction, - pub pubkey: String, - pub age: i64, - pub zcredits: u64, - pub uuid: Uuid, - pub is_mine: bool, - pub base_max_volume_aggr: BigDecimal, - pub base_max_volume_aggr_rat: BigRational, - pub base_max_volume_aggr_fraction: Fraction, - pub rel_max_volume_aggr: BigDecimal, - pub rel_max_volume_aggr_rat: BigRational, - pub rel_max_volume_aggr_fraction: Fraction, - pub base_confs: u64, - pub base_nota: bool, - pub rel_confs: u64, - pub rel_nota: bool, -} - #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct BestOrdersResponse { - pub result: HashMap>, + pub result: HashMap>, pub original_tickers: HashMap>, } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OrderbookResponse { - pub base: String, - pub rel: String, - #[serde(rename = "askdepth")] - pub ask_depth: usize, - #[serde(rename = "biddepth")] - pub bid_depth: usize, - #[serde(rename = "numasks")] - num_asks: usize, - #[serde(rename = "numbids")] - num_bids: usize, - pub netid: u16, - timestamp: u64, - pub total_asks_base_vol: BigDecimal, - pub total_asks_base_vol_rat: BigRational, - pub total_asks_base_vol_fraction: Fraction, - pub total_asks_rel_vol: BigDecimal, - pub total_asks_rel_vol_rat: BigRational, - pub total_asks_rel_vol_fraction: Fraction, - pub total_bids_base_vol: BigDecimal, - pub total_bids_base_vol_rat: BigRational, - pub total_bids_base_vol_fraction: Fraction, - pub total_bids_rel_vol: BigDecimal, - pub total_bids_rel_vol_rat: BigRational, - pub total_bids_rel_vol_fraction: Fraction, - pub asks: Vec, - pub bids: Vec, -} - #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct PairDepth { @@ -440,19 +295,6 @@ pub struct OrderbookDepthResponse { pub result: Vec, } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EnableElectrumResponse { - pub coin: String, - pub address: String, - pub balance: BigDecimal, - pub unspendable_balance: BigDecimal, - pub required_confirmations: u64, - pub mature_confirmations: Option, - pub requires_notarization: bool, - pub result: String, -} - #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(deny_unknown_fields)] pub struct TradeFeeForTest { @@ -643,15 +485,6 @@ pub struct TransactionDetails { pub memo: Option, } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct MyBalanceResponse { - pub address: String, - pub balance: BigDecimal, - pub unspendable_balance: BigDecimal, - pub coin: String, -} - #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct IguanaWalletBalance { @@ -996,13 +829,6 @@ pub struct UtxoFeeDetails { pub amount: BigDecimal, } -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct MmVersion { - pub result: String, - pub datetime: String, -} - #[derive(Debug, Deserialize, Eq, PartialEq)] #[serde(deny_unknown_fields, tag = "address_type", content = "address_data")] pub enum OrderbookAddress { @@ -1074,7 +900,7 @@ pub struct OrderbookV2Response { pub total_bids_rel_vol: MmNumberMultiRepr, } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] #[serde(deny_unknown_fields)] pub struct BestOrdersV2Response { pub orders: HashMap>, diff --git a/scripts/ci/validate-version-bump.py b/scripts/ci/validate-version-bump.py new file mode 100755 index 0000000000..ecb12bfa75 --- /dev/null +++ b/scripts/ci/validate-version-bump.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import subprocess +import sys +import toml + + +def get_previous_version(): + subprocess.run(['git', 'fetch', 'origin', 'main']) + previous_version = subprocess.check_output( + ['git', 'show', 'origin/main:./mm2src/mm2_bin_lib/Cargo.toml']).decode() + cargo_data = toml.loads(previous_version) + previous_version = cargo_data['package']['version'] + + return previous_version + + +def get_current_version(): + with open('./mm2src/mm2_bin_lib/Cargo.toml', 'r') as file: + cargo_toml_pr = file.read() + + cargo_data_pr = toml.loads(cargo_toml_pr) + + return cargo_data_pr['package']['version'] + + +current_version = get_current_version() +previous_version = get_previous_version() + +print(f"Main branch mm2 version: {previous_version}") +print(f"Current branch mm2 version: {current_version}") + +if previous_version == current_version: + print("Bump the mm2 version before merge!") + sys.exit(1)