diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 8156a6934b..467dc4ebc4 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,6 @@ FROM docker.io/debian:10 -MAINTAINER Onur Özkan +LABEL authors="Onur Özkan " RUN apt-get update -y @@ -11,6 +11,7 @@ RUN apt-get install -y \ curl \ wget \ unzip \ + libudev-dev \ gnupg RUN ln -s /usr/bin/python3 /bin/python @@ -49,8 +50,9 @@ RUN apt-get install -y \ docker-buildx-plugin RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal --default-toolchain nightly-2023-06-01 -y - -RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-linux-x86_64.zip -RUN unzip protoc-3.20.1-linux-x86_64.zip && mv ./include/google /usr/include/google - ENV PATH="/root/.cargo/bin:$PATH" +# TODO: Lock wasm-pack version +RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y +RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-linux-x86_64.zip +RUN unzip protoc-25.3-linux-x86_64.zip && mv ./include/google /usr/include/google + diff --git a/.dockerignore b/.dockerignore index 778ad13875..208af068ce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,5 +24,4 @@ cmake-build-debug /wasm-build.log # Opt out from history in order to speed the `COPY .` up. -# Note that we should create and/or update the MM_VERSION file when using `COPY .` to build a custom version. /.git diff --git a/.github/actions/build-cache/action.yml b/.github/actions/build-cache/action.yml new file mode 100644 index 0000000000..c5af862e1b --- /dev/null +++ b/.github/actions/build-cache/action.yml @@ -0,0 +1,10 @@ +name: 'Set up build cache' +description: 'Sets up caching for KDF builds' +runs: + using: 'composite' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up rust-cache + uses: Swatinem/rust-cache@v2 diff --git a/.github/actions/cargo-cache/action.yml b/.github/actions/cargo-cache/action.yml deleted file mode 100644 index 89069e7797..0000000000 --- a/.github/actions/cargo-cache/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -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/dev-build.yml b/.github/workflows/dev-build.yml index a4fce83cbe..585f9d4856 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -10,7 +10,6 @@ concurrency: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - MANUAL_MM_VERSION: true JEMALLOC_SYS_WITH_MALLOC_CONF: "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" jobs: @@ -38,41 +37,40 @@ jobs: with: deps: ('protoc') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo build --release + run: cargo build --release - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="mm2_$COMMIT_HASH-linux-x86-64.zip" + NAME="mm2_$KDF_BUILD_TAG-linux-x86-64.zip" zip $NAME target/release/mm2 -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="kdf_$COMMIT_HASH-linux-x86-64.zip" + NAME="kdf_$KDF_BUILD_TAG-linux-x86-64.zip" zip $NAME target/release/kdf -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -81,7 +79,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" - name: Login to dockerhub if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' @@ -90,7 +90,7 @@ jobs: - name: Build and push container image if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' run: | - CONTAINER_TAG="dev-$COMMIT_HASH" + CONTAINER_TAG="dev-$KDF_BUILD_TAG" docker build -t komodoofficial/komodo-defi-framework:"$CONTAINER_TAG" -t komodoofficial/komodo-defi-framework:dev-latest -f .docker/Dockerfile.dev-release . docker push komodoofficial/komodo-defi-framework:"$CONTAINER_TAG" docker push komodoofficial/komodo-defi-framework:dev-latest @@ -111,41 +111,39 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo build --release --target x86_64-apple-darwin + run: cargo build --release --target x86_64-apple-darwin - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="mm2_$COMMIT_HASH-mac-x86-64.zip" + NAME="mm2_$KDF_BUILD_TAG-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/mm2 -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ - + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="kdf_$COMMIT_HASH-mac-x86-64.zip" + NAME="kdf_$KDF_BUILD_TAG-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/kdf -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -154,7 +152,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" mac-arm64: timeout-minutes: 60 @@ -172,41 +172,40 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo build --release --target aarch64-apple-darwin + run: cargo build --release --target aarch64-apple-darwin - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="mm2_$COMMIT_HASH-mac-arm64.zip" + NAME="mm2_$KDF_BUILD_TAG-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/mm2 -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="kdf_$COMMIT_HASH-mac-arm64.zip" + NAME="kdf_$KDF_BUILD_TAG-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/kdf -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -215,7 +214,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" win-x86-64: timeout-minutes: 60 @@ -232,43 +233,40 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $Env:GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $Env:GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - if (test-path "./MM_VERSION") { - remove-item "./MM_VERSION" - } - echo $Env:COMMIT_HASH > ./MM_VERSION - cargo build --release + run: cargo build --release - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - $NAME="mm2_$Env:COMMIT_HASH-win-x86-64.zip" + $NAME="mm2_$Env:KDF_BUILD_TAG-win-x86-64.zip" 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll - mkdir $Env:BRANCH_NAME - mv $NAME ./$Env:BRANCH_NAME/ + $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - $NAME="kdf_$Env:COMMIT_HASH-win-x86-64.zip" + $NAME="kdf_$Env:KDF_BUILD_TAG-win-x86-64.zip" 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll - mv $NAME ./$Env:BRANCH_NAME/ + $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -277,7 +275,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + $SAFE_BRANCH_NAME = $Env:BRANCH_NAME -replace '/', '-' + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" mac-dylib-x86-64: timeout-minutes: 60 @@ -286,8 +286,8 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2023-06-01-x86_64-apple-darwin --no-self-update --profile=minimal - rustup default nightly-2023-06-01-x86_64-apple-darwin + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add x86_64-apple-darwin - name: Install build deps @@ -295,43 +295,42 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib + run: cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="mm2_$COMMIT_HASH-mac-dylib-x86-64.zip" + NAME="mm2_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" cp target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libmm2.a zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="kdf_$COMMIT_HASH-mac-dylib-x86-64.zip" + NAME="kdf_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -340,7 +339,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" wasm: timeout-minutes: 60 @@ -368,34 +369,33 @@ jobs: rustup target add wasm32-unknown-unknown - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y + # TODO: Lock wasm-pack version + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - wasm-pack build --release mm2src/mm2_bin_lib --target web --out-dir ../../target/target-wasm-release + run: wasm-pack build --release mm2src/mm2_bin_lib --target web --out-dir ../../target/target-wasm-release - name: Compress build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="kdf_$COMMIT_HASH-wasm.zip" + NAME="kdf_$KDF_BUILD_TAG-wasm.zip" (cd ./target/target-wasm-release && zip -r - .) > $NAME - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -404,7 +404,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" ios-aarch64: timeout-minutes: 60 @@ -422,43 +424,42 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib + run: cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="mm2_$COMMIT_HASH-ios-aarch64.zip" + NAME="mm2_$KDF_BUILD_TAG-ios-aarch64.zip" cp target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libmm2.a zip $NAME target/aarch64-apple-ios/release/libmm2.a -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="kdf_$COMMIT_HASH-ios-aarch64.zip" + NAME="kdf_$KDF_BUILD_TAG-ios-aarch64.zip" mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a zip $NAME target/aarch64-apple-ios/release/libkdf.a -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -467,7 +468,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" android-aarch64: timeout-minutes: 60 @@ -497,22 +500,19 @@ jobs: - name: Setup NDK run: ./scripts/ci/android-ndk.sh x86 23 - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - export PATH=$PATH:/android-ndk/bin CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib @@ -521,21 +521,23 @@ jobs: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="mm2_$COMMIT_HASH-android-aarch64.zip" + NAME="mm2_$KDF_BUILD_TAG-android-aarch64.zip" cp target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a zip $NAME target/aarch64-linux-android/release/libmm2.a -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="kdf_$COMMIT_HASH-android-aarch64.zip" + NAME="kdf_$KDF_BUILD_TAG-android-aarch64.zip" mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a zip $NAME target/aarch64-linux-android/release/libkdf.a -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -544,7 +546,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" android-armv7: timeout-minutes: 60 @@ -574,22 +578,19 @@ jobs: - name: Setup NDK run: ./scripts/ci/android-ndk.sh x86 23 - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - export PATH=$PATH:/android-ndk/bin CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib @@ -598,21 +599,23 @@ jobs: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="mm2_$COMMIT_HASH-android-armv7.zip" + NAME="mm2_$KDF_BUILD_TAG-android-armv7.zip" cp target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="kdf_$COMMIT_HASH-android-armv7.zip" + NAME="kdf_$KDF_BUILD_TAG-android-armv7.zip" mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -621,7 +624,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" deployment-commitment: if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' @@ -639,13 +644,13 @@ jobs: echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - name: Activate SSH uses: webfactory/ssh-agent@v0.5.4 @@ -657,10 +662,10 @@ jobs: git clone git@github.com:KomodoPlatform/atomicdex-deployments.git if [ -d "atomicdex-deployments/atomicDEX-API" ]; then cd atomicdex-deployments/atomicDEX-API - sed -i "1s/^.*$/$COMMIT_HASH/" .commit + sed -i "1s/^.*$/$KDF_BUILD_TAG/" .commit git config --global user.email "linuxci@komodoplatform.com" git config --global user.name "linuxci" git add .commit - git commit -m "[atomicDEX-API] $COMMIT_HASH is committed for git & container registry" + git commit -m "[atomicDEX-API] $KDF_BUILD_TAG is committed for git & container registry" git push fi diff --git a/.github/workflows/fmt-and-lint.yml b/.github/workflows/fmt-and-lint.yml index f5ea217eee..27e7f0cb85 100644 --- a/.github/workflows/fmt-and-lint.yml +++ b/.github/workflows/fmt-and-lint.yml @@ -18,7 +18,7 @@ jobs: - name: Install toolchain run: | - rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal --component rustfmt clippy + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal --component rustfmt,clippy rustup default nightly-2023-06-01 - name: Install build deps @@ -26,8 +26,8 @@ jobs: with: deps: ('protoc' 'libudev-dev') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: fmt check # Format checks aren't OS dependant. @@ -54,8 +54,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: clippy 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 704c614549..6ce7dedd0a 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -48,7 +48,9 @@ jobs: - name: Check PR labels env: LABEL_NAMES: ${{ toJson(github.event.pull_request.labels.*.name) }} - if: contains(env.LABEL_NAMES, 'under review') == contains(env.LABEL_NAMES, 'in progress') + if: "!((contains(env.LABEL_NAMES, 'pending review') && !contains(env.LABEL_NAMES, 'in progress') && !contains(env.LABEL_NAMES, 'blocked')) + || (!contains(env.LABEL_NAMES, 'pending review') && contains(env.LABEL_NAMES, 'in progress') && !contains(env.LABEL_NAMES, 'blocked')) + || (!contains(env.LABEL_NAMES, 'pending review') && !contains(env.LABEL_NAMES, 'in progress') && contains(env.LABEL_NAMES, 'blocked')))" run: | - echo "PR must have "exactly one" of these labels: ['under review', 'in progress']." + echo "PR must have "exactly one" of these labels: ['status: pending review', 'status: in progress', 'status: blocked']." exit 1 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index a74a589d10..7fec248c88 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -10,7 +10,6 @@ concurrency: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - MANUAL_MM_VERSION: true JEMALLOC_SYS_WITH_MALLOC_CONF: "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" jobs: @@ -38,35 +37,34 @@ jobs: with: deps: ('protoc') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo build --release + run: cargo build --release - name: Compress mm2 build output run: | - NAME="mm2_$COMMIT_HASH-linux-x86-64.zip" + NAME="mm2_$KDF_BUILD_TAG-linux-x86-64.zip" zip $NAME target/release/mm2 -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output run: | - NAME="kdf_$COMMIT_HASH-linux-x86-64.zip" + NAME="kdf_$KDF_BUILD_TAG-linux-x86-64.zip" zip $NAME target/release/kdf -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -75,7 +73,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" - name: Login to dockerhub run: docker login --username ${{ secrets.DOCKER_HUB_USERNAME }} --password ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} docker.io @@ -102,35 +102,34 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo build --release --target x86_64-apple-darwin + run: cargo build --release --target x86_64-apple-darwin - name: Compress mm2 build output run: | - NAME="mm2_$COMMIT_HASH-mac-x86-64.zip" + NAME="mm2_$KDF_BUILD_TAG-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/mm2 -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output run: | - NAME="kdf_$COMMIT_HASH-mac-x86-64.zip" + NAME="kdf_$KDF_BUILD_TAG-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/kdf -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -139,7 +138,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" mac-arm64: timeout-minutes: 60 @@ -157,35 +158,34 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo build --release --target aarch64-apple-darwin + run: cargo build --release --target aarch64-apple-darwin - name: Compress mm2 build output run: | - NAME="mm2_$COMMIT_HASH-mac-arm64.zip" + NAME="mm2_$KDF_BUILD_TAG-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/mm2 -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output run: | - NAME="kdf_$COMMIT_HASH-mac-arm64.zip" + NAME="kdf_$KDF_BUILD_TAG-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/kdf -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -194,7 +194,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" win-x86-64: timeout-minutes: 60 @@ -211,37 +213,34 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $Env:GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $Env:GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - if (test-path "./MM_VERSION") { - remove-item "./MM_VERSION" - } - echo $Env:COMMIT_HASH > ./MM_VERSION - cargo build --release + run: cargo build --release - name: Compress mm2 build output run: | - $NAME="mm2_$Env:COMMIT_HASH-win-x86-64.zip" + $NAME="mm2_$Env:KDF_BUILD_TAG-win-x86-64.zip" 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll - mkdir $Env:BRANCH_NAME - mv $NAME ./$Env:BRANCH_NAME/ + $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output run: | - $NAME="kdf_$Env:COMMIT_HASH-win-x86-64.zip" + $NAME="kdf_$Env:KDF_BUILD_TAG-win-x86-64.zip" 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll - mv $NAME ./$Env:BRANCH_NAME/ + $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -250,7 +249,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + $SAFE_BRANCH_NAME = $Env:BRANCH_NAME -replace '/', '-' + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" mac-dylib-x86-64: timeout-minutes: 60 @@ -261,43 +262,43 @@ jobs: run: | rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal rustup default nightly-2023-06-01 + rustup target add x86_64-apple-darwin - name: Install build deps uses: ./.github/actions/deps-install with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib + run: cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - name: Compress mm2 build output run: | - NAME="mm2_$COMMIT_HASH-mac-dylib-x86-64.zip" + NAME="mm2_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" cp target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libmm2.a zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output run: | - NAME="kdf_$COMMIT_HASH-mac-dylib-x86-64.zip" + NAME="kdf_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -306,7 +307,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" wasm: timeout-minutes: 60 @@ -334,31 +337,30 @@ jobs: rustup target add wasm32-unknown-unknown - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y + # TODO: Lock wasm-pack version + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - wasm-pack build --release mm2src/mm2_bin_lib --target web --out-dir ../../target/target-wasm-release + run: wasm-pack build --release mm2src/mm2_bin_lib --target web --out-dir ../../target/target-wasm-release - name: Compress build output run: | - NAME="kdf_$COMMIT_HASH-wasm.zip" + NAME="kdf_$KDF_BUILD_TAG-wasm.zip" (cd ./target/target-wasm-release && zip -r - .) > $NAME - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -367,7 +369,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" ios-aarch64: timeout-minutes: 60 @@ -385,37 +389,36 @@ jobs: with: deps: ('protoc', 'python3', 'paramiko') - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build - run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib + run: cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - name: Compress mm2 build output run: | - NAME="mm2_$COMMIT_HASH-ios-aarch64.zip" + NAME="mm2_$KDF_BUILD_TAG-ios-aarch64.zip" mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libmm2.a zip $NAME target/aarch64-apple-ios/release/libmm2.a -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output run: | - NAME="kdf_$COMMIT_HASH-ios-aarch64.zip" + NAME="kdf_$KDF_BUILD_TAG-ios-aarch64.zip" mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a zip $NAME target/aarch64-apple-ios/release/libkdf.a -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -424,7 +427,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" android-aarch64: timeout-minutes: 60 @@ -454,39 +459,38 @@ jobs: - name: Setup NDK run: ./scripts/ci/android-ndk.sh x86 23 - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - export PATH=$PATH:/android-ndk/bin CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - name: Compress mm2 build output run: | - NAME="mm2_$COMMIT_HASH-android-aarch64.zip" + NAME="mm2_$KDF_BUILD_TAG-android-aarch64.zip" mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a zip $NAME target/aarch64-linux-android/release/libmm2.a -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output run: | - NAME="kdf_$COMMIT_HASH-android-aarch64.zip" + NAME="kdf_$KDF_BUILD_TAG-android-aarch64.zip" mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a zip $NAME target/aarch64-linux-android/release/libkdf.a -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -495,7 +499,9 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" android-armv7: timeout-minutes: 60 @@ -525,39 +531,38 @@ jobs: - name: Setup NDK run: ./scripts/ci/android-ndk.sh x86 23 - - name: Calculate commit hash for PR commit + - name: Calculate build tag (commit hash) for PR commit if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - name: Calculate commit hash for merge commit + - name: Calculate build tag (commit hash) for merge commit if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + run: echo "KDF_BUILD_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | - rm -f ./MM_VERSION - echo $COMMIT_HASH > ./MM_VERSION - export PATH=$PATH:/android-ndk/bin CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - name: Compress mm2 build output run: | - NAME="mm2_$COMMIT_HASH-android-armv7.zip" + NAME="mm2_$KDF_BUILD_TAG-android-armv7.zip" mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j - mkdir $BRANCH_NAME - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME + mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output run: | - NAME="kdf_$COMMIT_HASH-android-armv7.zip" + NAME="kdf_$KDF_BUILD_TAG-android-armv7.zip" mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j - mv $NAME ./$BRANCH_NAME/ + SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact env: @@ -566,4 +571,6 @@ jobs: FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.FILE_SERVER_KEY != '' }} - run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + run: | + SAFE_BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + python3 ./scripts/ci/upload_artifact.py "$SAFE_BRANCH_NAME" "/uploads/$SAFE_BRANCH_NAME" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12a60bbc3c..c0da60616b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,8 +33,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -61,8 +61,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -89,8 +89,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -117,8 +117,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -149,8 +149,8 @@ jobs: - name: Set loopback address run: ./scripts/ci/lo0_config.sh - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -178,8 +178,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Download wget64 uses: ./.github/actions/download-and-verify @@ -214,8 +214,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -245,6 +245,9 @@ jobs: deps: ('protoc') - name: Install wasm-pack + # Use the latest wasm-pack for up-to-date compatibility coverage on KDF. + # As we don't share any build artifacts from this pipeline, we don't need + # to lock the version here. run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Download geckodriver @@ -261,8 +264,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: Build cache + uses: ./.github/actions/build-cache - name: Test - run: WASM_BINDGEN_TEST_TIMEOUT=480 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main + run: WASM_BINDGEN_TEST_TIMEOUT=600 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main diff --git a/.github/workflows/validate-cargo-lock.yml b/.github/workflows/validate-cargo-lock.yml new file mode 100644 index 0000000000..eb59173d28 --- /dev/null +++ b/.github/workflows/validate-cargo-lock.yml @@ -0,0 +1,16 @@ +name: Validate Cargo.lock +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + validate-cargo-lock: + name: Checking Cargo.lock file + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Validate Cargo.lock + run: cargo update -w --locked diff --git a/.gitignore b/.gitignore index 42f63e5115..47d4ca0e5e 100755 --- a/.gitignore +++ b/.gitignore @@ -37,10 +37,6 @@ scripts/mm2/seed/unparsed.txt /js/.kdf.* # Rust artefacts -/MM_DATETIME -/MM_DATETIME.tmp -/MM_VERSION -/MM_VERSION.tmp /target /targettest /clippytarget @@ -51,6 +47,7 @@ scripts/mm2/seed/unparsed.txt # Private IDE configuration /mm2src/*/.vscode/tasks.json /mm2src/*/.vscode/c_cpp_properties.json +/mm2src/*/Cargo.lock /.vscode/settings.json /.vscode/tasks.json /.vscode/c_cpp_properties.json @@ -74,9 +71,11 @@ MM2.json # mergetool *.orig +# Dumpster (files not intended for tracking) +hidden # Ignore containers runtime directories for dockerized tests # This directory contains temporary data used by Docker containers during tests execution. # It is recreated from container-state data each time test containers are started, # and should not be tracked in version control. -.docker/container-runtime/ \ No newline at end of file +.docker/container-runtime/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d8816316..132dda9919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,142 @@ +## v2.4.0-beta - 2025-05-02 + +### Features: + +**Experimental Namespace**: +- Introduced experimental namespace for APIs that may undergo breaking changes in future releases [#2372](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2372) + +**Event Streaming**: +- Refactored event-streaming system to support dynamic, API-driven subscription management [#2172](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2172) +- Added support for additional event types including transaction history events, swap events, and more [#2172](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2172) + +**Cosmos Staking**: +- The following staking operations were added for Cosmos chains: + - Delegation [#2322](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2322) + - Undelegation [#2330](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2330) + - Claiming delegation rewards [#2351](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2351) [#2373](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2373) +- Additional RPC endpoints were added for: + - Validator data queries [#2310](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2310) + - Delegation status and ongoing undelegations [#2377](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2377) +- Relocated staking RPCs under experimental::staking namespace with new method names [#2372](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2372) + +### Enhancements/Fixes: + +**Event Streaming**: +- UnknownClient error was moved to trace level [#2401](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2401) +- Initial addresses registration in UTXO balance streaming was fixed to properly track address balances when streamer is enabled [#2431](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2431) + +**Peer-to-Peer Network**: +- Implemented network time synchronization validation to avoid swap failures due to node clock drift [#2255](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2255) [#2302](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2302), with additional testing coverage [#2304](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2304) +- Removed static seed node IP addresses for improved reliability [#2407](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2407) +- Improved error handling in best_orders RPC when no peers respond to orderbook requests [#2318](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2318) +- Fixed peer-to-peer backward compatibility for swap negotiations by improving serialization of pubkey fields [#2353](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2353) + +**Trading Protocol Improvements**: +- Implemented zero DEX fees for v2 swaps (TPU) for KMD trading pairs [#2323](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2323) +- Added swap protocol versioning with fallback support to legacy swaps [#2324](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2324) +- Added pre-burn address outputs for collecting 25% of taker DEX fees: + - UTXO swaps (both v1 and v2 protocols) [#2112](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2112) + - Cosmos and ARRR swaps (legacy protocol only) [#2112](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2112) +- Fixed payment spend and secret extraction logic in swaps v2 [#2261](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2261) +- Removed unnecessary state during ETH funding validation in swaps v2 [#2334](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2334) +- Allowed skipping unnecessary P2P message handling in ETH swaps v2 [#2359](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2359) +- Improved swap robustness by checking for existing maker/taker payments before timeout validation [#2283](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2283) +- Fixed memory leak issue in legacy swaps tracking (running_swap) [#2301](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2301) +- Added `is_success` field to legacy swap status response, making success state more explicit [#2371](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2371) + +**Tendermint/Cosmos Improvements**: +- Improved transaction query reliability using standardized TxSearchRequest [#2384](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2384) +- Added token transaction history support [#2404](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2404) +- Fixed unhandled IBC and HTLC events [#2385](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2385) + +**Wallet Improvements**: +- Added an RPC to change mnemonic passwords [#2317](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2317) +- Enabled storage and usage of non-BIP39 mnemonics [#2312](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2312) +- Fixed hardware-wallet context initialization for UTXO withdrawals [#2333](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2333) +- Added validation to restrict wallet names to alphanumeric characters, dash, and underscore [#2400](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2400) +- Changed wallet file extension from .dat to .json to better reflect content [#2400](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2400) +- Implemented optional password strength validation for mnemonic encryption [#2400](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2400) + +**FIRO Integration**: +- Added support for FIRO Spark verbose transaction details [#2396](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2396) + +**Pirate/ARRR Integration**: +- Dockerized Zombie/Pirate tests for improved test environment reliability [#2374](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2374) + +**Database and File System Improvements**: +- Improved database architecture with context functions and global DB usage has started in [#2378](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2378), this is still under development under the new-db-arch compilation flag. +- Fixed file filtering logic to exclude directories [#2364](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2364) + +**RPC Enhancements**: +- Implemented dynamic RPC port allocation (rpcport: 0) allowing automatic selection of available ports [#2342](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2342) + +**NFT integration [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900)**: +- Fixed `update_nft` to work correctly with HD wallets using the enabled address [#2386](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2386) +- Fixed `withdraw_nft` method to work correctly in HD mode [#2424](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2424) + +**Simple Maker Bot [#1065](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1065)**: +- Added LiveCoinWatch price provider and removed deprecated Nomics provider [#2416](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2416) + +**UTXO Improvements**: +- Added support for parsing taproot output address types from `blockchain.transaction.get` verbose transactions [#2423](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2423) + +**Qtum Delegation**: +- Standardized parameter naming by using `validator_address` for QTUM delegation similar to Cosmos [#2419](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2419) + +**Security Fixes**: +- Fixed potential panics in hash processing by enforcing fixed-size arrays and proper length validation [#2279](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2279) +- Improved security of key derivation by validating key material length [#2356](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2356) +- Ensured consistent Argon2 parameters for wallet encryption/decryption [#2360](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2360) +- Fixed path traversal vulnerability in wallet file handling [#2400](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2400) + +### Other Changes: + +**Code and Dependencies**: +- Added default implementations for protocol-specific SwapOps functions [#2354](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2354) +- ETH address displaying now uses a generic trait [#2348](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2348) +- Removed unnecessary Arc wrappers from Ethereum websocket implementation [#2291](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2291) +- Updated dependencies: + - Replaced deprecated instant dependency [#2391](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2391) + - Completed migration to timed-map crate [#2247](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2247) [#2306](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2306) [#2308](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2308) + - Bumped libp2p from k-0.52.11 to k-0.52.12 to fix iOS platform build issues [#2326](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2326) + +**Build and Testing Improvements**: +- Rewrote main build script for clarity/stability and to eliminate cache invalidation [#2319](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2319) +- mm2_main error is now unified across native and wasm [#2389](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2389) +- mm2_main in wasm now returns a js promise by making it async [#2389](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2389) +- A lot of unstable tests were made more stable in [#2365](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2365) +- Added additional Sepolia testnet nodes for improved test coverage [#2358](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2358) +- Fixed failing Electrum protocol version test [#2412](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2412) +- Updated Docker build configuration for WASM to fix dependency version issues [#2294](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2294) +- Fixed WASM build by adding test-ext-api feature to required toml files [#2295](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2295) +- Improved CI performance with proper Rust caching implementation [#2303](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2303) +- Updated broken RPC link in Cosmos tests [#2305](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2305) +- Unlocked wasm-pack version constraints in CI workflows and Docker builds [#2307](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2307) +- Fixed mm2_p2p module development build to support individual module testing [#2311](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2311) +- Added Cargo.lock validation to CI process to prevent lockfile inconsistencies [#2309](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2309) +- Improved branch naming flexibility by allowing feature-specific patterns like `feat/swapstatus-is-success` [#2371](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2371) +- Fixed formatting and linting job failures by correcting the syntax for rustup component installation [#2390](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2390) + +**NB - Backwards compatibility breaking changes:** +- Event streaming model changed from static configuration to API-driven subscription [#2172](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2172) + +## v2.3.0-beta - 2024-12-19 + +### Features: +- **1inch Integration**: + - Initial code to connect to 1inch Liquidity Routing API (LRAPI) provider was added along with two new RPCs for 1inch classic swap API [#2222](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2222) + - New 'approve' and 'allowance' RPCs for ERC20 tokens were also added [#2222](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2222) + +### Enhancements/Fixes: +- **Error Handling**: + - KDF now checks main files (config/coins/etc.) before reading them to prevent potential panics [#2288](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2288) + - Special character restrictions (<, >, &) were removed from RPC request bodies that were incorrectly blocking valid password characters in the get_mnemonic RPC call [#2287](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2287) +- **Dependencies**: + - Removed unnecessary reliance on core [#2289](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2289) + - Bumped libp2p dependency to k-0.52.11 for security reasons [#2296](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2296) +- **Performance**: + - Replaced GStuff constructible with OnceCell for better performance [#2267](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2267) + ## v2.2.0-beta - 2024-11-22 **Features:** diff --git a/Cargo.lock b/Cargo.lock index 8e69b25e20..e2ebd94a86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,22 +173,22 @@ checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" [[package]] name = "async-io" -version = "1.13.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +checksum = "10da8f3146014722c89e7859e1d7bb97873125d7346d10ca642ffab794355828" dependencies = [ "async-lock", - "autocfg 1.1.0", "cfg-if 1.0.0", "concurrent-queue 2.2.0", + "futures-io", "futures-lite", - "log", "parking 2.1.0", "polling", - "rustix 0.37.7", + "rustix 0.38.44", "slab", - "socket2 0.4.9", + "tracing", "waker-fn", + "windows-sys 0.48.0", ] [[package]] @@ -281,7 +281,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.14", "libc", "winapi", ] @@ -306,7 +306,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", - "bitflags", + "bitflags 1.3.2", "bytes 1.4.0", "futures-util", "http 0.2.12", @@ -502,6 +502,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + [[package]] name = "bitvec" version = "0.18.5" @@ -805,7 +811,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -860,7 +866,6 @@ dependencies = [ "http 0.2.12", "hyper", "hyper-rustls 0.24.2", - "instant", "itertools", "js-sys", "jsonrpc-core", @@ -921,6 +926,7 @@ dependencies = [ "spv_validation", "tendermint-rpc", "time 0.3.20", + "timed-map", "tokio", "tokio-rustls 0.24.1", "tokio-tungstenite-wasm", @@ -935,6 +941,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", + "web-time", "web3", "webpki-roots 0.25.4", "winapi", @@ -1004,7 +1011,6 @@ dependencies = [ "http-body 0.1.0", "hyper", "hyper-rustls 0.24.2", - "instant", "itertools", "js-sys", "lazy_static", @@ -1033,6 +1039,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", + "web-time", "winapi", ] @@ -1827,7 +1834,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "blake2b_simd", "byteorder", @@ -1852,13 +1859,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.1" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ - "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -2183,13 +2189,8 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", "futures-core", - "futures-io", - "memchr", - "parking 2.1.0", "pin-project-lite 0.2.9", - "waker-fn", ] [[package]] @@ -2500,6 +2501,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -2776,19 +2783,19 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.7.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc0fa01ffc752e9dbc72818cdb072cd028b86be5e09dd04c5a643704fe101a9" +checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a" dependencies = [ "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "if-watch" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9465340214b296cd17a0009acdb890d6160010b8adf8f78a00d0d7ab270f79f" +checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" dependencies = [ "async-io", "core-foundation", @@ -2797,6 +2804,10 @@ dependencies = [ "if-addrs", "ipnet", "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", "rtnetlink", "system-configuration", "tokio", @@ -3073,7 +3084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ "arrayvec 0.5.1", - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "ryu", "static_assertions", @@ -3081,9 +3092,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" @@ -3100,7 +3111,7 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libp2p" version = "0.52.1" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -3132,7 +3143,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3143,7 +3154,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3154,7 +3165,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "fnv", @@ -3181,7 +3192,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "futures 0.3.28", "libp2p-core", @@ -3195,7 +3206,7 @@ dependencies = [ [[package]] name = "libp2p-floodsub" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", "cuckoofilter", @@ -3215,7 +3226,7 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.45.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", "base64 0.21.7", @@ -3246,7 +3257,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", "either", @@ -3286,7 +3297,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.44.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "data-encoding", "futures 0.3.28", @@ -3306,7 +3317,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "instant", "libp2p-core", @@ -3322,7 +3333,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "bytes 1.4.0", "curve25519-dalek 3.2.0", @@ -3346,7 +3357,7 @@ dependencies = [ [[package]] name = "libp2p-ping" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "futures 0.3.28", @@ -3363,7 +3374,7 @@ dependencies = [ [[package]] name = "libp2p-request-response" version = "0.25.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "async-trait", "futures 0.3.28", @@ -3380,7 +3391,7 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "fnv", @@ -3402,7 +3413,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.33.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "heck", "proc-macro-warning", @@ -3414,7 +3425,7 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "futures 0.3.28", "futures-timer", @@ -3430,7 +3441,7 @@ dependencies = [ [[package]] name = "libp2p-wasm-ext" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "futures 0.3.28", "js-sys", @@ -3443,7 +3454,7 @@ dependencies = [ [[package]] name = "libp2p-websocket" version = "0.42.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "futures 0.3.28", @@ -3462,7 +3473,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.44.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "futures 0.3.28", @@ -3622,9 +3633,9 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" @@ -3841,7 +3852,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "2.3.0-beta" +version = "2.4.0-beta" dependencies = [ "chrono", "common", @@ -3875,7 +3886,6 @@ dependencies = [ "futures 0.3.28", "gstuff", "hex", - "instant", "lazy_static", "libp2p", "mm2_err_handle", @@ -3890,9 +3900,11 @@ dependencies = [ "serde", "serde_json", "shared_ref_counter", + "timed-map", "tokio", "uuid", "wasm-bindgen-test", + "web-time", ] [[package]] @@ -3963,6 +3975,7 @@ dependencies = [ "futures 0.3.28", "parking_lot", "serde", + "serde_json", "tokio", "wasm-bindgen-test", ] @@ -4106,6 +4119,7 @@ dependencies = [ "sp-trie", "spv_validation", "testcontainers", + "timed-map", "tokio", "trading_api", "trie-db", @@ -4116,6 +4130,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", + "web-time", "web3", "winapi", ] @@ -4232,12 +4247,12 @@ dependencies = [ "futures-rustls 0.24.0", "futures-ticker", "hex", - "instant", "lazy_static", "libp2p", "log", "mm2_core", "mm2_event_stream", + "mm2_net", "mm2_number", "parking_lot", "rand 0.7.3", @@ -4250,8 +4265,10 @@ dependencies = [ "sha2 0.10.7", "smallvec 1.6.1", "syn 2.0.38", + "timed-map", "tokio", "void", + "web-time", ] [[package]] @@ -4380,7 +4397,7 @@ checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" [[package]] name = "multistream-select" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -4392,24 +4409,23 @@ dependencies = [ [[package]] name = "netlink-packet-core" -version = "0.4.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345b8ab5bd4e71a2986663e88c56856699d060e78e152e6e9d7966fcd5491297" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" dependencies = [ "anyhow", "byteorder", - "libc", "netlink-packet-utils", ] [[package]] name = "netlink-packet-route" -version = "0.12.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ea4302b9759a7a88242299225ea3688e63c85ea136371bb6cf94fd674efaab" +checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" dependencies = [ "anyhow", - "bitflags", + "bitflags 1.3.2", "byteorder", "libc", "netlink-packet-core", @@ -4430,9 +4446,9 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b4b14489ab424703c092062176d52ba55485a89c076b4f9db05092b7223aa6" +checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -4467,11 +4483,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "libc", ] @@ -4556,7 +4572,7 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.14", "libc", ] @@ -4852,18 +4868,17 @@ checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" [[package]] name = "polling" -version = "2.8.0" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ - "autocfg 1.1.0", - "bitflags", "cfg-if 1.0.0", "concurrent-queue 2.2.0", - "libc", - "log", + "hermit-abi 0.4.0", "pin-project-lite 0.2.9", - "windows-sys 0.48.0", + "rustix 0.38.44", + "tracing", + "windows-sys 0.59.0", ] [[package]] @@ -5112,7 +5127,7 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", "bytes 1.4.0", @@ -5379,7 +5394,7 @@ version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c49596760fce12ca21550ac21dc5a9617b2ea4b6e0aa7d8dab8ff2824fc2bba" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -5415,7 +5430,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -5641,22 +5656,27 @@ dependencies = [ "derive_more", "futures 0.3.28", "mm2_err_handle", + "mm2_event_stream", "ser_error", "ser_error_derive", "serde", "serde_derive", + "serde_json", ] [[package]] name = "rtnetlink" -version = "0.10.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322c53fd76a18698f1c27381d58091de3a043d356aa5bd0d510608b565f469a0" +checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" dependencies = [ "futures 0.3.28", "log", + "netlink-packet-core", "netlink-packet-route", + "netlink-packet-utils", "netlink-proto", + "netlink-sys", "nix", "thiserror", "tokio", @@ -5678,7 +5698,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -5713,9 +5733,9 @@ checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustc-hex" @@ -5747,7 +5767,7 @@ version = "0.36.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes", "libc", @@ -5757,16 +5777,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.7" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", - "errno 0.3.1", - "io-lifetimes", + "bitflags 2.8.0", + "errno 0.3.10", "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.45.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] @@ -5852,7 +5871,7 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rw-stream-sink" version = "0.4.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "futures 0.3.28", "pin-project", @@ -6016,7 +6035,7 @@ version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -6456,7 +6475,7 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77963e2aa8fadb589118c3aede2e78b6c4bcf1c01d588fbf33e915b390825fbd" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", "hash-db", "hash256-std-hasher", @@ -6734,20 +6753,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.8.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -6955,6 +6974,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "timed-map" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30565aee368a9b233f397f46cd803c59285b61d54c5b3ae378611bd467beecbe" +dependencies = [ + "rustc-hash", + "web-time", +] + [[package]] name = "tiny-keccak" version = "1.4.4" @@ -7774,6 +7803,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web3" version = "0.19.0" @@ -7890,15 +7929,31 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.34.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" dependencies = [ - "windows_aarch64_msvc 0.34.0", - "windows_i686_gnu 0.34.0", - "windows_i686_msvc 0.34.0", - "windows_x86_64_gnu 0.34.0", - "windows_x86_64_msvc 0.34.0", + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -7947,6 +8002,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.1" @@ -7977,6 +8041,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.1" @@ -7990,16 +8070,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] -name = "windows_aarch64_msvc" -version = "0.32.0" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.34.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" [[package]] name = "windows_aarch64_msvc" @@ -8014,16 +8094,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] -name = "windows_i686_gnu" -version = "0.32.0" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.34.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" [[package]] name = "windows_i686_gnu" @@ -8038,16 +8118,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] -name = "windows_i686_msvc" -version = "0.32.0" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.34.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" [[package]] name = "windows_i686_msvc" @@ -8062,16 +8148,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] -name = "windows_x86_64_gnu" -version = "0.32.0" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.34.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" [[package]] name = "windows_x86_64_gnu" @@ -8085,6 +8171,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.1" @@ -8098,16 +8190,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] -name = "windows_x86_64_msvc" -version = "0.32.0" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.34.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" [[package]] name = "windows_x86_64_msvc" @@ -8121,6 +8213,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.6.20" @@ -8214,7 +8312,7 @@ checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "base64 0.13.0", @@ -8239,7 +8337,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "bech32", @@ -8261,7 +8359,7 @@ dependencies = [ [[package]] name = "zcash_extras" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "ff 0.8.0", @@ -8277,7 +8375,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.0.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "blake2b_simd", "byteorder", @@ -8291,7 +8389,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "aes 0.8.3", "bitvec 0.18.5", @@ -8321,7 +8419,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "bellman", "blake2b_simd", diff --git a/docs/ANDROID.md b/docs/ANDROID.md index 75e4016650..439a75d65e 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -6,20 +6,14 @@ We need a Unix operating system (the build has been tested on Linux and Mac). We need a free access to the Docker (`docker run hello-world` should work). -We need the Nightly revision of Rust, such as - - rustup default nightly-2021-05-17 - ### Install cross cargo install cross ### Get the source code - git clone --depth=1 git@gitlab.com:KomodoPlatform/supernet.git -b mm2.1-cross - cd supernet - git log --pretty=format:'%h' -n 1 > MM_VERSION - git log --pretty=format:'%cI' -n 1 > MM_DATETIME + git clone --depth=1 https://github.com/KomodoPlatform/komodo-defi-framework + cd komodo-defi-framework ### Install extra packages into the Docker image @@ -27,19 +21,19 @@ The [Android NDK installer](https://github.com/rust-embedded/cross/tree/master/d #### armeabi-v7a ABI Docker image - (cd supernet && docker build --tag armv7-linux-androideabi-aga -f .docker/Dockerfile.armv7-linux-androideabi .) + (cd komodo-defi-framework && docker build --tag armv7-linux-androideabi-aga -f .docker/Dockerfile.armv7-linux-androideabi .) #### arm64-v8a ABI Docker image - (cd supernet && docker build --tag aarch64-linux-android-aga -f .docker/Dockerfile.aarch64-linux-android .) + (cd komodo-defi-framework && docker build --tag aarch64-linux-android-aga -f .docker/Dockerfile.aarch64-linux-android .) ### x86 ABI Docker image - (cd supernet && docker build --tag i686-linux-android-aga -f .docker/Dockerfile.i686-linux-android .) + (cd komodo-defi-framework && docker build --tag i686-linux-android-aga -f .docker/Dockerfile.i686-linux-android .) ### x86_64 ABI Docker image - (cd supernet && docker build --tag x86_64-linux-android-aga -f .docker/Dockerfile.x86_64-linux-android .) + (cd komodo-defi-framework && docker build --tag x86_64-linux-android-aga -f .docker/Dockerfile.x86_64-linux-android .) ### Setup the NDK_HOME variable diff --git a/docs/WASM_BUILD.md b/docs/WASM_BUILD.md index eb62fa7731..28355bbf80 100644 --- a/docs/WASM_BUILD.md +++ b/docs/WASM_BUILD.md @@ -1,12 +1,27 @@ # Building WASM binary +## From Container: + +If you want to build from source without installing prerequisites to your host system, you can do so by binding the source code inside a container and compiling it there. + +Build the image: + +```sh +docker build -t kdf-build-container -f .docker/Dockerfile . +``` + +Bind source code into container and compile it: +```sh +docker run -v "$(pwd)":/app -w /app kdf-build-container wasm-pack build mm2src/mm2_bin_lib --target web --out-dir wasm_build/deps/pkg/ +``` + ## Setting up the environment To build WASM binary from source, the following prerequisites are required: 1. Install `wasm-pack` ``` - cargo install wasm-pack + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh ``` 2. OSX specific: install `llvm` ``` @@ -39,4 +54,6 @@ If you want to disable optimizations to reduce the compilation time, run `wasm-p wasm-pack build mm2src/mm2_bin_lib --target web --out-dir wasm_build/deps/pkg/ --dev ``` -Please don't forget to specify `CC` and `AR` if you run the command on OSX. \ No newline at end of file +Please don't forget to specify `CC` and `AR` if you run the command on OSX. + + diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 290a0dd7f5..13aa9c2b72 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -12,7 +12,8 @@ enable-sia = [ ] default = [] run-docker-tests = [] -for-tests = [] +for-tests = ["dep:mocktopus"] +new-db-arch = [] [lib] path = "lp_coins.rs" @@ -33,6 +34,7 @@ cfg-if = "1.0" chain = { path = "../mm2_bitcoin/chain" } chrono = { version = "0.4.23", "features" = ["serde"] } common = { path = "../common" } +compatible-time = { version = "1.1.0", package = "web-time" } cosmrs = { version = "0.16", default-features = false } crossbeam = "0.8" crypto = { path = "../crypto" } @@ -69,10 +71,10 @@ mm2_io = { path = "../mm2_io" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number"} -mm2_p2p = { path = "../mm2_p2p" } +mm2_p2p = { path = "../mm2_p2p", default-features = false } mm2_rpc = { path = "../mm2_rpc" } mm2_state_machine = { path = "../mm2_state_machine" } -mocktopus = "0.8.0" +mocktopus = { version = "0.8.0", optional = true } num-traits = "0.2" parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } @@ -110,28 +112,28 @@ 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.20.0", default-features = false } zbase32 = "0.1.2" -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } -zcash_extras = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } -zcash_primitives = {features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } +zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } +zcash_extras = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } +zcash_primitives = {features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } [target.'cfg(target_arch = "wasm32")'.dependencies] blake2b_simd = "0.5" ff = "0.8" futures-util = "0.3" -instant = "0.1.12" jubjub = "0.5.1" js-sys = { version = "0.3.27" } mm2_db = { path = "../mm2_db" } mm2_metamask = { path = "../mm2_metamask" } mm2_test_helpers = { path = "../mm2_test_helpers" } time = { version = "0.3.20", features = ["wasm-bindgen"] } +timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } tonic = { version = "0.10", default-features = false, features = ["prost", "codegen", "gzip"] } tower-service = "0.3" wasm-bindgen = "0.2.86" wasm-bindgen-futures = { version = "0.4.1" } wasm-bindgen-test = { version = "0.3.2" } web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1", default-features = false, features = ["local-prover"] } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs = { version = "1" } @@ -140,7 +142,6 @@ 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.24", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -instant = { version = "0.1.12", features = ["wasm-bindgen"] } lightning = "0.0.113" lightning-background-processor = "0.0.113" lightning-invoice = { version = "0.21.0", features = ["serde"] } @@ -148,18 +149,21 @@ lightning-net-tokio = "0.0.113" rust-ini = { version = "0.13" } rustls = { version = "0.21", features = ["dangerous_configuration"] } secp256k1v24 = { version = "0.24", package = "secp256k1" } +timed-map = { version = "1.3", features = ["rustc-hash"] } tokio = { version = "1.20" } tokio-rustls = { version = "0.24" } tonic = { version = "0.10", features = ["tls", "tls-webpki-roots", "gzip"] } webpki-roots = { version = "0.25" } -zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1", default-features = false, features = ["local-prover", "multicore"] } +zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover", "multicore"] } [target.'cfg(windows)'.dependencies] winapi = "0.3" [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } +mocktopus = { version = "0.8.0" } +mm2_p2p = { path = "../mm2_p2p", features = ["application"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wagyu-zcash-parameters = { version = "0.2" } diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs index f3ba916c33..3ec047ee80 100644 --- a/mm2src/coins/coin_balance.rs +++ b/mm2src/coins/coin_balance.rs @@ -1,4 +1,4 @@ -use crate::hd_wallet::{HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, HDCoinHDAccount, +use crate::hd_wallet::{DisplayAddress, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, HDCoinHDAccount, HDPathAccountToAddressId, HDWalletCoinOps, HDWalletOps, HDXPubExtractor, NewAccountCreationError, NewAddressDerivingError}; use crate::{BalanceError, BalanceResult, CoinBalance, CoinBalanceMap, CoinWithDerivationMethod, DerivationMethod, @@ -238,7 +238,7 @@ where .await .map(|balance| { CoinBalanceReport::Iguana(IguanaWalletBalance { - address: self.address_formatter()(my_address), + address: my_address.display_address(), balance, }) }) @@ -351,7 +351,7 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { // So we can zip the derivation paths with the pairs `(Address, CoinBalance)`. .zip(der_paths) .map(|((address, balance), derivation_path)| HDAddressBalance { - address: self.address_formatter()(&address), + address: address.display_address(), derivation_path: RpcDerivationPath(derivation_path), chain, balance, @@ -411,8 +411,8 @@ pub enum AddressBalanceStatus { pub mod common_impl { use super::*; - use crate::hd_wallet::{create_new_account, ExtractExtendedPubkey, HDAccountOps, HDAccountStorageOps, HDAddressOps, - HDCoinExtendedPubkey, HDWalletOps}; + use crate::hd_wallet::{create_new_account, DisplayAddress, ExtractExtendedPubkey, HDAccountOps, + HDAccountStorageOps, HDAddressOps, HDCoinExtendedPubkey, HDWalletOps}; pub(crate) async fn enable_hd_account( coin: &Coin, @@ -579,7 +579,7 @@ pub mod common_impl { let hd_address = coin.generate_new_address(hd_wallet, hd_account, chain).await?; new_addresses.push(HDAddressBalance { - address: coin.address_formatter()(&hd_address.address()), + address: hd_address.address().display_address(), derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance: HDWalletBalanceObject::::new(), diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index 6075317bfc..3e9bbc7349 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -1,4 +1,4 @@ -use crate::eth::eth_swap_v2::{PaymentStatusErr, PrepareTxDataError, ValidatePaymentV2Err}; +use crate::eth::eth_swap_v2::{PrepareTxDataError, ValidatePaymentV2Err}; use crate::eth::nft_swap_v2::errors::{Erc721FunctionError, HtlcParamsError}; use crate::eth::{EthAssocTypesError, EthNftAssocTypesError, Web3RpcError}; use crate::{utxo::rpc_clients::UtxoRpcError, NumConversError, UnexpectedDerivationMethod}; @@ -85,16 +85,6 @@ impl From for ValidatePaymentError { } } -impl From for ValidatePaymentError { - fn from(err: PaymentStatusErr) -> Self { - match err { - PaymentStatusErr::Transport(e) => Self::Transport(e), - PaymentStatusErr::ABIError(e) | PaymentStatusErr::Internal(e) => Self::InternalError(e), - PaymentStatusErr::InvalidData(e) => Self::InvalidData(e), - } - } -} - impl From for ValidatePaymentError { fn from(err: HtlcParamsError) -> Self { match err { @@ -107,7 +97,6 @@ impl From for ValidatePaymentError { impl From for ValidatePaymentError { fn from(err: ValidatePaymentV2Err) -> Self { match err { - ValidatePaymentV2Err::UnexpectedPaymentState(e) => ValidatePaymentError::UnexpectedPaymentState(e), ValidatePaymentV2Err::WrongPaymentTx(e) => ValidatePaymentError::WrongPaymentTx(e), } } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index b8db8c7c12..7a82841687 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -27,8 +27,8 @@ use crate::coin_balance::{EnableCoinBalanceError, EnabledCoinBalanceParams, HDAc HDWalletBalance, HDWalletBalanceOps}; use crate::eth::eth_rpc::ETH_RPC_REQUEST_TIMEOUT; use crate::eth::web3_transport::websocket_transport::{WebsocketTransport, WebsocketTransportNode}; -use crate::hd_wallet::{HDAccountOps, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, HDPathAccountToAddressId, - HDWalletCoinOps, HDXPubExtractor}; +use crate::hd_wallet::{DisplayAddress, HDAccountOps, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, + HDPathAccountToAddressId, HDWalletCoinOps, HDXPubExtractor}; use crate::lp_price::get_base_price_in_rel; use crate::nft::nft_errors::ParseContractTypeError; use crate::nft::nft_structs::{ContractType, ConvertChain, NftInfo, TransactionNftDetails, WithdrawErc1155, @@ -54,8 +54,8 @@ use async_trait::async_trait; use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; use common::custom_futures::timeout::FutureTimerExt; -use common::executor::{abortable_queue::AbortableQueue, AbortOnDropHandle, AbortSettings, AbortableSystem, - AbortedError, SpawnAbortable, Timer}; +use common::executor::{abortable_queue::AbortableQueue, AbortSettings, AbortableSystem, AbortedError, SpawnAbortable, + Timer}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; use common::{now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -63,6 +63,8 @@ use crypto::privkey::key_pair_from_secret; use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; use derive_more::Display; use enum_derives::EnumFromStringify; + +use compatible_time::Instant; use ethabi::{Contract, Function, Token}; use ethcore_transaction::tx_builders::TxBuilderError; use ethcore_transaction::{Action, TransactionWrapper, TransactionWrapperBuilder as UnSignedEthTxBuilder, @@ -75,12 +77,9 @@ use futures::compat::Future01CompatExt; use futures::future::{join, join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; use futures01::Future; use http::Uri; -use instant::Instant; use mm2_core::mm_ctx::{MmArc, MmWeak}; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, BigUint, MmNumber}; -#[cfg(test)] use mocktopus::macros::*; use rand::seq::SliceRandom; use rlp::{DecoderError, Encodable, RlpStream}; use rpc::v1::types::Bytes as BytesJson; @@ -109,30 +108,30 @@ cfg_wasm32! { } use super::{coin_conf, lp_coinfind_or_err, AsyncMutex, BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, - CoinBalance, CoinFutSpawner, CoinProtocol, CoinTransportMetrics, CoinsContext, ConfirmPaymentInput, - EthValidateFeeArgs, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, - MarketCoinOps, MmCoin, MmCoinEnum, MyAddressError, MyWalletAddress, NegotiateSwapContractAddrErr, - NumConversError, NumConversResult, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, - RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, - RefundResult, RewardTarget, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignEthTransactionParams, - SignRawTransactionEnum, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, - SwapOps, SwapTxFeePolicy, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, - TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, - TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, - WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, - WithdrawRequest, WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, - INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG}; + CoinBalance, CoinProtocol, CoinTransportMetrics, CoinsContext, ConfirmPaymentInput, EthValidateFeeArgs, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MarketCoinOps, MmCoin, MmCoinEnum, + MyAddressError, MyWalletAddress, NegotiateSwapContractAddrErr, NumConversError, NumConversResult, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, + RawTransactionResult, RefundPaymentArgs, RewardTarget, RpcClientType, RpcTransportEventHandler, + RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SignEthTransactionParams, SignRawTransactionEnum, SignRawTransactionRequest, + SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxFeePolicy, TradeFee, TradePreimageError, + TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, + TransactionEnum, TransactionErr, TransactionFut, TransactionType, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, + VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, + WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, + INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, + INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG}; pub use rlp; cfg_native! { use std::path::PathBuf; } -mod eth_balance_events; +pub mod eth_balance_events; mod eth_rpc; #[cfg(test)] mod eth_tests; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; @@ -153,16 +152,14 @@ use eth_withdraw::{EthWithdraw, InitEthWithdraw, StandardEthWithdraw}; mod nonce; use nonce::ParityNonce; -mod eip1559_gas_fee; -pub(crate) use eip1559_gas_fee::FeePerGasEstimated; -use eip1559_gas_fee::{BlocknativeGasApiCaller, FeePerGasSimpleEstimator, GasApiConfig, GasApiProvider, - InfuraGasApiCaller}; +pub mod fee_estimation; +use fee_estimation::eip1559::{block_native::BlocknativeGasApiCaller, infura::InfuraGasApiCaller, + simple::FeePerGasSimpleEstimator, FeePerGasEstimated, GasApiConfig, GasApiProvider}; pub mod erc20; use erc20::get_token_decimals; - pub(crate) mod eth_swap_v2; -use eth_swap_v2::{EthPaymentType, PaymentMethod}; +use eth_swap_v2::{extract_id_from_tx_data, EthPaymentType, PaymentMethod, SpendTxSearchParams}; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.137.5:8565) contract address: 0x83965C539899cC0F918552e5A26915de40ee8852 @@ -580,7 +577,8 @@ impl TryFrom for PayForGasOption { type GasDetails = (U256, PayForGasOption); -#[derive(Debug, Display, EnumFromStringify)] +#[derive(Debug, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] pub enum Web3RpcError { #[display(fmt = "Transport: {}", _0)] Transport(String), @@ -814,29 +812,6 @@ impl From for EthPrivKeyBuildPolicy { } } -/// Gas fee estimator loop context, runs a loop to estimate max fee and max priority fee per gas according to EIP-1559 for the next block -/// -/// This FeeEstimatorContext handles rpc requests which start and stop gas fee estimation loop and handles the loop itself. -/// FeeEstimatorContext keeps the latest estimated gas fees to return them on rpc request -pub(crate) struct FeeEstimatorContext { - /// Latest estimated gas fee values - pub(crate) estimated_fees: Arc>, - /// Handler for estimator loop graceful shutdown - pub(crate) abort_handler: AsyncMutex>, -} - -/// Gas fee estimator creation state -pub(crate) enum FeeEstimatorState { - /// Gas fee estimation not supported for this coin - CoinNotSupported, - /// Platform coin required to be enabled for gas fee estimation for this coin - PlatformCoinRequired, - /// Fee estimator created, use simple internal estimator - Simple(AsyncMutex), - /// Fee estimator created, use provider or simple internal estimator (if provider fails) - Provider(AsyncMutex), -} - /// pImpl idiom. pub struct EthCoinImpl { ticker: String, @@ -877,8 +852,6 @@ pub struct EthCoinImpl { /// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets /// information (chain & contract type, amount etc.), where ownership and amount, in ERC1155 case, might change over time. pub nfts_infos: Arc>>, - /// Context for eth fee per gas estimator loop. Created if coin supports fee per gas estimation - pub(crate) platform_fee_estimator_state: Arc, /// Config provided gas limits for swap and send transactions pub(crate) gas_limit: EthGasLimit, /// Config provided gas limits v2 for swap v2 transactions @@ -1195,7 +1168,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), // TODO: should we return tx_hex 0x-prefixed (everywhere)? tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), // TODO: add 0x hash (use unified hash format for eth wherever it is returned) - from: vec![eth_coin.my_address()?], + from: vec![my_address.display_address()], to: vec![withdraw_type.to], contract_type: ContractType::Erc1155, token_address: withdraw_type.token_address, @@ -1222,8 +1195,8 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd let my_address = eth_coin.derivation_method.single_addr_or_err().await?; if token_owner != my_address { return MmError::err(WithdrawError::MyAddressNotNftOwner { - my_address: eth_addr_to_hex(&my_address), - token_owner: eth_addr_to_hex(&token_owner), + my_address: my_address.display_address(), + token_owner: token_owner.display_address(), }); } @@ -1286,7 +1259,7 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), // TODO: add 0x hash (use unified hash format for eth wherever it is returned) - from: vec![eth_coin.my_address()?], + from: vec![my_address.display_address()], to: vec![withdraw_type.to], contract_type: ContractType::Erc721, token_address: withdraw_type.token_address, @@ -1310,14 +1283,8 @@ impl Deref for EthCoin { #[async_trait] impl SwapOps for EthCoin { - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - let address = try_tx_s!(addr_from_raw_pubkey(fee_addr)); + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + let address = try_tx_s!(addr_from_raw_pubkey(self.dex_pubkey())); self.send_to_address( address, try_tx_s!(wei_from_big_decimal(&dex_fee.fee_amount().into(), self.decimals)), @@ -1384,7 +1351,6 @@ impl SwapOps for EthCoin { validate_fee_impl(self.clone(), EthValidateFeeArgs { fee_tx_hash: &tx.tx_hash(), expected_sender: validate_fee_args.expected_sender, - fee_addr: validate_fee_args.fee_addr, amount: &validate_fee_args.dex_fee.fee_amount().into(), min_block_number: validate_fee_args.min_block_number, uuid: validate_fee_args.uuid, @@ -1491,16 +1457,12 @@ impl SwapOps for EthCoin { .await } - fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - async fn extract_secret( &self, _secret_hash: &[u8], spend_tx: &[u8], watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(spend_tx)); let function_name = get_function_name("receiverSpend", watcher_reward); let function = try_s!(SWAP_CONTRACT.function(&function_name)); @@ -1522,7 +1484,7 @@ impl SwapOps for EthCoin { return ERR!("Invalid arguments in 'receiverSpend' call: {:?}", tokens); } match &tokens[2] { - Token::FixedBytes(secret) => Ok(secret.to_vec()), + Token::FixedBytes(secret) => Ok(try_s!(secret.as_slice().try_into())), _ => ERR!( "Expected secret to be fixed bytes, decoded function data is {:?}", tokens @@ -1530,14 +1492,6 @@ impl SwapOps for EthCoin { } } - fn is_auto_refundable(&self) -> bool { false } - - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) - } - fn negotiate_swap_contract_addr( &self, other_side_address: Option<&[u8]>, @@ -1572,7 +1526,7 @@ impl SwapOps for EthCoin { | EthPrivKeyPolicy::HDWallet { activated_key: ref key_pair, .. - } => key_pair_from_secret(key_pair.secret().as_bytes()).expect("valid key"), + } => key_pair_from_secret(key_pair.secret().as_fixed_bytes()).expect("valid key"), EthPrivKeyPolicy::Trezor => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => todo!(), @@ -1580,19 +1534,20 @@ impl SwapOps for EthCoin { } #[inline] - fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> [u8; 33] { match self.priv_key_policy { EthPrivKeyPolicy::Iguana(ref key_pair) | EthPrivKeyPolicy::HDWallet { activated_key: ref key_pair, .. - } => key_pair_from_secret(key_pair.secret().as_bytes()) + } => key_pair_from_secret(&key_pair.secret().to_fixed_bytes()) .expect("valid key") .public_slice() - .to_vec(), + .try_into() + .expect("valid key length!"), EthPrivKeyPolicy::Trezor => todo!(), #[cfg(target_arch = "wasm32")] - EthPrivKeyPolicy::Metamask(ref metamask_policy) => metamask_policy.public_key.as_bytes().to_vec(), + EthPrivKeyPolicy::Metamask(ref metamask_policy) => metamask_policy.public_key.0, } } @@ -1657,20 +1612,6 @@ impl SwapOps for EthCoin { } } -#[async_trait] -impl TakerSwapMakerCoin for EthCoin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for EthCoin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - #[async_trait] impl WatcherOps for EthCoin { fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { @@ -1722,7 +1663,6 @@ impl WatcherOps for EthCoin { validate_fee_impl(self.clone(), EthValidateFeeArgs { fee_tx_hash: &H256::from_slice(validate_fee_args.taker_fee_hash.as_slice()), expected_sender: &validate_fee_args.sender_pubkey, - fee_addr: &validate_fee_args.fee_addr, amount: &BigDecimal::from(0), min_block_number: validate_fee_args.min_block_number, uuid: &[], @@ -2332,13 +2272,12 @@ impl WatcherOps for EthCoin { #[async_trait] #[cfg_attr(test, mockable)] -#[async_trait] impl MarketCoinOps for EthCoin { fn ticker(&self) -> &str { &self.ticker[..] } fn my_address(&self) -> MmResult { match self.derivation_method() { - DerivationMethod::SingleAddress(my_address) => Ok(display_eth_address(my_address)), + DerivationMethod::SingleAddress(my_address) => Ok(my_address.display_address()), DerivationMethod::HDWallet(_) => MmError::err(MyAddressError::UnexpectedDerivationMethod( "'my_address' is deprecated for HD wallets".to_string(), )), @@ -2587,73 +2526,27 @@ impl MarketCoinOps for EthCoin { }, }; - let payment_func = try_tx_s!(SWAP_CONTRACT.function(&func_name)); - let decoded = try_tx_s!(decode_contract_call(payment_func, tx.unsigned().data())); - let id = match decoded.first() { - Some(Token::FixedBytes(bytes)) => bytes.clone(), - invalid_token => { - return Err(TransactionErr::Plain(ERRL!( - "Expected Token::FixedBytes, got {:?}", - invalid_token - ))) - }, - }; - - loop { - if now_sec() > args.wait_until { - return TX_PLAIN_ERR!( - "Waited too long until {} for transaction {:?} to be spent ", - args.wait_until, - tx, - ); - } - - let current_block = match self.current_block().compat().await { - Ok(b) => b, - Err(e) => { - error!("Error getting block number: {}", e); - Timer::sleep(5.).await; - continue; - }, - }; - - let events = match self - .spend_events(swap_contract_address, args.from_block, current_block) - .compat() - .await - { - Ok(ev) => ev, - Err(e) => { - error!("Error getting spend events: {}", e); - Timer::sleep(5.).await; - continue; - }, - }; - - let found = events.iter().find(|event| &event.data.0[..32] == id.as_slice()); + let id = try_tx_s!(extract_id_from_tx_data(tx.unsigned().data(), &SWAP_CONTRACT, &func_name).await); - if let Some(event) = found { - if let Some(tx_hash) = event.transaction_hash { - let transaction = match self.transaction(TransactionId::Hash(tx_hash)).await { - Ok(Some(t)) => t, - Ok(None) => { - info!("Tx {} not found yet", tx_hash); - Timer::sleep(args.check_every).await; - continue; - }, - Err(e) => { - error!("Get tx {} error: {}", tx_hash, e); - Timer::sleep(args.check_every).await; - continue; - }, - }; - - return Ok(TransactionEnum::from(try_tx_s!(signed_tx_from_web3_tx(transaction)))); - } - } + let find_params = SpendTxSearchParams { + swap_contract_address, + event_name: "ReceiverSpent", + abi_contract: &SWAP_CONTRACT, + swap_id: &try_tx_s!(id.as_slice().try_into()), + from_block: args.from_block, + wait_until: args.wait_until, + check_every: args.check_every, + }; + let tx_hash = self + .find_transaction_hash_by_event(find_params) + .await + .map_err(|e| TransactionErr::Plain(e.get_inner().to_string()))?; - Timer::sleep(5.).await; - } + let spend_tx = self + .wait_for_transaction(tx_hash, args.wait_until, args.check_every) + .await + .map_err(|e| TransactionErr::Plain(e.get_inner().to_string()))?; + Ok(TransactionEnum::from(spend_tx)) } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { @@ -2697,6 +2590,9 @@ impl MarketCoinOps for EthCoin { MmNumber::from(1) / MmNumber::from(10u64.pow(pow)) } + #[inline] + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.priv_key_policy.is_trezor() } } @@ -3367,8 +3263,8 @@ impl EthCoin { spent_by_me, received_by_me, total_amount, - to: vec![display_eth_address(&call_data.to)], - from: vec![display_eth_address(&call_data.from)], + to: vec![call_data.to.display_address()], + from: vec![call_data.from.display_address()], coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: trace.block_number, @@ -3747,8 +3643,8 @@ impl EthCoin { spent_by_me, received_by_me, total_amount, - to: vec![display_eth_address(&to_addr)], - from: vec![display_eth_address(&from_addr)], + to: vec![to_addr.display_address()], + from: vec![from_addr.display_address()], coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: block_number.as_u64(), @@ -4537,7 +4433,9 @@ impl EthCoin { let function = ERC20_CONTRACT.function("balanceOf")?; let data = function.encode_input(&[Token::Address(address)])?; - let res = coin.call_request(address, *token_addr, None, Some(data.into())).await?; + let res = coin + .call_request(address, *token_addr, None, Some(data.into()), BlockNumber::Latest) + .await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { Token::Uint(number) => Ok(number), @@ -4601,7 +4499,7 @@ impl EthCoin { let function = ERC20_CONTRACT.function("balanceOf")?; let data = function.encode_input(&[Token::Address(address)])?; let res = self - .call_request(address, token_address, None, Some(data.into())) + .call_request(address, token_address, None, Some(data.into()), BlockNumber::Latest) .await?; let decoded = function.decode_output(&res.0)?; @@ -4628,7 +4526,7 @@ impl EthCoin { let my_address = self.derivation_method.single_addr_or_err().await?; let data = function.encode_input(&[Token::Address(my_address), Token::Uint(token_id_u256)])?; let result = self - .call_request(my_address, token_addr, None, Some(data.into())) + .call_request(my_address, token_addr, None, Some(data.into()), BlockNumber::Latest) .await?; let decoded = function.decode_output(&result.0)?; match decoded[0] { @@ -4659,7 +4557,7 @@ impl EthCoin { let data = function.encode_input(&[Token::Uint(token_id_u256)])?; let my_address = self.derivation_method.single_addr_or_err().await?; let result = self - .call_request(my_address, token_addr, None, Some(data.into())) + .call_request(my_address, token_addr, None, Some(data.into()), BlockNumber::Latest) .await?; let decoded = function.decode_output(&result.0)?; match decoded[0] { @@ -4737,6 +4635,7 @@ impl EthCoin { to: Address, value: Option, data: Option, + block_number: BlockNumber, ) -> Result { let request = CallRequest { from: Some(from), @@ -4748,7 +4647,7 @@ impl EthCoin { ..CallRequest::default() }; - self.call(request, Some(BlockId::Number(BlockNumber::Latest))).await + self.call(request, Some(BlockId::Number(block_number))).await } pub fn allowance(&self, spender: Address) -> Web3RpcFut { @@ -4764,7 +4663,7 @@ impl EthCoin { let data = function.encode_input(&[Token::Address(my_address), Token::Address(spender)])?; let res = coin - .call_request(my_address, *token_addr, None, Some(data.into())) + .call_request(my_address, *token_addr, None, Some(data.into()), BlockNumber::Latest) .await?; let decoded = function.decode_output(&res.0)?; @@ -4864,25 +4763,30 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - /// Gets `ReceiverSpent` events from etomic swap smart contract since `from_block` - fn spend_events( + /// Returns events from `from_block` to `to_block` or current `latest` block. + /// According to ["eth_getLogs" doc](https://docs.infura.io/api/networks/ethereum/json-rpc-methods/eth_getlogs) `toBlock` is optional, default is "latest". + async fn events_from_block( &self, swap_contract_address: Address, + event_name: &str, from_block: u64, - to_block: u64, - ) -> Box, Error = String> + Send> { - let contract_event = try_fus!(SWAP_CONTRACT.event("ReceiverSpent")); - let filter = FilterBuilder::default() + to_block: Option, + swap_contract: &Contract, + ) -> MmResult, FindPaymentSpendError> { + let contract_event = swap_contract.event(event_name)?; + let mut filter_builder = FilterBuilder::default() .topics(Some(vec![contract_event.signature()]), None, None, None) .from_block(BlockNumber::Number(from_block.into())) - .to_block(BlockNumber::Number(to_block.into())) - .address(vec![swap_contract_address]) - .build(); - - let coin = self.clone(); - - let fut = async move { coin.logs(filter).await.map_err(|e| ERRL!("{}", e)) }; - Box::new(fut.boxed().compat()) + .address(vec![swap_contract_address]); + if let Some(block) = to_block { + filter_builder = filter_builder.to_block(BlockNumber::Number(block.into())); + } + let filter = filter_builder.build(); + let events_logs = self + .logs(filter) + .await + .map_err(|e| FindPaymentSpendError::Transport(e.to_string()))?; + Ok(events_logs) } fn validate_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { @@ -5192,9 +5096,16 @@ impl EthCoin { .single_addr_or_err() .await .map_err(|e| ERRL!("{}", e))?; - coin.call_request(my_address, swap_contract_address, None, Some(data.into())) - .await - .map_err(|e| ERRL!("{}", e)) + coin.call_request( + my_address, + swap_contract_address, + None, + Some(data.into()), + // TODO worth reviewing places where we could use BlockNumber::Pending + BlockNumber::Latest, + ) + .await + .map_err(|e| ERRL!("{}", e)) }; Box::new(fut.boxed().compat().and_then(move |bytes| { @@ -5244,10 +5155,16 @@ impl EthCoin { let to_block = current_block.min(from_block + self.logs_block_range); let spend_events = try_s!( - self.spend_events(swap_contract_address, from_block, to_block) - .compat() - .await + self.events_from_block( + swap_contract_address, + "ReceiverSpent", + from_block, + Some(to_block), + &SWAP_CONTRACT + ) + .await ); + let found = spend_events.iter().find(|event| &event.data.0[..32] == id.as_slice()); if let Some(event) = found { @@ -5358,14 +5275,14 @@ impl EthCoin { } /// Get gas base fee and suggest priority tip fees for the next block (see EIP-1559) - pub async fn get_eip1559_gas_fee(&self) -> Web3RpcResult { + pub async fn get_eip1559_gas_fee(&self, use_simple: bool) -> Web3RpcResult { let coin = self.clone(); let history_estimator_fut = FeePerGasSimpleEstimator::estimate_fee_by_history(&coin); let ctx = MmArc::from_weak(&coin.ctx).ok_or_else(|| MmError::new(Web3RpcError::Internal("ctx is null".into())))?; + let gas_api_conf = ctx.conf["gas_api"].clone(); - if gas_api_conf.is_null() { - debug!("No eth gas api provider config, using only history estimator"); + if gas_api_conf.is_null() || use_simple { return history_estimator_fut .await .map_err(|e| MmError::new(Web3RpcError::Internal(e.to_string()))); @@ -5402,7 +5319,7 @@ impl EthCoin { Ok(PayForGasOption::Legacy(LegacyGasPrice { gas_price })) }, SwapTxFeePolicy::Low | SwapTxFeePolicy::Medium | SwapTxFeePolicy::High => { - let fee_per_gas = coin.get_eip1559_gas_fee().await?; + let fee_per_gas = coin.get_eip1559_gas_fee(false).await?; let pay_result = match swap_fee_policy { SwapTxFeePolicy::Low => PayForGasOption::Eip1559(Eip1559FeePerGas { max_fee_per_gas: fee_per_gas.low.max_fee_per_gas, @@ -5558,16 +5475,6 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - async fn spawn_balance_stream_if_enabled(&self, ctx: &MmArc) -> Result<(), String> { - if let Some(stream_config) = &ctx.event_stream_configuration { - if let EventInitStatus::Failed(err) = EventBehaviour::spawn_if_active(self.clone(), stream_config).await { - return ERR!("Failed spawning balance events. Error: {}", err); - } - } - - Ok(()) - } - /// Requests the nonce from all available nodes and returns the highest nonce available with the list of nodes that returned the highest nonce. /// Transactions will be sent using the nodes that returned the highest nonce. pub fn get_addr_nonce( @@ -5694,7 +5601,7 @@ impl EthTxFeeDetails { impl MmCoin for EthCoin { fn is_asset_chain(&self) -> bool { false } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(get_raw_transaction_impl(self.clone(), req).boxed().compat()) @@ -5892,10 +5799,10 @@ impl MmCoin for EthCoin { stage: FeeApproxStage, ) -> TradePreimageResult { let dex_fee_amount = wei_from_big_decimal(&dex_fee_amount.fee_amount().into(), self.decimals)?; - // pass the dummy params let to_addr = addr_from_raw_pubkey(&DEX_FEE_ADDR_RAW_PUBKEY) .expect("addr_from_raw_pubkey should never fail with DEX_FEE_ADDR_RAW_PUBKEY"); + let my_address = self.derivation_method.single_addr_or_err().await?; let (eth_value, data, call_addr, fee_coin) = match &self.coin_type { EthCoinType::Eth => (dex_fee_amount, Vec::new(), &to_addr, &self.ticker), EthCoinType::Erc20 { platform, token_addr } => { @@ -5905,8 +5812,6 @@ impl MmCoin for EthCoin { }, EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; - - let my_address = self.derivation_method.single_addr_or_err().await?; let fee_policy_for_estimate = get_swap_fee_policy_for_estimate(self.get_swap_transaction_fee_policy()); let pay_for_gas_option = self.get_swap_pay_for_gas_option(fee_policy_for_estimate).await?; let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); @@ -6016,8 +5921,7 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - let sender_addr = try_f!( addr_from_raw_pubkey(validate_fee_args.expected_sender).map_to_mm(ValidatePaymentError::InvalidParameter) ); - let fee_addr = - try_f!(addr_from_raw_pubkey(validate_fee_args.fee_addr).map_to_mm(ValidatePaymentError::InvalidParameter)); + let fee_addr = try_f!(addr_from_raw_pubkey(coin.dex_pubkey()).map_to_mm(ValidatePaymentError::InvalidParameter)); let amount = validate_fee_args.amount.clone(); let min_block_number = validate_fee_args.min_block_number; @@ -6568,7 +6472,6 @@ pub async fn eth_coin_from_conf_and_request( // all spawned futures related to `ETH` coin will be aborted as well. let abortable_system = try_s!(ctx.abortable_system.create_subsystem()); - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; let gas_limit: EthGasLimit = extract_gas_limit_from_conf(conf)?; let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(conf)?; @@ -6596,16 +6499,12 @@ pub async fn eth_coin_from_conf_and_request( address_nonce_locks, erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), - platform_fee_estimator_state, gas_limit, gas_limit_v2, abortable_system, }; - let coin = EthCoin(Arc::new(coin)); - coin.spawn_balance_stream_if_enabled(ctx).await?; - - Ok(coin) + Ok(EthCoin(Arc::new(coin))) } /// Displays the address in mixed-case checksum form @@ -6641,16 +6540,12 @@ pub fn checksum_address(addr: &str) -> String { /// `eth_addr_to_hex` converts Address to hex format. /// Note: the result will be in lowercase. -pub fn eth_addr_to_hex(address: &Address) -> String { format!("{:#02x}", address) } +fn eth_addr_to_hex(address: &Address) -> String { format!("{:#x}", 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) } -/// `display_eth_address` converts Address to mixed-case checksum form. -#[inline] -pub fn display_eth_address(addr: &Address) -> String { checksum_address(ð_addr_to_hex(addr)) } - fn increase_by_percent_one_gwei(num: U256, percent: u64) -> U256 { let one_gwei = U256::from(10u64.pow(9)); let percent = (num / U256::from(100)) * U256::from(percent); @@ -6725,18 +6620,11 @@ pub async fn get_eth_address( let (_, derivation_method) = build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None).await?; - let my_address = match derivation_method { - EthDerivationMethod::SingleAddress(my_address) => my_address, - EthDerivationMethod::HDWallet(_) => { - return Err(MmError::new(GetEthAddressError::UnexpectedDerivationMethod( - UnexpectedDerivationMethod::UnsupportedError("HDWallet is not supported for NFT yet!".to_owned()), - ))); - }, - }; + let my_address = derivation_method.single_addr_or_err().await?; Ok(MyWalletAddress { coin: ticker.to_owned(), - wallet_address: display_eth_address(&my_address), + wallet_address: my_address.display_address(), }) } @@ -7006,6 +6894,7 @@ impl ParseCoinAssocTypes for EthCoin { } fn parse_address(&self, address: &str) -> Result { + // crate `Address::from_str` supports both address variants with and without `0x` prefix Address::from_str(address).map_to_mm(|e| EthAssocTypesError::InvalidHexString(e.to_string())) } @@ -7038,6 +6927,10 @@ impl ToBytes for Address { fn to_bytes(&self) -> Vec { self.0.to_vec() } } +impl AddrToString for Address { + fn addr_to_string(&self) -> String { eth_addr_to_hex(self) } +} + impl ToBytes for BigUint { fn to_bytes(&self) -> Vec { self.to_bytes_be() } } @@ -7315,31 +7208,34 @@ impl TakerCoinSwapOpsV2 for EthCoin { self.refund_taker_payment_with_timelock_impl(args).await } - /// Eth doesnt have preimages + fn skip_taker_payment_spend_preimage(&self) -> bool { true } + + /// Eth skips taker_payment_spend_preimage, as it doesnt need it async fn gen_taker_payment_spend_preimage( &self, - args: &GenTakerPaymentSpendArgs<'_, Self>, + _args: &GenTakerPaymentSpendArgs<'_, Self>, _swap_unique_data: &[u8], ) -> GenPreimageResult { - Ok(TxPreimageWithSig { - preimage: args.taker_tx.clone(), - signature: args.taker_tx.signature(), - }) + MmError::err(TxGenError::Other( + "EVM-based coin doesn't have taker_payment_spend_preimage. Report the Bug!".to_string(), + )) } - /// Eth doesnt have preimages + /// Eth skips taker_payment_spend_preimage, as it doesnt need it async fn validate_taker_payment_spend_preimage( &self, _gen_args: &GenTakerPaymentSpendArgs<'_, Self>, _preimage: &TxPreimageWithSig, ) -> ValidateTakerPaymentSpendPreimageResult { - Ok(()) + MmError::err(ValidateTakerPaymentSpendPreimageError::InvalidPreimage( + "EVM-based coin skips taker_payment_spend_preimage validation. Report the Bug!".to_string(), + )) } - /// Wrapper for [EthCoin::sign_and_broadcast_taker_payment_spend_impl] + /// Eth doesnt have preimages async fn sign_and_broadcast_taker_payment_spend( &self, - _preimage: &TxPreimageWithSig, + _preimage: Option<&TxPreimageWithSig>, gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], _swap_unique_data: &[u8], @@ -7347,14 +7243,20 @@ impl TakerCoinSwapOpsV2 for EthCoin { self.sign_and_broadcast_taker_payment_spend_impl(gen_args, secret).await } - /// Wrapper for [EthCoin::wait_for_taker_payment_spend_impl] - async fn wait_for_taker_payment_spend( + /// Wrapper for [EthCoin::find_taker_payment_spend_tx_impl] + async fn find_taker_payment_spend_tx( &self, taker_payment: &Self::Tx, - _from_block: u64, + from_block: u64, wait_until: u64, - ) -> MmResult { - self.wait_for_taker_payment_spend_impl(taker_payment, wait_until).await + ) -> MmResult { + const CHECK_EVERY: f64 = 10.; + self.find_taker_payment_spend_tx_impl(taker_payment, from_block, wait_until, CHECK_EVERY) + .await + } + + async fn extract_secret_v2(&self, _secret_hash: &[u8], spend_tx: &Self::Tx) -> Result<[u8; 32], String> { + self.extract_secret_v2_impl(spend_tx).await } } @@ -7384,6 +7286,11 @@ impl CommonSwapOpsV2 for EthCoin { fn derive_htlc_pubkey_v2_bytes(&self, swap_unique_data: &[u8]) -> Vec { self.derive_htlc_pubkey_v2(swap_unique_data).to_bytes() } + + #[inline(always)] + fn taker_pubkey_bytes(&self) -> Option> { + Some(self.derive_htlc_pubkey_v2(&[]).to_bytes()) // unique_data not used for non-private coins + } } #[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] @@ -7414,7 +7321,6 @@ impl EthCoin { address_nonce_locks: Arc::clone(&self.address_nonce_locks), erc20_tokens_infos: Arc::clone(&self.erc20_tokens_infos), nfts_infos: Arc::clone(&self.nfts_infos), - platform_fee_estimator_state: Arc::clone(&self.platform_fee_estimator_state), gas_limit: EthGasLimit::default(), gas_limit_v2: EthGasLimitV2::default(), abortable_system: self.abortable_system.create_subsystem().unwrap(), diff --git a/mm2src/coins/eth/eip1559_gas_fee.rs b/mm2src/coins/eth/eip1559_gas_fee.rs deleted file mode 100644 index 32ac186169..0000000000 --- a/mm2src/coins/eth/eip1559_gas_fee.rs +++ /dev/null @@ -1,499 +0,0 @@ -//! Provides estimations of base and priority fee per gas or fetch estimations from a gas api provider - -use super::web3_transport::FeeHistoryResult; -use super::{wei_from_gwei_decimal, wei_to_gwei_decimal, Web3RpcError, Web3RpcResult}; -use crate::{EthCoin, NumConversError}; -use ethereum_types::U256; -use mm2_err_handle::mm_error::MmError; -use mm2_err_handle::or_mm_error::OrMmError; -use mm2_number::BigDecimal; -use num_traits::FromPrimitive; -use std::convert::TryFrom; -use url::Url; -use web3::types::BlockNumber; - -pub(crate) use gas_api::BlocknativeGasApiCaller; -pub(crate) use gas_api::InfuraGasApiCaller; - -use gas_api::{BlocknativeBlockPricesResponse, InfuraFeePerGas}; - -const FEE_PER_GAS_LEVELS: usize = 3; - -/// Indicates which provider was used to get fee per gas estimations -#[derive(Clone, Debug)] -pub enum EstimationSource { - /// filled by default values - Empty, - /// internal simple estimator - Simple, - Infura, - Blocknative, -} - -impl ToString for EstimationSource { - fn to_string(&self) -> String { - match self { - EstimationSource::Empty => "empty".into(), - EstimationSource::Simple => "simple".into(), - EstimationSource::Infura => "infura".into(), - EstimationSource::Blocknative => "blocknative".into(), - } - } -} - -impl Default for EstimationSource { - fn default() -> Self { Self::Empty } -} - -enum PriorityLevelId { - Low = 0, - Medium = 1, - High = 2, -} - -/// Supported gas api providers -#[derive(Deserialize)] -pub enum GasApiProvider { - Infura, - Blocknative, -} - -#[derive(Deserialize)] -pub struct GasApiConfig { - /// gas api provider name to use - pub provider: GasApiProvider, - /// gas api provider or proxy base url (scheme, host and port without the relative part) - pub url: Url, -} - -/// Priority level estimated max fee per gas -#[derive(Clone, Debug, Default)] -pub struct FeePerGasLevel { - /// estimated max priority tip fee per gas in wei - pub max_priority_fee_per_gas: U256, - /// estimated max fee per gas in wei - pub max_fee_per_gas: U256, - /// estimated transaction min wait time in mempool in ms for this priority level - pub min_wait_time: Option, - /// estimated transaction max wait time in mempool in ms for this priority level - pub max_wait_time: Option, -} - -/// Internal struct for estimated fee per gas for several priority levels, in wei -/// low/medium/high levels are supported -#[derive(Default, Debug, Clone)] -pub struct FeePerGasEstimated { - /// base fee for the next block in wei - pub base_fee: U256, - /// estimated low priority fee - pub low: FeePerGasLevel, - /// estimated medium priority fee - pub medium: FeePerGasLevel, - /// estimated high priority fee - pub high: FeePerGasLevel, - /// which estimator used - pub source: EstimationSource, - /// base trend (up or down) - pub base_fee_trend: String, - /// priority trend (up or down) - pub priority_fee_trend: String, -} - -impl TryFrom for FeePerGasEstimated { - type Error = MmError; - - fn try_from(infura_fees: InfuraFeePerGas) -> Result { - Ok(Self { - base_fee: wei_from_gwei_decimal(&infura_fees.estimated_base_fee)?, - low: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.low.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal(&infura_fees.low.suggested_max_priority_fee_per_gas)?, - min_wait_time: Some(infura_fees.low.min_wait_time_estimate), - max_wait_time: Some(infura_fees.low.max_wait_time_estimate), - }, - medium: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.medium.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal( - &infura_fees.medium.suggested_max_priority_fee_per_gas, - )?, - min_wait_time: Some(infura_fees.medium.min_wait_time_estimate), - max_wait_time: Some(infura_fees.medium.max_wait_time_estimate), - }, - high: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.high.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal(&infura_fees.high.suggested_max_priority_fee_per_gas)?, - min_wait_time: Some(infura_fees.high.min_wait_time_estimate), - max_wait_time: Some(infura_fees.high.max_wait_time_estimate), - }, - source: EstimationSource::Infura, - base_fee_trend: infura_fees.base_fee_trend, - priority_fee_trend: infura_fees.priority_fee_trend, - }) - } -} - -impl TryFrom for FeePerGasEstimated { - type Error = MmError; - - fn try_from(block_prices: BlocknativeBlockPricesResponse) -> Result { - if block_prices.block_prices.is_empty() { - return Ok(FeePerGasEstimated::default()); - } - if block_prices.block_prices[0].estimated_prices.len() < FEE_PER_GAS_LEVELS { - return Ok(FeePerGasEstimated::default()); - } - Ok(Self { - base_fee: wei_from_gwei_decimal(&block_prices.block_prices[0].base_fee_per_gas)?, - low: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal( - &block_prices.block_prices[0].estimated_prices[2].max_fee_per_gas, - )?, - max_priority_fee_per_gas: wei_from_gwei_decimal( - &block_prices.block_prices[0].estimated_prices[2].max_priority_fee_per_gas, - )?, - min_wait_time: None, - max_wait_time: None, - }, - medium: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal( - &block_prices.block_prices[0].estimated_prices[1].max_fee_per_gas, - )?, - max_priority_fee_per_gas: wei_from_gwei_decimal( - &block_prices.block_prices[0].estimated_prices[1].max_priority_fee_per_gas, - )?, - min_wait_time: None, - max_wait_time: None, - }, - high: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal( - &block_prices.block_prices[0].estimated_prices[0].max_fee_per_gas, - )?, - max_priority_fee_per_gas: wei_from_gwei_decimal( - &block_prices.block_prices[0].estimated_prices[0].max_priority_fee_per_gas, - )?, - min_wait_time: None, - max_wait_time: None, - }, - source: EstimationSource::Blocknative, - base_fee_trend: String::default(), - priority_fee_trend: String::default(), - }) - } -} - -/// Simple priority fee per gas estimator based on fee history -/// normally used if gas api provider is not available -pub(crate) struct FeePerGasSimpleEstimator {} - -impl FeePerGasSimpleEstimator { - // TODO: add minimal max fee and priority fee - /// depth to look for fee history to estimate priority fees - const FEE_PRIORITY_DEPTH: u64 = 5u64; - - /// percentiles to pass to eth_feeHistory - const HISTORY_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [25.0, 50.0, 75.0]; - - /// percentile to predict next base fee over historical rewards - const BASE_FEE_PERCENTILE: f64 = 75.0; - - /// percentiles to calc max priority fee over historical rewards - const PRIORITY_FEE_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [50.0, 50.0, 50.0]; - - /// adjustment for max fee per gas picked up by sampling - const ADJUST_MAX_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.1, 1.175, 1.25]; // 1.25 assures max_fee_per_gas will be over next block base_fee - - /// adjustment for max priority fee picked up by sampling - const ADJUST_MAX_PRIORITY_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.0, 1.0, 1.0]; - - /// block depth for eth_feeHistory - pub fn history_depth() -> u64 { Self::FEE_PRIORITY_DEPTH } - - /// percentiles for priority rewards obtained with eth_feeHistory - pub fn history_percentiles() -> &'static [f64] { &Self::HISTORY_PERCENTILES } - - /// percentile for vector - fn percentile_of(v: &[U256], percent: f64) -> U256 { - let mut v_mut = v.to_owned(); - v_mut.sort(); - - // validate bounds: - let percent = if percent > 100.0 { 100.0 } else { percent }; - let percent = if percent < 0.0 { 0.0 } else { percent }; - - let value_pos = ((v_mut.len() - 1) as f64 * percent / 100.0).round() as usize; - v_mut[value_pos] - } - - /// Estimate simplified gas priority fees based on fee history - pub async fn estimate_fee_by_history(coin: &EthCoin) -> Web3RpcResult { - let res: Result = coin - .eth_fee_history( - U256::from(Self::history_depth()), - BlockNumber::Latest, - Self::history_percentiles(), - ) - .await; - - match res { - Ok(fee_history) => Ok(Self::calculate_with_history(&fee_history)?), - Err(_) => MmError::err(Web3RpcError::Internal("Eth requests failed".into())), - } - } - - fn predict_base_fee(base_fees: &[U256]) -> U256 { Self::percentile_of(base_fees, Self::BASE_FEE_PERCENTILE) } - - fn priority_fee_for_level( - level: PriorityLevelId, - base_fee: BigDecimal, - fee_history: &FeeHistoryResult, - ) -> Web3RpcResult { - let level_index = level as usize; - let level_rewards = fee_history - .priority_rewards - .as_ref() - .or_mm_err(|| Web3RpcError::Internal("expected reward in eth_feeHistory".into()))? - .iter() - .map(|rewards| rewards.get(level_index).copied().unwrap_or_else(|| U256::from(0))) - .collect::>(); - - // Calculate the max priority fee per gas based on the rewards percentile. - let max_priority_fee_per_gas = Self::percentile_of(&level_rewards, Self::PRIORITY_FEE_PERCENTILES[level_index]); - // Convert the priority fee to BigDecimal gwei, falling back to 0 on error. - let max_priority_fee_per_gas_gwei = - wei_to_gwei_decimal(max_priority_fee_per_gas).unwrap_or_else(|_| BigDecimal::from(0)); - - // Calculate the max fee per gas by adjusting the base fee and adding the priority fee. - let adjust_max_fee = - BigDecimal::from_f64(Self::ADJUST_MAX_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); - let adjust_max_priority_fee = - BigDecimal::from_f64(Self::ADJUST_MAX_PRIORITY_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); - - // TODO: consider use checked ops - let max_fee_per_gas_dec = base_fee * adjust_max_fee + max_priority_fee_per_gas_gwei * adjust_max_priority_fee; - - Ok(FeePerGasLevel { - max_priority_fee_per_gas, - max_fee_per_gas: wei_from_gwei_decimal(&max_fee_per_gas_dec)?, - // TODO: Consider adding default wait times if applicable (and mark them as uncertain). - min_wait_time: None, - max_wait_time: None, - }) - } - - /// estimate priority fees by fee history - fn calculate_with_history(fee_history: &FeeHistoryResult) -> Web3RpcResult { - // For estimation of max fee and max priority fee we use latest block base_fee but adjusted. - // Apparently for this simple fee estimator for assured high priority we should assume - // that the real base_fee may go up by 1,25 (i.e. if the block is full). This is covered by high priority ADJUST_MAX_FEE multiplier - let latest_base_fee = fee_history - .base_fee_per_gas - .first() - .cloned() - .unwrap_or_else(|| U256::from(0)); - let latest_base_fee_dec = wei_to_gwei_decimal(latest_base_fee).unwrap_or_else(|_| BigDecimal::from(0)); - - // The predicted base fee is not used for calculating eip1559 values here and is provided for other purposes - // (f.e if the caller would like to do own estimates of max fee and max priority fee) - let predicted_base_fee = Self::predict_base_fee(&fee_history.base_fee_per_gas); - Ok(FeePerGasEstimated { - base_fee: predicted_base_fee, - low: Self::priority_fee_for_level(PriorityLevelId::Low, latest_base_fee_dec.clone(), fee_history)?, - medium: Self::priority_fee_for_level(PriorityLevelId::Medium, latest_base_fee_dec.clone(), fee_history)?, - high: Self::priority_fee_for_level(PriorityLevelId::High, latest_base_fee_dec, fee_history)?, - source: EstimationSource::Simple, - base_fee_trend: String::default(), - priority_fee_trend: String::default(), - }) - } -} - -mod gas_api { - use std::convert::TryInto; - - use super::FeePerGasEstimated; - use crate::eth::{Web3RpcError, Web3RpcResult}; - use http::StatusCode; - use mm2_err_handle::mm_error::MmError; - use mm2_err_handle::prelude::*; - use mm2_net::transport::slurp_url_with_headers; - use mm2_number::BigDecimal; - use serde_json::{self as json}; - use url::Url; - - lazy_static! { - /// API key for testing - static ref INFURA_GAS_API_AUTH_TEST: String = std::env::var("INFURA_GAS_API_AUTH_TEST").unwrap_or_default(); - } - - #[derive(Clone, Debug, Deserialize)] - pub(crate) struct InfuraFeePerGasLevel { - #[serde(rename = "suggestedMaxPriorityFeePerGas")] - pub suggested_max_priority_fee_per_gas: BigDecimal, - #[serde(rename = "suggestedMaxFeePerGas")] - pub suggested_max_fee_per_gas: BigDecimal, - #[serde(rename = "minWaitTimeEstimate")] - pub min_wait_time_estimate: u32, - #[serde(rename = "maxWaitTimeEstimate")] - pub max_wait_time_estimate: u32, - } - - /// Infura gas api response - /// see https://docs.infura.io/api/infura-expansion-apis/gas-api/api-reference/gasprices-type2 - #[allow(dead_code)] - #[derive(Debug, Deserialize)] - pub(crate) struct InfuraFeePerGas { - pub low: InfuraFeePerGasLevel, - pub medium: InfuraFeePerGasLevel, - pub high: InfuraFeePerGasLevel, - #[serde(rename = "estimatedBaseFee")] - pub estimated_base_fee: BigDecimal, - #[serde(rename = "networkCongestion")] - pub network_congestion: BigDecimal, - #[serde(rename = "latestPriorityFeeRange")] - pub latest_priority_fee_range: Vec, - #[serde(rename = "historicalPriorityFeeRange")] - pub historical_priority_fee_range: Vec, - #[serde(rename = "historicalBaseFeeRange")] - pub historical_base_fee_range: Vec, - #[serde(rename = "priorityFeeTrend")] - pub priority_fee_trend: String, // we are not using enum here bcz values not mentioned in docs could be received - #[serde(rename = "baseFeeTrend")] - pub base_fee_trend: String, - } - - /// Infura gas api provider caller - #[allow(dead_code)] - pub(crate) struct InfuraGasApiCaller {} - - #[allow(dead_code)] - impl InfuraGasApiCaller { - const INFURA_GAS_FEES_ENDPOINT: &'static str = "networks/1/suggestedGasFees"; // Support only main chain - - fn get_infura_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { - let mut url = base_url.clone(); - url.set_path(Self::INFURA_GAS_FEES_ENDPOINT); - let headers = vec![("Authorization", INFURA_GAS_API_AUTH_TEST.as_str())]; - (url, headers) - } - - async fn make_infura_gas_api_request( - url: &Url, - headers: Vec<(&'static str, &'static str)>, - ) -> Result> { - let resp = slurp_url_with_headers(url.as_str(), headers) - .await - .mm_err(|e| e.to_string())?; - if resp.0 != StatusCode::OK { - let error = format!("{} failed with status code {}", url, resp.0); - return MmError::err(error); - } - let estimated_fees = json::from_slice(&resp.2).map_to_mm(|e| e.to_string())?; - Ok(estimated_fees) - } - - /// Fetch fee per gas estimations from infura provider - pub async fn fetch_infura_fee_estimation(base_url: &Url) -> Web3RpcResult { - let (url, headers) = Self::get_infura_gas_api_url(base_url); - let infura_estimated_fees = Self::make_infura_gas_api_request(&url, headers) - .await - .mm_err(Web3RpcError::Transport)?; - infura_estimated_fees.try_into().mm_err(Into::into) - } - } - - lazy_static! { - /// API key for testing - static ref BLOCKNATIVE_GAS_API_AUTH_TEST: String = std::env::var("BLOCKNATIVE_GAS_API_AUTH_TEST").unwrap_or_default(); - } - - #[allow(dead_code)] - #[derive(Clone, Debug, Deserialize)] - pub(crate) struct BlocknativeBlockPrices { - #[serde(rename = "blockNumber")] - pub block_number: u32, - #[serde(rename = "estimatedTransactionCount")] - pub estimated_transaction_count: u32, - #[serde(rename = "baseFeePerGas")] - pub base_fee_per_gas: BigDecimal, - #[serde(rename = "estimatedPrices")] - pub estimated_prices: Vec, - } - - #[allow(dead_code)] - #[derive(Clone, Debug, Deserialize)] - pub(crate) struct BlocknativeEstimatedPrices { - pub confidence: u32, - pub price: BigDecimal, - #[serde(rename = "maxPriorityFeePerGas")] - pub max_priority_fee_per_gas: BigDecimal, - #[serde(rename = "maxFeePerGas")] - pub max_fee_per_gas: BigDecimal, - } - - /// Blocknative gas prices response - /// see https://docs.blocknative.com/gas-prediction/gas-platform - #[allow(dead_code)] - #[derive(Debug, Deserialize)] - pub(crate) struct BlocknativeBlockPricesResponse { - pub system: String, - pub network: String, - pub unit: String, - #[serde(rename = "maxPrice")] - pub max_price: BigDecimal, - #[serde(rename = "currentBlockNumber")] - pub current_block_number: u32, - #[serde(rename = "msSinceLastBlock")] - pub ms_since_last_block: u32, - #[serde(rename = "blockPrices")] - pub block_prices: Vec, - } - - /// Blocknative gas api provider caller - #[allow(dead_code)] - pub(crate) struct BlocknativeGasApiCaller {} - - #[allow(dead_code)] - impl BlocknativeGasApiCaller { - const BLOCKNATIVE_GAS_PRICES_ENDPOINT: &'static str = "gasprices/blockprices"; - const BLOCKNATIVE_GAS_PRICES_LOW: &'static str = "10"; - const BLOCKNATIVE_GAS_PRICES_MEDIUM: &'static str = "50"; - const BLOCKNATIVE_GAS_PRICES_HIGH: &'static str = "90"; - - fn get_blocknative_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { - let mut url = base_url.clone(); - url.set_path(Self::BLOCKNATIVE_GAS_PRICES_ENDPOINT); - url.query_pairs_mut() - .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_LOW) - .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_MEDIUM) - .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_HIGH) - .append_pair("withBaseFees", "true"); - - let headers = vec![("Authorization", BLOCKNATIVE_GAS_API_AUTH_TEST.as_str())]; - (url, headers) - } - - async fn make_blocknative_gas_api_request( - url: &Url, - headers: Vec<(&'static str, &'static str)>, - ) -> Result> { - let resp = slurp_url_with_headers(url.as_str(), headers) - .await - .mm_err(|e| e.to_string())?; - if resp.0 != StatusCode::OK { - let error = format!("{} failed with status code {}", url, resp.0); - return MmError::err(error); - } - let block_prices = json::from_slice(&resp.2).map_err(|e| e.to_string())?; - Ok(block_prices) - } - - /// Fetch fee per gas estimations from blocknative provider - pub async fn fetch_blocknative_fee_estimation(base_url: &Url) -> Web3RpcResult { - let (url, headers) = Self::get_blocknative_gas_api_url(base_url); - let block_prices = Self::make_blocknative_gas_api_request(&url, headers) - .await - .mm_err(Web3RpcError::Transport)?; - block_prices.try_into().mm_err(Into::into) - } - } -} diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index 0cc798ad7e..0cb7afe134 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -1,21 +1,50 @@ -use async_trait::async_trait; -use common::{executor::{AbortSettings, SpawnAbortable, Timer}, - log, Future01CompatExt}; -use ethereum_types::Address; -use futures::{channel::oneshot::{self, Receiver, Sender}, - stream::FuturesUnordered, - StreamExt}; -use instant::Instant; -use mm2_core::mm_ctx::MmArc; +use super::EthCoin; +use crate::{eth::{u256_to_big_decimal, Erc20TokenDetails}, + BalanceError, CoinWithDerivationMethod}; +use common::{executor::Timer, log, Future01CompatExt}; use mm2_err_handle::prelude::MmError; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - ErrorEventName, Event, EventName, EventStreamConfiguration}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; use mm2_number::BigDecimal; + +use async_trait::async_trait; +use compatible_time::Instant; +use ethereum_types::Address; +use futures::{channel::oneshot, stream::FuturesUnordered, StreamExt}; +use serde::Deserialize; +use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; -use super::EthCoin; -use crate::{eth::{u256_to_big_decimal, Erc20TokenDetails}, - BalanceError, CoinWithDerivationMethod, MmCoin}; +#[derive(Deserialize)] +#[serde(deny_unknown_fields, default)] +struct EthBalanceStreamingConfig { + /// The time in seconds to wait before re-polling the balance and streaming. + pub stream_interval_seconds: f64, +} + +impl Default for EthBalanceStreamingConfig { + fn default() -> Self { + Self { + stream_interval_seconds: 10.0, + } + } +} + +pub struct EthBalanceEventStreamer { + /// The period in seconds between each balance check. + interval: f64, + coin: EthCoin, +} + +impl EthBalanceEventStreamer { + pub fn try_new(config: Option, coin: EthCoin) -> serde_json::Result { + let config: EthBalanceStreamingConfig = config.map(serde_json::from_value).unwrap_or(Ok(Default::default()))?; + + Ok(Self { + interval: config.stream_interval_seconds, + coin, + }) + } +} struct BalanceData { ticker: String, @@ -23,6 +52,7 @@ struct BalanceData { balance: BigDecimal, } +#[derive(Serialize)] struct BalanceFetchError { ticker: String, address: String, @@ -113,15 +143,18 @@ async fn fetch_balance( } #[async_trait] -impl EventBehaviour for EthCoin { - fn event_name() -> EventName { EventName::CoinBalance } - - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } - - async fn handle(self, interval: f64, tx: oneshot::Sender) { - const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; - - async fn start_polling(coin: EthCoin, ctx: MmArc, interval: f64) { +impl EventStreamer for EthBalanceEventStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + async fn start_polling(streamer_id: String, broadcaster: Broadcaster, coin: EthCoin, interval: f64) { async fn sleep_remaining_time(interval: f64, now: Instant) { // If the interval is x seconds, // our goal is to broadcast changed balances every x seconds. @@ -145,12 +178,7 @@ impl EventBehaviour for EthCoin { Err(e) => { log::error!("Failed getting addresses for {}. Error: {}", coin.ticker, e); let e = serde_json::to_value(e).expect("Serialization shouldn't fail."); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", EthCoin::error_event_name(), coin.ticker), - e.to_string(), - )) - .await; + broadcaster.broadcast(Event::err(streamer_id.clone(), e)); sleep_remaining_time(interval, now).await; continue; }, @@ -181,60 +209,24 @@ impl EventBehaviour for EthCoin { err.address, err.error ); - let e = serde_json::to_value(err.error).expect("Serialization shouldn't fail."); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}:{}", EthCoin::error_event_name(), err.ticker, err.address), - e.to_string(), - )) - .await; + let e = serde_json::to_value(err).expect("Serialization shouldn't fail."); + broadcaster.broadcast(Event::err(streamer_id.clone(), e)); }, }; } if !balance_updates.is_empty() { - ctx.stream_channel_controller - .broadcast(Event::new( - EthCoin::event_name().to_string(), - json!(balance_updates).to_string(), - )) - .await; + broadcaster.broadcast(Event::new(streamer_id.clone(), json!(balance_updates))); } sleep_remaining_time(interval, now).await; } } - let ctx = match MmArc::from_weak(&self.ctx) { - Some(ctx) => ctx, - None => { - let msg = "MM context must have been initialized already."; - tx.send(EventInitStatus::Failed(msg.to_owned())) - .expect(RECEIVER_DROPPED_MSG); - panic!("{}", msg); - }, - }; - - tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); - start_polling(self, ctx, interval).await - } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - log::info!("{} event is activated for {}", Self::event_name(), self.ticker,); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = - AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::event_name(), self.ticker)); - self.spawner().spawn_with_settings(fut, settings); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive - } + start_polling(self.streamer_id(), broadcaster, self.coin, self.interval).await } } diff --git a/mm2src/coins/eth/eth_hd_wallet.rs b/mm2src/coins/eth/eth_hd_wallet.rs index ceff4ccb79..1b1f2e1a6e 100644 --- a/mm2src/coins/eth/eth_hd_wallet.rs +++ b/mm2src/coins/eth/eth_hd_wallet.rs @@ -11,6 +11,12 @@ pub type EthHDAddress = HDAddress; pub type EthHDAccount = HDAccount; pub type EthHDWallet = HDWallet; +impl DisplayAddress for Address { + /// converts `Address` to mixed-case checksum form. + #[inline] + fn display_address(&self) -> String { checksum_address(&self.addr_to_string()) } +} + #[async_trait] impl ExtractExtendedPubkey for EthCoin { type ExtendedPublicKey = Secp256k1ExtendedPublicKey; @@ -31,8 +37,6 @@ impl ExtractExtendedPubkey for EthCoin { impl HDWalletCoinOps for EthCoin { type HDWallet = EthHDWallet; - fn address_formatter(&self) -> fn(&Address) -> String { display_eth_address } - fn address_from_extended_pubkey( &self, extended_pubkey: &Secp256k1ExtendedPublicKey, diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 3dc6711126..f353f186ba 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -5,7 +5,7 @@ use super::web3_transport::FeeHistoryResult; use super::{web3_transport::Web3Transport, EthCoin}; use common::{custom_futures::timeout::FutureTimerExt, log::debug}; -use instant::Duration; +use compatible_time::Duration; use serde_json::Value; use web3::types::{Address, Block, BlockId, BlockNumber, Bytes, CallRequest, FeeHistory, Filter, Log, Proof, SyncState, Trace, TraceFilter, Transaction, TransactionId, TransactionReceipt, TransactionRequest, Work, H256, diff --git a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs index 3089604ede..d576ee43f6 100644 --- a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs @@ -1,8 +1,7 @@ -use super::{validate_amount, validate_from_to_and_status, EthPaymentType, PaymentMethod, PrepareTxDataError, - ZERO_VALUE}; +use super::{validate_amount, validate_from_to_addresses, EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE}; use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; use crate::eth::{decode_contract_call, get_function_input_data, wei_from_big_decimal, EthCoin, EthCoinType, - MakerPaymentStateV2, SignedEthTx, MAKER_SWAP_V2}; + SignedEthTx, MAKER_SWAP_V2}; use crate::{ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SendMakerPaymentArgs, SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TransactionErr, ValidateMakerPaymentArgs}; use ethabi::{Function, Token}; @@ -18,19 +17,10 @@ use web3::types::TransactionId; const ETH_MAKER_PAYMENT: &str = "ethMakerPayment"; const ERC20_MAKER_PAYMENT: &str = "erc20MakerPayment"; -/// state index for `MakerPayment` structure from `EtomicSwapMakerV2.sol` -/// -/// struct MakerPayment { -/// bytes20 paymentHash; -/// uint32 paymentLockTime; -/// MakerPaymentState state; -/// } -const MAKER_PAYMENT_STATE_INDEX: usize = 2; - -struct MakerPaymentArgs { +struct MakerPaymentArgs<'a> { taker_address: Address, - taker_secret_hash: [u8; 32], - maker_secret_hash: [u8; 32], + taker_secret_hash: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], payment_time_lock: u64, } @@ -43,20 +33,20 @@ struct MakerValidationArgs<'a> { payment_time_lock: u64, } -struct MakerRefundTimelockArgs { +struct MakerRefundTimelockArgs<'a> { payment_amount: U256, taker_address: Address, - taker_secret_hash: [u8; 32], - maker_secret_hash: [u8; 32], + taker_secret_hash: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], payment_time_lock: u64, token_address: Address, } -struct MakerRefundSecretArgs { +struct MakerRefundSecretArgs<'a> { payment_amount: U256, taker_address: Address, - taker_secret: [u8; 32], - maker_secret_hash: [u8; 32], + taker_secret: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], payment_time_lock: u64, token_address: Address, } @@ -136,15 +126,6 @@ impl EthCoin { let maker_secret_hash = args.maker_secret_hash.try_into()?; validate_amount(&args.amount).map_to_mm(ValidatePaymentError::InternalError)?; let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); - let maker_status = self - .payment_status_v2( - maker_swap_v2_contract, - Token::FixedBytes(swap_id.clone()), - &MAKER_SWAP_V2, - EthPaymentType::MakerPayments, - MAKER_PAYMENT_STATE_INDEX, - ) - .await?; let tx_from_rpc = self .transaction(TransactionId::Hash(args.maker_payment_tx.tx_hash())) @@ -156,13 +137,7 @@ impl EthCoin { )) })?; let maker_address = public_to_address(args.maker_pub); - validate_from_to_and_status( - tx_from_rpc, - maker_address, - maker_swap_v2_contract, - maker_status, - MakerPaymentStateV2::PaymentSent as u8, - )?; + validate_from_to_addresses(tx_from_rpc, maker_address, maker_swap_v2_contract)?; let validation_args = { let amount = wei_from_big_decimal(&args.amount, self.decimals)?; @@ -271,7 +246,6 @@ impl EthCoin { ) .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; - let taker_secret = try_tx_s!(args.taker_secret.try_into()); let maker_secret_hash = try_tx_s!(args.maker_secret_hash.try_into()); let payment_amount = try_tx_s!(wei_from_big_decimal(&args.amount, self.decimals)); let args = { @@ -279,7 +253,7 @@ impl EthCoin { MakerRefundSecretArgs { payment_amount, taker_address, - taker_secret, + taker_secret: args.taker_secret, maker_secret_hash, payment_time_lock: args.time_lock, token_address, @@ -326,9 +300,9 @@ impl EthCoin { } /// Prepares data for EtomicSwapMakerV2 contract [ethMakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L30) method - async fn prepare_maker_eth_payment_data(&self, args: &MakerPaymentArgs) -> Result, PrepareTxDataError> { + async fn prepare_maker_eth_payment_data(&self, args: &MakerPaymentArgs<'_>) -> Result, PrepareTxDataError> { let function = MAKER_SWAP_V2.function(ETH_MAKER_PAYMENT)?; - let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let data = function.encode_input(&[ Token::FixedBytes(id), Token::Address(args.taker_address), @@ -342,12 +316,12 @@ impl EthCoin { /// Prepares data for EtomicSwapMakerV2 contract [erc20MakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L64) method async fn prepare_maker_erc20_payment_data( &self, - args: &MakerPaymentArgs, + args: &MakerPaymentArgs<'_>, payment_amount: U256, token_address: Address, ) -> Result, PrepareTxDataError> { let function = MAKER_SWAP_V2.function(ERC20_MAKER_PAYMENT)?; - let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let data = function.encode_input(&[ Token::FixedBytes(id), Token::Uint(payment_amount), @@ -363,10 +337,10 @@ impl EthCoin { /// Prepares data for EtomicSwapMakerV2 contract [refundMakerPaymentTimelock](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L144) method async fn prepare_refund_maker_payment_timelock_data( &self, - args: MakerRefundTimelockArgs, + args: MakerRefundTimelockArgs<'_>, ) -> Result, PrepareTxDataError> { let function = MAKER_SWAP_V2.function("refundMakerPaymentTimelock")?; - let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let data = function.encode_input(&[ Token::FixedBytes(id), Token::Uint(args.payment_amount), @@ -381,10 +355,10 @@ impl EthCoin { /// Prepares data for EtomicSwapMakerV2 contract [refundMakerPaymentSecret](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L190) method async fn prepare_refund_maker_payment_secret_data( &self, - args: MakerRefundSecretArgs, + args: MakerRefundSecretArgs<'_>, ) -> Result, PrepareTxDataError> { let function = MAKER_SWAP_V2.function("refundMakerPaymentSecret")?; - let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let data = function.encode_input(&[ Token::FixedBytes(id), Token::Uint(args.payment_amount), @@ -422,7 +396,7 @@ impl EthCoin { /// Validation function for ETH maker payment data fn validate_eth_maker_payment_data( decoded: &[Token], - args: &MakerValidationArgs, + args: &MakerValidationArgs<'_>, func: &Function, tx_value: U256, ) -> Result<(), MmError> { diff --git a/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs index fea91b0408..ac0594cfa7 100644 --- a/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs @@ -1,21 +1,20 @@ -use super::{check_decoded_length, validate_amount, validate_from_to_and_status, validate_payment_state, - EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE}; +use super::{check_decoded_length, extract_id_from_tx_data, validate_amount, validate_from_to_addresses, + EthPaymentType, PaymentMethod, PrepareTxDataError, SpendTxSearchParams, ZERO_VALUE}; use crate::eth::{decode_contract_call, get_function_input_data, wei_from_big_decimal, EthCoin, EthCoinType, ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, SignedEthTx, SwapTxTypeWithSecretHash, TakerPaymentStateV2, TransactionErr, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs, TAKER_SWAP_V2}; -use crate::{FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, SearchForFundingSpendErr, - WaitForPaymentSpendError}; -use common::executor::Timer; -use common::now_sec; -use ethabi::{Function, Token}; +use crate::{FindPaymentSpendError, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + SearchForFundingSpendErr}; +use enum_derives::EnumFromStringify; +use ethabi::{Contract, Function, Token}; use ethcore_transaction::Action; use ethereum_types::{Address, Public, U256}; use ethkey::public_to_address; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult}; use std::convert::TryInto; -use web3::types::TransactionId; +use web3::types::{BlockNumber, TransactionId}; const ETH_TAKER_PAYMENT: &str = "ethTakerPayment"; const ERC20_TAKER_PAYMENT: &str = "erc20TakerPayment"; @@ -31,32 +30,32 @@ const TAKER_PAYMENT_APPROVE: &str = "takerPaymentApprove"; /// } const TAKER_PAYMENT_STATE_INDEX: usize = 3; -struct TakerFundingArgs { +struct TakerFundingArgs<'a> { dex_fee: U256, payment_amount: U256, maker_address: Address, - taker_secret_hash: [u8; 32], - maker_secret_hash: [u8; 32], + taker_secret_hash: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], funding_time_lock: u64, payment_time_lock: u64, } -struct TakerRefundTimelockArgs { +struct TakerRefundTimelockArgs<'a> { dex_fee: U256, payment_amount: U256, maker_address: Address, - taker_secret_hash: [u8; 32], - maker_secret_hash: [u8; 32], + taker_secret_hash: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], payment_time_lock: u64, token_address: Address, } -struct TakerRefundSecretArgs { +struct TakerRefundSecretArgs<'a> { dex_fee: U256, payment_amount: U256, maker_address: Address, - taker_secret: [u8; 32], - maker_secret_hash: [u8; 32], + taker_secret: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], payment_time_lock: u64, token_address: Address, } @@ -158,15 +157,6 @@ impl EthCoin { let maker_secret_hash = args.maker_secret_hash.try_into()?; validate_amount(&args.trading_amount).map_err(ValidateSwapV2TxError::Internal)?; let swap_id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); - let taker_status = self - .payment_status_v2( - taker_swap_v2_contract, - Token::FixedBytes(swap_id.clone()), - &TAKER_SWAP_V2, - EthPaymentType::TakerPayments, - TAKER_PAYMENT_STATE_INDEX, - ) - .await?; let tx_from_rpc = self.transaction(TransactionId::Hash(args.funding_tx.tx_hash())).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { @@ -176,13 +166,7 @@ impl EthCoin { )) })?; let taker_address = public_to_address(args.taker_pub); - validate_from_to_and_status( - tx_from_rpc, - taker_address, - taker_swap_v2_contract, - taker_status, - TakerPaymentStateV2::PaymentSent as u8, - )?; + validate_from_to_addresses(tx_from_rpc, taker_address, taker_swap_v2_contract)?; let validation_args = { let dex_fee = wei_from_big_decimal(&args.dex_fee.fee_amount().into(), self.decimals)?; @@ -236,18 +220,6 @@ impl EthCoin { .taker_swap_v2_details(ETH_TAKER_PAYMENT, ERC20_TAKER_PAYMENT) .await?; let decoded = try_tx_s!(decode_contract_call(send_func, args.funding_tx.unsigned().data())); - let taker_status = try_tx_s!( - self.payment_status_v2( - taker_swap_v2_contract, - decoded[0].clone(), - &TAKER_SWAP_V2, - EthPaymentType::TakerPayments, - TAKER_PAYMENT_STATE_INDEX, - ) - .await - ); - validate_payment_state(args.funding_tx, taker_status, TakerPaymentStateV2::PaymentSent as u8) - .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let data = try_tx_s!( self.prepare_taker_payment_approve_data(args, decoded, token_address) .await @@ -348,7 +320,6 @@ impl EthCoin { ) .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; - let taker_secret = try_tx_s!(args.taker_secret.try_into()); let maker_secret_hash = try_tx_s!(args.maker_secret_hash.try_into()); let dex_fee = try_tx_s!(wei_from_big_decimal( &args.dex_fee.fee_amount().to_decimal(), @@ -365,7 +336,7 @@ impl EthCoin { dex_fee, payment_amount, maker_address, - taker_secret, + taker_secret: args.taker_secret, maker_secret_hash, payment_time_lock: args.payment_time_lock, token_address, @@ -383,23 +354,27 @@ impl EthCoin { .await } - /// Checks that taker payment state is `TakerApproved`. - /// Accepts a taker-approved payment transaction and returns it if the state is correct. + /// Checks that taker payment state is `TakerApproved`. Called by maker. + /// Accepts a taker payment transaction and returns it if the state is correct. pub(crate) async fn search_for_taker_funding_spend_impl( &self, tx: &SignedEthTx, ) -> Result>, SearchForFundingSpendErr> { let (decoded, taker_swap_v2_contract) = self - .get_decoded_and_swap_contract(tx, TAKER_PAYMENT_APPROVE) + .get_funding_decoded_and_swap_contract(tx) .await .map_err(|e| SearchForFundingSpendErr::Internal(ERRL!("{}", e)))?; let taker_status = self .payment_status_v2( taker_swap_v2_contract, - decoded[0].clone(), // id from takerPaymentApprove + decoded[0].clone(), // id from ethTakerPayment or erc20TakerPayment &TAKER_SWAP_V2, EthPaymentType::TakerPayments, TAKER_PAYMENT_STATE_INDEX, + // Use the latest confirmed block to ensure smart contract has the correct taker payment state (`TakerPaymentStateV2::TakerApproved`) + // before the maker sends the spend transaction, which reveals the maker's secret. + // TPU state machine waits confirmations only for send payment tx, not approve tx. + BlockNumber::Latest, ) .await .map_err(|e| SearchForFundingSpendErr::Internal(ERRL!("{}", e)))?; @@ -409,8 +384,8 @@ impl EthCoin { Ok(None) } - /// Taker swap contract `spendTakerPayment` method is called for EVM based chains. - /// Returns maker spent payment transaction. + /// Returns maker spent taker payment transaction. Called by maker. + /// Taker swap contract's `spendTakerPayment` method is called for EVM-based chains. pub(crate) async fn sign_and_broadcast_taker_payment_spend_impl( &self, gen_args: &GenTakerPaymentSpendArgs<'_, Self>, @@ -421,26 +396,10 @@ impl EthCoin { .gas_limit(&self.coin_type, EthPaymentType::TakerPayments, PaymentMethod::Spend) .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; - let (taker_swap_v2_contract, approve_func, token_address) = self - .taker_swap_v2_details(TAKER_PAYMENT_APPROVE, TAKER_PAYMENT_APPROVE) + let (taker_swap_v2_contract, taker_payment, token_address) = self + .taker_swap_v2_details(ETH_TAKER_PAYMENT, ERC20_TAKER_PAYMENT) .await?; - let decoded = try_tx_s!(decode_contract_call(approve_func, gen_args.taker_tx.unsigned().data())); - let taker_status = try_tx_s!( - self.payment_status_v2( - taker_swap_v2_contract, - decoded[0].clone(), - &TAKER_SWAP_V2, - EthPaymentType::TakerPayments, - TAKER_PAYMENT_STATE_INDEX, - ) - .await - ); - validate_payment_state( - gen_args.taker_tx, - taker_status, - TakerPaymentStateV2::TakerApproved as u8, - ) - .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let decoded = try_tx_s!(decode_contract_call(taker_payment, gen_args.taker_tx.unsigned().data())); let data = try_tx_s!( self.prepare_spend_taker_payment_data(gen_args, secret, decoded, token_address) .await @@ -457,41 +416,43 @@ impl EthCoin { Ok(spend_payment_tx) } - /// Checks that taker payment state is `MakerSpent`. - /// Accepts maker spent payment transaction and returns it if payment status is correct. - pub(crate) async fn wait_for_taker_payment_spend_impl( + pub(crate) async fn find_taker_payment_spend_tx_impl( &self, - taker_payment: &SignedEthTx, + taker_payment: &SignedEthTx, // it's approve_tx in Eth case, as in sign_and_send_taker_funding_spend we return approve_tx tx for it + from_block: u64, wait_until: u64, - ) -> MmResult { - let (decoded, taker_swap_v2_contract) = self - .get_decoded_and_swap_contract(taker_payment, "spendTakerPayment") - .await?; - loop { - let taker_status = self - .payment_status_v2( - taker_swap_v2_contract, - decoded[0].clone(), // id from spendTakerPayment - &TAKER_SWAP_V2, - EthPaymentType::TakerPayments, - TAKER_PAYMENT_STATE_INDEX, - ) - .await?; - if taker_status == U256::from(TakerPaymentStateV2::MakerSpent as u8) { - return Ok(taker_payment.clone()); - } - let now = now_sec(); - if now > wait_until { - return MmError::err(WaitForPaymentSpendError::Timeout { wait_until, now }); - } - Timer::sleep(10.).await; - } + check_every: f64, + ) -> MmResult { + let taker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + FindPaymentSpendError::Internal("Expected swap_v2_contracts to be Some, but found None".to_string()) + })? + .taker_swap_v2_contract; + let id_array = extract_id_from_tx_data(taker_payment.unsigned().data(), &TAKER_SWAP_V2, TAKER_PAYMENT_APPROVE) + .await? + .as_slice() + .try_into()?; + + let params = SpendTxSearchParams { + swap_contract_address: taker_swap_v2_contract, + event_name: "TakerPaymentSpent", + abi_contract: &TAKER_SWAP_V2, + swap_id: &id_array, + from_block, + wait_until, + check_every, + }; + let tx_hash = self.find_transaction_hash_by_event(params).await?; + + let spend_tx = self.wait_for_transaction(tx_hash, wait_until, check_every).await?; + Ok(spend_tx) } /// Prepares data for EtomicSwapTakerV2 contract [ethTakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L44) method - async fn prepare_taker_eth_funding_data(&self, args: &TakerFundingArgs) -> Result, PrepareTxDataError> { + async fn prepare_taker_eth_funding_data(&self, args: &TakerFundingArgs<'_>) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function(ETH_TAKER_PAYMENT)?; - let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let data = function.encode_input(&[ Token::FixedBytes(id), Token::Uint(args.dex_fee), @@ -507,11 +468,11 @@ impl EthCoin { /// Prepares data for EtomicSwapTakerV2 contract [erc20TakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L83) method async fn prepare_taker_erc20_funding_data( &self, - args: &TakerFundingArgs, + args: &TakerFundingArgs<'_>, token_address: Address, ) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function(ERC20_TAKER_PAYMENT)?; - let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let data = function.encode_input(&[ Token::FixedBytes(id), Token::Uint(args.payment_amount), @@ -529,10 +490,10 @@ impl EthCoin { /// Prepares data for EtomicSwapTakerV2 contract [refundTakerPaymentTimelock](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L208) method async fn prepare_taker_refund_payment_timelock_data( &self, - args: TakerRefundTimelockArgs, + args: TakerRefundTimelockArgs<'_>, ) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function("refundTakerPaymentTimelock")?; - let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let data = function.encode_input(&[ Token::FixedBytes(id), Token::Uint(args.payment_amount), @@ -548,10 +509,10 @@ impl EthCoin { /// Prepares data for EtomicSwapTakerV2 contract [refundTakerPaymentSecret](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L267) method async fn prepare_taker_refund_payment_secret_data( &self, - args: &TakerRefundSecretArgs, + args: &TakerRefundSecretArgs<'_>, ) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function("refundTakerPaymentSecret")?; - let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let data = function.encode_input(&[ Token::FixedBytes(id), Token::Uint(args.payment_amount), @@ -575,24 +536,13 @@ impl EthCoin { let function = TAKER_SWAP_V2.function(TAKER_PAYMENT_APPROVE)?; let data = match self.coin_type { EthCoinType::Eth => { - check_decoded_length(&decoded, 7)?; - let dex_fee = match &decoded[1] { - Token::Uint(value) => value, - _ => return Err(PrepareTxDataError::Internal("Invalid token type for dex fee".into())), - }; - let amount = args - .funding_tx - .unsigned() - .value() - .checked_sub(*dex_fee) - .ok_or_else(|| { - PrepareTxDataError::Internal("Underflow occurred while calculating amount".into()) - })?; + let (dex_fee, amount) = + get_dex_fee_and_amount_from_eth_payment_data(&decoded, args.funding_tx.unsigned().value())?; function.encode_input(&[ - decoded[0].clone(), // id from ethTakerPayment - Token::Uint(amount), // calculated payment amount (tx value - dexFee) - decoded[1].clone(), // dexFee from ethTakerPayment - decoded[2].clone(), // receiver from ethTakerPayment + decoded[0].clone(), // id from ethTakerPayment + Token::Uint(amount), // calculated payment amount (tx value - dexFee) + Token::Uint(dex_fee), // dexFee from ethTakerPayment + decoded[2].clone(), // receiver from ethTakerPayment Token::FixedBytes(args.taker_secret_hash.to_vec()), Token::FixedBytes(args.maker_secret_hash.to_vec()), Token::Address(token_address), // should be zero address Address::default() @@ -611,7 +561,9 @@ impl EthCoin { ])? }, EthCoinType::Nft { .. } => { - return Err(PrepareTxDataError::Internal("EthCoinType must be ETH or ERC20".into())) + return Err(PrepareTxDataError::Internal( + "NFT protocol is not supported for ETH and ERC20 Swaps".into(), + )) }, }; Ok(data) @@ -625,19 +577,40 @@ impl EthCoin { decoded: Vec, token_address: Address, ) -> Result, PrepareTxDataError> { - check_decoded_length(&decoded, 7)?; let function = TAKER_SWAP_V2.function("spendTakerPayment")?; let taker_address = public_to_address(args.taker_pub); - let data = function.encode_input(&[ - decoded[0].clone(), // id from takerPaymentApprove - decoded[1].clone(), // amount from takerPaymentApprove - decoded[2].clone(), // dexFee from takerPaymentApprove - Token::Address(taker_address), // taker address - decoded[4].clone(), // takerSecretHash from ethTakerPayment - Token::FixedBytes(secret.to_vec()), // makerSecret - Token::Address(token_address), // tokenAddress - ])?; - Ok(data) + match self.coin_type { + EthCoinType::Eth => { + let (dex_fee, amount) = + get_dex_fee_and_amount_from_eth_payment_data(&decoded, args.taker_tx.unsigned().value())?; + let data = function.encode_input(&[ + decoded[0].clone(), // id from ethTakerPayment + Token::Uint(amount), // calculated payment amount (tx value - dexFee) + Token::Uint(dex_fee), // dexFee from ethTakerPayment + Token::Address(taker_address), // taker address + decoded[3].clone(), // takerSecretHash from ethTakerPayment + Token::FixedBytes(secret.to_vec()), // makerSecret + Token::Address(token_address), // tokenAddress + ])?; + Ok(data) + }, + EthCoinType::Erc20 { .. } => { + check_decoded_length(&decoded, 9)?; + let data = function.encode_input(&[ + decoded[0].clone(), // id from erc20TakerPayment + decoded[1].clone(), // amount from erc20TakerPayment + decoded[2].clone(), // dexFee from erc20TakerPayment + Token::Address(taker_address), // taker address + decoded[5].clone(), // takerSecretHash from erc20TakerPayment + Token::FixedBytes(secret.to_vec()), // makerSecret + Token::Address(token_address), // tokenAddress + ])?; + Ok(data) + }, + EthCoinType::Nft { .. } => Err(PrepareTxDataError::Internal( + "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), + )), + } } /// Retrieves the taker smart contract address, the corresponding function, and the token address. @@ -666,14 +639,14 @@ impl EthCoin { Ok((taker_swap_v2_contract, func, token_address)) } - async fn get_decoded_and_swap_contract( + async fn get_funding_decoded_and_swap_contract( &self, tx: &SignedEthTx, - function_name: &str, ) -> Result<(Vec, Address), PrepareTxDataError> { let decoded = { let func = match self.coin_type { - EthCoinType::Eth | EthCoinType::Erc20 { .. } => TAKER_SWAP_V2.function(function_name)?, + EthCoinType::Eth => TAKER_SWAP_V2.function(ETH_TAKER_PAYMENT)?, + EthCoinType::Erc20 { .. } => TAKER_SWAP_V2.function(ERC20_TAKER_PAYMENT)?, EthCoinType::Nft { .. } => { return Err(PrepareTxDataError::Internal( "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), @@ -692,6 +665,94 @@ impl EthCoin { Ok((decoded, taker_swap_v2_contract)) } + + /// Extracts the maker's secret from the input of transaction that calls the `spendTakerPayment` smart contract method. + /// + /// function spendTakerPayment( + /// bytes32 id, + /// uint256 amount, + /// uint256 dexFee, + /// address taker, + /// bytes32 takerSecretHash, + /// bytes32 makerSecret, + /// address tokenAddress + /// ) + pub(crate) async fn extract_secret_v2_impl(&self, spend_tx: &SignedEthTx) -> Result<[u8; 32], String> { + let function = try_s!(TAKER_SWAP_V2.function("spendTakerPayment")); + // should be 0xcc90c199 + let expected_signature = function.short_signature(); + let signature = &spend_tx.unsigned().data()[0..4]; + if signature != expected_signature { + return ERR!( + "Expected 'spendTakerPayment' contract call signature: {:?}, found {:?}", + expected_signature, + signature + ); + }; + let decoded = try_s!(decode_contract_call(function, spend_tx.unsigned().data())); + if decoded.len() < 7 { + return ERR!("Invalid arguments in 'spendTakerPayment' call: {:?}", decoded); + } + match &decoded[5] { + Token::FixedBytes(secret) => Ok(try_s!(secret.as_slice().try_into())), + _ => ERR!( + "Expected secret to be fixed bytes, but decoded function data is {:?}", + decoded + ), + } + } + + /// Retrieves the payment status from a given smart contract address based on the swap ID and state type. + async fn payment_status_v2( + &self, + swap_address: Address, + swap_id: Token, + contract_abi: &Contract, + payment_type: EthPaymentType, + state_index: usize, + block_number: BlockNumber, + ) -> Result { + let function = contract_abi.function(payment_type.as_str())?; + let data = function.encode_input(&[swap_id])?; + let bytes = self + .call_request( + self.my_addr().await, + swap_address, + None, + Some(data.into()), + block_number, + ) + .await?; + let decoded_tokens = function.decode_output(&bytes.0)?; + + let state = decoded_tokens.get(state_index).ok_or_else(|| { + PaymentStatusErr::Internal(format!( + "Payment status must contain 'state' as the {} token", + state_index + )) + })?; + match state { + Token::Uint(state) => Ok(*state), + _ => Err(PaymentStatusErr::InvalidData(format!( + "Payment status must be Uint, got {:?}", + state + ))), + } + } +} + +#[derive(Debug, Display, EnumFromStringify)] +enum PaymentStatusErr { + #[from_stringify("ethabi::Error")] + #[display(fmt = "ABI error: {}", _0)] + ABIError(String), + #[from_stringify("web3::Error")] + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[display(fmt = "Invalid data error: {}", _0)] + InvalidData(String), } /// Validation function for ETH taker payment data @@ -766,3 +827,23 @@ fn validate_erc20_taker_payment_data( } Ok(()) } + +fn get_dex_fee_and_amount_from_eth_payment_data( + decoded: &Vec, + tx_value: U256, +) -> Result<(U256, U256), PrepareTxDataError> { + check_decoded_length(decoded, 7)?; + let dex_fee = match decoded.get(1) { + Some(Token::Uint(dex_fee)) => *dex_fee, + _ => { + return Err(PrepareTxDataError::Internal(format!( + "Invalid token type for dex fee, got decoded function data: {:?}", + decoded + ))) + }, + }; + let amount = tx_value + .checked_sub(dex_fee) + .ok_or_else(|| PrepareTxDataError::Internal("Underflow occurred while calculating amount".into()))?; + Ok((dex_fee, amount)) +} diff --git a/mm2src/coins/eth/eth_swap_v2/mod.rs b/mm2src/coins/eth/eth_swap_v2/mod.rs index 798a232d56..eb23fd667d 100644 --- a/mm2src/coins/eth/eth_swap_v2/mod.rs +++ b/mm2src/coins/eth/eth_swap_v2/mod.rs @@ -1,13 +1,17 @@ -use crate::eth::{EthCoin, EthCoinType, ParseCoinAssocTypes, Transaction, TransactionErr}; +use crate::eth::{decode_contract_call, signed_tx_from_web3_tx, EthCoin, EthCoinType, Transaction, TransactionErr}; +use crate::{FindPaymentSpendError, MarketCoinOps}; +use common::executor::Timer; +use common::log::{error, info}; +use common::now_sec; use enum_derives::EnumFromStringify; use ethabi::{Contract, Token}; use ethcore_transaction::SignedTransaction as SignedEthTx; -use ethereum_types::{Address, U256}; +use ethereum_types::{Address, H256, U256}; use futures::compat::Future01CompatExt; -use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::prelude::{MmError, MmResult}; use mm2_number::BigDecimal; use num_traits::Signed; -use web3::types::Transaction as Web3Tx; +use web3::types::{Transaction as Web3Tx, TransactionId}; pub(crate) mod eth_maker_swap_v2; pub(crate) mod eth_taker_swap_v2; @@ -41,66 +45,31 @@ pub enum PaymentMethod { #[derive(Debug, Display)] pub(crate) enum ValidatePaymentV2Err { - UnexpectedPaymentState(String), WrongPaymentTx(String), } #[derive(Debug, Display, EnumFromStringify)] -pub(crate) enum PaymentStatusErr { +pub(crate) enum PrepareTxDataError { #[from_stringify("ethabi::Error")] #[display(fmt = "ABI error: {}", _0)] ABIError(String), - #[from_stringify("web3::Error")] - #[display(fmt = "Transport error: {}", _0)] - Transport(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), #[display(fmt = "Invalid data error: {}", _0)] InvalidData(String), } -#[derive(Debug, Display, EnumFromStringify)] -pub(crate) enum PrepareTxDataError { - #[from_stringify("ethabi::Error")] - #[display(fmt = "ABI error: {}", _0)] - ABIError(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), +pub(crate) struct SpendTxSearchParams<'a> { + pub(crate) swap_contract_address: Address, + pub(crate) event_name: &'a str, + pub(crate) abi_contract: &'a Contract, + pub(crate) swap_id: &'a [u8; 32], + pub(crate) from_block: u64, + pub(crate) wait_until: u64, + pub(crate) check_every: f64, } impl EthCoin { - /// Retrieves the payment status from a given smart contract address based on the swap ID and state type. - pub(crate) async fn payment_status_v2( - &self, - swap_address: Address, - swap_id: Token, - contract_abi: &Contract, - payment_type: EthPaymentType, - state_index: usize, - ) -> Result { - let function_name = payment_type.as_str(); - let function = contract_abi.function(function_name)?; - let data = function.encode_input(&[swap_id])?; - let bytes = self - .call_request(self.my_addr().await, swap_address, None, Some(data.into())) - .await?; - let decoded_tokens = function.decode_output(&bytes.0)?; - - let state = decoded_tokens.get(state_index).ok_or_else(|| { - PaymentStatusErr::Internal(format!( - "Payment status must contain 'state' as the {} token", - state_index - )) - })?; - match state { - Token::Uint(state) => Ok(*state), - _ => Err(PaymentStatusErr::InvalidData(format!( - "Payment status must be Uint, got {:?}", - state - ))), - } - } - pub(super) fn get_token_address(&self) -> Result { match &self.coin_type { EthCoinType::Eth => Ok(Address::default()), @@ -108,35 +77,107 @@ impl EthCoin { EthCoinType::Nft { .. } => Err("NFT protocol is not supported for ETH and ERC20 Swaps".to_string()), } } -} -pub(crate) fn validate_payment_state( - tx: &SignedEthTx, - state: U256, - expected_state: u8, -) -> Result<(), PrepareTxDataError> { - if state != U256::from(expected_state) { - return Err(PrepareTxDataError::Internal(format!( - "Payment {:?} state is not `{}`, got `{}`", - tx, expected_state, state - ))); + /// A helper function that scans blocks for a specific event containing the given `swap_id`, + /// returning transaction hash of spend transaction once found. + /// **NOTE:** The current function implementation assumes that `swap_id` is the first 32 bytes of the transaction input data. + pub(crate) async fn find_transaction_hash_by_event( + &self, + params: SpendTxSearchParams<'_>, + ) -> MmResult { + loop { + let now = now_sec(); + if now > params.wait_until { + return MmError::err(FindPaymentSpendError::Timeout { + wait_until: params.wait_until, + now, + }); + } + + let current_block = match self.current_block().compat().await { + Ok(b) => b, + Err(e) => { + error!("Error getting block number: {}", e); + Timer::sleep(params.check_every).await; + continue; + }, + }; + + let mut next_from_block = params.from_block; + while next_from_block <= current_block { + let to_block = std::cmp::min(next_from_block + self.logs_block_range - 1, current_block); + + // Fetch events for the current block range + let events = match self + .events_from_block( + params.swap_contract_address, + params.event_name, + next_from_block, + Some(to_block), + params.abi_contract, + ) + .await + { + Ok(events) => events, + Err(e) => { + error!( + "Error getting {} events from {} to {} block: {}", + params.event_name, next_from_block, to_block, e + ); + Timer::sleep(params.check_every).await; + continue; + }, + }; + + // Check if any event matches the SWAP ID + if let Some(found_event) = events + .into_iter() + .find(|event| event.data.0.len() >= 32 && &event.data.0[..32] == params.swap_id) + { + if let Some(hash) = found_event.transaction_hash { + return Ok(hash); + } + } + + next_from_block += self.logs_block_range; + } + + Timer::sleep(params.check_every).await; + } + } + + /// Waits until the specified transaction is found by its hash or the given timeout is reached + pub(crate) async fn wait_for_transaction( + &self, + tx_hash: H256, + wait_until: u64, + check_every: f64, + ) -> MmResult { + loop { + let now = now_sec(); + if now > wait_until { + return MmError::err(FindPaymentSpendError::Timeout { wait_until, now }); + } + + match self.transaction(TransactionId::Hash(tx_hash)).await { + Ok(Some(t)) => { + let transaction = signed_tx_from_web3_tx(t).map_err(FindPaymentSpendError::Internal)?; + return Ok(transaction); + }, + Ok(None) => info!("Transaction {} not found yet", tx_hash), + Err(e) => error!("Get transaction {} error: {}", tx_hash, e), + }; + + Timer::sleep(check_every).await; + } } - Ok(()) } -pub(crate) fn validate_from_to_and_status( +pub(crate) fn validate_from_to_addresses( tx_from_rpc: &Web3Tx, expected_from: Address, expected_to: Address, - status: U256, - expected_status: u8, ) -> Result<(), MmError> { - if status != U256::from(expected_status) { - return MmError::err(ValidatePaymentV2Err::UnexpectedPaymentState(format!( - "Payment state is not `PaymentSent`, got {}", - status - ))); - } if tx_from_rpc.from != Some(expected_from) { return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( "Payment tx {:?} was sent from wrong address, expected {:?}", @@ -201,3 +242,19 @@ impl EthCoin { Ok(()) } } + +pub(crate) async fn extract_id_from_tx_data( + tx_data: &[u8], + abi_contract: &Contract, + func_name: &str, +) -> Result, FindPaymentSpendError> { + let func = abi_contract.function(func_name)?; + let decoded = decode_contract_call(func, tx_data)?; + match decoded.first() { + Some(Token::FixedBytes(bytes)) => Ok(bytes.clone()), + invalid_token => Err(FindPaymentSpendError::InvalidData(format!( + "Expected Token::FixedBytes, got {:?}", + invalid_token + ))), + } +} diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index e799ba43d3..2d2c43d08f 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::IguanaPrivKey; use common::block_on; +use futures_util::future; use mm2_core::mm_ctx::MmCtxBuilder; cfg_native!( @@ -9,7 +10,7 @@ cfg_native!( use common::{now_sec, block_on_f01}; use ethkey::{Generator, Random}; - use mm2_test_helpers::for_tests::{ETH_MAINNET_CHAIN_ID, ETH_MAINNET_NODE, ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, + use mm2_test_helpers::for_tests::{ETH_MAINNET_CHAIN_ID, ETH_MAINNET_NODES, ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, ETH_SEPOLIA_TOKEN_CONTRACT}; use mocktopus::mocking::*; @@ -163,7 +164,7 @@ fn test_wei_from_big_decimal() { fn test_wait_for_payment_spend_timeout() { const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; - EthCoin::spend_events.mock_safe(|_, _, _, _| MockResult::Return(Box::new(futures01::future::ok(vec![])))); + EthCoin::events_from_block.mock_safe(|_, _, _, _, _, _| MockResult::Return(Box::pin(future::ok(vec![])))); EthCoin::current_block.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(900)))); let key_pair = Random.generate().unwrap(); @@ -216,16 +217,13 @@ fn test_withdraw_impl_manual_fee() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), coin: "ETH".to_string(), - max: false, fee: Some(WithdrawFee::EthGas { gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; block_on_f01(coin.get_balance()).unwrap(); @@ -265,16 +263,13 @@ fn test_withdraw_impl_fee_details() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), coin: "JST".to_string(), - max: false, fee: Some(WithdrawFee::EthGas { gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; block_on_f01(coin.get_balance()).unwrap(); @@ -511,7 +506,7 @@ fn test_get_fee_to_send_taker_fee() { let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); let actual = block_on(coin.get_fee_to_send_taker_fee( - DexFee::Standard(MmNumber::from(dex_fee_amount.clone())), + DexFee::Standard(MmNumber::from(&dex_fee_amount)), FeeApproxStage::WithoutApprox, )) .expect("!get_fee_to_send_taker_fee"); @@ -552,7 +547,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { platform: "ETH".to_string(), token_addr: Address::from_str("0xaD22f63404f7305e4713CcBd4F296f34770513f4").unwrap(), }, - &[ETH_MAINNET_NODE], + ETH_MAINNET_NODES, None, ETH_MAINNET_CHAIN_ID, ); @@ -573,7 +568,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { #[cfg(not(target_arch = "wasm32"))] #[test] fn validate_dex_fee_invalid_sender_eth() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, ETH_MAINNET_NODES, None, ETH_MAINNET_CHAIN_ID); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( @@ -586,7 +581,6 @@ fn validate_dex_fee_invalid_sender_eth() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], @@ -606,7 +600,7 @@ fn validate_dex_fee_invalid_sender_erc() { platform: "ETH".to_string(), token_addr: Address::from_str("0xa1d6df714f91debf4e0802a542e13067f31b8262").unwrap(), }, - &[ETH_MAINNET_NODE], + ETH_MAINNET_NODES, None, ETH_MAINNET_CHAIN_ID, ); @@ -622,7 +616,6 @@ fn validate_dex_fee_invalid_sender_erc() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], @@ -647,7 +640,7 @@ fn sender_compressed_pub(tx: &SignedEthTx) -> [u8; 33] { #[cfg(not(target_arch = "wasm32"))] #[test] fn validate_dex_fee_eth_confirmed_before_min_block() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, ETH_MAINNET_NODES, None, ETH_MAINNET_CHAIN_ID); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( @@ -662,7 +655,6 @@ fn validate_dex_fee_eth_confirmed_before_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &compressed_public, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 11784793, uuid: &[], @@ -682,7 +674,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { platform: "ETH".to_string(), token_addr: Address::from_str("0xa1d6df714f91debf4e0802a542e13067f31b8262").unwrap(), }, - &[ETH_MAINNET_NODE], + ETH_MAINNET_NODES, None, ETH_MAINNET_CHAIN_ID, ); @@ -701,7 +693,6 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &compressed_public, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 11823975, uuid: &[], @@ -716,7 +707,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { #[cfg(not(target_arch = "wasm32"))] #[test] fn test_negotiate_swap_contract_addr_no_fallback() { - let (_, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); + let (_, coin) = eth_coin_for_test(EthCoinType::Eth, ETH_MAINNET_NODES, None, ETH_MAINNET_CHAIN_ID); let input = None; let error = coin.negotiate_swap_contract_addr(input).unwrap_err().into_inner(); @@ -748,7 +739,7 @@ fn test_negotiate_swap_contract_addr_has_fallback() { let (_, coin) = eth_coin_for_test( EthCoinType::Eth, - &[ETH_MAINNET_NODE], + ETH_MAINNET_NODES, Some(fallback), ETH_MAINNET_CHAIN_ID, ); @@ -1045,5 +1036,5 @@ fn test_gas_limit_conf() { fn test_h256_to_str() { let h = H256::from_str("5136701f11060010841c9708c3eb26f6606a070b8ae43f4b98b6d7b10a545258").unwrap(); let b: BytesJson = h.0.to_vec().into(); - println!("H256={}", format!("0x{:02x}", b)); + println!("H256=0x{:02x}", b); } diff --git a/mm2src/coins/eth/fee_estimation/eip1559/block_native.rs b/mm2src/coins/eth/fee_estimation/eip1559/block_native.rs new file mode 100644 index 0000000000..7676fc46cd --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eip1559/block_native.rs @@ -0,0 +1,158 @@ +use super::{EstimationSource, FeePerGasEstimated, FeePerGasLevel, FEE_PER_GAS_LEVELS}; +use crate::eth::{wei_from_gwei_decimal, Web3RpcError, Web3RpcResult}; +use crate::NumConversError; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::prelude::*; +use mm2_net::transport::slurp_url_with_headers; +use mm2_number::BigDecimal; + +use http::StatusCode; +use serde_json::{self as json}; +use std::convert::TryFrom; +use std::convert::TryInto; +use url::Url; + +lazy_static! { + /// API key for testing + static ref BLOCKNATIVE_GAS_API_AUTH_TEST: String = std::env::var("BLOCKNATIVE_GAS_API_AUTH_TEST").unwrap_or_default(); +} + +#[allow(dead_code)] +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct BlocknativeBlockPrices { + #[serde(rename = "blockNumber")] + pub block_number: u32, + #[serde(rename = "estimatedTransactionCount")] + pub estimated_transaction_count: u32, + #[serde(rename = "baseFeePerGas")] + pub base_fee_per_gas: BigDecimal, + #[serde(rename = "estimatedPrices")] + pub estimated_prices: Vec, +} + +#[allow(dead_code)] +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct BlocknativeEstimatedPrices { + pub confidence: u32, + pub price: BigDecimal, + #[serde(rename = "maxPriorityFeePerGas")] + pub max_priority_fee_per_gas: BigDecimal, + #[serde(rename = "maxFeePerGas")] + pub max_fee_per_gas: BigDecimal, +} + +/// Blocknative gas prices response +/// see https://docs.blocknative.com/gas-prediction/gas-platform +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct BlocknativeBlockPricesResponse { + pub system: String, + pub network: String, + pub unit: String, + #[serde(rename = "maxPrice")] + pub max_price: BigDecimal, + #[serde(rename = "currentBlockNumber")] + pub current_block_number: u32, + #[serde(rename = "msSinceLastBlock")] + pub ms_since_last_block: u32, + #[serde(rename = "blockPrices")] + pub block_prices: Vec, +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(block_prices: BlocknativeBlockPricesResponse) -> Result { + if block_prices.block_prices.is_empty() { + return Ok(FeePerGasEstimated::default()); + } + if block_prices.block_prices[0].estimated_prices.len() < FEE_PER_GAS_LEVELS { + return Ok(FeePerGasEstimated::default()); + } + Ok(Self { + base_fee: wei_from_gwei_decimal(&block_prices.block_prices[0].base_fee_per_gas)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[2].max_fee_per_gas, + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[2].max_priority_fee_per_gas, + )?, + min_wait_time: None, + max_wait_time: None, + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[1].max_fee_per_gas, + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[1].max_priority_fee_per_gas, + )?, + min_wait_time: None, + max_wait_time: None, + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[0].max_fee_per_gas, + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[0].max_priority_fee_per_gas, + )?, + min_wait_time: None, + max_wait_time: None, + }, + source: EstimationSource::Blocknative, + base_fee_trend: String::default(), + priority_fee_trend: String::default(), + }) + } +} + +/// Blocknative gas api provider caller +#[allow(dead_code)] +pub(crate) struct BlocknativeGasApiCaller {} + +#[allow(dead_code)] +impl BlocknativeGasApiCaller { + const BLOCKNATIVE_GAS_PRICES_ENDPOINT: &'static str = "gasprices/blockprices"; + const BLOCKNATIVE_GAS_PRICES_LOW: &'static str = "10"; + const BLOCKNATIVE_GAS_PRICES_MEDIUM: &'static str = "50"; + const BLOCKNATIVE_GAS_PRICES_HIGH: &'static str = "90"; + + fn get_blocknative_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { + let mut url = base_url.clone(); + url.set_path(Self::BLOCKNATIVE_GAS_PRICES_ENDPOINT); + url.query_pairs_mut() + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_LOW) + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_MEDIUM) + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_HIGH) + .append_pair("withBaseFees", "true"); + + let headers = vec![("Authorization", BLOCKNATIVE_GAS_API_AUTH_TEST.as_str())]; + (url, headers) + } + + async fn make_blocknative_gas_api_request( + url: &Url, + headers: Vec<(&'static str, &'static str)>, + ) -> Result> { + let resp = slurp_url_with_headers(url.as_str(), headers) + .await + .mm_err(|e| e.to_string())?; + if resp.0 != StatusCode::OK { + let error = format!("{} failed with status code {}", url, resp.0); + return MmError::err(error); + } + let block_prices = json::from_slice(&resp.2).map_err(|e| e.to_string())?; + Ok(block_prices) + } + + /// Fetch fee per gas estimations from blocknative provider + pub async fn fetch_blocknative_fee_estimation(base_url: &Url) -> Web3RpcResult { + let (url, headers) = Self::get_blocknative_gas_api_url(base_url); + let block_prices = Self::make_blocknative_gas_api_request(&url, headers) + .await + .mm_err(Web3RpcError::Transport)?; + block_prices.try_into().mm_err(Into::into) + } +} diff --git a/mm2src/coins/eth/fee_estimation/eip1559/infura.rs b/mm2src/coins/eth/fee_estimation/eip1559/infura.rs new file mode 100644 index 0000000000..2af9e22fc5 --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eip1559/infura.rs @@ -0,0 +1,127 @@ +use super::{EstimationSource, FeePerGasEstimated, FeePerGasLevel}; +use crate::eth::{wei_from_gwei_decimal, Web3RpcError, Web3RpcResult}; +use crate::NumConversError; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::prelude::*; +use mm2_net::transport::slurp_url_with_headers; +use mm2_number::BigDecimal; + +use http::StatusCode; +use serde_json::{self as json}; +use std::convert::TryFrom; +use std::convert::TryInto; +use url::Url; + +lazy_static! { + /// API key for testing + static ref INFURA_GAS_API_AUTH_TEST: String = std::env::var("INFURA_GAS_API_AUTH_TEST").unwrap_or_default(); +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct InfuraFeePerGasLevel { + #[serde(rename = "suggestedMaxPriorityFeePerGas")] + pub suggested_max_priority_fee_per_gas: BigDecimal, + #[serde(rename = "suggestedMaxFeePerGas")] + pub suggested_max_fee_per_gas: BigDecimal, + #[serde(rename = "minWaitTimeEstimate")] + pub min_wait_time_estimate: u32, + #[serde(rename = "maxWaitTimeEstimate")] + pub max_wait_time_estimate: u32, +} + +/// Infura gas api response +/// see https://docs.infura.io/api/infura-expansion-apis/gas-api/api-reference/gasprices-type2 +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct InfuraFeePerGas { + pub low: InfuraFeePerGasLevel, + pub medium: InfuraFeePerGasLevel, + pub high: InfuraFeePerGasLevel, + #[serde(rename = "estimatedBaseFee")] + pub estimated_base_fee: BigDecimal, + #[serde(rename = "networkCongestion")] + pub network_congestion: BigDecimal, + #[serde(rename = "latestPriorityFeeRange")] + pub latest_priority_fee_range: Vec, + #[serde(rename = "historicalPriorityFeeRange")] + pub historical_priority_fee_range: Vec, + #[serde(rename = "historicalBaseFeeRange")] + pub historical_base_fee_range: Vec, + #[serde(rename = "priorityFeeTrend")] + pub priority_fee_trend: String, // we are not using enum here bcz values not mentioned in docs could be received + #[serde(rename = "baseFeeTrend")] + pub base_fee_trend: String, +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(infura_fees: InfuraFeePerGas) -> Result { + Ok(Self { + base_fee: wei_from_gwei_decimal(&infura_fees.estimated_base_fee)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.low.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&infura_fees.low.suggested_max_priority_fee_per_gas)?, + min_wait_time: Some(infura_fees.low.min_wait_time_estimate), + max_wait_time: Some(infura_fees.low.max_wait_time_estimate), + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.medium.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal( + &infura_fees.medium.suggested_max_priority_fee_per_gas, + )?, + min_wait_time: Some(infura_fees.medium.min_wait_time_estimate), + max_wait_time: Some(infura_fees.medium.max_wait_time_estimate), + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.high.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&infura_fees.high.suggested_max_priority_fee_per_gas)?, + min_wait_time: Some(infura_fees.high.min_wait_time_estimate), + max_wait_time: Some(infura_fees.high.max_wait_time_estimate), + }, + source: EstimationSource::Infura, + base_fee_trend: infura_fees.base_fee_trend, + priority_fee_trend: infura_fees.priority_fee_trend, + }) + } +} + +/// Infura gas api provider caller +#[allow(dead_code)] +pub(crate) struct InfuraGasApiCaller {} + +#[allow(dead_code)] +impl InfuraGasApiCaller { + const INFURA_GAS_FEES_ENDPOINT: &'static str = "networks/1/suggestedGasFees"; // Support only main chain + + fn get_infura_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { + let mut url = base_url.clone(); + url.set_path(Self::INFURA_GAS_FEES_ENDPOINT); + let headers = vec![("Authorization", INFURA_GAS_API_AUTH_TEST.as_str())]; + (url, headers) + } + + async fn make_infura_gas_api_request( + url: &Url, + headers: Vec<(&'static str, &'static str)>, + ) -> Result> { + let resp = slurp_url_with_headers(url.as_str(), headers) + .await + .mm_err(|e| e.to_string())?; + if resp.0 != StatusCode::OK { + let error = format!("{} failed with status code {}", url, resp.0); + return MmError::err(error); + } + let estimated_fees = json::from_slice(&resp.2).map_to_mm(|e| e.to_string())?; + Ok(estimated_fees) + } + + /// Fetch fee per gas estimations from infura provider + pub async fn fetch_infura_fee_estimation(base_url: &Url) -> Web3RpcResult { + let (url, headers) = Self::get_infura_gas_api_url(base_url); + let infura_estimated_fees = Self::make_infura_gas_api_request(&url, headers) + .await + .mm_err(Web3RpcError::Transport)?; + infura_estimated_fees.try_into().mm_err(Into::into) + } +} diff --git a/mm2src/coins/eth/fee_estimation/eip1559/mod.rs b/mm2src/coins/eth/fee_estimation/eip1559/mod.rs new file mode 100644 index 0000000000..b4c3ffbfbc --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eip1559/mod.rs @@ -0,0 +1,89 @@ +//! Provides estimations of base and priority fee per gas or fetch estimations from a gas api provider +pub mod block_native; +pub mod infura; +pub mod simple; + +use ethereum_types::U256; +use url::Url; + +const FEE_PER_GAS_LEVELS: usize = 3; + +/// Indicates which provider was used to get fee per gas estimations +#[derive(Clone, Debug)] +pub enum EstimationSource { + /// filled by default values + Empty, + /// internal simple estimator + Simple, + Infura, + Blocknative, +} + +impl ToString for EstimationSource { + fn to_string(&self) -> String { + match self { + EstimationSource::Empty => "empty".into(), + EstimationSource::Simple => "simple".into(), + EstimationSource::Infura => "infura".into(), + EstimationSource::Blocknative => "blocknative".into(), + } + } +} + +impl Default for EstimationSource { + fn default() -> Self { Self::Empty } +} + +enum PriorityLevelId { + Low = 0, + Medium = 1, + High = 2, +} + +/// Supported gas api providers +#[derive(Clone, Deserialize)] +pub enum GasApiProvider { + Infura, + Blocknative, +} + +#[derive(Clone, Deserialize)] +pub struct GasApiConfig { + /// gas api provider name to use + pub provider: GasApiProvider, + /// gas api provider or proxy base url (scheme, host and port without the relative part) + pub url: Url, +} + +/// Priority level estimated max fee per gas +#[derive(Clone, Debug, Default)] +pub struct FeePerGasLevel { + /// estimated max priority tip fee per gas in wei + pub max_priority_fee_per_gas: U256, + /// estimated max fee per gas in wei + pub max_fee_per_gas: U256, + /// estimated transaction min wait time in mempool in ms for this priority level + pub min_wait_time: Option, + /// estimated transaction max wait time in mempool in ms for this priority level + pub max_wait_time: Option, +} + +/// Internal struct for estimated fee per gas for several priority levels, in wei +/// low/medium/high levels are supported +#[derive(Default, Debug, Clone)] +pub struct FeePerGasEstimated { + /// base fee for the next block in wei + pub base_fee: U256, + /// estimated low priority fee + pub low: FeePerGasLevel, + /// estimated medium priority fee + pub medium: FeePerGasLevel, + /// estimated high priority fee + pub high: FeePerGasLevel, + /// which estimator used + pub source: EstimationSource, + /// base trend (up or down) + pub base_fee_trend: String, + /// priority trend (up or down) + pub priority_fee_trend: String, +} diff --git a/mm2src/coins/eth/fee_estimation/eip1559/simple.rs b/mm2src/coins/eth/fee_estimation/eip1559/simple.rs new file mode 100644 index 0000000000..995d27447c --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eip1559/simple.rs @@ -0,0 +1,136 @@ +use super::{EstimationSource, FeePerGasEstimated, FeePerGasLevel, PriorityLevelId, FEE_PER_GAS_LEVELS}; +use crate::eth::web3_transport::FeeHistoryResult; +use crate::eth::{wei_from_gwei_decimal, wei_to_gwei_decimal, EthCoin, Web3RpcError, Web3RpcResult}; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::or_mm_error::OrMmError; +use mm2_number::BigDecimal; + +use ethereum_types::U256; +use num_traits::FromPrimitive; +use web3::types::BlockNumber; + +/// Simple priority fee per gas estimator based on fee history +/// normally used if gas api provider is not available +pub(crate) struct FeePerGasSimpleEstimator {} + +impl FeePerGasSimpleEstimator { + // TODO: add minimal max fee and priority fee + /// depth to look for fee history to estimate priority fees + const FEE_PRIORITY_DEPTH: u64 = 5u64; + + /// percentiles to pass to eth_feeHistory + const HISTORY_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [25.0, 50.0, 75.0]; + + /// percentile to predict next base fee over historical rewards + const BASE_FEE_PERCENTILE: f64 = 75.0; + + /// percentiles to calc max priority fee over historical rewards + const PRIORITY_FEE_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [50.0, 50.0, 50.0]; + + /// adjustment for max fee per gas picked up by sampling + const ADJUST_MAX_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.1, 1.175, 1.25]; // 1.25 assures max_fee_per_gas will be over next block base_fee + + /// adjustment for max priority fee picked up by sampling + const ADJUST_MAX_PRIORITY_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.0, 1.0, 1.0]; + + /// block depth for eth_feeHistory + pub fn history_depth() -> u64 { Self::FEE_PRIORITY_DEPTH } + + /// percentiles for priority rewards obtained with eth_feeHistory + pub fn history_percentiles() -> &'static [f64] { &Self::HISTORY_PERCENTILES } + + /// percentile for vector + fn percentile_of(v: &[U256], percent: f64) -> U256 { + let mut v_mut = v.to_owned(); + v_mut.sort(); + + // validate bounds: + let percent = if percent > 100.0 { 100.0 } else { percent }; + let percent = if percent < 0.0 { 0.0 } else { percent }; + + let value_pos = ((v_mut.len() - 1) as f64 * percent / 100.0).round() as usize; + v_mut[value_pos] + } + + /// Estimate simplified gas priority fees based on fee history + pub async fn estimate_fee_by_history(coin: &EthCoin) -> Web3RpcResult { + let res: Result = coin + .eth_fee_history( + U256::from(Self::history_depth()), + BlockNumber::Latest, + Self::history_percentiles(), + ) + .await; + + match res { + Ok(fee_history) => Ok(Self::calculate_with_history(&fee_history)?), + Err(_) => MmError::err(Web3RpcError::Internal("Eth requests failed".into())), + } + } + + fn predict_base_fee(base_fees: &[U256]) -> U256 { Self::percentile_of(base_fees, Self::BASE_FEE_PERCENTILE) } + + fn priority_fee_for_level( + level: PriorityLevelId, + base_fee: BigDecimal, + fee_history: &FeeHistoryResult, + ) -> Web3RpcResult { + let level_index = level as usize; + let level_rewards = fee_history + .priority_rewards + .as_ref() + .or_mm_err(|| Web3RpcError::Internal("expected reward in eth_feeHistory".into()))? + .iter() + .map(|rewards| rewards.get(level_index).copied().unwrap_or_else(|| U256::from(0))) + .collect::>(); + + // Calculate the max priority fee per gas based on the rewards percentile. + let max_priority_fee_per_gas = Self::percentile_of(&level_rewards, Self::PRIORITY_FEE_PERCENTILES[level_index]); + // Convert the priority fee to BigDecimal gwei, falling back to 0 on error. + let max_priority_fee_per_gas_gwei = + wei_to_gwei_decimal(max_priority_fee_per_gas).unwrap_or_else(|_| BigDecimal::from(0)); + + // Calculate the max fee per gas by adjusting the base fee and adding the priority fee. + let adjust_max_fee = + BigDecimal::from_f64(Self::ADJUST_MAX_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); + let adjust_max_priority_fee = + BigDecimal::from_f64(Self::ADJUST_MAX_PRIORITY_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); + + // TODO: consider use checked ops + let max_fee_per_gas_dec = base_fee * adjust_max_fee + max_priority_fee_per_gas_gwei * adjust_max_priority_fee; + + Ok(FeePerGasLevel { + max_priority_fee_per_gas, + max_fee_per_gas: wei_from_gwei_decimal(&max_fee_per_gas_dec)?, + // TODO: Consider adding default wait times if applicable (and mark them as uncertain). + min_wait_time: None, + max_wait_time: None, + }) + } + + /// estimate priority fees by fee history + fn calculate_with_history(fee_history: &FeeHistoryResult) -> Web3RpcResult { + // For estimation of max fee and max priority fee we use latest block base_fee but adjusted. + // Apparently for this simple fee estimator for assured high priority we should assume + // that the real base_fee may go up by 1,25 (i.e. if the block is full). This is covered by high priority ADJUST_MAX_FEE multiplier + let latest_base_fee = fee_history + .base_fee_per_gas + .first() + .cloned() + .unwrap_or_else(|| U256::from(0)); + let latest_base_fee_dec = wei_to_gwei_decimal(latest_base_fee).unwrap_or_else(|_| BigDecimal::from(0)); + + // The predicted base fee is not used for calculating eip1559 values here and is provided for other purposes + // (f.e if the caller would like to do own estimates of max fee and max priority fee) + let predicted_base_fee = Self::predict_base_fee(&fee_history.base_fee_per_gas); + Ok(FeePerGasEstimated { + base_fee: predicted_base_fee, + low: Self::priority_fee_for_level(PriorityLevelId::Low, latest_base_fee_dec.clone(), fee_history)?, + medium: Self::priority_fee_for_level(PriorityLevelId::Medium, latest_base_fee_dec.clone(), fee_history)?, + high: Self::priority_fee_for_level(PriorityLevelId::High, latest_base_fee_dec, fee_history)?, + source: EstimationSource::Simple, + base_fee_trend: String::default(), + priority_fee_trend: String::default(), + }) + } +} diff --git a/mm2src/coins/eth/fee_estimation/eth_fee_events.rs b/mm2src/coins/eth/fee_estimation/eth_fee_events.rs new file mode 100644 index 0000000000..0af1f13579 --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/eth_fee_events.rs @@ -0,0 +1,95 @@ +use super::ser::FeePerGasEstimated; +use crate::eth::EthCoin; +use common::executor::Timer; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; + +use async_trait::async_trait; +use compatible_time::Instant; +use futures::channel::oneshot; +use serde::Deserialize; +use std::convert::TryFrom; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +/// Types of estimators available. +/// Simple - simple internal gas price estimator based on historical data. +/// Provider - gas price estimator using external provider (using gas api). +pub enum EstimatorType { + Simple, + Provider, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields, default)] +pub struct EthFeeStreamingConfig { + /// The time in seconds to wait before re-estimating the gas fees. + pub estimate_every: f64, + /// The type of the estimator to use. + pub estimator_type: EstimatorType, +} + +impl Default for EthFeeStreamingConfig { + fn default() -> Self { + Self { + // TODO: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2172#discussion_r1785054117 + estimate_every: 15.0, + estimator_type: EstimatorType::Simple, + } + } +} + +pub struct EthFeeEventStreamer { + config: EthFeeStreamingConfig, + coin: EthCoin, +} + +impl EthFeeEventStreamer { + #[inline(always)] + pub fn new(config: EthFeeStreamingConfig, coin: EthCoin) -> Self { Self { config, coin } } +} + +#[async_trait] +impl EventStreamer for EthFeeEventStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { format!("FEE_ESTIMATION:{}", self.coin.ticker) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + let use_simple = matches!(self.config.estimator_type, EstimatorType::Simple); + loop { + let now = Instant::now(); + match self + .coin + .get_eip1559_gas_fee(use_simple) + .await + .map(FeePerGasEstimated::try_from) + { + Ok(Ok(fee)) => { + let fee = serde_json::to_value(fee).expect("Serialization shouldn't fail"); + broadcaster.broadcast(Event::new(self.streamer_id(), fee)); + }, + Ok(Err(err)) => { + let err = json!({ "error": err.to_string() }); + broadcaster.broadcast(Event::err(self.streamer_id(), err)); + }, + Err(err) => { + let err = serde_json::to_value(err).expect("Serialization shouldn't fail"); + broadcaster.broadcast(Event::err(self.streamer_id(), err)); + }, + } + let sleep_time = self.config.estimate_every - now.elapsed().as_secs_f64(); + if sleep_time >= 0.1 { + Timer::sleep(sleep_time).await; + } + } + } +} diff --git a/mm2src/coins/eth/fee_estimation/mod.rs b/mm2src/coins/eth/fee_estimation/mod.rs new file mode 100644 index 0000000000..ffe9683acd --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod eip1559; +pub mod eth_fee_events; +pub mod rpc; +mod ser; diff --git a/mm2src/coins/eth/fee_estimation/rpc.rs b/mm2src/coins/eth/fee_estimation/rpc.rs new file mode 100644 index 0000000000..6fb3b84498 --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/rpc.rs @@ -0,0 +1,54 @@ +use super::eth_fee_events::EstimatorType; +use super::ser::FeePerGasEstimated; +use crate::{lp_coinfind, MmCoinEnum}; +use common::HttpStatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::{MmError, MmResult}; + +use http::StatusCode; +use std::convert::TryFrom; + +#[derive(Deserialize)] +pub struct GetFeeEstimationRequest { + coin: String, + estimator_type: EstimatorType, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetFeeEstimationRequestError { + CoinNotFound, + Internal(String), + CoinNotSupported, +} + +impl HttpStatusCode for GetFeeEstimationRequestError { + fn status_code(&self) -> StatusCode { + match self { + GetFeeEstimationRequestError::CoinNotFound => StatusCode::NOT_FOUND, + GetFeeEstimationRequestError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + GetFeeEstimationRequestError::CoinNotSupported => StatusCode::NOT_IMPLEMENTED, + } + } +} + +pub async fn get_eth_estimated_fee_per_gas( + ctx: MmArc, + req: GetFeeEstimationRequest, +) -> MmResult { + match lp_coinfind(&ctx, &req.coin).await { + Ok(Some(MmCoinEnum::EthCoin(coin))) => { + let use_simple = matches!(req.estimator_type, EstimatorType::Simple); + let fee = coin + .get_eip1559_gas_fee(use_simple) + .await + .map_err(|e| GetFeeEstimationRequestError::Internal(e.to_string()))?; + let ser_fee = + FeePerGasEstimated::try_from(fee).map_err(|e| GetFeeEstimationRequestError::Internal(e.to_string()))?; + Ok(ser_fee) + }, + Ok(Some(_)) => MmError::err(GetFeeEstimationRequestError::CoinNotSupported), + Ok(None) => MmError::err(GetFeeEstimationRequestError::CoinNotFound), + Err(e) => MmError::err(GetFeeEstimationRequestError::Internal(e)), + } +} diff --git a/mm2src/coins/eth/fee_estimation/ser.rs b/mm2src/coins/eth/fee_estimation/ser.rs new file mode 100644 index 0000000000..2446ce260c --- /dev/null +++ b/mm2src/coins/eth/fee_estimation/ser.rs @@ -0,0 +1,80 @@ +//! Serializable version of fee estimation data. +use crate::eth::{fee_estimation::eip1559, wei_to_gwei_decimal}; +use crate::NumConversError; +use mm2_err_handle::mm_error::MmError; +use mm2_number::BigDecimal; + +use std::convert::TryFrom; + +/// Estimated fee per gas units +#[derive(Serialize)] +pub enum EstimationUnits { + Gwei, +} + +/// Priority level estimated max fee per gas +#[derive(Serialize)] +pub struct FeePerGasLevel { + /// estimated max priority tip fee per gas in gwei + pub max_priority_fee_per_gas: BigDecimal, + /// estimated max fee per gas in gwei + pub max_fee_per_gas: BigDecimal, + /// estimated transaction min wait time in mempool in ms for this priority level + pub min_wait_time: Option, + /// estimated transaction max wait time in mempool in ms for this priority level + pub max_wait_time: Option, +} + +/// External struct for estimated fee per gas for several priority levels, in gwei +/// low/medium/high levels are supported +#[derive(Serialize)] +pub struct FeePerGasEstimated { + /// base fee for the next block in gwei + pub base_fee: BigDecimal, + /// estimated low priority fee + pub low: FeePerGasLevel, + /// estimated medium priority fee + pub medium: FeePerGasLevel, + /// estimated high priority fee + pub high: FeePerGasLevel, + /// which estimator used + pub source: String, + /// base trend (up or down) + pub base_fee_trend: String, + /// priority trend (up or down) + pub priority_fee_trend: String, + /// fee units + pub units: EstimationUnits, +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(fees: eip1559::FeePerGasEstimated) -> Result { + Ok(Self { + base_fee: wei_to_gwei_decimal(fees.base_fee)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal(fees.low.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.low.max_priority_fee_per_gas)?, + min_wait_time: fees.low.min_wait_time, + max_wait_time: fees.low.max_wait_time, + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal(fees.medium.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.medium.max_priority_fee_per_gas)?, + min_wait_time: fees.medium.min_wait_time, + max_wait_time: fees.medium.max_wait_time, + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal(fees.high.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.high.max_priority_fee_per_gas)?, + min_wait_time: fees.high.min_wait_time, + max_wait_time: fees.high.max_wait_time, + }, + source: fees.source.to_string(), + base_fee_trend: fees.base_fee_trend, + priority_fee_trend: fees.priority_fee_trend, + units: EstimationUnits::Gwei, + }) + } +} diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs index cc6d5cd375..781748832c 100644 --- a/mm2src/coins/eth/for_tests.rs +++ b/mm2src/coins/eth/for_tests.rs @@ -76,7 +76,6 @@ pub(crate) fn eth_coin_from_keypair( max_eth_tx_type: None, erc20_tokens_infos: Default::default(), nfts_infos: Arc::new(Default::default()), - platform_fee_estimator_state: Arc::new(FeeEstimatorState::CoinNotSupported), gas_limit, gas_limit_v2, abortable_system: AbortableQueue::default(), diff --git a/mm2src/coins/eth/nft_swap_v2/mod.rs b/mm2src/coins/eth/nft_swap_v2/mod.rs index f4c909bd32..ab81124792 100644 --- a/mm2src/coins/eth/nft_swap_v2/mod.rs +++ b/mm2src/coins/eth/nft_swap_v2/mod.rs @@ -1,6 +1,6 @@ -use ethabi::{Contract, Token}; +use ethabi::Token; use ethcore_transaction::Action; -use ethereum_types::{Address, U256}; +use ethereum_types::U256; use ethkey::public_to_address; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult}; @@ -10,10 +10,9 @@ use web3::types::TransactionId; use super::ContractType; use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; -use crate::eth::eth_swap_v2::{validate_from_to_and_status, validate_payment_state, EthPaymentType, PaymentMethod, - PaymentStatusErr, PrepareTxDataError, ZERO_VALUE}; -use crate::eth::{decode_contract_call, EthCoin, EthCoinType, MakerPaymentStateV2, SignedEthTx, ERC1155_CONTRACT, - ERC721_CONTRACT, NFT_MAKER_SWAP_V2}; +use crate::eth::eth_swap_v2::{validate_from_to_addresses, PaymentMethod, PrepareTxDataError, ZERO_VALUE}; +use crate::eth::{decode_contract_call, EthCoin, EthCoinType, SignedEthTx, ERC1155_CONTRACT, ERC721_CONTRACT, + NFT_MAKER_SWAP_V2}; use crate::{ParseCoinAssocTypes, RefundNftMakerPaymentArgs, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, TransactionErr, ValidateNftMakerPaymentArgs}; @@ -81,15 +80,6 @@ impl EthCoin { let token_address = args.nft_swap_info.token_address; let maker_address = public_to_address(args.maker_pub); let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); - let maker_status = self - .payment_status_v2( - nft_maker_swap_v2_contract, - Token::FixedBytes(swap_id.clone()), - &NFT_MAKER_SWAP_V2, - EthPaymentType::MakerPayments, - 2, - ) - .await?; let tx_from_rpc = self .transaction(TransactionId::Hash(args.maker_payment_tx.tx_hash())) .await?; @@ -99,13 +89,7 @@ impl EthCoin { args.maker_payment_tx.tx_hash() )) })?; - validate_from_to_and_status( - tx_from_rpc, - maker_address, - *token_address, - maker_status, - MakerPaymentStateV2::PaymentSent as u8, - )?; + validate_from_to_addresses(tx_from_rpc, maker_address, *token_address)?; let (decoded, bytes_index) = get_decoded_tx_data_and_bytes_index(contract_type, &tx_from_rpc.input.0)?; @@ -163,18 +147,8 @@ impl EthCoin { args.maker_payment_tx.unsigned().data() )); - let (state, htlc_params) = try_tx_s!( - self.status_and_htlc_params_from_tx_data( - nft_maker_swap_v2_contract, - &NFT_MAKER_SWAP_V2, - &decoded, - bytes_index, - EthPaymentType::MakerPayments, - 2 - ) - .await - ); - let data = try_tx_s!(self.prepare_spend_nft_maker_v2_data(&args, decoded, htlc_params, state)); + let htlc_params = try_tx_s!(self.htlc_params_from_tx_data(&decoded, bytes_index,).await); + let data = try_tx_s!(self.prepare_spend_nft_maker_v2_data(&args, decoded, htlc_params)); let gas_limit = self .gas_limit_v2 .nft_gas_limit(args.contract_type, PaymentMethod::Spend); @@ -210,19 +184,8 @@ impl EthCoin { args.maker_payment_tx.unsigned().data() )); - let (state, htlc_params) = try_tx_s!( - self.status_and_htlc_params_from_tx_data( - nft_maker_swap_v2_contract, - &NFT_MAKER_SWAP_V2, - &decoded, - bytes_index, - EthPaymentType::MakerPayments, - 2 - ) - .await - ); - let data = - try_tx_s!(self.prepare_refund_nft_maker_payment_v2_timelock(&args, decoded, htlc_params, state)); + let htlc_params = try_tx_s!(self.htlc_params_from_tx_data(&decoded, bytes_index,).await); + let data = try_tx_s!(self.prepare_refund_nft_maker_payment_v2_timelock(&args, decoded, htlc_params)); let gas_limit = self .gas_limit_v2 .nft_gas_limit(args.contract_type, PaymentMethod::RefundTimelock); @@ -258,20 +221,9 @@ impl EthCoin { args.maker_payment_tx.unsigned().data() )); - let (state, htlc_params) = try_tx_s!( - self.status_and_htlc_params_from_tx_data( - nft_maker_swap_v2_contract, - &NFT_MAKER_SWAP_V2, - &decoded, - bytes_index, - EthPaymentType::MakerPayments, - 2 - ) - .await - ); - - let data = - try_tx_s!(self.prepare_refund_nft_maker_payment_v2_secret(&args, decoded, htlc_params, state)); + let htlc_params = try_tx_s!(self.htlc_params_from_tx_data(&decoded, bytes_index,).await); + + let data = try_tx_s!(self.prepare_refund_nft_maker_payment_v2_secret(&args, decoded, htlc_params)); let gas_limit = self .gas_limit_v2 .nft_gas_limit(args.contract_type, PaymentMethod::RefundSecret); @@ -351,10 +303,7 @@ impl EthCoin { args: &SpendNftMakerPaymentArgs<'_, Self>, decoded: Vec, htlc_params: Vec, - state: U256, ) -> Result, PrepareTxDataError> { - validate_payment_state(args.maker_payment_tx, state, MakerPaymentStateV2::PaymentSent as u8)?; - let spend_func = match args.contract_type { ContractType::Erc1155 => NFT_MAKER_SWAP_V2.function("spendErc1155MakerPayment")?, ContractType::Erc721 => NFT_MAKER_SWAP_V2.function("spendErc721MakerPayment")?, @@ -382,10 +331,7 @@ impl EthCoin { args: &RefundNftMakerPaymentArgs<'_, Self>, decoded: Vec, htlc_params: Vec, - state: U256, ) -> Result, PrepareTxDataError> { - validate_payment_state(args.maker_payment_tx, state, MakerPaymentStateV2::PaymentSent as u8)?; - let refund_func = match args.contract_type { ContractType::Erc1155 => NFT_MAKER_SWAP_V2.function("refundErc1155MakerPaymentTimelock")?, ContractType::Erc721 => NFT_MAKER_SWAP_V2.function("refundErc721MakerPaymentTimelock")?, @@ -413,10 +359,7 @@ impl EthCoin { args: &RefundNftMakerPaymentArgs<'_, Self>, decoded: Vec, htlc_params: Vec, - state: U256, ) -> Result, PrepareTxDataError> { - validate_payment_state(args.maker_payment_tx, state, MakerPaymentStateV2::PaymentSent as u8)?; - let refund_func = match args.contract_type { ContractType::Erc1155 => NFT_MAKER_SWAP_V2.function("refundErc1155MakerPaymentSecret")?, ContractType::Erc721 => NFT_MAKER_SWAP_V2.function("refundErc721MakerPaymentSecret")?, @@ -438,39 +381,22 @@ impl EthCoin { Ok(data) } - async fn status_and_htlc_params_from_tx_data( + async fn htlc_params_from_tx_data( &self, - swap_address: Address, - contract_abi: &Contract, decoded_data: &[Token], index: usize, - payment_type: EthPaymentType, - state_index: usize, - ) -> Result<(U256, Vec), PaymentStatusErr> { + ) -> Result, PrepareTxDataError> { let data_bytes = match decoded_data.get(index) { Some(Token::Bytes(data_bytes)) => data_bytes, _ => { - return Err(PaymentStatusErr::InvalidData(ERRL!( + return Err(PrepareTxDataError::InvalidData(ERRL!( "Failed to decode HTLCParams from data_bytes" ))) }, }; - let htlc_params = - ethabi::decode(htlc_params(), data_bytes).map_err(|e| PaymentStatusErr::ABIError(ERRL!("{}", e)))?; - - let state = self - .payment_status_v2( - swap_address, - // swap_id has 0 index - htlc_params[0].clone(), - contract_abi, - payment_type, - state_index, - ) - .await?; - - Ok((state, htlc_params)) + ethabi::decode(htlc_params(), data_bytes).map_err(|e| PrepareTxDataError::ABIError(ERRL!("{}", e)))?; + Ok(htlc_params) } } diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index cee2313ba2..4e04bfebed 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -8,9 +8,9 @@ use crate::nft::nft_errors::{GetNftInfoError, ParseChainTypeError}; use crate::nft::nft_structs::Chain; #[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; use common::executor::AbortedError; +use compatible_time::Instant; use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError}; use enum_derives::EnumFromTrait; -use instant::Instant; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; @@ -402,7 +402,7 @@ impl EthCoin { return MmError::err(EthTokenActivationError::CustomTokenError( CustomTokenError::TokenWithSameContractAlreadyActivated { ticker: token.ticker().to_string(), - contract_address: display_eth_address(&protocol.token_addr), + contract_address: protocol.token_addr.display_address(), }, )); }, @@ -441,7 +441,6 @@ impl EthCoin { platform: protocol.platform, token_addr: protocol.token_addr, }; - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &token_conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &token_conf, &coin_type).await?; let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&token_conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; @@ -474,7 +473,6 @@ impl EthCoin { address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), - platform_fee_estimator_state, gas_limit, gas_limit_v2, abortable_system, @@ -533,7 +531,6 @@ impl EthCoin { let coin_type = EthCoinType::Nft { platform: self.ticker.clone(), }; - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; @@ -563,7 +560,6 @@ impl EthCoin { address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), nfts_infos: Arc::new(AsyncMutex::new(nft_infos)), - platform_fee_estimator_state, gas_limit, gas_limit_v2, abortable_system, @@ -669,7 +665,6 @@ pub async fn eth_coin_from_conf_and_request_v2( // all spawned futures related to `ETH` coin will be aborted as well. let abortable_system = ctx.abortable_system.create_subsystem()?; let coin_type = EthCoinType::Eth; - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; let gas_limit: EthGasLimit = extract_gas_limit_from_conf(conf) .map_to_mm(|e| EthActivationV2Error::InternalError(format!("invalid gas_limit config {}", e)))?; @@ -699,18 +694,12 @@ pub async fn eth_coin_from_conf_and_request_v2( address_nonce_locks, erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), - platform_fee_estimator_state, gas_limit, gas_limit_v2, abortable_system, }; - let coin = EthCoin(Arc::new(coin)); - coin.spawn_balance_stream_if_enabled(ctx) - .await - .map_err(EthActivationV2Error::FailedSpawningBalanceEvents)?; - - Ok(coin) + Ok(EthCoin(Arc::new(coin))) } /// Processes the given `priv_key_policy` and generates corresponding `KeyPair`. diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 6d19573781..11f7d69a10 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -11,20 +11,20 @@ use crate::eth::web3_transport::Web3SendOut; use crate::eth::{EthCoin, RpcTransportEventHandlerShared}; use crate::{MmCoin, RpcTransportEventHandler}; use common::executor::{AbortSettings, SpawnAbortable, Timer}; -use common::expirable_map::ExpirableMap; use common::log; +use compatible_time::{Duration, Instant}; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; use futures_ticker::Ticker; use futures_util::{FutureExt, SinkExt, StreamExt}; -use instant::{Duration, Instant}; use jsonrpc_core::Call; use mm2_p2p::Keypair; use proxy_signature::{ProxySign, RawMessage}; use std::sync::atomic::AtomicBool; use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; +use timed_map::TimedMap; use tokio_tungstenite_wasm::WebSocketStream; use web3::error::{Error, TransportError}; use web3::helpers::to_string; @@ -52,8 +52,8 @@ pub struct WebsocketTransport { #[derive(Debug)] struct ControllerChannel { - tx: Arc>>, - rx: Arc>>, + tx: UnboundedSender, + rx: AsyncMutex>, } enum ControllerMessage { @@ -86,11 +86,10 @@ impl WebsocketTransport { node, event_handlers, request_id: Arc::new(AtomicUsize::new(1)), - controller_channel: ControllerChannel { - tx: Arc::new(AsyncMutex::new(req_tx)), - rx: Arc::new(AsyncMutex::new(req_rx)), - } - .into(), + controller_channel: Arc::new(ControllerChannel { + tx: req_tx, + rx: AsyncMutex::new(req_rx), + }), connection_guard: Arc::new(AsyncMutex::new(())), proxy_sign_keypair: None, last_request_failed: Arc::new(AtomicBool::new(false)), @@ -137,7 +136,7 @@ impl WebsocketTransport { &self, request: Option, wsocket: &mut WebSocketStream, - response_notifiers: &mut ExpirableMap>>, + response_notifiers: &mut TimedMap>>, ) -> OuterAction { match request { Some(ControllerMessage::Request(WsRequest { @@ -145,7 +144,7 @@ impl WebsocketTransport { serialized_request, response_notifier, })) => { - response_notifiers.insert( + response_notifiers.insert_expirable( request_id, response_notifier, // Since request will be cancelled when timeout occurs, we are free to drop its state. @@ -188,7 +187,7 @@ impl WebsocketTransport { async fn handle_response( &self, message: Option>, - response_notifiers: &mut ExpirableMap>>, + response_notifiers: &mut TimedMap>>, ) -> OuterAction { match message { Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { @@ -249,7 +248,8 @@ impl WebsocketTransport { let _guard = self.connection_guard.lock().await; // List of awaiting requests - let mut response_notifiers: ExpirableMap>> = ExpirableMap::default(); + let mut response_notifiers: TimedMap>> = + TimedMap::new_with_map_kind(timed_map::MapKind::FxHashMap).expiration_tick_cap(30); let mut wsocket = match self .attempt_to_establish_socket_connection(MAX_ATTEMPTS, SLEEP_DURATION) @@ -298,7 +298,7 @@ impl WebsocketTransport { } pub(crate) async fn stop_connection_loop(&self) { - let mut tx = self.controller_channel.tx.lock().await; + let mut tx = self.controller_channel.tx.clone(); tx.send(ControllerMessage::Close) .await .expect("receiver channel must be alive"); @@ -357,12 +357,11 @@ async fn send_request( serialized_request = serde_json::to_string(&wrapper)?; } - let mut tx = transport.controller_channel.tx.lock().await; - let (notification_sender, notification_receiver) = oneshot::channel::>(); event_handlers.on_outgoing_request(&request_bytes); + let mut tx = transport.controller_channel.tx.clone(); tx.send(ControllerMessage::Request(WsRequest { request_id, serialized_request, diff --git a/mm2src/coins/hd_wallet/address_ops.rs b/mm2src/coins/hd_wallet/address_ops.rs index 45c38e717c..b279850e72 100644 --- a/mm2src/coins/hd_wallet/address_ops.rs +++ b/mm2src/coins/hd_wallet/address_ops.rs @@ -1,7 +1,21 @@ use bip32::DerivationPath; -use std::fmt::Display; use std::hash::Hash; +/// A trait for converting an address into a string suitable for display in logs, errors, or messages. +pub trait DisplayAddress { + fn display_address(&self) -> String; +} + +/// Should convert coin `Self::Address` type into a properly formatted string representation. +/// +/// Don't use `to_string` directly on `Self::Address` types in generic TPU code! +/// It may produce abbreviated or non-standard formats (e.g. `ethereum_types::Address` will be like this `0x7cc9…3874`), +/// which are not guaranteed to be parsable back into the original `Address` type. +/// This function should ensure the resulting string is consistently formatted and fully reversible. +pub trait AddrToString { + fn addr_to_string(&self) -> String; +} + /// `HDAddressOps` Trait /// /// Defines operations associated with an HD (Hierarchical Deterministic) address. @@ -9,7 +23,7 @@ use std::hash::Hash; /// in the structure `m / purpose' / coin_type' / account' / chain (or change) / address_index`. /// This allows for managing individual addresses within a specific account and chain. pub trait HDAddressOps { - type Address: Clone + Display + Eq + Hash + Send + Sync; + type Address: Clone + DisplayAddress + Eq + Hash + Send + Sync; type Pubkey: Clone; fn address(&self) -> Self::Address; diff --git a/mm2src/coins/hd_wallet/coin_ops.rs b/mm2src/coins/hd_wallet/coin_ops.rs index 6a70b8c037..27b92d0aa6 100644 --- a/mm2src/coins/hd_wallet/coin_ops.rs +++ b/mm2src/coins/hd_wallet/coin_ops.rs @@ -1,5 +1,5 @@ -use super::{inner_impl, AccountUpdatingError, AddressDerivingError, ExtendedPublicKeyOps, HDAccountOps, HDCoinAddress, - HDCoinExtendedPubkey, HDCoinHDAccount, HDCoinHDAddress, HDConfirmAddress, HDWalletOps, +use super::{inner_impl, AccountUpdatingError, AddressDerivingError, DisplayAddress, ExtendedPublicKeyOps, + HDAccountOps, HDCoinExtendedPubkey, HDCoinHDAccount, HDCoinHDAddress, HDConfirmAddress, HDWalletOps, NewAddressDeriveConfirmError, NewAddressDerivingError}; use crate::hd_wallet::{HDAddressOps, HDWalletStorageOps, TrezorCoinError}; use async_trait::async_trait; @@ -25,13 +25,6 @@ pub trait HDWalletCoinOps { /// Any type that represents a Hierarchical Deterministic (HD) wallet. type HDWallet: HDWalletOps + HDWalletStorageOps + Send + Sync; - /// Returns a formatter function for address representation. - /// Useful when an address has multiple display formats. - /// For example, Ethereum addresses can be fully displayed or truncated. - /// By default, the formatter uses the Display trait of the address type, which truncates Ethereum addresses. - /// Implement this function if a different display format is required. - fn address_formatter(&self) -> fn(&HDCoinAddress) -> String { |address| address.to_string() } - /// Derives an address for the coin that implements this trait from an extended public key and a derivation path. fn address_from_extended_pubkey( &self, @@ -189,7 +182,7 @@ pub trait HDWalletCoinOps { let trezor_coin = self.trezor_coin()?; let derivation_path = hd_address.derivation_path().clone(); - let expected_address = hd_address.address().to_string(); + let expected_address = hd_address.address().display_address(); // Ask the user to confirm if the given `expected_address` is the same as on the HW display. confirm_address .confirm_address(trezor_coin, derivation_path, expected_address) diff --git a/mm2src/coins/hd_wallet/mod.rs b/mm2src/coins/hd_wallet/mod.rs index 1d77db3543..666293cb60 100644 --- a/mm2src/coins/hd_wallet/mod.rs +++ b/mm2src/coins/hd_wallet/mod.rs @@ -16,7 +16,7 @@ mod account_ops; pub use account_ops::HDAccountOps; mod address_ops; -pub use address_ops::HDAddressOps; +pub use address_ops::{AddrToString, DisplayAddress, HDAddressOps}; mod coin_ops; pub use coin_ops::{HDAddressId, HDWalletCoinOps}; @@ -78,7 +78,7 @@ pub struct HDAddress { impl HDAddressOps for HDAddress where - Address: Clone + Display + Eq + Hash + Send + Sync, + Address: Clone + DisplayAddress + Eq + Hash + Send + Sync, Pubkey: Clone, { type Address = Address; diff --git a/mm2src/coins/hd_wallet/withdraw_ops.rs b/mm2src/coins/hd_wallet/withdraw_ops.rs index 7f1aa8b19c..5b7ddf48cf 100644 --- a/mm2src/coins/hd_wallet/withdraw_ops.rs +++ b/mm2src/coins/hd_wallet/withdraw_ops.rs @@ -1,4 +1,4 @@ -use super::{HDPathAccountToAddressId, HDWalletOps, HDWithdrawError}; +use super::{DisplayAddress, HDPathAccountToAddressId, HDWalletOps, HDWithdrawError}; use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps}; use async_trait::async_trait; use bip32::DerivationPath; @@ -10,7 +10,7 @@ type HDCoinPubKey = <<<::HDWallet as HDWalletOps>::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Pubkey; /// Represents the source of the funds for a withdrawal operation. -#[derive(Clone, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum WithdrawFrom { /// The address id of the sender address which is specified by the account id, chain, and address id. @@ -82,7 +82,7 @@ pub trait HDCoinWithdrawOps: HDWalletCoinOps { let hd_address = self.derive_address(&hd_account, chain, address_id).await?; let address = hd_address.address(); if !is_address_activated { - let error = format!("'{}' address is not activated", address); + let error = format!("'{}' address is not activated", address.display_address()); return MmError::err(HDWithdrawError::UnexpectedFromAddress(error)); } diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 67b27ba8f4..949c39a857 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -15,19 +15,17 @@ use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_clt use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; use crate::utxo::{sat_from_big_decimal, utxo_common, BlockchainNetwork}; -use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, - FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, - RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, - TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, - TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TxMarshalingErr, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RawTransactionError, + RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendPaymentArgs, SignRawTransactionRequest, SignatureError, + SignatureResult, SpendPaymentArgs, SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, + TradePreimageValue, Transaction, TransactionEnum, TransactionErr, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, UtxoStandardCoin, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; + ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WeakSpawner, WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcoin::bech32::ToBase32; use bitcoin::hashes::Hash; @@ -73,7 +71,7 @@ use secp256k1v24::PublicKey; use serde::Deserialize; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; use std::fmt; use std::io::Cursor; use std::net::SocketAddr; @@ -610,13 +608,7 @@ impl LightningCoin { #[async_trait] impl SwapOps for LightningCoin { // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning - async fn send_taker_fee( - &self, - _fee_addr: &[u8], - _dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { + async fn send_taker_fee(&self, _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { Ok(TransactionEnum::LightningPayment(PaymentHash([1; 32]))) } @@ -784,22 +776,18 @@ impl SwapOps for LightningCoin { } } - fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - async fn extract_secret( &self, _secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { let payment_hash = payment_hash_from_slice(spend_tx).map_err(|e| e.to_string())?; let payment_hex = hex::encode(payment_hash.0); match self.db.get_payment_from_db(payment_hash).await { Ok(Some(payment)) => match payment.preimage { - Some(preimage) => Ok(preimage.0.to_vec()), + Some(preimage) => Ok(preimage.0), None => ERR!("Preimage for payment {} should be found on the database", payment_hex), }, Ok(None) => ERR!("Payment {} is not in the database when it should be!", payment_hex), @@ -857,8 +845,8 @@ impl SwapOps for LightningCoin { } #[inline] - fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { - self.channel_manager.get_our_node_id().serialize().to_vec() + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> [u8; 33] { + self.channel_manager.get_our_node_id().serialize() } #[inline] @@ -915,19 +903,13 @@ impl SwapOps for LightningCoin { } fn maker_locktime_multiplier(&self) -> f64 { 1.5 } -} -#[async_trait] -impl TakerSwapMakerCoin for LightningCoin { async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } async fn on_taker_payment_refund_success(&self, maker_payment: &[u8]) -> RefundResult<()> { self.on_swap_refund(maker_payment).await } -} -#[async_trait] -impl MakerSwapTakerCoin for LightningCoin { async fn on_maker_payment_refund_start(&self, taker_payment: &[u8]) -> RefundResult<()> { self.on_swap_refund(taker_payment).await } @@ -952,77 +934,7 @@ fn payment_hash_from_slice(data: &[u8]) -> Result TransactionFut { - unimplemented!(); - } - - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - unimplemented!(); - } - - fn create_taker_payment_refund_preimage( - &self, - _taker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_contract_address: &Option, - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - unimplemented!(); - } - - fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { - unimplemented!() - } - - async fn watcher_search_for_swap_tx_spend( - &self, - _input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String> { - unimplemented!(); - } - - async fn get_taker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _coin_amount: Option, - _other_coin_amount: Option, - _reward_amount: Option, - _wait_until: u64, - ) -> Result> { - unimplemented!() - } - - async fn get_maker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _reward_amount: Option, - _wait_until: u64, - ) -> Result, MmError> { - unimplemented!() - } -} +impl WatcherOps for LightningCoin {} #[async_trait] impl MarketCoinOps for LightningCoin { @@ -1046,7 +958,8 @@ impl MarketCoinOps for LightningCoin { .map_err(|_| SignatureError::InternalError("Error accessing node keys".to_string()))?; let private = Private { prefix: 239, - secret: H256::from(*secret_key.as_ref()), + secret: H256::from_slice(secret_key.as_ref()) + .map_to_mm(|err| SignatureError::InvalidRequest(err.to_string()))?, compressed: true, checksum_type: ChecksumType::DSHA256, }; @@ -1058,10 +971,11 @@ impl MarketCoinOps for LightningCoin { let message_hash = self .sign_message_hash(message) .ok_or(VerificationError::PrefixNotFound)?; - let signature = CompactSignature::from( + let signature = CompactSignature::try_from( zbase32::decode_full_bytes_str(signature) .map_err(|e| VerificationError::SignatureDecodingError(e.to_string()))?, - ); + ) + .map_to_mm(|err| VerificationError::SignatureDecodingError(err.to_string()))?; let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; Ok(recovered_pubkey.to_string() == pubkey) } @@ -1248,6 +1162,8 @@ impl MarketCoinOps for LightningCoin { // Todo: doesn't take routing fees into account too, There is no way to know the route to the other side of the swap when placing the order, need to find a workaround for this fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.platform.coin.is_trezor() } } @@ -1261,7 +1177,7 @@ struct LightningProtocolInfo { impl MmCoin for LightningCoin { fn is_asset_chain(&self) -> bool { false } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.platform.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.platform.abortable_system.weak_spawner() } fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { let fut = async move { diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index a3a4c22776..e7d57cb217 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -6,7 +6,7 @@ use crate::utxo::rpc_clients::{BlockHashOrHeight, ConfirmedTransactionInfo, Elec use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::utxo_standard::UtxoStandardCoin; use crate::utxo::GetConfirmedTxError; -use crate::{CoinFutSpawner, MarketCoinOps, MmCoin, WaitForHTLCTxSpendArgs}; +use crate::{MarketCoinOps, MmCoin, WaitForHTLCTxSpendArgs, WeakSpawner}; use bitcoin::blockdata::block::BlockHeader; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::Transaction; @@ -74,7 +74,7 @@ pub async fn update_best_block( ) }, ElectrumBlockHeader::V14(h) => { - let block_header = match deserialize(&h.hex.into_vec()) { + let block_header = match deserialize(&h.hex.0) { Ok(header) => header, Err(e) => { error!("Block header deserialization error: {}", e.to_string()); @@ -216,7 +216,7 @@ impl Platform { #[inline] fn rpc_client(&self) -> &UtxoRpcClientEnum { &self.coin.as_ref().rpc_client } - pub fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + pub fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } pub async fn set_latest_fees(&self) -> UtxoRpcResult<()> { let platform_coin = &self.coin; diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 16ade17a6b..4de2ab31fc 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -32,6 +32,7 @@ #![feature(hash_raw_entry)] #![feature(stmt_expr_attributes)] #![feature(result_flattening)] +#![feature(local_key_cell_methods)] // for tests #[macro_use] extern crate common; #[macro_use] extern crate gstuff; @@ -45,10 +46,9 @@ use async_trait::async_trait; use base58::FromBase58Error; use bip32::ExtendedPrivateKey; use common::custom_futures::timeout::TimeoutError; -use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner}, - AbortSettings, AbortedError, SpawnAbortable, SpawnFuture}; +use common::executor::{abortable_queue::WeakSpawner, AbortedError, SpawnFuture}; use common::log::{warn, LogOnError}; -use common::{calc_total_pages, now_sec, ten, HttpStatusCode}; +use common::{calc_total_pages, now_sec, ten, HttpStatusCode, DEX_BURN_ADDR_RAW_PUBKEY, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoCtxError, DerivationPath, GlobalHDAccountArc, HDPathToCoin, HwRpcError, KeyPairPolicy, RpcDerivationPath, Secp256k1ExtendedPublicKey, Secp256k1Secret, WithHwRpcError}; @@ -65,9 +65,12 @@ use keys::{AddressFormat as UtxoAddressFormat, KeyPair, NetworkPrefix as CashAdd use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; use mm2_metrics::MetricsWeak; +use mm2_number::BigRational; use mm2_number::{bigdecimal::{BigDecimal, ParseBigDecimalError, Zero}, BigUint, MmNumber, ParseBigIntError}; use mm2_rpc::data::legacy::{EnabledCoin, GetEnabledResponse, Mm2RpcResult}; +#[cfg(any(test, feature = "for-tests"))] +use mocktopus::macros::*; use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -76,7 +79,6 @@ use std::array::TryFromSliceError; use std::cmp::Ordering; use std::collections::hash_map::{HashMap, RawEntryMut}; use std::collections::HashSet; -use std::future::Future as Future03; use std::num::{NonZeroUsize, TryFromIntError}; use std::ops::{Add, AddAssign, Deref}; use std::str::FromStr; @@ -87,6 +89,7 @@ use std::time::Duration; use std::{fmt, iter}; use utxo_signer::with_key_pair::UtxoSignWithKeyPairError; use zcash_primitives::transaction::Transaction as ZTransaction; + cfg_native! { use crate::lightning::LightningCoin; use crate::lightning::ln_conf::PlatformCoinConfirmationTargets; @@ -212,6 +215,7 @@ pub mod watcher_common; pub mod coin_errors; use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentResult}; +use crypto::secret_hash_algo::SecretHashAlgo; #[doc(hidden)] #[cfg(test)] @@ -219,7 +223,7 @@ pub mod coins_tests; pub mod eth; use eth::erc20::get_erc20_ticker_by_contract_address; -use eth::eth_swap_v2::{PaymentStatusErr, PrepareTxDataError, ValidatePaymentV2Err}; +use eth::eth_swap_v2::{PrepareTxDataError, ValidatePaymentV2Err}; use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetailsErr, EthTxFeeDetails, GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx}; @@ -249,7 +253,9 @@ use tendermint::{CosmosTransaction, TendermintCoin, TendermintFeeDetails, Tender #[doc(hidden)] #[allow(unused_variables)] +#[cfg(any(test, feature = "for-tests"))] pub mod test_coin; +#[cfg(any(test, feature = "for-tests"))] pub use test_coin::TestCoin; pub mod tx_history_storage; @@ -274,6 +280,7 @@ use script::Script; pub mod z_coin; use crate::coin_balance::{BalanceObjectOps, HDWalletBalanceObject}; +use crate::hd_wallet::{AddrToString, DisplayAddress}; use z_coin::{ZCoin, ZcoinProtocolInfo}; pub type TransactionFut = Box + Send>; @@ -282,8 +289,7 @@ pub type BalanceResult = Result>; pub type BalanceFut = Box> + Send>; pub type NonZeroBalanceFut = Box> + Send>; pub type NumConversResult = Result>; -pub type StakingInfosResult = Result>; -pub type StakingInfosFut = Box> + Send>; +pub type StakingInfosFut = Box> + Send>; pub type DelegationResult = Result>; pub type DelegationFut = Box> + Send>; pub type WithdrawResult = Result>; @@ -705,7 +711,6 @@ pub struct WatcherValidateTakerFeeInput { pub taker_fee_hash: Vec, pub sender_pubkey: Vec, pub min_block_number: u64, - pub fee_addr: Vec, pub lock_duration: u64, } @@ -992,7 +997,6 @@ pub struct CheckIfMyPaymentSentArgs<'a> { pub struct ValidateFeeArgs<'a> { pub fee_tx: &'a TransactionEnum, pub expected_sender: &'a [u8], - pub fee_addr: &'a [u8], pub dex_fee: &'a DexFee, pub min_block_number: u64, pub uuid: &'a [u8], @@ -1001,7 +1005,6 @@ pub struct ValidateFeeArgs<'a> { pub struct EthValidateFeeArgs<'a> { pub fee_tx_hash: &'a H256, pub expected_sender: &'a [u8], - pub fee_addr: &'a [u8], pub amount: &'a BigDecimal, pub min_block_number: u64, pub uuid: &'a [u8], @@ -1072,8 +1075,9 @@ pub enum WatcherRewardError { /// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets). #[async_trait] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] pub trait SwapOps { - async fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult; + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult; async fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult; @@ -1119,9 +1123,7 @@ pub trait SwapOps { secret_hash: &[u8], spend_tx: &[u8], watcher_reward: bool, - ) -> Result, String>; - - fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result>; + ) -> Result<[u8; 32], String>; /// Whether the refund transaction can be sent now /// For example: there are no additional conditions for ETH, but for some UTXO coins we should wait for @@ -1136,10 +1138,15 @@ pub trait SwapOps { } /// Whether the swap payment is refunded automatically or not when the locktime expires, or the other side fails the HTLC. - fn is_auto_refundable(&self) -> bool; + /// lightning specific + fn is_auto_refundable(&self) -> bool { false } - /// Waits for an htlc to be refunded automatically. - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()>; + /// Waits for an htlc to be refunded automatically. - lightning specific + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { + MmError::err(RefundError::Internal( + "wait_for_htlc_refund is not supported for this coin!".into(), + )) + } fn negotiate_swap_contract_addr( &self, @@ -1151,33 +1158,47 @@ pub trait SwapOps { fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair; /// Derives an HTLC key-pair and returns a public key corresponding to that key. - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec; + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> [u8; 33]; fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr>; - /// Instructions from the taker on how the maker should send his payment. + /// Instructions from the taker on how the maker should send his payment. - lightning specific async fn maker_payment_instructions( &self, - args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError>; + _args: PaymentInstructionArgs<'_>, + ) -> Result>, MmError> { + Ok(None) + } - /// Instructions from the maker on how the taker should send his payment. + /// Instructions from the maker on how the taker should send his payment. - lightning specific async fn taker_payment_instructions( &self, - args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError>; + _args: PaymentInstructionArgs<'_>, + ) -> Result>, MmError> { + Ok(None) + } + /// lightning specific fn validate_maker_payment_instructions( &self, - instructions: &[u8], - args: PaymentInstructionArgs<'_>, - ) -> Result>; + _instructions: &[u8], + _args: PaymentInstructionArgs<'_>, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin( + "validate_maker_payment_instructions is not supported for this coin!".into(), + )) + } + /// lightning specific fn validate_taker_payment_instructions( &self, - instructions: &[u8], - args: PaymentInstructionArgs<'_>, - ) -> Result>; + _instructions: &[u8], + _args: PaymentInstructionArgs<'_>, + ) -> Result> { + MmError::err(ValidateInstructionsErr::UnsupportedCoin( + "validate_taker_payment_instructions is not supported for this coin!".into(), + )) + } fn is_supported_by_watchers(&self) -> bool { false } @@ -1185,77 +1206,155 @@ pub trait SwapOps { fn contract_supports_watchers(&self) -> bool { true } fn maker_locktime_multiplier(&self) -> f64 { 2.0 } -} -/// Operations on maker coin from taker swap side -#[async_trait] -pub trait TakerSwapMakerCoin { + fn dex_pubkey(&self) -> &[u8] { &DEX_FEE_ADDR_RAW_PUBKEY } + + fn burn_pubkey(&self) -> &[u8] { + #[cfg(feature = "for-tests")] + { + lazy_static! { + static ref TEST_BURN_ADDR_RAW_PUBKEY: Option> = std::env::var("TEST_BURN_ADDR_RAW_PUBKEY") + .ok() + .map(|env_pubkey| hex::decode(env_pubkey).expect("valid hex")); + } + if let Some(test_pk) = TEST_BURN_ADDR_RAW_PUBKEY.as_ref() { + return test_pk; + } + } + &DEX_BURN_ADDR_RAW_PUBKEY + } + /// Performs an action on Maker coin payment just before the Taker Swap payment refund begins - async fn on_taker_payment_refund_start(&self, maker_payment: &[u8]) -> RefundResult<()>; + /// Operation on maker coin from taker swap side + /// Currently lightning specific + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + /// Performs an action on Maker coin payment after the Taker Swap payment is refunded successfully - async fn on_taker_payment_refund_success(&self, maker_payment: &[u8]) -> RefundResult<()>; -} + /// Operation on maker coin from taker swap side + /// Currently lightning specific + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -/// Operations on taker coin from maker swap side -#[async_trait] -pub trait MakerSwapTakerCoin { /// Performs an action on Taker coin payment just before the Maker Swap payment refund begins - async fn on_maker_payment_refund_start(&self, taker_payment: &[u8]) -> RefundResult<()>; + /// Operation on taker coin from maker swap side + /// Currently lightning specific + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + /// Performs an action on Taker coin payment after the Maker Swap payment is refunded successfully - async fn on_maker_payment_refund_success(&self, taker_payment: &[u8]) -> RefundResult<()>; + /// Operation on taker coin from maker swap side + /// Currently lightning specific + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } } #[async_trait] pub trait WatcherOps { - fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut; + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + Box::new( + futures::future::ready(Err(TransactionErr::Plain( + "send_maker_payment_spend_preimage is not implemented".to_string(), + ))) + .compat(), + ) + } - fn send_taker_payment_refund_preimage(&self, watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut; + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + Box::new( + futures::future::ready(Err(TransactionErr::Plain( + "send_taker_payment_refund_preimage is not implemented".to_string(), + ))) + .compat(), + ) + } fn create_taker_payment_refund_preimage( &self, - taker_payment_tx: &[u8], - time_lock: u64, - maker_pub: &[u8], - secret_hash: &[u8], - swap_contract_address: &Option, - swap_unique_data: &[u8], - ) -> TransactionFut; + _taker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + Box::new( + futures::future::ready(Err(TransactionErr::Plain( + "create_taker_payment_refund_preimage is not implemented".to_string(), + ))) + .compat(), + ) + } fn create_maker_payment_spend_preimage( &self, - maker_payment_tx: &[u8], - time_lock: u64, - maker_pub: &[u8], - secret_hash: &[u8], - swap_unique_data: &[u8], - ) -> TransactionFut; - - fn watcher_validate_taker_fee(&self, input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()>; - - fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()>; - - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()>; + _maker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + Box::new( + futures::future::ready(Err(TransactionErr::Plain( + "create_maker_payment_spend_preimage is not implemented".to_string(), + ))) + .compat(), + ) + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + Box::new( + futures::future::ready(MmError::err(ValidatePaymentError::InternalError( + "watcher_validate_taker_fee is not implemented".to_string(), + ))) + .compat(), + ) + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + Box::new( + futures::future::ready(MmError::err(ValidatePaymentError::InternalError( + "watcher_validate_taker_payment is not implemented".to_string(), + ))) + .compat(), + ) + } + + fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { + Box::new( + futures::future::ready(MmError::err(ValidatePaymentError::InternalError( + "taker_validates_payment_spend_or_refund is not implemented".to_string(), + ))) + .compat(), + ) + } async fn watcher_search_for_swap_tx_spend( &self, - input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String>; + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + Err("watcher_search_for_swap_tx_spend is not implemented".to_string()) + } async fn get_taker_watcher_reward( &self, - other_coin: &MmCoinEnum, - coin_amount: Option, - other_coin_amount: Option, - reward_amount: Option, - wait_until: u64, - ) -> Result>; + _other_coin: &MmCoinEnum, + _coin_amount: Option, + _other_coin_amount: Option, + _reward_amount: Option, + _wait_until: u64, + ) -> Result> { + Err(WatcherRewardError::InternalError( + "get_taker_watcher_reward is not implemented".to_string(), + ))? + } async fn get_maker_watcher_reward( &self, - other_coin: &MmCoinEnum, - reward_amount: Option, - wait_until: u64, - ) -> Result, MmError>; + _other_coin: &MmCoinEnum, + _reward_amount: Option, + _wait_until: u64, + ) -> Result, MmError> { + Err(WatcherRewardError::InternalError( + "get_maker_watcher_reward is not implemented".to_string(), + ))? + } } /// Helper struct wrapping arguments for [TakerCoinSwapOpsV2::send_taker_funding] @@ -1288,7 +1387,7 @@ pub struct RefundFundingSecretArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub funding_time_lock: u64, pub payment_time_lock: u64, pub maker_pubkey: &'a Coin::Pubkey, - pub taker_secret: &'a [u8], + pub taker_secret: &'a [u8; 32], pub taker_secret_hash: &'a [u8], pub maker_secret_hash: &'a [u8], pub dex_fee: &'a DexFee, @@ -1360,8 +1459,6 @@ pub struct GenTakerPaymentSpendArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub maker_address: &'a Coin::Address, /// Taker's pubkey pub taker_pub: &'a Coin::Pubkey, - /// Pubkey of address, receiving DEX fees - pub dex_fee_pub: &'a [u8], /// DEX fee pub dex_fee: &'a DexFee, /// Additional reward for maker (premium) @@ -1385,8 +1482,6 @@ pub enum TxGenError { Rpc(String), /// Error during conversion of BigDecimal amount to coin's specific monetary units (satoshis, wei, etc.). NumConversion(String), - /// Address derivation error. - AddressDerivation(String), /// Problem with tx preimage signing. Signing(String), /// Legacy error produced by usage of try_s/try_fus and other similar macros. @@ -1397,6 +1492,8 @@ pub enum TxGenError { TxFeeTooHigh(String), /// Previous tx is not valid PrevTxIsNotValid(String), + /// Previous tx output value too low + PrevOutputTooLow(String), /// Other errors, can be used to return an error that can happen only in specific coin protocol implementation Other(String), } @@ -1454,20 +1551,9 @@ impl From for ValidateSwapV2TxError { fn from(err: UtxoRpcError) -> Self { ValidateSwapV2TxError::Rpc(err.to_string()) } } -impl From for ValidateSwapV2TxError { - fn from(err: PaymentStatusErr) -> Self { - match err { - PaymentStatusErr::Internal(e) => ValidateSwapV2TxError::Internal(e), - PaymentStatusErr::Transport(e) => ValidateSwapV2TxError::Rpc(e), - PaymentStatusErr::ABIError(e) | PaymentStatusErr::InvalidData(e) => ValidateSwapV2TxError::InvalidData(e), - } - } -} - impl From for ValidateSwapV2TxError { fn from(err: ValidatePaymentV2Err) -> Self { match err { - ValidatePaymentV2Err::UnexpectedPaymentState(e) => ValidateSwapV2TxError::UnexpectedPaymentState(e), ValidatePaymentV2Err::WrongPaymentTx(e) => ValidateSwapV2TxError::WrongPaymentTx(e), } } @@ -1477,6 +1563,7 @@ impl From for ValidateSwapV2TxError { fn from(err: PrepareTxDataError) -> Self { match err { PrepareTxDataError::ABIError(e) | PrepareTxDataError::Internal(e) => ValidateSwapV2TxError::Internal(e), + PrepareTxDataError::InvalidData(e) => ValidateSwapV2TxError::InvalidData(e), } } } @@ -1536,7 +1623,7 @@ pub trait ToBytes { /// Defines associated types specific to each coin (Pubkey, Address, etc.) #[async_trait] pub trait ParseCoinAssocTypes { - type Address: Send + Sync + fmt::Display; + type Address: Send + Sync + fmt::Display + AddrToString; type AddressParseError: fmt::Debug + Send + fmt::Display; type Pubkey: ToBytes + Send + Sync; type PubkeyParseError: fmt::Debug + Send + fmt::Display; @@ -1667,7 +1754,7 @@ pub struct RefundMakerPaymentSecretArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> /// The hash of the secret generated by maker, taker needs it to spend the payment pub maker_secret_hash: &'a [u8], /// Taker's secret - pub taker_secret: &'a [u8], + pub taker_secret: &'a [u8; 32], /// Taker's HTLC pubkey pub taker_pub: &'a Coin::Pubkey, /// Unique data of specific swap @@ -1702,7 +1789,7 @@ pub struct SpendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { /// The hash of the secret generated by maker, taker needs it to spend the payment pub maker_secret_hash: &'a [u8], /// The secret generated by maker, revealed when maker spends taker's payment - pub maker_secret: &'a [u8], + pub maker_secret: [u8; 32], /// Maker's HTLC pubkey pub maker_pub: &'a Coin::Pubkey, /// Unique data of specific swap @@ -1786,7 +1873,7 @@ pub trait MakerNftSwapOpsV2: ParseCoinAssocTypes + ParseNftAssocTypes + Send + S /// Enum representing errors that can occur while waiting for taker payment spend. #[derive(Display, Debug, EnumFromStringify)] -pub enum WaitForPaymentSpendError { +pub enum FindPaymentSpendError { /// Timeout error variant, indicating that the wait for taker payment spend has timed out. #[display( fmt = "Timed out waiting for taker payment spend, wait_until {}, now {}", @@ -1801,6 +1888,7 @@ pub enum WaitForPaymentSpendError { }, /// Invalid input transaction error variant, containing additional information about the error. InvalidInputTx(String), + #[from_stringify("TryFromSliceError")] Internal(String), #[from_stringify("ethabi::Error")] #[display(fmt = "ABI error: {}", _0)] @@ -1809,33 +1897,23 @@ pub enum WaitForPaymentSpendError { Transport(String), } -impl From for WaitForPaymentSpendError { +impl From for FindPaymentSpendError { fn from(err: WaitForOutputSpendErr) -> Self { match err { - WaitForOutputSpendErr::Timeout { wait_until, now } => WaitForPaymentSpendError::Timeout { wait_until, now }, + WaitForOutputSpendErr::Timeout { wait_until, now } => FindPaymentSpendError::Timeout { wait_until, now }, WaitForOutputSpendErr::NoOutputWithIndex(index) => { - WaitForPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index)) + FindPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index)) }, } } } -impl From for WaitForPaymentSpendError { - fn from(e: PaymentStatusErr) -> Self { - match e { - PaymentStatusErr::ABIError(e) => Self::ABIError(e), - PaymentStatusErr::Transport(e) => Self::Transport(e), - PaymentStatusErr::Internal(e) => Self::Internal(e), - PaymentStatusErr::InvalidData(e) => Self::InvalidData(e), - } - } -} - -impl From for WaitForPaymentSpendError { +impl From for FindPaymentSpendError { fn from(e: PrepareTxDataError) -> Self { match e { PrepareTxDataError::ABIError(e) => Self::ABIError(e), PrepareTxDataError::Internal(e) => Self::Internal(e), + PrepareTxDataError::InvalidData(e) => Self::InvalidData(e), } } } @@ -1875,7 +1953,7 @@ impl fmt::Debug for FundingTxSpend { } /// Enum representing errors that can occur during the search for funding spend. -#[derive(Debug)] +#[derive(Debug, EnumFromStringify)] pub enum SearchForFundingSpendErr { /// Variant indicating an invalid input transaction error with additional information. InvalidInputTx(String), @@ -1885,6 +1963,7 @@ pub enum SearchForFundingSpendErr { Rpc(String), /// Variant indicating an error during conversion of the `from_block` argument with associated `TryFromIntError`. FromBlockConversionErr(TryFromIntError), + #[from_stringify("ethabi::Error")] Internal(String), } @@ -1942,6 +2021,11 @@ pub trait TakerCoinSwapOpsV2: ParseCoinAssocTypes + CommonSwapOpsV2 + Send + Syn async fn refund_combined_taker_payment(&self, args: RefundTakerPaymentArgs<'_>) -> Result; + /// A bool flag that allows skipping the generation and P2P message broadcasting of `TakerPaymentSpendPreimage` on the Taker side, + /// as well as its reception and validation on the Maker side. + /// This is typically used for coins that rely on smart contracts. + fn skip_taker_payment_spend_preimage(&self) -> bool { false } + /// Generates and signs taker payment spend preimage. The preimage and signature should be /// shared with maker to proceed with protocol execution. async fn gen_taker_payment_spend_preimage( @@ -1960,19 +2044,21 @@ pub trait TakerCoinSwapOpsV2: ParseCoinAssocTypes + CommonSwapOpsV2 + Send + Syn /// Sign and broadcast taker payment spend on maker's side. async fn sign_and_broadcast_taker_payment_spend( &self, - preimage: &TxPreimageWithSig, + preimage: Option<&TxPreimageWithSig>, gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], ) -> Result; - /// Wait until taker payment spend is found on-chain - async fn wait_for_taker_payment_spend( + /// Wait until taker payment spend transaction is found on-chain + async fn find_taker_payment_spend_tx( &self, taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult; + ) -> MmResult; + + async fn extract_secret_v2(&self, secret_hash: &[u8], spend_tx: &Self::Tx) -> Result<[u8; 32], String>; } #[async_trait] @@ -1981,6 +2067,9 @@ pub trait CommonSwapOpsV2: ParseCoinAssocTypes + Send + Sync + 'static { fn derive_htlc_pubkey_v2(&self, swap_unique_data: &[u8]) -> Self::Pubkey; fn derive_htlc_pubkey_v2_bytes(&self, swap_unique_data: &[u8]) -> Vec; + + /// Returns taker pubkey for non-private coins, for dex fee calculation + fn taker_pubkey_bytes(&self) -> Option>; } /// Operations that coins have independently from the MarketMaker. @@ -2051,8 +2140,15 @@ pub trait MarketCoinOps { /// Get the minimum amount to trade. fn min_trading_vol(&self) -> MmNumber; + /// Is privacy coin like zcash or pirate fn is_privacy(&self) -> bool { false } + /// Is KMD coin + fn is_kmd(&self) -> bool { false } + + /// Should burn part of dex fee coin + fn should_burn_dex_fee(&self) -> bool; + fn is_trezor(&self) -> bool; } @@ -2112,7 +2208,7 @@ pub trait GetWithdrawSenderAddress { /// Instead, accept a generic type from withdraw implementations. /// This way we won't have to update the payload for every platform when /// one of them requires specific addition. -#[derive(Clone, Deserialize)] +#[derive(Clone, Default, Deserialize)] pub struct WithdrawRequest { coin: String, from: Option, @@ -2135,24 +2231,68 @@ pub struct WithdrawRequest { #[serde(tag = "type")] pub enum StakingDetails { Qtum(QtumDelegationRequest), + Cosmos(Box), } -#[allow(dead_code)] #[derive(Deserialize)] pub struct AddDelegateRequest { pub coin: String, pub staking_details: StakingDetails, } -#[allow(dead_code)] #[derive(Deserialize)] pub struct RemoveDelegateRequest { pub coin: String, + pub staking_details: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum ClaimingDetails { + Cosmos(rpc_command::tendermint::staking::ClaimRewardsPayload), +} + +#[derive(Deserialize)] +pub struct ClaimStakingRewardsRequest { + pub coin: String, + pub claiming_details: ClaimingDetails, } #[derive(Deserialize)] -pub struct GetStakingInfosRequest { +pub struct DelegationsInfo { pub coin: String, + info_details: DelegationsInfoDetails, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum DelegationsInfoDetails { + Qtum, + Cosmos(rpc_command::tendermint::staking::SimpleListQuery), +} + +#[derive(Deserialize)] +pub struct UndelegationsInfo { + pub coin: String, + info_details: UndelegationsInfoDetails, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum UndelegationsInfoDetails { + Cosmos(rpc_command::tendermint::staking::SimpleListQuery), +} + +#[derive(Deserialize)] +pub struct ValidatorsInfo { + pub coin: String, + info_details: ValidatorsInfoDetails, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum ValidatorsInfoDetails { + Cosmos(rpc_command::tendermint::staking::ValidatorsQuery), } #[derive(Serialize, Deserialize)] @@ -2173,15 +2313,9 @@ impl WithdrawRequest { pub fn new_max(coin: String, to: String) -> WithdrawRequest { WithdrawRequest { coin, - from: None, to, - amount: 0.into(), max: true, - fee: None, - memo: None, - ibc_source_channel: None, - #[cfg(target_arch = "wasm32")] - broadcast: false, + ..Default::default() } } } @@ -2282,6 +2416,7 @@ impl KmdRewardsDetails { pub enum TransactionType { StakingDelegation, RemoveDelegation, + ClaimDelegationRewards, #[default] StandardTransfer, TokenTransfer(BytesJson), @@ -2291,7 +2426,9 @@ pub enum TransactionType { token_id: Option, }, NftTransfer, - TendermintIBCTransfer, + TendermintIBCTransfer { + token_id: Option, + }, } /// Transaction details @@ -2686,59 +2823,59 @@ impl From for BalanceError { #[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] -pub enum StakingInfosError { - #[display(fmt = "Staking infos not available for: {}", coin)] - CoinDoesntSupportStakingInfos { coin: String }, +pub enum StakingInfoError { #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, #[from_stringify("UnexpectedDerivationMethod")] #[display(fmt = "Derivation method is not supported: {}", _0)] UnexpectedDerivationMethod(String), + #[display(fmt = "Invalid payload: {}", reason)] + InvalidPayload { reason: String }, #[display(fmt = "Transport error: {}", _0)] Transport(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), } -impl From for StakingInfosError { +impl From for StakingInfoError { fn from(e: UtxoRpcError) -> Self { match e { UtxoRpcError::Transport(rpc) | UtxoRpcError::ResponseParseError(rpc) => { - StakingInfosError::Transport(rpc.to_string()) + StakingInfoError::Transport(rpc.to_string()) }, - UtxoRpcError::InvalidResponse(error) => StakingInfosError::Transport(error), - UtxoRpcError::Internal(error) => StakingInfosError::Internal(error), + UtxoRpcError::InvalidResponse(error) => StakingInfoError::Transport(error), + UtxoRpcError::Internal(error) => StakingInfoError::Internal(error), } } } -impl From for StakingInfosError { +impl From for StakingInfoError { fn from(e: Qrc20AddressError) -> Self { match e { - Qrc20AddressError::UnexpectedDerivationMethod(e) => StakingInfosError::UnexpectedDerivationMethod(e), + Qrc20AddressError::UnexpectedDerivationMethod(e) => StakingInfoError::UnexpectedDerivationMethod(e), Qrc20AddressError::ScriptHashTypeNotSupported { script_hash_type } => { - StakingInfosError::Internal(format!("Script hash type '{}' is not supported", script_hash_type)) + StakingInfoError::Internal(format!("Script hash type '{}' is not supported", script_hash_type)) }, } } } -impl HttpStatusCode for StakingInfosError { +impl HttpStatusCode for StakingInfoError { fn status_code(&self) -> StatusCode { match self { - StakingInfosError::NoSuchCoin { .. } - | StakingInfosError::CoinDoesntSupportStakingInfos { .. } - | StakingInfosError::UnexpectedDerivationMethod(_) => StatusCode::BAD_REQUEST, - StakingInfosError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - StakingInfosError::Transport(_) => StatusCode::BAD_GATEWAY, + StakingInfoError::NoSuchCoin { .. } + | StakingInfoError::InvalidPayload { .. } + | StakingInfoError::UnexpectedDerivationMethod(_) => StatusCode::BAD_REQUEST, + StakingInfoError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + StakingInfoError::Transport(_) => StatusCode::BAD_GATEWAY, } } } -impl From for StakingInfosError { +impl From for StakingInfoError { fn from(e: CoinFindError) -> Self { match e { - CoinFindError::NoSuchCoin { coin } => StakingInfosError::NoSuchCoin { coin }, + CoinFindError::NoSuchCoin { coin } => StakingInfoError::NoSuchCoin { coin }, } } } @@ -2763,6 +2900,32 @@ pub enum DelegationError { CoinDoesntSupportDelegation { coin: String }, #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, + #[display( + fmt = "Delegator '{}' does not have any delegation on validator '{}'.", + delegator_addr, + validator_addr + )] + CanNotUndelegate { + delegator_addr: String, + validator_addr: String, + }, + #[display( + fmt = "Max available amount to undelegate is '{}' but '{}' was requested.", + available, + requested + )] + TooMuchToUndelegate { + available: BigDecimal, + requested: BigDecimal, + }, + #[display( + fmt = "Fee ({}) exceeds reward ({}) which makes this unprofitable. Set 'force' to true in the request to bypass this check.", + fee, + reward + )] + UnprofitableReward { reward: BigDecimal, fee: BigDecimal }, + #[display(fmt = "There is no reward for {} to claim.", coin)] + NothingToClaim { coin: String }, #[display(fmt = "{}", _0)] CannotInteractWithSmartContract(String), #[from_stringify("ScriptHashTypeNotSupported")] @@ -2774,6 +2937,8 @@ pub enum DelegationError { DelegationOpsNotSupported { reason: String }, #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[display(fmt = "Invalid payload: {}", reason)] + InvalidPayload { reason: String }, #[from_stringify("MyAddressError")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), @@ -2791,18 +2956,16 @@ impl From for DelegationError { } } -impl From for DelegationError { - fn from(e: StakingInfosError) -> Self { +impl From for DelegationError { + fn from(e: StakingInfoError) -> Self { match e { - StakingInfosError::CoinDoesntSupportStakingInfos { coin } => { - DelegationError::CoinDoesntSupportDelegation { coin } - }, - StakingInfosError::NoSuchCoin { coin } => DelegationError::NoSuchCoin { coin }, - StakingInfosError::Transport(e) => DelegationError::Transport(e), - StakingInfosError::UnexpectedDerivationMethod(reason) => { + StakingInfoError::NoSuchCoin { coin } => DelegationError::NoSuchCoin { coin }, + StakingInfoError::Transport(e) => DelegationError::Transport(e), + StakingInfoError::UnexpectedDerivationMethod(reason) => { DelegationError::DelegationOpsNotSupported { reason } }, - StakingInfosError::Internal(e) => DelegationError::InternalError(e), + StakingInfoError::Internal(e) => DelegationError::InternalError(e), + StakingInfoError::InvalidPayload { reason } => DelegationError::InvalidPayload { reason }, } } } @@ -3266,9 +3429,7 @@ impl From for VerificationError { /// NB: Implementations are expected to follow the pImpl idiom, providing cheap reference-counted cloning and garbage collection. #[async_trait] -pub trait MmCoin: - SwapOps + TakerSwapMakerCoin + MakerSwapTakerCoin + WatcherOps + MarketCoinOps + Send + Sync + 'static -{ +pub trait MmCoin: SwapOps + WatcherOps + MarketCoinOps + Send + Sync + 'static { // `MmCoin` is an extension fulcrum for something that doesn't fit the `MarketCoinOps`. Practical examples: // name (might be required for some APIs, CoinMarketCap for instance); // coin statistics that we might want to share with UI; @@ -3291,8 +3452,8 @@ pub trait MmCoin: /// /// # Note /// - /// `CoinFutSpawner` doesn't prevent the spawned futures from being aborted. - fn spawner(&self) -> CoinFutSpawner; + /// `WeakSpawner` doesn't prevent the spawned futures from being aborted. + fn spawner(&self) -> WeakSpawner; fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut; @@ -3418,43 +3579,6 @@ pub trait MmCoin: fn on_token_deactivated(&self, ticker: &str); } -/// The coin futures spawner. It's used to spawn futures that can be aborted immediately or after a timeout -/// on the the coin deactivation. -/// -/// # Note -/// -/// `CoinFutSpawner` doesn't prevent the spawned futures from being aborted. -#[derive(Clone)] -pub struct CoinFutSpawner { - inner: WeakSpawner, -} - -impl CoinFutSpawner { - pub fn new(system: &AbortableQueue) -> CoinFutSpawner { - CoinFutSpawner { - inner: system.weak_spawner(), - } - } -} - -impl SpawnFuture for CoinFutSpawner { - fn spawn(&self, f: F) - where - F: Future03 + Send + 'static, - { - self.inner.spawn(f) - } -} - -impl SpawnAbortable for CoinFutSpawner { - fn spawn_with_settings(&self, fut: F, settings: AbortSettings) - where - F: Future03 + Send + 'static, - { - self.inner.spawn_with_settings(fut, settings) - } -} - #[derive(Clone)] #[allow(clippy::large_enum_variant)] pub enum MmCoinEnum { @@ -3471,6 +3595,7 @@ pub enum MmCoinEnum { LightningCoin(LightningCoin), #[cfg(feature = "enable-sia")] SiaCoin(SiaCoin), + #[cfg(any(test, feature = "for-tests"))] Test(TestCoin), } @@ -3482,6 +3607,7 @@ impl From for MmCoinEnum { fn from(c: EthCoin) -> MmCoinEnum { MmCoinEnum::EthCoin(c) } } +#[cfg(any(test, feature = "for-tests"))] impl From for MmCoinEnum { fn from(c: TestCoin) -> MmCoinEnum { MmCoinEnum::Test(c) } } @@ -3542,6 +3668,7 @@ impl Deref for MmCoinEnum { MmCoinEnum::ZCoin(ref c) => c, #[cfg(feature = "enable-sia")] MmCoinEnum::SiaCoin(ref c) => c, + #[cfg(any(test, feature = "for-tests"))] MmCoinEnum::Test(ref c) => c, } } @@ -3555,7 +3682,7 @@ impl MmCoinEnum { MmCoinEnum::Qrc20Coin(ref c) => c.as_ref().rpc_client.is_native(), MmCoinEnum::Bch(ref c) => c.as_ref().rpc_client.is_native(), MmCoinEnum::SlpToken(ref c) => c.as_ref().rpc_client.is_native(), - #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::ZCoin(ref c) => c.as_ref().rpc_client.is_native(), _ => false, } @@ -3564,6 +3691,21 @@ impl MmCoinEnum { pub fn is_eth(&self) -> bool { matches!(self, MmCoinEnum::EthCoin(_)) } fn is_platform_coin(&self) -> bool { self.ticker() == self.platform_ticker() } + + /// Determines the secret hash algorithm for a coin, prioritizing specific algorithms for certain protocols. + /// # Attention + /// When adding new coins, update this function to specify their appropriate secret hash algorithm. + /// Otherwise, the function will default to `SecretHashAlgo::DHASH160`, which may not be correct for the new coin. + pub fn secret_hash_algo_v2(&self) -> SecretHashAlgo { + match self { + MmCoinEnum::Tendermint(_) | MmCoinEnum::TendermintToken(_) | MmCoinEnum::EthCoin(_) => { + SecretHashAlgo::SHA256 + }, + #[cfg(not(target_arch = "wasm32"))] + MmCoinEnum::LightningCoin(_) => SecretHashAlgo::SHA256, + _ => SecretHashAlgo::DHASH160, + } + } } #[async_trait] @@ -3612,32 +3754,173 @@ impl MmCoinStruct { } } +/// Represents how to burn part of dex fee. +#[derive(Clone, Debug, PartialEq)] +pub enum DexFeeBurnDestination { + /// Burn by sending to utxo opreturn output + KmdOpReturn, + /// Send non-kmd coins to a dedicated account to exchange for kmd coins and burn them + PreBurnAccount, +} + /// Represents the different types of DEX fees. #[derive(Clone, Debug, PartialEq)] pub enum DexFee { + /// No dex fee is taken (if taker is dex pubkey) + NoFee, /// Standard dex fee which will be sent to the dex fee address Standard(MmNumber), - /// Dex fee with the burn amount. - /// - `fee_amount` goes to the dex fee address. - /// - `burn_amount` will be added as `OP_RETURN` output in the dex fee transaction. + /// Dex fee with the burn amount WithBurn { + /// Amount to go to the dex fee address fee_amount: MmNumber, + /// Amount to be burned burn_amount: MmNumber, + /// This indicates how to burn the burn_amount + burn_destination: DexFeeBurnDestination, }, } impl DexFee { - /// Creates a new `DexFee` with burn amounts. - pub fn with_burn(fee_amount: MmNumber, burn_amount: MmNumber) -> DexFee { - DexFee::WithBurn { - fee_amount, - burn_amount, + const DEX_FEE_SHARE: &str = "0.75"; + + /// Recreates a `DexFee` from separate fields (usually stored in db). + #[cfg(any(test, feature = "for-tests"))] + pub fn create_from_fields(fee_amount: MmNumber, burn_amount: MmNumber, ticker: &str) -> DexFee { + if fee_amount == MmNumber::default() && burn_amount == MmNumber::default() { + return DexFee::NoFee; + } + if burn_amount > MmNumber::default() { + let burn_destination = match ticker { + "KMD" => DexFeeBurnDestination::KmdOpReturn, + _ => DexFeeBurnDestination::PreBurnAccount, + }; + DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination, + } + } else { + DexFee::Standard(fee_amount) + } + } + + /// Calculates DEX fee with known taker_pubkey (for some takers dexfee may be zero). + pub fn new_with_taker_pubkey( + taker_coin: &dyn MmCoin, + maker_ticker: &str, + trade_amount: &MmNumber, + taker_pubkey: &[u8], + ) -> DexFee { + if !taker_coin.is_privacy() && taker_coin.burn_pubkey() == taker_pubkey { + return DexFee::NoFee; // no dex fee if the taker is the burn pubkey + } + Self::new_from_taker_coin(taker_coin, maker_ticker, trade_amount) + } + + /// Calculates DEX fee with a threshold based on min tx amount of the taker coin. + /// With this fn we may calculate the max dex fee amount, when taker_pubkey is not known yet. + pub fn new_from_taker_coin(taker_coin: &dyn MmCoin, maker_ticker: &str, trade_amount: &MmNumber) -> DexFee { + // calc dex fee + let rate = Self::dex_fee_rate(taker_coin.ticker(), maker_ticker); + let dex_fee = trade_amount * &rate; + let min_tx_amount = MmNumber::from(taker_coin.min_tx_amount()); + + if taker_coin.is_kmd() { + // use a special dex fee option for kmd + return Self::calc_dex_fee_for_op_return(dex_fee, min_tx_amount); + } + if taker_coin.should_burn_dex_fee() { + // send part of dex fee to the 'pre-burn' account + return Self::calc_dex_fee_for_burn_account(dex_fee, min_tx_amount); + } + if dex_fee <= min_tx_amount { + return DexFee::Standard(min_tx_amount); + } + DexFee::Standard(dex_fee) + } + + /// Returns dex fee discount if KMD is traded + pub fn dex_fee_rate(base: &str, rel: &str) -> MmNumber { + #[cfg(any(feature = "for-tests", test))] + let fee_discount_tickers: &[&str] = match std::env::var("MYCOIN_FEE_DISCOUNT") { + Ok(_) => &["KMD", "MYCOIN"], + Err(_) => &["KMD"], + }; + + #[cfg(not(any(feature = "for-tests", test)))] + let fee_discount_tickers: &[&str] = &["KMD"]; + + if fee_discount_tickers.contains(&base) || fee_discount_tickers.contains(&rel) { + // 1/777 - 10% + BigRational::new(9.into(), 7770.into()).into() + } else { + BigRational::new(1.into(), 777.into()).into() + } + } + + /// Drops the dex fee in KMD by 25%. This cut will be burned during the taker fee payment. + /// + /// Also the cut can be decreased if the new dex fee amount is less than the minimum transaction amount. + fn calc_dex_fee_for_op_return(dex_fee: MmNumber, min_tx_amount: MmNumber) -> DexFee { + if dex_fee <= min_tx_amount { + return DexFee::Standard(min_tx_amount); + } + // Dex fee with 25% burn amount cut + let new_fee = &dex_fee * &MmNumber::from(Self::DEX_FEE_SHARE); + if new_fee >= min_tx_amount { + // Use the max burn value, which is 25%. + DexFee::WithBurn { + fee_amount: new_fee.clone(), + burn_amount: dex_fee - new_fee, + burn_destination: DexFeeBurnDestination::KmdOpReturn, + } + } else { + // Burn only the exceeding amount because fee after 25% cut is less than `min_tx_amount`. + DexFee::WithBurn { + fee_amount: min_tx_amount.clone(), + burn_amount: dex_fee - min_tx_amount, + burn_destination: DexFeeBurnDestination::KmdOpReturn, + } + } + } + + /// Drops the dex fee in non-KMD by 25%. This cut will be sent to an output designated as 'burn account' during the taker fee payment + /// (so it cannot be dust). + /// + /// The cut can be set to zero if any of resulting amounts is less than the minimum transaction amount. + fn calc_dex_fee_for_burn_account(dex_fee: MmNumber, min_tx_amount: MmNumber) -> DexFee { + if dex_fee <= min_tx_amount { + return DexFee::Standard(min_tx_amount); + } + // Dex fee with 25% burn amount cut + let new_fee = &dex_fee * &MmNumber::from(Self::DEX_FEE_SHARE); + let burn_amount = &dex_fee - &new_fee; + if new_fee >= min_tx_amount && burn_amount >= min_tx_amount { + // Use the max burn value, which is 25%. Ensure burn_amount is not dust + return DexFee::WithBurn { + fee_amount: new_fee, + burn_amount, + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + } + // If the new dex fee is dust set it to min_tx_amount and check the updated burn_amount is not dust. + let burn_amount = &dex_fee - &min_tx_amount; + if new_fee < min_tx_amount && burn_amount >= min_tx_amount { + DexFee::WithBurn { + fee_amount: min_tx_amount, + burn_amount, + burn_destination: DexFeeBurnDestination::PreBurnAccount, + } + } else { + DexFee::Standard(dex_fee) } } /// Gets the fee amount associated with the dex fee. pub fn fee_amount(&self) -> MmNumber { match self { + DexFee::NoFee => 0.into(), DexFee::Standard(t) => t.clone(), DexFee::WithBurn { fee_amount, .. } => fee_amount.clone(), } @@ -3646,7 +3929,7 @@ impl DexFee { /// Gets the burn amount associated with the dex fee, if applicable. pub fn burn_amount(&self) -> Option { match self { - DexFee::Standard(_) => None, + DexFee::Standard(_) | DexFee::NoFee => None, DexFee::WithBurn { burn_amount, .. } => Some(burn_amount.clone()), } } @@ -3654,22 +3937,24 @@ impl DexFee { /// Calculates the total spend amount, considering both the fee and burn amounts. pub fn total_spend_amount(&self) -> MmNumber { match self { + DexFee::NoFee => 0.into(), DexFee::Standard(t) => t.clone(), DexFee::WithBurn { fee_amount, burn_amount, + .. } => fee_amount + burn_amount, } } /// Converts the fee amount to micro-units based on the specified decimal places. - pub fn fee_uamount(&self, decimals: u8) -> NumConversResult { + pub fn fee_amount_as_u64(&self, decimals: u8) -> NumConversResult { let fee_amount = self.fee_amount(); utxo::sat_from_big_decimal(&fee_amount.into(), decimals) } /// Converts the burn amount to micro-units, if applicable, based on the specified decimal places. - pub fn burn_uamount(&self, decimals: u8) -> NumConversResult> { + pub fn burn_amount_as_u64(&self, decimals: u8) -> NumConversResult> { if let Some(burn_amount) = self.burn_amount() { Ok(Some(utxo::sat_from_big_decimal(&burn_amount.into(), decimals)?)) } else { @@ -3708,11 +3993,11 @@ impl CoinsContext { platform_coin_tokens: PaMutex::new(HashMap::new()), coins: AsyncMutex::new(HashMap::new()), balance_update_handlers: AsyncMutex::new(vec![]), - account_balance_task_manager: AccountBalanceTaskManager::new_shared(), - create_account_manager: CreateAccountTaskManager::new_shared(), - get_new_address_manager: GetNewAddressTaskManager::new_shared(), - scan_addresses_manager: ScanAddressesTaskManager::new_shared(), - withdraw_task_manager: WithdrawTaskManager::new_shared(), + account_balance_task_manager: AccountBalanceTaskManager::new_shared(ctx.event_stream_manager.clone()), + create_account_manager: CreateAccountTaskManager::new_shared(ctx.event_stream_manager.clone()), + get_new_address_manager: GetNewAddressTaskManager::new_shared(ctx.event_stream_manager.clone()), + scan_addresses_manager: ScanAddressesTaskManager::new_shared(ctx.event_stream_manager.clone()), + withdraw_task_manager: WithdrawTaskManager::new_shared(ctx.event_stream_manager.clone()), #[cfg(target_arch = "wasm32")] tx_history_db: ConstructibleDb::new(ctx).into_shared(), #[cfg(target_arch = "wasm32")] @@ -4854,41 +5139,129 @@ pub async fn sign_raw_transaction(ctx: MmArc, req: SignRawTransactionRequest) -> pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> DelegationResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match coin { - MmCoinEnum::QtumCoin(qtum) => qtum.remove_delegation().compat().await, - _ => { - return MmError::err(DelegationError::CoinDoesntSupportDelegation { - coin: coin.ticker().to_string(), - }) + + match req.staking_details { + Some(StakingDetails::Cosmos(req)) => { + if req.withdraw_from.is_some() { + return MmError::err(DelegationError::InvalidPayload { + reason: "Can't use `withdraw_from` field on 'remove_delegation' RPC for Cosmos.".to_owned(), + }); + } + + let MmCoinEnum::Tendermint(tendermint) = coin else { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }); + }; + + tendermint.undelegate(*req).await + }, + + Some(StakingDetails::Qtum(_)) => MmError::err(DelegationError::InvalidPayload { + reason: "staking_details isn't supported for Qtum".into(), + }), + + None => match coin { + MmCoinEnum::QtumCoin(qtum) => qtum.remove_delegation().compat().await, + _ => { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }) + }, }, } } -pub async fn get_staking_infos(ctx: MmArc, req: GetStakingInfosRequest) -> StakingInfosResult { +pub async fn delegations_info(ctx: MmArc, req: DelegationsInfo) -> Result> { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match coin { - MmCoinEnum::QtumCoin(qtum) => qtum.get_delegation_infos().compat().await, - _ => { - return MmError::err(StakingInfosError::CoinDoesntSupportStakingInfos { - coin: coin.ticker().to_string(), - }) + + match req.info_details { + DelegationsInfoDetails::Qtum => { + let MmCoinEnum::QtumCoin(qtum) = coin else { + return MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Qtum coin", req.coin), + }); + }; + + qtum.get_delegation_infos().compat().await.map(|v| json!(v)) + }, + + DelegationsInfoDetails::Cosmos(r) => match coin { + MmCoinEnum::Tendermint(t) => Ok(t.delegations_list(r.paging).await.map(|v| json!(v))?), + MmCoinEnum::TendermintToken(_) => MmError::err(StakingInfoError::InvalidPayload { + reason: "Tokens are not supported for delegation".into(), + }), + _ => MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin), + }), }, } } -pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationResult { +pub async fn ongoing_undelegations_info(ctx: MmArc, req: UndelegationsInfo) -> Result> { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - // Need to find a way to do a proper dispatch - let coin_concrete = match coin { - MmCoinEnum::QtumCoin(qtum) => qtum, - _ => { - return MmError::err(DelegationError::CoinDoesntSupportDelegation { - coin: coin.ticker().to_string(), - }) + + match req.info_details { + UndelegationsInfoDetails::Cosmos(r) => match coin { + MmCoinEnum::Tendermint(t) => Ok(t.ongoing_undelegations_list(r.paging).await.map(|v| json!(v))?), + MmCoinEnum::TendermintToken(_) => MmError::err(StakingInfoError::InvalidPayload { + reason: "Tokens are not supported for delegation".into(), + }), + _ => MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin), + }), }, - }; + } +} + +pub async fn validators_info(ctx: MmArc, req: ValidatorsInfo) -> Result> { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + + match req.info_details { + ValidatorsInfoDetails::Cosmos(payload) => rpc_command::tendermint::staking::validators_rpc(coin, payload) + .await + .map(|v| json!(v)), + } +} + +pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match req.staking_details { - StakingDetails::Qtum(qtum_staking) => coin_concrete.add_delegation(qtum_staking).compat().await, + StakingDetails::Qtum(req) => { + let MmCoinEnum::QtumCoin(qtum) = coin else { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }); + }; + + qtum.add_delegation(req).compat().await + }, + StakingDetails::Cosmos(req) => { + let MmCoinEnum::Tendermint(tendermint) = coin else { + return MmError::err(DelegationError::CoinDoesntSupportDelegation { + coin: coin.ticker().to_string(), + }); + }; + + tendermint.delegate(*req).await + }, + } +} + +pub async fn claim_staking_rewards(ctx: MmArc, req: ClaimStakingRewardsRequest) -> DelegationResult { + match req.claiming_details { + ClaimingDetails::Cosmos(r) => { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + + let MmCoinEnum::Tendermint(tendermint) = coin else { + return MmError::err(DelegationError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", req.coin), + }); + }; + + tendermint.claim_staking_rewards(r).await + }, } } @@ -5578,7 +5951,7 @@ where .await? .into_iter() .map(|empty_address| HDAddressBalance { - address: coin.address_formatter()(&empty_address.address()), + address: empty_address.address().display_address(), derivation_path: RpcDerivationPath(empty_address.derivation_path().clone()), chain, balance: HDWalletBalanceObject::::new(), @@ -5587,7 +5960,7 @@ where // Then push this non-empty address. balances.push(HDAddressBalance { - address: coin.address_formatter()(&checking_address), + address: checking_address.display_address(), derivation_path: RpcDerivationPath(checking_address_der_path.clone()), chain, balance: non_empty_balance, @@ -5615,9 +5988,9 @@ where #[cfg(test)] mod tests { use super::*; - use common::block_on; use mm2_test_helpers::for_tests::RICK; + use mocktopus::mocking::{MockResult, Mockable}; #[test] fn test_lp_coinfind() { @@ -5668,6 +6041,143 @@ mod tests { assert!(matches!(Some(coin), _found)); } + + #[test] + fn test_dex_fee_amount() { + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.0001").into())); + let rel = "ETH"; + let amount = 1.into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount); + let expected_fee = DexFee::WithBurn { + fee_amount: amount.clone() / 777u64.into() * "0.75".into(), + burn_amount: amount / 777u64.into() * "0.25".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + assert_eq!(expected_fee, actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + let base = "KMD"; + let kmd = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.0001").into())); + let rel = "ETH"; + let amount = 1.into(); + let actual_fee = DexFee::new_from_taker_coin(&kmd, rel, &amount); + let expected_fee = amount.clone() * (9, 7770).into() * MmNumber::from("0.75"); + let expected_burn_amount = amount * (9, 7770).into() * MmNumber::from("0.25"); + assert_eq!( + DexFee::WithBurn { + fee_amount: expected_fee, + burn_amount: expected_burn_amount, + burn_destination: DexFeeBurnDestination::KmdOpReturn, + }, + actual_fee + ); + TestCoin::should_burn_dex_fee.clear_mock(); + + // check the case when KMD taker fee is close to dust (0.75 of fee < dust) + let base = "KMD"; + let kmd = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "BTC"; + let amount = (1001 * 777, 90000000).into(); + let actual_fee = DexFee::new_from_taker_coin(&kmd, rel, &amount); + assert_eq!( + DexFee::WithBurn { + fee_amount: "0.00001".into(), // equals to min_tx_amount + burn_amount: "0.00000001".into(), + burn_destination: DexFeeBurnDestination::KmdOpReturn, + }, + actual_fee + ); + TestCoin::should_burn_dex_fee.clear_mock(); + + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "KMD"; + let amount = 1.into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount); + let expected_fee = DexFee::WithBurn { + fee_amount: amount.clone() * (9, 7770).into() * "0.75".into(), + burn_amount: amount * (9, 7770).into() * "0.25".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + assert_eq!(expected_fee, actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + // whole dex fee (0.001 * 9 / 7770) less than min tx amount (0.00001) + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "KMD"; + let amount: MmNumber = "0.001".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount); + assert_eq!(DexFee::Standard("0.00001".into()), actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + // 75% of dex fee (0.03 * 9/7770 * 0.75) is over the min tx amount (0.00001) + // but non-kmd burn amount is less than the min tx amount + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "KMD"; + let amount: MmNumber = "0.03".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount); + assert_eq!(DexFee::Standard(amount * (9, 7770).into()), actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + // burning from eth currently not supported + let base = "USDT-ERC20"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(false)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "BTC"; + let amount: MmNumber = "1".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount); + assert_eq!(DexFee::Standard(amount / "777".into()), actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + + let base = "NUCLEUS"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.000001").into())); + let rel = "IRIS"; + let amount: MmNumber = "0.008".parse::().unwrap().into(); + let actual_fee = DexFee::new_from_taker_coin(&btc, rel, &amount); + let std_fee = amount / "777".into(); + let fee_amount = std_fee.clone() * "0.75".into(); + let burn_amount = std_fee - fee_amount.clone(); + assert_eq!( + DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }, + actual_fee + ); + TestCoin::should_burn_dex_fee.clear_mock(); + + // test NoFee if taker is dex + let base = "BTC"; + let btc = TestCoin::new(base); + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + TestCoin::dex_pubkey.mock_safe(|_| MockResult::Return(DEX_BURN_ADDR_RAW_PUBKEY.as_slice())); + TestCoin::min_tx_amount.mock_safe(|_| MockResult::Return(MmNumber::from("0.00001").into())); + let rel = "KMD"; + let amount: MmNumber = "0.03".parse::().unwrap().into(); + let actual_fee = DexFee::new_with_taker_pubkey(&btc, rel, &amount, DEX_BURN_ADDR_RAW_PUBKEY.as_slice()); + assert_eq!(DexFee::NoFee, actual_fee); + TestCoin::should_burn_dex_fee.clear_mock(); + TestCoin::dex_pubkey.clear_mock(); + } } #[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] @@ -5680,7 +6190,7 @@ pub mod for_tests { use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; use mm2_number::BigDecimal; - use rpc_task::RpcTaskStatus; + use rpc_task::{RpcInitReq, RpcTaskStatus}; use std::str::FromStr; /// Helper to call init_withdraw and wait for completion @@ -5692,17 +6202,18 @@ pub mod for_tests { from_derivation_path: Option<&str>, fee: Option, ) -> MmResult { - let withdraw_req = WithdrawRequest { - amount: BigDecimal::from_str(amount).unwrap(), - from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath { - derivation_path: from_derivation_path.to_owned(), - }), - to: to.to_owned(), - coin: ticker.to_owned(), - max: false, - fee, - memo: None, - ibc_source_channel: None, + let withdraw_req = RpcInitReq { + client_id: 0, + inner: WithdrawRequest { + amount: BigDecimal::from_str(amount).unwrap(), + from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath { + derivation_path: from_derivation_path.to_owned(), + }), + to: to.to_owned(), + coin: ticker.to_owned(), + fee, + ..Default::default() + }, }; let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); let timeout = wait_until_ms(150000); diff --git a/mm2src/coins/lp_price.rs b/mm2src/coins/lp_price.rs index fb339d3752..1a720d3e3d 100644 --- a/mm2src/coins/lp_price.rs +++ b/mm2src/coins/lp_price.rs @@ -80,8 +80,9 @@ pub enum Provider { Coinpaprika, #[serde(rename = "forex")] Forex, - #[serde(rename = "nomics")] - Nomics, + #[serde(rename = "livecoinwatch")] + LiveCoinWatch, + #[cfg(any(test, feature = "for-tests"))] #[serde(rename = "testcoin")] TestCoin, #[serde(rename = "unknown", other)] diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index f7348a5e78..d35de25ddc 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -1,20 +1,18 @@ -use crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError}; -use crate::tendermint::{TENDERMINT_ASSET_PROTOCOL_TYPE, TENDERMINT_COIN_PROTOCOL_TYPE}; +use crate::hd_wallet::{AddressDerivingError, DisplayAddress, InvalidBip44ChainError}; +use crate::tendermint::{BCH_COIN_PROTOCOL_TYPE, BCH_TOKEN_PROTOCOL_TYPE, TENDERMINT_ASSET_PROTOCOL_TYPE, + TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::tx_history_storage::{CreateTxHistoryStorageError, FilteringAddresses, GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; -use crate::MyAddressError; use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDPathAccountToAddressId, - HistorySyncState, MmCoin, MmCoinEnum, Transaction, TransactionData, TransactionDetails, TransactionType, - TxFeeDetails, UtxoRpcError}; + HistorySyncState, MmCoin, MmCoinEnum, MyAddressError, Transaction, TransactionData, TransactionDetails, + TransactionType, TxFeeDetails, UtxoRpcError}; use async_trait::async_trait; use bitcrypto::sha256; use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; -use crypto::StandardHDPath; use derive_more::Display; use enum_derives::EnumFromStringify; use futures::compat::Future01CompatExt; -use keys::{Address, CashAddress}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -134,18 +132,6 @@ pub trait TxHistoryStorage: Send + Sync + 'static { ) -> Result>; } -pub trait DisplayAddress { - fn display_address(&self) -> String; -} - -impl DisplayAddress for Address { - fn display_address(&self) -> String { self.to_string() } -} - -impl DisplayAddress for CashAddress { - fn display_address(&self) -> String { self.encode().expect("A valid cash address") } -} - pub struct TxDetailsBuilder<'a, Addr: DisplayAddress, Tx: Transaction> { coin: String, tx: &'a Tx, @@ -224,7 +210,8 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T bytes_for_hash.extend_from_slice(&token_id.0); sha256(&bytes_for_hash).to_vec().into() }, - TransactionType::CustomTendermintMsg { token_id, .. } => { + TransactionType::TendermintIBCTransfer { token_id } + | TransactionType::CustomTendermintMsg { token_id, .. } => { if let Some(token_id) = token_id { let mut bytes_for_hash = tx_hash.0.clone(); bytes_for_hash.extend_from_slice(&token_id.0); @@ -235,10 +222,10 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T }, TransactionType::StakingDelegation | TransactionType::RemoveDelegation + | TransactionType::ClaimDelegationRewards | TransactionType::FeeForTokenTx | TransactionType::StandardTransfer - | TransactionType::NftTransfer - | TransactionType::TendermintIBCTransfer => tx_hash.clone(), + | TransactionType::NftTransfer => tx_hash.clone(), }; TransactionDetails { @@ -272,7 +259,6 @@ pub enum MyTxHistoryTarget { account_id: u32, }, AddressId(HDPathAccountToAddressId), - AddressDerivationPath(StandardHDPath), } #[derive(Clone, Deserialize)] @@ -429,11 +415,6 @@ where .transactions .into_iter() .map(|mut details| { - // it can be the platform ticker instead of the token ticker for a pre-saved record - if details.coin != request.coin { - details.coin = request.coin.clone(); - } - // TODO // !! temporary solution !! // for tendermint, tx_history_v2 implementation doesn't include amount parsing logic. @@ -483,6 +464,16 @@ where }, } }, + BCH_COIN_PROTOCOL_TYPE | BCH_TOKEN_PROTOCOL_TYPE => { + // SLP tokens are part of BCH transactions and SLP transactions might be stored with the BCH ticker. + // Ideally, we should avoid this workaround and instead fix the incorrect ticker logic when inserting + // transactions with the wrong ticker. + // + // Original PR: https://github.com/KomodoPlatform/komodo-defi-framework/pull/1175. + if details.coin != request.coin { + details.coin = request.coin.clone(); + } + }, _ => {}, }; diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index afc5f260a9..ebd4c83146 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -11,15 +11,16 @@ pub(crate) mod storage; #[cfg(any(test, target_arch = "wasm32"))] mod nft_tests; -use crate::{coin_conf, get_my_address, lp_coinfind_or_err, CoinsContext, HDPathAccountToAddressId, MarketCoinOps, - MmCoinEnum, MmCoinStruct, MyAddressReq, WithdrawError}; +use crate::hd_wallet::AddrToString; +use crate::{lp_coinfind_or_err, CoinWithDerivationMethod, CoinsContext, MarketCoinOps, MmCoinEnum, MmCoinStruct, + WithdrawError}; use nft_errors::{GetNftInfoError, UpdateNftError}; use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftList, NftListReq, NftMetadataReq, NftTransferHistory, NftTransferHistoryFromMoralis, NftTransfersReq, NftsTransferHistoryList, TransactionNftDetails, UpdateNftReq, WithdrawNftReq}; -use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721, EthCoin, EthCoinType, - EthTxFeeDetails, LegacyGasPrice, PayForGasOption}; +use crate::eth::{withdraw_erc1155, withdraw_erc721, EthCoin, EthCoinType, EthTxFeeDetails, LegacyGasPrice, + PayForGasOption}; use crate::nft::nft_errors::{ClearNftDbError, MetaFromUrlError, ProtectFromSpamError, TransferConfirmationsError, UpdateSpamPhishingError}; use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNftDbReq, NftCommon, NftCtx, NftInfo, @@ -237,9 +238,8 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft NftTransferHistoryStorageOps::init(&storage, chain).await?; None }; - // TODO activate and use global NFT instead of ETH coin after adding enable nft using coin conf support - let coin_enum = lp_coinfind_or_err(&ctx, chain.to_ticker()).await?; - let eth_coin = match coin_enum { + let coin_enum = lp_coinfind_or_err(&ctx, chain.to_nft_ticker()).await?; + let global_nft = match coin_enum { MmCoinEnum::EthCoin(eth_coin) => eth_coin, _ => { return MmError::err(UpdateNftError::CoinDoesntSupportNft { @@ -247,6 +247,8 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft }) }, }; + let my_address = global_nft.derivation_method().single_addr_or_err().await?; + let my_address_str = my_address.addr_to_string(); let proxy_sign = if req.komodo_proxy { let uri = Uri::from_str(req.url.as_ref()).map_err(|e| UpdateNftError::Internal(e.to_string()))?; let proxy_sign = RawMessage::sign(p2p_ctx.keypair(), &uri, 0, common::PROXY_REQUEST_EXPIRATION_SEC) @@ -263,14 +265,14 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft proxy_sign, }; - let nft_transfers = get_moralis_nft_transfers(&ctx, from_block, eth_coin, &wrapper).await?; + let nft_transfers = get_moralis_nft_transfers(from_block, global_nft, &my_address_str, &wrapper).await?; storage.add_transfers_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 nft list from moralis. - let nft_list = cache_nfts_from_moralis(&ctx, &storage, &wrapper).await?; + let nft_list = cache_nfts_from_moralis(&my_address_str, &storage, &wrapper).await?; update_meta_in_transfers(&storage, chain, nft_list).await?; update_transfers_with_empty_meta(&storage, &wrapper).await?; update_spam(&storage, *chain, &req.url_antispam).await?; @@ -280,7 +282,7 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft Err(_) => { // if there is an error, then NFT LIST table doesn't exist, so we need to cache nft list from moralis. NftListStorageOps::init(&storage, chain).await?; - let nft_list = cache_nfts_from_moralis(&ctx, &storage, &wrapper).await?; + let nft_list = cache_nfts_from_moralis(&my_address_str, &storage, &wrapper).await?; update_meta_in_transfers(&storage, chain, nft_list).await?; update_transfers_with_empty_meta(&storage, &wrapper).await?; update_spam(&storage, *chain, &req.url_antispam).await?; @@ -303,7 +305,7 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft last_nft_block: nft_block.to_string(), }); } - update_nft_list(ctx.clone(), &storage, scanned_block + 1, &wrapper).await?; + update_nft_list(&storage, scanned_block + 1, &my_address_str, &wrapper).await?; update_nft_global_in_coins_ctx(&ctx, &storage, *chain).await?; update_transfers_with_empty_meta(&storage, &wrapper).await?; update_spam(&storage, *chain, &req.url_antispam).await?; @@ -378,13 +380,13 @@ where if !token_addresses.is_empty() { let addresses = token_addresses .iter() - .map(eth_addr_to_hex) + .map(Address::addr_to_string) .collect::>() .join(","); let spam_res = send_spam_request(&chain, url_antispam, addresses).await?; for (address, is_spam) in spam_res.result.into_iter() { if is_spam { - let address_hex = eth_addr_to_hex(&address); + let address_hex = address.addr_to_string(); storage .update_nft_spam_by_token_address(&chain, address_hex.clone(), is_spam) .await?; @@ -495,7 +497,7 @@ pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResu proxy_sign, }; - let token_address_str = eth_addr_to_hex(&req.token_address); + let token_address_str = req.token_address.addr_to_string(); let mut moralis_meta = match get_moralis_metadata(token_address_str.clone(), req.token_id.clone(), &wrapper).await { Ok(moralis_meta) => moralis_meta, Err(_) => { @@ -592,7 +594,7 @@ async fn refresh_possible_spam( where T: NftListStorageOps + NftTransferHistoryStorageOps, { - let address_hex = eth_addr_to_hex(&nft_db.common.token_address); + let address_hex = nft_db.common.token_address.addr_to_string(); let spam_res = send_spam_request(chain, url_antispam, address_hex.clone()).await?; if let Some(true) = spam_res.result.get(&nft_db.common.token_address) { nft_db.common.possible_spam = true; @@ -635,13 +637,13 @@ where Ok(()) } -async fn get_moralis_nft_list(ctx: &MmArc, wrapper: &UrlSignWrapper<'_>) -> MmResult, GetNftInfoError> { +async fn get_moralis_nft_list( + wallet_address: &str, + wrapper: &UrlSignWrapper<'_>, +) -> MmResult, GetNftInfoError> { let mut res_list = Vec::new(); let chain = wrapper.chain; - let ticker = chain.to_ticker(); - let conf = coin_conf(ctx, ticker); - let my_address = get_eth_address(ctx, &conf, ticker, &HDPathAccountToAddressId::default()).await?; - let uri_without_cursor = construct_moralis_uri_for_nft(wrapper.orig_url, &my_address.wallet_address, chain)?; + let uri_without_cursor = construct_moralis_uri_for_nft(wrapper.orig_url, wallet_address, chain)?; // The cursor returned in the previous response (used for getting the next page). let mut cursor = String::new(); @@ -683,7 +685,7 @@ pub(crate) async fn get_nfts_for_activation( proxy_sign: Option, ) -> MmResult, GetNftInfoError> { let mut nfts_map = HashMap::new(); - let uri_without_cursor = construct_moralis_uri_for_nft(orig_url, ð_addr_to_hex(my_address), chain)?; + let uri_without_cursor = construct_moralis_uri_for_nft(orig_url, &my_address.addr_to_string(), chain)?; // The cursor returned in the previous response (used for getting the next page). let mut cursor = String::new(); @@ -719,7 +721,7 @@ fn process_nft_list_for_activation( Some(contract_type) => contract_type, None => continue, }; - let token_address_str = eth_addr_to_hex(&nft_moralis.common.token_address); + let token_address_str = nft_moralis.common.token_address.addr_to_string(); let nft_info = NftInfo { token_address: nft_moralis.common.token_address, token_id: nft_moralis.token_id.0.clone(), @@ -734,16 +736,13 @@ fn process_nft_list_for_activation( } async fn get_moralis_nft_transfers( - ctx: &MmArc, from_block: Option, - eth_coin: EthCoin, + global_nft: EthCoin, + wallet_address: &str, wrapper: &UrlSignWrapper<'_>, ) -> MmResult, GetNftInfoError> { let chain = wrapper.chain; let mut res_list = Vec::new(); - let ticker = chain.to_ticker(); - let conf = coin_conf(ctx, ticker); - let my_address = get_eth_address(ctx, &conf, ticker, &HDPathAccountToAddressId::default()).await?; let mut uri_without_cursor = wrapper.orig_url.clone(); uri_without_cursor @@ -751,7 +750,7 @@ async fn get_moralis_nft_transfers( .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? .push(MORALIS_API) .push(MORALIS_ENDPOINT_V) - .push(&my_address.wallet_address) + .push(wallet_address) .push("nft") .push("transfers"); let from_block = match from_block { @@ -767,13 +766,12 @@ async fn get_moralis_nft_transfers( // 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 { // Create a new URL instance from uri_without_cursor and modify its query to include the cursor if present let uri = format!("{}{}", uri_without_cursor, cursor); let response = build_and_send_request(uri.as_str(), &wrapper.proxy_sign).await?; if let Some(transfer_list) = response["result"].as_array() { - process_transfer_list(transfer_list, chain, wallet_address.as_str(), ð_coin, &mut res_list).await?; + process_transfer_list(transfer_list, chain, wallet_address, &global_nft, &mut res_list).await?; // 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() { @@ -793,7 +791,7 @@ async fn process_transfer_list( transfer_list: &[Json], chain: &Chain, wallet_address: &str, - eth_coin: &EthCoin, + global_nft: &EthCoin, res_list: &mut Vec, ) -> MmResult<(), GetNftInfoError> { for transfer in transfer_list { @@ -802,9 +800,9 @@ async fn process_transfer_list( Some(contract_type) => contract_type, None => continue, }; - let status = get_transfer_status(wallet_address, ð_addr_to_hex(&transfer_moralis.common.to_address)); + let status = get_transfer_status(wallet_address, &transfer_moralis.common.to_address.addr_to_string()); let block_timestamp = parse_rfc3339_to_timestamp(&transfer_moralis.block_timestamp)?; - let fee_details = get_fee_details(eth_coin, &transfer_moralis.common.transaction_hash).await; + let fee_details = get_fee_details(global_nft, &transfer_moralis.common.transaction_hash).await; let transfer_history = NftTransferHistory { common: NftTransferCommon { block_hash: transfer_moralis.common.block_hash, @@ -843,7 +841,6 @@ async fn process_transfer_list( Ok(()) } -// TODO: get fee details from non fungible token instead of eth coin? async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option { let hash = H256::from_str(transaction_hash).ok()?; let receipt = eth_coin.web3().await.ok()?.eth().transaction_receipt(hash).await.ok()?; @@ -1016,20 +1013,15 @@ fn get_transfer_status(my_wallet: &str, to_address: &str) -> TransferStatus { /// `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, scan_from_block: u64, + wallet_address: &str, wrapper: &UrlSignWrapper<'_>, ) -> MmResult<(), UpdateNftError> { let chain = wrapper.chain; let transfers = storage.get_transfers_from_block(*chain, scan_from_block).await?; - let req = MyAddressReq { - coin: chain.to_ticker().to_string(), - path_to_address: HDPathAccountToAddressId::default(), - }; - let my_address = get_my_address(ctx.clone(), req).await?.wallet_address.to_lowercase(); for transfer in transfers.into_iter() { - handle_nft_transfer(storage, wrapper, transfer, &my_address).await?; + handle_nft_transfer(storage, wrapper, transfer, wallet_address).await?; } Ok(()) } @@ -1061,18 +1053,18 @@ async fn handle_send_erc721 storage .get_nft( chain, - eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_address.addr_to_string(), transfer.token_id.clone(), ) .await? .ok_or_else(|| UpdateNftError::TokenNotFoundInWallet { - token_address: eth_addr_to_hex(&transfer.common.token_address), + token_address: transfer.common.token_address.addr_to_string(), token_id: transfer.token_id.to_string(), })?; storage .remove_nft_from_list( chain, - eth_addr_to_hex(&transfer.common.token_address), + transfer.common.token_address.addr_to_string(), transfer.token_id, transfer.block_number, ) @@ -1087,7 +1079,7 @@ async fn handle_receive_erc721 MmResult<(), UpdateNftError> { let chain = wrapper.chain; - let token_address_str = eth_addr_to_hex(&transfer.common.token_address); + let token_address_str = transfer.common.token_address.addr_to_string(); match storage .get_nft(chain, token_address_str.clone(), transfer.token_id.clone()) .await? @@ -1095,7 +1087,7 @@ async fn handle_receive_erc721 { // 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(&transfer.common.from_address) { + if my_address != transfer.common.from_address.addr_to_string() { return MmError::err(UpdateNftError::AttemptToReceiveAlreadyOwnedErc721 { tx_hash: transfer.common.transaction_hash, }); @@ -1136,7 +1128,7 @@ async fn handle_send_erc1155 MmResult<(), UpdateNftError> { - let token_address_str = eth_addr_to_hex(&transfer.common.token_address); + let token_address_str = transfer.common.token_address.addr_to_string(); let mut nft_db = storage .get_nft(chain, token_address_str.clone(), transfer.token_id.clone()) .await? @@ -1173,7 +1165,7 @@ async fn handle_receive_erc1155 MmResult<(), UpdateNftError> { let chain = wrapper.chain; - let token_address_str = eth_addr_to_hex(&transfer.common.token_address); + let token_address_str = transfer.common.token_address.addr_to_string(); let mut nft = match storage .get_nft(chain, token_address_str.clone(), transfer.token_id.clone()) .await? @@ -1181,7 +1173,7 @@ async fn handle_receive_erc1155 { // 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(&transfer.common.from_address) { + if my_address != transfer.common.from_address.addr_to_string() { nft_db.common.amount += transfer.common.amount; } nft_db.block_number = transfer.block_number; @@ -1294,11 +1286,11 @@ async fn mark_as_spam_and_build_empty_meta( - ctx: &MmArc, + wallet_address: &str, storage: &T, wrapper: &UrlSignWrapper<'_>, ) -> MmResult, UpdateNftError> { - let nft_list = get_moralis_nft_list(ctx, wrapper).await?; + let nft_list = get_moralis_nft_list(wallet_address, wrapper).await?; let last_scanned_block = NftTransferHistoryStorageOps::get_last_block_number(storage, wrapper.chain) .await? .unwrap_or(0); diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs index 12e8d326a0..74d5032cbe 100644 --- a/mm2src/coins/nft/nft_errors.rs +++ b/mm2src/coins/nft/nft_errors.rs @@ -217,6 +217,7 @@ pub enum UpdateNftError { }, #[display(fmt = "Private key policy is not allowed: {}", _0)] PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), } impl From for UpdateNftError { @@ -264,6 +265,10 @@ impl From for UpdateNftError { } } +impl From for UpdateNftError { + fn from(e: UnexpectedDerivationMethod) -> Self { Self::UnexpectedDerivationMethod(e) } +} + impl HttpStatusCode for UpdateNftError { fn status_code(&self) -> StatusCode { match self { @@ -283,7 +288,8 @@ impl HttpStatusCode for UpdateNftError { | UpdateNftError::ProtectFromSpamError(_) | UpdateNftError::NoSuchCoin { .. } | UpdateNftError::CoinDoesntSupportNft { .. } - | UpdateNftError::PrivKeyPolicyNotAllowed(_) => StatusCode::INTERNAL_SERVER_ERROR, + | UpdateNftError::PrivKeyPolicyNotAllowed(_) + | UpdateNftError::UnexpectedDerivationMethod(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index e1412933d4..34be85f2b5 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -16,13 +16,12 @@ use std::sync::Arc; use url::Url; use crate::eth::EthTxFeeDetails; -use crate::nft::eth_addr_to_hex; +use crate::hd_wallet::AddrToString; use crate::nft::nft_errors::{LockDBError, ParseChainTypeError, ParseContractTypeError}; -use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; -use crate::{TransactionType, TxFeeDetails, WithdrawFee}; - #[cfg(not(target_arch = "wasm32"))] use crate::nft::storage::NftMigrationOps; +use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; +use crate::{TransactionType, TxFeeDetails, WithdrawFee}; cfg_native! { use db_common::async_sql_conn::AsyncConnection; @@ -703,7 +702,7 @@ pub struct TransferMeta { impl From for TransferMeta { fn from(nft_db: Nft) -> Self { TransferMeta { - token_address: eth_addr_to_hex(&nft_db.common.token_address), + token_address: nft_db.common.token_address.addr_to_string(), token_id: nft_db.token_id, token_uri: nft_db.common.token_uri, token_domain: nft_db.common.token_domain, diff --git a/mm2src/coins/nft/nft_tests.rs b/mm2src/coins/nft/nft_tests.rs index 71001d8f21..b5659ae52e 100644 --- a/mm2src/coins/nft/nft_tests.rs +++ b/mm2src/coins/nft/nft_tests.rs @@ -1,7 +1,7 @@ -use crate::eth::eth_addr_to_hex; +use crate::hd_wallet::AddrToString; use crate::nft::nft_structs::{Chain, NftFromMoralis, NftListFilters, NftTransferHistoryFilters, NftTransferHistoryFromMoralis, PhishingDomainReq, PhishingDomainRes, SpamContractReq, - SpamContractRes, TransferMeta, UriMeta}; + SpamContractRes, TransferMeta}; use crate::nft::storage::db_test_helpers::{get_nft_ctx, nft, nft_list, nft_transfer_history}; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps, RemoveNftResult}; use crate::nft::{check_moralis_ipfs_bafy, get_domain_from_url, is_malicious, process_metadata_for_spam_link, @@ -98,7 +98,7 @@ cross_test!(test_moralis_requests, { 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)); + assert_eq!(TEST_WALLET_ADDR_EVM, nft_moralis.common.owner_of.addr_to_string()); } let uri_history = format!( @@ -112,7 +112,7 @@ cross_test!(test_moralis_requests, { let transfer_moralis: NftTransferHistoryFromMoralis = serde_json::from_str(&first_transfer.to_string()).unwrap(); assert_eq!( TEST_WALLET_ADDR_EVM, - eth_addr_to_hex(&transfer_moralis.common.to_address) + transfer_moralis.common.to_address.addr_to_string() ); let uri_meta = format!( @@ -152,16 +152,24 @@ cross_test!(test_antispam_scan_endpoints, { assert!(phishing_res.result.get("disposal-account-case-1f677.web.app").unwrap()); }); -cross_test!(test_camo, { - let hex_token_uri = hex::encode("https://tikimetadata.s3.amazonaws.com/tiki_box.json"); - let uri_decode = format!("{}/url/decode/{}", BLOCKLIST_API_ENDPOINT, hex_token_uri); - let decode_res = send_request_to_uri(&uri_decode, None).await.unwrap(); - let uri_meta: UriMeta = serde_json::from_value(decode_res).unwrap(); - assert_eq!( - uri_meta.raw_image_url.unwrap(), - "https://tikimetadata.s3.amazonaws.com/tiki_box.png" - ); -}); +// Disabled on Linux: https://github.com/KomodoPlatform/komodo-defi-framework/issues/2367 +cross_test!( + test_camo, + { + use crate::nft::nft_structs::UriMeta; + + let hex_token_uri = hex::encode("https://tikimetadata.s3.amazonaws.com/tiki_box.json"); + let uri_decode = format!("{}/url/decode/{}", BLOCKLIST_API_ENDPOINT, hex_token_uri); + let decode_res = send_request_to_uri(&uri_decode, None).await.unwrap(); + let uri_meta: UriMeta = serde_json::from_value(decode_res).unwrap(); + assert_eq!( + uri_meta.raw_image_url.unwrap(), + "https://tikimetadata.s3.amazonaws.com/tiki_box.png" + ); + }, + target_os = "macos", + target_os = "windows" +); cross_test!(test_add_get_nfts, { let chain = Chain::Bsc; @@ -253,7 +261,7 @@ cross_test!(test_nft_amount, { 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.token_id.clone()) + .get_nft_amount(&chain, nft.common.token_address.addr_to_string(), nft.token_id.clone()) .await .unwrap() .unwrap(); @@ -268,7 +276,7 @@ cross_test!(test_nft_amount, { .await .unwrap(); let amount = storage - .get_nft_amount(&chain, eth_addr_to_hex(&nft.common.token_address), nft.token_id) + .get_nft_amount(&chain, nft.common.token_address.addr_to_string(), nft.token_id) .await .unwrap() .unwrap(); @@ -290,7 +298,7 @@ cross_test!(test_refresh_metadata, { .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_add = nft.common.token_address.addr_to_string(); let token_id = nft.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(); diff --git a/mm2src/coins/nft/storage/sql_storage.rs b/mm2src/coins/nft/storage/sql_storage.rs index bffdaef27b..c3e575b321 100644 --- a/mm2src/coins/nft/storage/sql_storage.rs +++ b/mm2src/coins/nft/storage/sql_storage.rs @@ -1,4 +1,4 @@ -use crate::nft::eth_addr_to_hex; +use crate::hd_wallet::AddrToString; use crate::nft::nft_structs::{Chain, ContractType, ConvertChain, Nft, NftCommon, NftList, NftListFilters, NftTokenAddrId, NftTransferCommon, NftTransferHistory, NftTransferHistoryFilters, NftsTransferHistoryList, TransferMeta, UriMeta}; @@ -686,7 +686,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { }; let details_json = json::to_string(&details_json).expect("serialization should not fail"); let params = [ - Some(eth_addr_to_hex(&nft.common.token_address)), + Some(nft.common.token_address.addr_to_string()), Some(nft.token_id.to_string()), Some(nft.chain.to_string()), Some(nft.common.amount.to_string()), @@ -818,7 +818,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { nft.uri_meta.external_url, nft.uri_meta.external_domain, nft.uri_meta.image_details.map(|v| v.to_string()), - Some(eth_addr_to_hex(&nft.common.token_address)), + Some(nft.common.token_address.addr_to_string()), Some(nft.token_id.to_string()), ]; sql_transaction.execute(&sql, params)?; @@ -866,7 +866,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { let sql_transaction = conn.transaction()?; let params = [ Some(nft.common.amount.to_string()), - Some(eth_addr_to_hex(&nft.common.token_address)), + Some(nft.common.token_address.addr_to_string()), Some(nft.token_id.to_string()), ]; sql_transaction.execute(&sql, params)?; @@ -890,7 +890,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { let params = [ Some(nft.common.amount.to_string()), Some(nft.block_number.to_string()), - Some(eth_addr_to_hex(&nft.common.token_address)), + Some(nft.common.token_address.addr_to_string()), Some(nft.token_id.to_string()), ]; sql_transaction.execute(&sql, params)?; @@ -1112,7 +1112,7 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { Some(transfer.block_number.to_string()), Some(transfer.block_timestamp.to_string()), Some(transfer.contract_type.to_string()), - Some(eth_addr_to_hex(&transfer.common.token_address)), + Some(transfer.common.token_address.addr_to_string()), Some(transfer.token_id.to_string()), Some(transfer.status.to_string()), Some(transfer.common.amount.to_string()), diff --git a/mm2src/coins/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs index 99e76ba04f..5ad0190179 100644 --- a/mm2src/coins/nft/storage/wasm/wasm_storage.rs +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -1,4 +1,4 @@ -use crate::eth::eth_addr_to_hex; +use crate::hd_wallet::AddrToString; use crate::nft::nft_structs::{Chain, ContractType, Nft, NftList, NftListFilters, NftTransferHistory, NftsTransferHistoryList, TransferMeta, TransferStatus}; use crate::nft::storage::wasm::nft_idb::NftCacheIDBLocked; @@ -258,7 +258,7 @@ impl NftListStorageOps for NftCacheIDBLocked<'_> { let table = db_transaction.table::().await?; let index_keys = MultiIndex::new(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_address.addr_to_string())? .with_value(BeBigUint::from(nft.token_id.clone()))?; let nft_item = NftListTable::from_nft(&nft)?; @@ -293,7 +293,7 @@ impl NftListStorageOps for NftCacheIDBLocked<'_> { let index_keys = MultiIndex::new(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_address.addr_to_string())? .with_value(BeBigUint::from(nft.token_id.clone()))?; let nft_item = NftListTable::from_nft(&nft)?; @@ -317,7 +317,7 @@ impl NftListStorageOps for NftCacheIDBLocked<'_> { let index_keys = MultiIndex::new(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_address.addr_to_string())? .with_value(BeBigUint::from(nft.token_id.clone()))?; let nft_item = NftListTable::from_nft(&nft)?; @@ -378,7 +378,7 @@ impl NftListStorageOps for NftCacheIDBLocked<'_> { let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(&chain_str)? - .with_value(eth_addr_to_hex(&nft.common.token_address))? + .with_value(nft.common.token_address.addr_to_string())? .with_value(BeBigUint::from(nft.token_id.clone()))?; let item = NftListTable::from_nft(&nft)?; @@ -808,7 +808,7 @@ async fn update_nft_phishing_for_index( let nft_item = NftListTable::from_nft(&nft)?; let index_keys = MultiIndex::new(CHAIN_TOKEN_ADD_TOKEN_ID_INDEX) .with_value(chain)? - .with_value(eth_addr_to_hex(&nft.common.token_address))? + .with_value(nft.common.token_address.addr_to_string())? .with_value(BeBigUint::from(nft.token_id))?; table.replace_item_by_unique_multi_index(index_keys, &nft_item).await?; } @@ -886,7 +886,7 @@ impl NftListTable { 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_address: nft.common.token_address.addr_to_string(), token_id: BeBigUint::from(nft.token_id.clone()), chain: nft.chain.to_string(), amount: nft.common.amount.to_string(), @@ -989,7 +989,7 @@ impl NftTransferHistoryTable { block_number: BeBigUint::from(transfer.block_number), block_timestamp: BeBigUint::from(transfer.block_timestamp), contract_type: transfer.contract_type, - token_address: eth_addr_to_hex(&transfer.common.token_address), + token_address: transfer.common.token_address.addr_to_string(), token_id: BeBigUint::from(transfer.token_id.clone()), status: transfer.status, amount: transfer.common.amount.to_string(), diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index df14fea09a..3049a56ec2 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -16,20 +16,16 @@ use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, Broadca UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; -use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, - DexFee, Eip1559Ops, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, - MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, - PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, - RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxFeePolicy, - TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, - TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, - ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, Eip1559Ops, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MarketCoinOps, MmCoin, + NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, + RawTransactionRequest, RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, + SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxFeePolicy, + TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, + TransactionDetails, TransactionEnum, TransactionErr, TransactionResult, TransactionType, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, + ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawError, + WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -759,14 +755,8 @@ impl UtxoCommonOps for Qrc20Coin { #[async_trait] impl SwapOps for Qrc20Coin { - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - let to_address = try_tx_s!(self.contract_address_from_raw_pubkey(fee_addr)); + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + let to_address = try_tx_s!(self.contract_address_from_raw_pubkey(self.dex_pubkey())); let amount = try_tx_s!(wei_from_big_decimal(&dex_fee.fee_amount().into(), self.utxo.decimals)); let transfer_output = try_tx_s!(self.transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)); @@ -864,7 +854,7 @@ impl SwapOps for Qrc20Coin { )); } let fee_addr = self - .contract_address_from_raw_pubkey(validate_fee_args.fee_addr) + .contract_address_from_raw_pubkey(self.dex_pubkey()) .map_to_mm(ValidatePaymentError::WrongPaymentTx)?; let expected_value = wei_from_big_decimal(&validate_fee_args.dex_fee.fee_amount().into(), self.utxo.decimals)?; @@ -981,29 +971,16 @@ impl SwapOps for Qrc20Coin { .await } - #[inline] - fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { - utxo_common::check_all_inputs_signed_by_pub(tx, expected_pub) - } - #[inline] async fn extract_secret( &self, secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { self.extract_secret_impl(secret_hash, spend_tx) } - fn is_auto_refundable(&self) -> bool { false } - - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) - } - fn negotiate_swap_contract_addr( &self, other_side_address: Option<&[u8]>, @@ -1034,7 +1011,7 @@ impl SwapOps for Qrc20Coin { utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) } - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> [u8; 33] { utxo_common::derive_htlc_pubkey(self, swap_unique_data) } @@ -1042,124 +1019,10 @@ impl SwapOps for Qrc20Coin { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } - - async fn maker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - async fn taker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } } #[async_trait] -impl TakerSwapMakerCoin for Qrc20Coin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for Qrc20Coin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl WatcherOps for Qrc20Coin { - fn create_maker_payment_spend_preimage( - &self, - _maker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - unimplemented!(); - } - - fn create_taker_payment_refund_preimage( - &self, - _taker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_contract_address: &Option, - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - unimplemented!(); - } - - fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { - unimplemented!() - } - - async fn watcher_search_for_swap_tx_spend( - &self, - _input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String> { - unimplemented!(); - } - - async fn get_taker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _coin_amount: Option, - _other_coin_amount: Option, - _reward_amount: Option, - _wait_until: u64, - ) -> Result> { - unimplemented!() - } - - async fn get_maker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _reward_amount: Option, - _wait_until: u64, - ) -> Result, MmError> { - unimplemented!() - } -} +impl WatcherOps for Qrc20Coin {} #[async_trait] impl MarketCoinOps for Qrc20Coin { @@ -1280,6 +1143,9 @@ impl MarketCoinOps for Qrc20Coin { MmNumber::from(1) / MmNumber::from(10u64.pow(pow)) } + #[inline] + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } @@ -1287,7 +1153,7 @@ impl MarketCoinOps for Qrc20Coin { impl MmCoin for Qrc20Coin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo) } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { Box::new(qrc20_withdraw(self.clone(), req).boxed().compat()) diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index af3c41f078..e6c6eb7085 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -39,7 +39,9 @@ impl TxInternalId { return ERR!("Incorrect bytes len {}, expected {}", bytes.len(), EXPECTED_LEN); } - let tx_hash: H256Json = bytes[0..32].into(); + let mut tx_hash = [0u8; 32]; + tx_hash.copy_from_slice(&bytes[0..32]); + let tx_hash = H256Json::from(tx_hash); let buf = bytes[32..].to_vec(); let mut cursor = Cursor::new(buf); @@ -192,7 +194,7 @@ impl Qrc20Coin { let receipts = try_s!(self.utxo.rpc_client.get_transaction_receipts(&tx_hash).compat().await); // request Qtum transaction details to get a tx_hex, timestamp, block_height and calculate a miner_fee let mut input_transactions = HistoryUtxoTxMap::new(); - let qtum_details = try_s!(utxo_common::tx_details_by_hash(self, &tx_hash.0, &mut input_transactions).await); + let qtum_details = try_s!(utxo_common::tx_details_by_hash(self, &tx_hash, &mut input_transactions).await); // Deserialize the UtxoTx to get a script pubkey let qtum_tx: UtxoTx = try_s!(deserialize( try_s!(qtum_details.tx.tx_hex().ok_or("unexpected tx type")).as_slice() @@ -823,17 +825,21 @@ fn is_transfer_event_log(log: &LogEntry) -> bool { mod tests { use super::*; use common::block_on; + use hex::FromHex; use mm2_metrics::{MetricType, MetricsJson, MetricsOps}; use mm2_test_helpers::for_tests::find_metrics_in_json; use qrc20_tests::qrc20_coin_for_test; #[test] fn test_tx_internal_id() { - let tx_hash = hex::decode("39104d29d77ba83c5c6c63ab7a0f096301c443b4538dc6b30140453a40caa80a").unwrap(); - let expected_id = TxInternalId::new(tx_hash.as_slice().into(), 13, 257); + let tx_hash: [u8; 32] = hex::decode("39104d29d77ba83c5c6c63ab7a0f096301c443b4538dc6b30140453a40caa80a") + .unwrap() + .try_into() + .unwrap(); + let expected_id = TxInternalId::new(tx_hash.into(), 13, 257); let actual_bytes: BytesJson = expected_id.clone().into(); - let mut expected_bytes = tx_hash; + let mut expected_bytes = Vec::from(tx_hash); expected_bytes.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 13]); expected_bytes.extend_from_slice(&[0, 0, 0, 0, 0, 0, 1, 1]); assert_eq!(actual_bytes, expected_bytes.into()); @@ -852,10 +858,10 @@ mod tests { let (ctx, coin) = qrc20_coin_for_test(priv_key, None); ctx.metrics.init(); - let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") - .unwrap() - .as_slice() - .into(); + let tx_hash: H256Json = + <[u8; 32]>::from_hex("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") + .unwrap() + .into(); let tx_height = 699545; let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); @@ -884,10 +890,10 @@ mod tests { let (ctx, coin) = qrc20_coin_for_test(priv_key, None); ctx.metrics.init(); - let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") - .unwrap() - .as_slice() - .into(); + let tx_hash: H256Json = + <[u8; 32]>::from_hex("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") + .unwrap() + .into(); let tx_height = 699545; let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); @@ -926,10 +932,10 @@ mod tests { let (ctx, coin) = qrc20_coin_for_test(priv_key, None); ctx.metrics.init(); - let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") - .unwrap() - .as_slice() - .into(); + let tx_hash: H256Json = + <[u8; 32]>::from_hex("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") + .unwrap() + .into(); let tx_height = 699545; let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); @@ -937,9 +943,8 @@ mod tests { .into_iter() .map(|(mut id, tx)| { // just another tx_hash - id.tx_hash = hex::decode("8a7270110ab7b56142b3bac89999276beb70320a7fe7666f460a05aa615eb0a0") + id.tx_hash = <[u8; 32]>::from_hex("8a7270110ab7b56142b3bac89999276beb70320a7fe7666f460a05aa615eb0a0") .unwrap() - .as_slice() .into(); (id, tx) }) @@ -966,10 +971,10 @@ mod tests { ]; let (ctx, coin) = qrc20_coin_for_test(priv_key, None); - let tx_hash: H256Json = hex::decode("35e03bc529528a853ee75dde28f27eec8ed7b152b6af7ab6dfa5d55ea46f25ac") - .unwrap() - .as_slice() - .into(); + let tx_hash: H256Json = + <[u8; 32]>::from_hex("35e03bc529528a853ee75dde28f27eec8ed7b152b6af7ab6dfa5d55ea46f25ac") + .unwrap() + .into(); let tx_height = 681443; let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut history_map_expected = HistoryMapByHash::new(); @@ -991,10 +996,10 @@ mod tests { ]; let (ctx, coin) = qrc20_coin_for_test(priv_key, None); - let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") - .unwrap() - .as_slice() - .into(); + let tx_hash: H256Json = + <[u8; 32]>::from_hex("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") + .unwrap() + .into(); let tx_height = 699545; let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut history_map_expected = HistoryMapByHash::new(); @@ -1019,14 +1024,14 @@ mod tests { let metrics = MetricsArc::new(); metrics.init(); - let tx_hash_invalid: H256Json = hex::decode("0000000000000000000000000000000000000000000000000000000000000000") - .unwrap() - .as_slice() - .into(); - let tx_hash: H256Json = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") - .unwrap() - .as_slice() - .into(); + let tx_hash_invalid: H256Json = + <[u8; 32]>::from_hex("0000000000000000000000000000000000000000000000000000000000000000") + .unwrap() + .into(); + let tx_hash: H256Json = + <[u8; 32]>::from_hex("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb") + .unwrap() + .into(); let tx_height = 699545; let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut history_map_expected = HistoryMapByHash::new(); diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 3e6dbc94dd..caf5546de2 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -6,6 +6,7 @@ use itertools::Itertools; use keys::Address; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::Zero; +use mm2_test_helpers::electrums::tqtum_electrums; use rpc::v1::types::ToTxHash; use std::convert::TryFrom; use std::mem::discriminant; @@ -37,7 +38,7 @@ pub fn qrc20_coin_for_test(priv_key: [u8; 32], fallback_swap: Option<&str>) -> ( }); let req = json!({ "method": "electrum", - "servers": [{"url":"electrum1.cipig.net:10071"}, {"url":"electrum2.cipig.net:10071"}, {"url":"electrum3.cipig.net:10071"}], + "servers": tqtum_electrums(), "swap_contract_address": "0xba8b71f3544b93e2f681f996da519a98ace0107a", "fallback_swap_contract": fallback_swap, }); @@ -88,13 +89,9 @@ fn test_withdraw_to_p2sh_address_should_fail() { let req = WithdrawRequest { amount: 10.into(), - from: None, to: p2sh_address.to_string(), coin: "QRC20".into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let err = block_on_f01(coin.withdraw(req)).unwrap_err().into_inner(); let expect = WithdrawError::InvalidAddress("QRC20 can be sent to P2PKH addresses only".to_owned()); @@ -132,16 +129,13 @@ fn test_withdraw_impl_fee_details() { let withdraw_req = WithdrawRequest { amount: 10.into(), - from: None, to: "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(), coin: "QRC20".into(), - max: false, fee: Some(WithdrawFee::Qrc20Gas { gas_limit: 2_500_000, gas_price: 40, }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); @@ -339,18 +333,21 @@ fn test_validate_fee() { let result = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.clone().into()), min_block_number: 0, uuid: &[], })); assert!(result.is_ok()); - let fee_addr_dif = hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(); + // wrong dex address + ::dex_pubkey.mock_safe(|_| { + MockResult::Return(Box::leak(Box::new( + hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(), + ))) + }); let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &fee_addr_dif, dex_fee: &DexFee::Standard(amount.clone().into()), min_block_number: 0, uuid: &[], @@ -362,11 +359,11 @@ fn test_validate_fee() { ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("QRC20 Fee tx was sent to wrong address")), _ => panic!("Expected `WrongPaymentTx` wrong receiver address, found {:?}", err), } + ::dex_pubkey.clear_mock(); let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.clone().into()), min_block_number: 0, uuid: &[], @@ -382,7 +379,6 @@ fn test_validate_fee() { let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.clone().into()), min_block_number: 2000000, uuid: &[], @@ -399,7 +395,6 @@ fn test_validate_fee() { let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount_dif.into()), min_block_number: 0, uuid: &[], @@ -420,7 +415,6 @@ fn test_validate_fee() { let err = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], @@ -481,8 +475,8 @@ fn test_extract_secret() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); - let expected_secret = &[1; 32]; - let secret_hash = &*dhash160(expected_secret); + let expected_secret = [1; 32]; + let secret_hash = &*dhash160(&expected_secret); // taker spent maker payment - d3f5dab4d54c14b3d7ed8c7f5c8cc7f47ccf45ce589fdc7cd5140a3c1c3df6e1 let tx_hex = hex::decode("01000000033f56ecafafc8602fde083ba868d1192d6649b8433e42e1a2d79ba007ea4f7abb010000006b48304502210093404e90e40d22730013035d31c404c875646dcf2fad9aa298348558b6d65ba60220297d045eac5617c1a3eddb71d4bca9772841afa3c4c9d6c68d8d2d42ee6de3950121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff9cac7fe90d597922a1d92e05306c2215628e7ea6d5b855bfb4289c2944f4c73a030000006b483045022100b987da58c2c0c40ce5b6ef2a59e8124ed4ef7a8b3e60c7fb631139280019bc93022069649bcde6fe4dd5df9462a1fcae40598488d6af8c324cd083f5c08afd9568be0121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff70b9870f2b0c65d220a839acecebf80f5b44c3ca4c982fa2fdc5552c037f5610010000006a473044022071b34dd3ebb72d29ca24f3fa0fc96571c815668d3b185dd45cc46a7222b6843f02206c39c030e618d411d4124f7b3e7ca1dd5436775bd8083a85712d123d933a51300121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff020000000000000000c35403a0860101284ca402ed292b806a1835a1b514ad643f2acdb5c8db6b6a9714accff3275ea0d79a3f23be8fd00000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2c02288d4010000001976a914783cf0be521101942da509846ea476e683aad83288ac0f047f5f").unwrap(); @@ -505,10 +499,10 @@ fn test_extract_secret_malicious() { // 1 - with an invalid secret (this case should be processed correctly) // 2 - correct spend tx let spend_tx = hex::decode("01000000022bc8299981ec0cea664cdf9df4f8306396a02e2067d6ac2d3770b34646d2bc2a010000006b483045022100eb13ef2d99ac1cd9984045c2365654b115dd8a7815b7fbf8e2a257f0b93d1592022060d648e73118c843e97f75fafc94e5ff6da70ec8ba36ae255f8c96e2626af6260121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffffd92a0a10ac6d144b36033916f67ae79889f40f35096629a5cd87be1a08f40ee7010000006b48304502210080cdad5c4770dfbeb760e215494c63cc30da843b8505e75e7bf9e8dad18568000220234c0b11c41bfbcdd50046c69059976aedabe17657fe43d809af71e9635678e20121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff030000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000202020202020202020202020202020202020202020202020202020202020202000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac20000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2b8ea82d3010000001976a914783cf0be521101942da509846ea476e683aad83288ac735d855f").unwrap(); - let expected_secret = &[1; 32]; - let secret_hash = &*dhash160(expected_secret); + let expected_secret = [1; 32]; + let secret_hash = &*dhash160(&expected_secret); let actual = block_on(coin.extract_secret(secret_hash, &spend_tx, false)); - assert_eq!(actual, Ok(expected_secret.to_vec())); + assert_eq!(actual, Ok(expected_secret)); } #[test] @@ -569,10 +563,10 @@ fn test_transfer_details_by_hash() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); let tx_hash_bytes = hex::decode("85ede12ccc12fb1709c4d9e403e96c0c394b0916f2f6098d41d8dfa00013fcdb").unwrap(); - let tx_hash: H256Json = tx_hash_bytes.as_slice().into(); + let tx_hash: [u8; 32] = tx_hash_bytes.clone().try_into().unwrap(); let tx_hex:BytesJson = hex::decode("0100000001426d27fde82e12e1ce84e73ca41e2a30420f4c94aaa37b30d4c5b8b4f762c042040000006a473044022032665891693ee732571cefaa6d322ec5114c78259f2adbe03a0d7e6b65fbf40d022035c9319ca41e5423e09a8a613ac749a20b8f5ad6ba4ad6bb60e4a020b085d009012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff050000000000000000625403a08601012844095ea7b30000000000000000000000001549128bbfb33b997949b4105b6a6371c998e212000000000000000000000000000000000000000000000000000000000000000014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000625403a08601012844095ea7b30000000000000000000000001549128bbfb33b997949b4105b6a6371c998e21200000000000000000000000000000000000000000000000000000000000927c014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000835403a0860101284c640c565ae300000000000000000000000000000000000000000000000000000000000493e0000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000000000000000000000000000000000000000000000141549128bbfb33b997949b4105b6a6371c998e212c20000000000000000835403a0860101284c640c565ae300000000000000000000000000000000000000000000000000000000000493e0000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000000000000000000000000000000000000000000001141549128bbfb33b997949b4105b6a6371c998e212c231754b04000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acf7cd8b5f").unwrap().into(); - let details = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); + let details = block_on(coin.transfer_details_by_hash(tx_hash.into())).unwrap(); let mut it = details.into_iter().sorted_by(|(id_x, _), (id_y, _)| id_x.cmp(id_y)); let expected_fee_details = |total_gas_fee: &str| -> TxFeeDetails { diff --git a/mm2src/coins/qrc20/swap.rs b/mm2src/coins/qrc20/swap.rs index 7370926684..26dea25ab8 100644 --- a/mm2src/coins/qrc20/swap.rs +++ b/mm2src/coins/qrc20/swap.rs @@ -26,7 +26,7 @@ pub struct Erc20PaymentDetails { pub struct ReceiverSpendDetails { pub swap_id: Vec, pub value: U256, - pub secret: Vec, + pub secret: [u8; 32], pub token_address: H160, pub sender: H160, } @@ -298,11 +298,11 @@ impl Qrc20Coin { Ok(found) } - pub fn extract_secret_impl(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + pub fn extract_secret_impl(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { let secret_hash = if secret_hash.len() == 32 { ripemd160(secret_hash) } else { - chain::hash::H160::from(secret_hash) + chain::hash::H160::from_slice(secret_hash)? }; let spend_tx: UtxoTx = try_s!(deserialize(spend_tx).map_err(|e| ERRL!("{:?}", e))); @@ -936,7 +936,7 @@ pub fn receiver_spend_call_details_from_script_pubkey(script_pubkey: &Script) -> }; let secret = match decoded.next() { - Some(Token::FixedBytes(hash)) => hash, + Some(Token::FixedBytes(hash)) => try_s!(hash.as_slice().try_into()), Some(token) => return ERR!("Payment tx 'secret_hash' arg is invalid, found {:?}", token), None => return ERR!("Couldn't find 'secret_hash' in erc20Payment call"), }; @@ -970,7 +970,7 @@ fn find_receiver_spend_with_swap_id_and_secret_hash( let expected_secret_hash = if expected_secret_hash.len() == 32 { ripemd160(expected_secret_hash) } else { - chain::hash::H160::from(expected_secret_hash) + chain::hash::H160::from_slice(expected_secret_hash).expect("this shouldn't fail") }; for (output_idx, output) in tx.outputs.iter().enumerate() { diff --git a/mm2src/coins/rpc_command/get_estimated_fees.rs b/mm2src/coins/rpc_command/get_estimated_fees.rs deleted file mode 100644 index 811ecb448e..0000000000 --- a/mm2src/coins/rpc_command/get_estimated_fees.rs +++ /dev/null @@ -1,331 +0,0 @@ -//! RPCs to start/stop gas fee estimator and get estimated base and priority fee per gas - -use crate::eth::{wei_to_gwei_decimal, EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; -use crate::{lp_coinfind_or_err, AsyncMutex, CoinFindError, MmCoinEnum, NumConversError}; -use common::executor::{spawn_abortable, Timer}; -use common::log::debug; -use common::{HttpStatusCode, StatusCode}; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; -use mm2_number::BigDecimal; -use serde::{Deserialize, Serialize}; -use serde_json::{self as json, Value as Json}; -use std::convert::{TryFrom, TryInto}; -use std::ops::Deref; -use std::sync::Arc; - -const FEE_ESTIMATOR_NAME: &str = "eth_gas_fee_estimator_loop"; - -/// Estimated fee per gas units -#[derive(Clone, Debug, Serialize)] -pub enum EstimationUnits { - Gwei, -} - -impl Default for EstimationUnits { - fn default() -> Self { Self::Gwei } -} - -/// Priority level estimated max fee per gas -#[derive(Clone, Debug, Default, Serialize)] -pub struct FeePerGasLevel { - /// estimated max priority tip fee per gas in gwei - pub max_priority_fee_per_gas: BigDecimal, - /// estimated max fee per gas in gwei - pub max_fee_per_gas: BigDecimal, - /// estimated transaction min wait time in mempool in ms for this priority level - pub min_wait_time: Option, - /// estimated transaction max wait time in mempool in ms for this priority level - pub max_wait_time: Option, -} - -/// External struct for estimated fee per gas for several priority levels, in gwei -/// low/medium/high levels are supported -#[derive(Default, Debug, Clone, Serialize)] -pub struct FeePerGasEstimatedExt { - /// base fee for the next block in gwei - pub base_fee: BigDecimal, - /// estimated low priority fee - pub low: FeePerGasLevel, - /// estimated medium priority fee - pub medium: FeePerGasLevel, - /// estimated high priority fee - pub high: FeePerGasLevel, - /// which estimator used - pub source: String, - /// base trend (up or down) - pub base_fee_trend: String, - /// priority trend (up or down) - pub priority_fee_trend: String, - /// fee units - pub units: EstimationUnits, -} - -impl TryFrom for FeePerGasEstimatedExt { - type Error = MmError; - - fn try_from(fees: FeePerGasEstimated) -> Result { - Ok(Self { - base_fee: wei_to_gwei_decimal(fees.base_fee)?, - low: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal(fees.low.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal(fees.low.max_priority_fee_per_gas)?, - min_wait_time: fees.low.min_wait_time, - max_wait_time: fees.low.max_wait_time, - }, - medium: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal(fees.medium.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal(fees.medium.max_priority_fee_per_gas)?, - min_wait_time: fees.medium.min_wait_time, - max_wait_time: fees.medium.max_wait_time, - }, - high: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal(fees.high.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal(fees.high.max_priority_fee_per_gas)?, - min_wait_time: fees.high.min_wait_time, - max_wait_time: fees.high.max_wait_time, - }, - source: fees.source.to_string(), - base_fee_trend: fees.base_fee_trend, - priority_fee_trend: fees.priority_fee_trend, - units: EstimationUnits::Gwei, - }) - } -} - -#[derive(Debug, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum FeeEstimatorError { - #[display(fmt = "No such coin {}", coin)] - NoSuchCoin { coin: String }, - #[display(fmt = "Gas fee estimation not supported for this coin")] - CoinNotSupported, - #[display(fmt = "Platform coin needs to be enabled for gas fee estimation")] - PlatformCoinRequired, - #[display(fmt = "Gas fee estimator is already started")] - AlreadyStarted, - #[display(fmt = "Transport error: {}", _0)] - Transport(String), - #[display(fmt = "Gas fee estimator is not running")] - NotRunning, - #[display(fmt = "Internal error: {}", _0)] - InternalError(String), -} - -impl HttpStatusCode for FeeEstimatorError { - fn status_code(&self) -> StatusCode { - match self { - FeeEstimatorError::NoSuchCoin { .. } - | FeeEstimatorError::CoinNotSupported - | FeeEstimatorError::PlatformCoinRequired - | FeeEstimatorError::AlreadyStarted - | FeeEstimatorError::NotRunning => StatusCode::BAD_REQUEST, - FeeEstimatorError::Transport(_) | FeeEstimatorError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for FeeEstimatorError { - fn from(e: NumConversError) -> Self { FeeEstimatorError::InternalError(e.to_string()) } -} - -impl From for FeeEstimatorError { - fn from(e: String) -> Self { FeeEstimatorError::InternalError(e) } -} - -impl From for FeeEstimatorError { - fn from(e: CoinFindError) -> Self { - match e { - CoinFindError::NoSuchCoin { coin } => FeeEstimatorError::NoSuchCoin { coin }, - } - } -} - -/// Gas fee estimator configuration -#[derive(Deserialize)] -enum FeeEstimatorConf { - NotConfigured, - #[serde(rename = "simple")] - Simple, - #[serde(rename = "provider")] - Provider, -} - -impl Default for FeeEstimatorConf { - fn default() -> Self { Self::NotConfigured } -} - -impl FeeEstimatorState { - /// Creates gas FeeEstimatorContext if configured for this coin and chain id, otherwise returns None. - /// The created context object (or None) is wrapped into a FeeEstimatorState so a gas fee rpc caller may know the reason why it was not created - pub(crate) async fn init_fee_estimator( - ctx: &MmArc, - conf: &Json, - coin_type: &EthCoinType, - ) -> Result, String> { - let fee_estimator_json = conf["gas_fee_estimator"].clone(); - let fee_estimator_conf: FeeEstimatorConf = if !fee_estimator_json.is_null() { - try_s!(json::from_value(fee_estimator_json)) - } else { - Default::default() - }; - match (fee_estimator_conf, coin_type) { - (FeeEstimatorConf::Simple, EthCoinType::Eth) => { - let fee_estimator_state = FeeEstimatorState::Simple(FeeEstimatorContext::new()); - Ok(Arc::new(fee_estimator_state)) - }, - (FeeEstimatorConf::Provider, EthCoinType::Eth) => { - let fee_estimator_state = FeeEstimatorState::Provider(FeeEstimatorContext::new()); - Ok(Arc::new(fee_estimator_state)) - }, - (_, EthCoinType::Erc20 { platform, .. }) | (_, EthCoinType::Nft { platform, .. }) => { - let platform_coin = lp_coinfind_or_err(ctx, platform).await; - match platform_coin { - Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin.platform_fee_estimator_state.clone()), - _ => Ok(Arc::new(FeeEstimatorState::PlatformCoinRequired)), - } - }, - (FeeEstimatorConf::NotConfigured, _) => Ok(Arc::new(FeeEstimatorState::CoinNotSupported)), - } - } -} - -impl FeeEstimatorContext { - fn new() -> AsyncMutex { - AsyncMutex::new(FeeEstimatorContext { - estimated_fees: Default::default(), - abort_handler: AsyncMutex::new(None), - }) - } - - /// Fee estimation update period in secs, basically equals to eth blocktime - const fn get_refresh_interval() -> f64 { 15.0 } - - fn get_estimator_ctx(coin: &EthCoin) -> Result<&AsyncMutex, MmError> { - match coin.platform_fee_estimator_state.deref() { - FeeEstimatorState::CoinNotSupported => MmError::err(FeeEstimatorError::CoinNotSupported), - FeeEstimatorState::PlatformCoinRequired => MmError::err(FeeEstimatorError::PlatformCoinRequired), - FeeEstimatorState::Simple(fee_estimator_ctx) | FeeEstimatorState::Provider(fee_estimator_ctx) => { - Ok(fee_estimator_ctx) - }, - } - } - - async fn start_if_not_running(coin: &EthCoin) -> Result<(), MmError> { - let estimator_ctx = Self::get_estimator_ctx(coin)?; - let estimator_ctx = estimator_ctx.lock().await; - let mut handler = estimator_ctx.abort_handler.lock().await; - if handler.is_some() { - return MmError::err(FeeEstimatorError::AlreadyStarted); - } - *handler = Some(spawn_abortable(Self::fee_estimator_loop(coin.clone()))); - Ok(()) - } - - async fn request_to_stop(coin: &EthCoin) -> Result<(), MmError> { - let estimator_ctx = Self::get_estimator_ctx(coin)?; - let estimator_ctx = estimator_ctx.lock().await; - let mut handle_guard = estimator_ctx.abort_handler.lock().await; - // Handler will be dropped here, stopping the spawned loop immediately - handle_guard - .take() - .map(|_| ()) - .or_mm_err(|| FeeEstimatorError::NotRunning) - } - - async fn get_estimated_fees(coin: &EthCoin) -> Result> { - let estimator_ctx = Self::get_estimator_ctx(coin)?; - let estimator_ctx = estimator_ctx.lock().await; - let estimated_fees = estimator_ctx.estimated_fees.lock().await; - Ok(estimated_fees.clone()) - } - - async fn check_if_estimator_supported(ctx: &MmArc, ticker: &str) -> Result> { - let eth_coin = match lp_coinfind_or_err(ctx, ticker).await? { - MmCoinEnum::EthCoin(eth) => eth, - _ => return MmError::err(FeeEstimatorError::CoinNotSupported), - }; - let _ = Self::get_estimator_ctx(ð_coin)?; - Ok(eth_coin) - } - - /// Loop polling gas fee estimator - /// - /// This loop periodically calls get_eip1559_gas_fee which fetches fee per gas estimations from a gas api provider or calculates them internally - /// The retrieved data are stored in the fee estimator context - /// To connect to the chain and gas api provider the web3 instances are used from an EthCoin coin passed in the start rpc param, - /// so this coin must be enabled first. - /// Once the loop started any other EthCoin in mainnet may request fee estimations. - /// It is up to GUI to start and stop the loop when it needs it (considering that the data in context may be used - /// for any coin with Eth or Erc20 type from the mainnet). - async fn fee_estimator_loop(coin: EthCoin) { - loop { - let started = common::now_float(); - if let Ok(estimator_ctx) = Self::get_estimator_ctx(&coin) { - let estimated_fees = coin.get_eip1559_gas_fee().await.unwrap_or_default(); - let estimator_ctx = estimator_ctx.lock().await; - *estimator_ctx.estimated_fees.lock().await = estimated_fees; - } - - let elapsed = common::now_float() - started; - debug!("{FEE_ESTIMATOR_NAME} call to provider processed in {} seconds", elapsed); - - let wait_secs = FeeEstimatorContext::get_refresh_interval() - elapsed; - let wait_secs = if wait_secs < 0.0 { 0.0 } else { wait_secs }; - Timer::sleep(wait_secs).await; - } - } -} - -/// Rpc request to start or stop gas fee estimator -#[derive(Deserialize)] -pub struct FeeEstimatorStartStopRequest { - coin: String, -} - -/// Rpc response to request to start or stop gas fee estimator -#[derive(Serialize)] -pub struct FeeEstimatorStartStopResponse { - result: String, -} - -pub type FeeEstimatorStartStopResult = Result>; - -/// Rpc request to get latest estimated fee per gas -#[derive(Deserialize)] -pub struct FeeEstimatorRequest { - /// coin ticker - coin: String, -} - -pub type FeeEstimatorResult = Result>; - -/// Start gas priority fee estimator loop -pub async fn start_eth_fee_estimator(ctx: MmArc, req: FeeEstimatorStartStopRequest) -> FeeEstimatorStartStopResult { - let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; - FeeEstimatorContext::start_if_not_running(&coin).await?; - Ok(FeeEstimatorStartStopResponse { - result: "Success".to_string(), - }) -} - -/// Stop gas priority fee estimator loop -pub async fn stop_eth_fee_estimator(ctx: MmArc, req: FeeEstimatorStartStopRequest) -> FeeEstimatorStartStopResult { - let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; - FeeEstimatorContext::request_to_stop(&coin).await?; - Ok(FeeEstimatorStartStopResponse { - result: "Success".to_string(), - }) -} - -/// Get latest estimated fee per gas for a eth coin -/// -/// Estimation loop for this coin must be stated. -/// Only main chain is supported -/// -/// Returns latest estimated fee per gas for the next block -pub async fn get_eth_estimated_fee_per_gas(ctx: MmArc, req: FeeEstimatorRequest) -> FeeEstimatorResult { - let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; - let estimated_fees = FeeEstimatorContext::get_estimated_fees(&coin).await?; - estimated_fees.try_into().mm_err(Into::into) -} diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index 35796de9c2..1213905c74 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -16,8 +16,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes}; use std::time::Duration; pub type GetNewAddressUserAction = HwRpcTaskUserAction; @@ -379,13 +379,15 @@ pub async fn get_new_address( /// TODO remove once GUI integrates `task::get_new_address::init`. pub async fn init_get_new_address( ctx: MmArc, - req: GetNewAddressRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(GetNewAddressRpcError::Internal)?; let spawner = coin.spawner(); let task = InitGetNewAddressTask { ctx, coin, req }; - let task_id = GetNewAddressTaskManager::spawn_rpc_task(&coins_ctx.get_new_address_manager, &spawner, task)?; + let task_id = + GetNewAddressTaskManager::spawn_rpc_task(&coins_ctx.get_new_address_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } @@ -432,7 +434,7 @@ pub async fn cancel_get_new_address( pub(crate) mod common_impl { use super::*; use crate::coin_balance::{HDAddressBalanceScanner, HDWalletBalanceObject, HDWalletBalanceOps}; - use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDCoinHDAccount, HDWalletOps}; + use crate::hd_wallet::{DisplayAddress, HDAccountOps, HDAddressOps, HDCoinAddress, HDCoinHDAccount, HDWalletOps}; use crate::CoinWithDerivationMethod; use crypto::RpcDerivationPath; use std::collections::HashSet; @@ -472,7 +474,7 @@ pub(crate) mod common_impl { Ok(GetNewAddressResponse { new_address: HDAddressBalance { - address: coin.address_formatter()(&address), + address: address.display_address(), derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, @@ -510,7 +512,7 @@ pub(crate) mod common_impl { let address = hd_address.address(); let balance = coin.known_address_balance(&address).await?; - let formatted_address = coin.address_formatter()(&address); + let formatted_address = address.display_address(); coin.prepare_addresses_for_balance_stream_if_enabled(HashSet::from([formatted_address.clone()])) .await .map_err(|e| GetNewAddressRpcError::FailedScripthashSubscription(e.to_string()))?; diff --git a/mm2src/coins/rpc_command/init_account_balance.rs b/mm2src/coins/rpc_command/init_account_balance.rs index 39e92cb12a..b18e6bdbce 100644 --- a/mm2src/coins/rpc_command/init_account_balance.rs +++ b/mm2src/coins/rpc_command/init_account_balance.rs @@ -7,7 +7,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; pub type AccountBalanceUserAction = SerdeInfallible; pub type AccountBalanceAwaitingStatus = SerdeInfallible; @@ -89,13 +90,15 @@ impl RpcTask for InitAccountBalanceTask { pub async fn init_account_balance( ctx: MmArc, - req: InitAccountBalanceRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let spawner = coin.spawner(); let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(HDAccountBalanceRpcError::Internal)?; let task = InitAccountBalanceTask { coin, req }; - let task_id = AccountBalanceTaskManager::spawn_rpc_task(&coins_ctx.account_balance_task_manager, &spawner, task)?; + let task_id = + AccountBalanceTaskManager::spawn_rpc_task(&coins_ctx.account_balance_task_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 833998a1c3..c323ffe78a 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -14,8 +14,8 @@ use mm2_err_handle::prelude::*; use parking_lot::Mutex as PaMutex; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes}; use std::sync::Arc; use std::time::Duration; @@ -329,8 +329,9 @@ impl RpcTask for InitCreateAccountTask { pub async fn init_create_new_account( ctx: MmArc, - req: CreateNewAccountRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(CreateAccountRpcError::Internal)?; let spawner = coin.spawner(); @@ -340,7 +341,8 @@ pub async fn init_create_new_account( req, task_state: CreateAccountState::default(), }; - let task_id = CreateAccountTaskManager::spawn_rpc_task(&coins_ctx.create_account_manager, &spawner, task)?; + let task_id = + CreateAccountTaskManager::spawn_rpc_task(&coins_ctx.create_account_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs index eaf51277b6..d24c7229fe 100644 --- a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -8,7 +8,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; pub type ScanAddressesUserAction = SerdeInfallible; pub type ScanAddressesAwaitingStatus = SerdeInfallible; @@ -108,13 +109,15 @@ impl RpcTask for InitScanAddressesTask { pub async fn init_scan_for_new_addresses( ctx: MmArc, - req: ScanAddressesRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; let spawner = coin.spawner(); let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(HDAccountBalanceRpcError::Internal)?; let task = InitScanAddressesTask { req, coin }; - let task_id = ScanAddressesTaskManager::spawn_rpc_task(&coins_ctx.scan_addresses_manager, &spawner, task)?; + let task_id = + ScanAddressesTaskManager::spawn_rpc_task(&coins_ctx.scan_addresses_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs index 43b86cf19a..e82ccd4d63 100644 --- a/mm2src/coins/rpc_command/init_withdraw.rs +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -7,7 +7,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatusAlias, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatusAlias, + RpcTaskTypes}; pub type WithdrawAwaitingStatus = HwRpcTaskAwaitingStatus; pub type WithdrawUserAction = HwRpcTaskUserAction; @@ -32,7 +33,11 @@ pub trait CoinWithdrawInit { ) -> WithdrawInitResult; } -pub async fn init_withdraw(ctx: MmArc, request: WithdrawRequest) -> WithdrawInitResult { +pub async fn init_withdraw( + ctx: MmArc, + request: RpcInitReq, +) -> WithdrawInitResult { + let (client_id, request) = (request.client_id, request.inner); let coin = lp_coinfind_or_err(&ctx, &request.coin).await?; let spawner = coin.spawner(); let task = WithdrawTask { @@ -41,7 +46,7 @@ pub async fn init_withdraw(ctx: MmArc, request: WithdrawRequest) -> WithdrawInit request, }; let coins_ctx = CoinsContext::from_ctx(&ctx).map_to_mm(WithdrawError::InternalError)?; - let task_id = WithdrawTaskManager::spawn_rpc_task(&coins_ctx.withdraw_task_manager, &spawner, task)?; + let task_id = WithdrawTaskManager::spawn_rpc_task(&coins_ctx.withdraw_task_manager, &spawner, task, client_id)?; Ok(InitWithdrawResponse { task_id }) } diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index 0bec5ef493..c401853b2d 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -1,7 +1,6 @@ pub mod account_balance; pub mod get_current_mtp; pub mod get_enabled_coins; -pub mod get_estimated_fees; pub mod get_new_address; pub mod hd_account_balance_rpc_error; pub mod init_account_balance; diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs index 3e2b664aec..9a3d714bd3 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1,5 +1,6 @@ mod ibc_chains; mod ibc_transfer_channels; +pub mod staking; pub use ibc_chains::*; pub use ibc_transfer_channels::*; diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs new file mode 100644 index 0000000000..190477b7bd --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -0,0 +1,189 @@ +use common::PagingOptions; +use cosmrs::staking::{Commission, Description, Validator}; +use mm2_err_handle::prelude::MmError; +use mm2_number::BigDecimal; + +use crate::{hd_wallet::WithdrawFrom, tendermint::TendermintCoinRpcError, MmCoinEnum, StakingInfoError, WithdrawFee}; + +/// Represents current status of the validator. +#[derive(Debug, Default, Deserialize)] +pub(crate) enum ValidatorStatus { + All, + /// Validator is in the active set and participates in consensus. + #[default] + Bonded, + /// Validator is not in the active set and does not participate in consensus. + /// Accordingly, they do not receive rewards and cannot be slashed. + /// It is still possible to delegate tokens to a validator in this state. + Unbonded, +} + +impl ToString for ValidatorStatus { + fn to_string(&self) -> String { + match self { + // An empty string doesn't filter any validators and we get an unfiltered result. + ValidatorStatus::All => String::default(), + ValidatorStatus::Bonded => "BOND_STATUS_BONDED".into(), + ValidatorStatus::Unbonded => "BOND_STATUS_UNBONDED".into(), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct ValidatorsQuery { + #[serde(flatten)] + paging: PagingOptions, + #[serde(default)] + filter_by_status: ValidatorStatus, +} + +#[derive(Clone, Serialize)] +pub struct ValidatorsQueryResponse { + validators: Vec, +} + +impl From for StakingInfoError { + fn from(e: TendermintCoinRpcError) -> Self { + match e { + TendermintCoinRpcError::InvalidResponse(e) + | TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) => StakingInfoError::Transport(e), + TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => StakingInfoError::Internal(e), + TendermintCoinRpcError::UnexpectedAccountType { .. } => StakingInfoError::Internal( + "RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal." + .into(), + ), + } + } +} + +pub async fn validators_rpc( + coin: MmCoinEnum, + req: ValidatorsQuery, +) -> Result> { + fn maybe_jsonize_description(description: Option) -> Option { + description.map(|d| { + json!({ + "moniker": d.moniker, + "identity": d.identity, + "website": d.website, + "security_contact": d.security_contact, + "details": d.details, + }) + }) + } + + fn maybe_jsonize_commission(commission: Option) -> Option { + commission.map(|c| { + let rates = c.commission_rates.map(|cr| { + json!({ + "rate": cr.rate, + "max_rate": cr.max_rate, + "max_change_rate": cr.max_change_rate + }) + }); + + json!({ + "commission_rates": rates, + "update_time": c.update_time + }) + }) + } + + fn jsonize_validator(v: Validator) -> serde_json::Value { + json!({ + "operator_address": v.operator_address, + "consensus_pubkey": v.consensus_pubkey, + "jailed": v.jailed, + "status": v.status, + "tokens": v.tokens, + "delegator_shares": v.delegator_shares, + "description": maybe_jsonize_description(v.description), + "unbonding_height": v.unbonding_height, + "unbonding_time": v.unbonding_time, + "commission": maybe_jsonize_commission(v.commission), + "min_self_delegation": v.min_self_delegation, + }) + } + + let validators = match coin { + MmCoinEnum::Tendermint(coin) => coin.validators_list(req.filter_by_status, req.paging).await?, + MmCoinEnum::TendermintToken(token) => { + token + .platform_coin + .validators_list(req.filter_by_status, req.paging) + .await? + }, + other => { + return MmError::err(StakingInfoError::InvalidPayload { + reason: format!("{} is not a Cosmos coin", other.ticker()), + }) + }, + }; + + Ok(ValidatorsQueryResponse { + validators: validators.into_iter().map(jsonize_validator).collect(), + }) +} + +#[derive(Clone, Debug, Deserialize)] +pub struct DelegationPayload { + pub validator_address: String, + pub fee: Option, + pub withdraw_from: Option, + #[serde(default)] + pub memo: String, + #[serde(default)] + pub amount: BigDecimal, + #[serde(default)] + pub max: bool, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ClaimRewardsPayload { + pub validator_address: String, + pub fee: Option, + #[serde(default)] + pub memo: String, + /// If transaction fee exceeds the reward amount users will be + /// prevented from claiming their rewards as it will not be profitable. + /// Setting `force` to `true` disables this logic. + #[serde(default)] + pub force: bool, +} + +#[derive(Debug, Deserialize)] +pub struct SimpleListQuery { + #[serde(flatten)] + pub(crate) paging: PagingOptions, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct DelegationsQueryResponse { + pub(crate) delegations: Vec, +} + +#[derive(Debug, PartialEq, Serialize)] +pub(crate) struct Delegation { + pub(crate) validator_address: String, + pub(crate) delegated_amount: BigDecimal, + pub(crate) reward_amount: BigDecimal, +} + +#[derive(Serialize)] +pub struct UndelegationsQueryResponse { + pub(crate) ongoing_undelegations: Vec, +} + +#[derive(Serialize)] +pub(crate) struct Undelegation { + pub(crate) validator_address: String, + pub(crate) entries: Vec, +} + +#[derive(Serialize)] +pub(crate) struct UndelegationEntry { + pub(crate) creation_height: i64, + pub(crate) completion_datetime: String, + pub(crate) balance: BigDecimal, +} diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index bc57aaaf10..bb5ec12353 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1,17 +1,12 @@ use super::{BalanceError, CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, - RawTransactionRequest, SwapOps, TradeFee, TransactionEnum, TransactionFut}; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, - ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, RefundResult, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, - SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, + RawTransactionRequest, SwapOps, TradeFee, TransactionEnum}; +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, + DexFee, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicy, + RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, + SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, ValidatePaymentResult, ValidateWatcherSpendInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawRequest}; + ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidatePaymentResult, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use common::executor::AbortedError; pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature}; @@ -213,7 +208,7 @@ pub struct SiaCoinProtocolInfo; impl MmCoin for SiaCoin { fn is_asset_chain(&self) -> bool { false } - fn spawner(&self) -> CoinFutSpawner { unimplemented!() } + fn spawner(&self) -> WeakSpawner { unimplemented!() } fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } @@ -394,18 +389,14 @@ impl MarketCoinOps for SiaCoin { fn min_trading_vol(&self) -> MmNumber { unimplemented!() } + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.0.priv_key_policy.is_trezor() } } #[async_trait] impl SwapOps for SiaCoin { - async fn send_taker_fee( - &self, - _fee_addr: &[u8], - _dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { + async fn send_taker_fee(&self, _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { unimplemented!() } @@ -478,155 +469,33 @@ impl SwapOps for SiaCoin { unimplemented!() } - fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - async fn extract_secret( &self, _secret_hash: &[u8], _spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { unimplemented!() } - fn is_auto_refundable(&self) -> bool { false } - - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { unimplemented!() } - fn negotiate_swap_contract_addr( &self, _other_side_address: Option<&[u8]>, ) -> Result, MmError> { - unimplemented!() + Ok(None) } fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } - fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { unimplemented!() } + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> [u8; 33] { unimplemented!() } async fn can_refund_htlc(&self, _locktime: u64) -> Result { unimplemented!() } fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } - - async fn maker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - unimplemented!() - } - - async fn taker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - unimplemented!() - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - unimplemented!() - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - unimplemented!() - } -} - -#[async_trait] -impl TakerSwapMakerCoin for SiaCoin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for SiaCoin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } } #[async_trait] -impl WatcherOps for SiaCoin { - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - unimplemented!(); - } - - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - unimplemented!(); - } - - fn create_taker_payment_refund_preimage( - &self, - _taker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_contract_address: &Option, - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn create_maker_payment_spend_preimage( - &self, - _maker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { - unimplemented!() - } - - async fn watcher_search_for_swap_tx_spend( - &self, - _input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String> { - unimplemented!(); - } - - async fn get_taker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _coin_amount: Option, - _other_coin_amount: Option, - _reward_amount: Option, - _wait_until: u64, - ) -> Result> { - unimplemented!() - } - - async fn get_maker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _reward_amount: Option, - _wait_until: u64, - ) -> Result, MmError> { - unimplemented!() - } -} +impl WatcherOps for SiaCoin {} #[cfg(test)] mod tests { diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index 78009b5db8..ac88f1bac9 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod ethermint_account; pub mod htlc; mod ibc; mod rpc; -mod tendermint_balance_events; +pub mod tendermint_balance_events; mod tendermint_coin; mod tendermint_token; pub mod tendermint_tx_history_v2; @@ -16,5 +16,7 @@ pub use cosmrs::AccountId; pub use tendermint_coin::*; pub use tendermint_token::*; +pub(crate) const BCH_COIN_PROTOCOL_TYPE: &str = "BCH"; +pub(crate) const BCH_TOKEN_PROTOCOL_TYPE: &str = "SLPTOKEN"; pub(crate) const TENDERMINT_COIN_PROTOCOL_TYPE: &str = "TENDERMINT"; pub(crate) const TENDERMINT_ASSET_PROTOCOL_TYPE: &str = "TENDERMINTTOKEN"; diff --git a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs index 9224ea997f..4a5d13c05f 100644 --- a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs @@ -138,7 +138,7 @@ mod tests { #[wasm_bindgen_test] async fn test_get_abci_info() { - let client = HttpClient::new("https://rpc.sentry-02.theta-testnet.polypore.xyz", None).unwrap(); + let client = HttpClient::new("http://34.80.202.172:26657", None).unwrap(); client.abci_info().await.unwrap(); } } diff --git a/mm2src/coins/tendermint/tendermint_balance_events.rs b/mm2src/coins/tendermint/tendermint_balance_events.rs index c512cf8277..eed451b5dd 100644 --- a/mm2src/coins/tendermint/tendermint_balance_events.rs +++ b/mm2src/coins/tendermint/tendermint_balance_events.rs @@ -1,26 +1,44 @@ use async_trait::async_trait; -use common::{executor::{AbortSettings, SpawnAbortable}, - http_uri_to_ws_address, log, PROXY_REQUEST_EXPIRATION_SEC}; -use futures::channel::oneshot::{self, Receiver, Sender}; +use common::{http_uri_to_ws_address, log, PROXY_REQUEST_EXPIRATION_SEC}; +use futures::channel::oneshot; use futures_util::{SinkExt, StreamExt}; use jsonrpc_core::{Id as RpcId, Params as RpcParams, Value as RpcValue, Version as RpcVersion}; -use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - ErrorEventName, Event, EventName, EventStreamConfiguration}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; use mm2_number::BigDecimal; use proxy_signature::RawMessage; use std::collections::{HashMap, HashSet}; use super::TendermintCoin; -use crate::{tendermint::TendermintCommons, utxo::utxo_common::big_decimal_from_sat_unsigned, MarketCoinOps, MmCoin}; +use crate::{tendermint::TendermintCommons, utxo::utxo_common::big_decimal_from_sat_unsigned, MarketCoinOps}; -#[async_trait] -impl EventBehaviour for TendermintCoin { - fn event_name() -> EventName { EventName::CoinBalance } +pub struct TendermintBalanceEventStreamer { + coin: TendermintCoin, +} + +impl TendermintBalanceEventStreamer { + pub fn new(coin: TendermintCoin) -> Self { Self { coin } } +} - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } +#[async_trait] +impl EventStreamer for TendermintBalanceEventStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + let streamer_id = self.streamer_id(); + let coin = self.coin; + let account_id = coin.account_id.to_string(); + let mut current_balances: HashMap = HashMap::new(); - async fn handle(self, _interval: f64, tx: oneshot::Sender) { fn generate_subscription_query( query_filter: String, proxy_sign_keypair: &Option, @@ -48,24 +66,8 @@ impl EventBehaviour for TendermintCoin { serde_json::to_string(&q).expect("This should never happen") } - let ctx = match MmArc::from_weak(&self.ctx) { - Some(ctx) => ctx, - None => { - let msg = "MM context must have been initialized already."; - tx.send(EventInitStatus::Failed(msg.to_owned())) - .expect("Receiver is dropped, which should never happen."); - panic!("{}", msg); - }, - }; - - let account_id = self.account_id.to_string(); - let mut current_balances: HashMap = HashMap::new(); - - tx.send(EventInitStatus::Success) - .expect("Receiver is dropped, which should never happen."); - loop { - let client = match self.rpc_client().await { + let client = match coin.rpc_client().await { Ok(client) => client, Err(e) => { log::error!("{e}"); @@ -139,18 +141,13 @@ impl EventBehaviour for TendermintCoin { let mut balance_updates = vec![]; for denom in denoms { - if let Some((ticker, decimals)) = self.active_ticker_and_decimals_from_denom(&denom) { - let balance_denom = match self.account_balance_for_denom(&self.account_id, denom).await { + if let Some((ticker, decimals)) = coin.active_ticker_and_decimals_from_denom(&denom) { + let balance_denom = match coin.account_balance_for_denom(&coin.account_id, denom).await { Ok(balance_denom) => balance_denom, Err(e) => { log::error!("Failed getting balance for '{ticker}'. Error: {e}"); let e = serde_json::to_value(e).expect("Serialization should't fail."); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", Self::error_event_name(), ticker), - e.to_string(), - )) - .await; + broadcaster.broadcast(Event::err(streamer_id.clone(), e)); continue; }, @@ -180,41 +177,10 @@ impl EventBehaviour for TendermintCoin { } if !balance_updates.is_empty() { - ctx.stream_channel_controller - .broadcast(Event::new( - Self::event_name().to_string(), - json!(balance_updates).to_string(), - )) - .await; + broadcaster.broadcast(Event::new(streamer_id.clone(), json!(balance_updates))); } } } } } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - log::info!( - "{} event is activated for {}. `stream_interval_seconds`({}) has no effect on this.", - Self::event_name(), - self.ticker(), - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = AbortSettings::info_on_abort(format!( - "{} event is stopped for {}.", - Self::event_name(), - self.ticker() - )); - self.spawner().spawn_with_settings(fut, settings); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive - } - } } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 323637599d..3f5f8a33de 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,6 +6,9 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; +use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, Delegation, DelegationPayload, + DelegationsQueryResponse, Undelegation, UndelegationEntry, + UndelegationsQueryResponse, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -14,20 +17,16 @@ use crate::tendermint::ibc::IBC_OUT_SOURCE_PORT; use crate::utxo::sat_from_big_decimal; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, - CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, - HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicy, + CoinBalance, ConfirmPaymentInput, DelegationError, DexFee, FeeApproxStage, FoundSwapTxSpend, + HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, - RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, - SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, ToBytes, TradeFee, + RawTransactionResult, RefundPaymentArgs, RpcCommonOps, SearchForSwapTxSpendInput, SendPaymentArgs, + SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, ToBytes, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, - ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; + ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bip32::DerivationPath; @@ -35,17 +34,28 @@ use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; use common::log::{debug, warn}; -use common::{get_utc_timestamp, now_sec, Future01CompatExt, DEX_FEE_ADDR_PUBKEY}; -use cosmrs::bank::MsgSend; +use common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY}; +use compatible_time::Duration; +use cosmrs::bank::{MsgMultiSend, MsgSend, MultiSendIo}; use cosmrs::crypto::secp256k1::SigningKey; +use cosmrs::distribution::MsgWithdrawDelegatorReward; use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}; -use cosmrs::proto::cosmos::bank::v1beta1::{MsgSend as MsgSendProto, QueryBalanceRequest, QueryBalanceResponse}; +use cosmrs::proto::cosmos::bank::v1beta1::{MsgMultiSend as MsgMultiSendProto, MsgSend as MsgSendProto, + QueryBalanceRequest, QueryBalanceResponse}; +use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest; use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, GetLatestBlockResponse}; -use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto; -use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, - SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; +use cosmrs::proto::cosmos::base::v1beta1::{Coin as CoinProto, DecCoin}; +use cosmrs::proto::cosmos::distribution::v1beta1::{QueryDelegationRewardsRequest, QueryDelegationRewardsResponse}; +use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, + QueryDelegatorDelegationsRequest, QueryDelegatorDelegationsResponse, + QueryDelegatorUnbondingDelegationsRequest, + QueryDelegatorUnbondingDelegationsResponse, QueryValidatorsRequest, + QueryValidatorsResponse as QueryValidatorsResponseProto}; +use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, SimulateRequest, SimulateResponse, Tx, TxBody, + TxRaw}; use cosmrs::proto::prost::{DecodeError, Message}; +use cosmrs::staking::{MsgDelegate, MsgUndelegate, QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; @@ -59,21 +69,22 @@ use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use hex::FromHexError; -use instant::Duration; use itertools::Itertools; use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; +use mm2_number::bigdecimal::ParseBigDecimalError; use mm2_number::MmNumber; use mm2_p2p::p2p_ctx::P2PContext; +use num_traits::Zero; use parking_lot::Mutex as PaMutex; use primitives::hash::H256; use regex::Regex; use rpc::v1::types::Bytes as BytesJson; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use std::io; use std::num::NonZeroU32; use std::ops::Deref; @@ -81,6 +92,8 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use uuid::Uuid; +#[cfg(test)] use mocktopus::macros::*; + // ABCI Request Paths const ABCI_GET_LATEST_BLOCK_PATH: &str = "/cosmos.base.tendermint.v1beta1.Service/GetLatestBlock"; const ABCI_GET_BLOCK_BY_HEIGHT_PATH: &str = "/cosmos.base.tendermint.v1beta1.Service/GetBlockByHeight"; @@ -88,7 +101,11 @@ const ABCI_SIMULATE_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/Simulate"; const ABCI_QUERY_ACCOUNT_PATH: &str = "/cosmos.auth.v1beta1.Query/Account"; const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance"; const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; -const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; +const ABCI_VALIDATORS_PATH: &str = "/cosmos.staking.v1beta1.Query/Validators"; +const ABCI_DELEGATION_PATH: &str = "/cosmos.staking.v1beta1.Query/Delegation"; +const ABCI_DELEGATOR_DELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorDelegations"; +const ABCI_DELEGATOR_UNDELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorUnbondingDelegations"; +const ABCI_DELEGATION_REWARDS_PATH: &str = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -153,6 +170,8 @@ impl RpcNode { #[async_trait] pub trait TendermintCommons { + fn denom_to_ticker(&self, denom: &str) -> Option; + fn platform_denom(&self) -> &Denom; fn set_history_sync_state(&self, new_state: HistorySyncState); @@ -175,7 +194,7 @@ pub struct TendermintFeeDetails { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct TendermintProtocolInfo { - decimals: u8, + pub decimals: u8, denom: String, pub account_prefix: String, chain_id: String, @@ -361,7 +380,7 @@ pub struct TendermintCoinImpl { /// My address pub account_id: AccountId, pub(super) account_prefix: String, - pub(super) activation_policy: TendermintActivationPolicy, + pub activation_policy: TendermintActivationPolicy, pub(crate) decimals: u8, pub(super) denom: Denom, chain_id: ChainId, @@ -373,7 +392,7 @@ pub struct TendermintCoinImpl { pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, pub(crate) chain_registry_name: Option, - pub(crate) ctx: MmWeak, + pub ctx: MmWeak, pub(crate) is_keplr_from_ledger: bool, } @@ -401,6 +420,7 @@ pub enum TendermintInitErrorKind { RpcClientInitError(String), InvalidChainId(String), InvalidDenom(String), + InvalidProtocolData(String), InvalidPathToAddress(String), #[display(fmt = "'derivation_path' field is not found in config")] DerivationPathIsNotSet, @@ -419,10 +439,10 @@ pub enum TendermintInitErrorKind { #[display(fmt = "avg_blocktime must be in-between '0' and '255'.")] AvgBlockTimeInvalid, BalanceStreamInitError(String), - #[display(fmt = "Watcher features can not be used with pubkey-only activation policy.")] - CantUseWatchersWithPubkeyPolicy, } +/// TODO: Rename this into `ClientRpcError` because this is very +/// confusing atm. #[derive(Display, Debug, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum TendermintCoinRpcError { @@ -449,13 +469,18 @@ impl From for WithdrawError { fn from(err: TendermintCoinRpcError) -> Self { WithdrawError::Transport(err.to_string()) } } +impl From for DelegationError { + fn from(err: TendermintCoinRpcError) -> Self { DelegationError::Transport(err.to_string()) } +} + impl From for BalanceError { fn from(err: TendermintCoinRpcError) -> Self { match err { TendermintCoinRpcError::InvalidResponse(e) => BalanceError::InvalidResponse(e), TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e), - TendermintCoinRpcError::PerformError(e) => BalanceError::Transport(e), - TendermintCoinRpcError::RpcClientError(e) => BalanceError::Transport(e), + TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { + BalanceError::Transport(e) + }, TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs")) @@ -469,8 +494,9 @@ impl From for ValidatePaymentError { match err { TendermintCoinRpcError::InvalidResponse(e) => ValidatePaymentError::InvalidRpcResponse(e), TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e), - TendermintCoinRpcError::PerformError(e) => ValidatePaymentError::Transport(e), - TendermintCoinRpcError::RpcClientError(e) => ValidatePaymentError::Transport(e), + TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { + ValidatePaymentError::Transport(e) + }, TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs")) @@ -594,6 +620,21 @@ impl TendermintCommons for TendermintCoin { Ok(u64::try_from(timestamp.seconds).ok()) } + fn denom_to_ticker(&self, denom: &str) -> Option { + if self.denom.as_ref() == denom { + return Some(self.ticker.clone()); + } + + let ctx = MmArc::from_weak(&self.ctx)?; + + ctx.conf["coins"].as_array()?.iter().find_map(|coin| { + coin["protocol"]["protocol_data"]["denom"] + .as_str() + .filter(|&d| d.to_lowercase() == denom.to_lowercase()) + .and_then(|_| coin["coin"].as_str().map(|s| s.to_owned())) + }) + } + async fn get_all_balances(&self) -> MmResult { let platform_balance_denom = self .account_balance_for_denom(&self.account_id, self.denom.to_string()) @@ -627,6 +668,7 @@ impl TendermintCommons for TendermintCoin { } } +#[cfg_attr(test, mockable)] impl TendermintCoin { #[allow(clippy::too_many_arguments)] pub async fn init( @@ -638,7 +680,7 @@ impl TendermintCoin { tx_history: bool, activation_policy: TendermintActivationPolicy, is_keplr_from_ledger: bool, - ) -> MmResult { + ) -> MmResult { if nodes.is_empty() { return MmError::err(TendermintInitError { ticker, @@ -775,7 +817,7 @@ impl TendermintCoin { priv_key: &Secp256k1Secret, tx_payload: Any, timeout_height: u64, - memo: String, + memo: &str, ) -> cosmrs::Result> { let fee_amount = Coin { denom: self.denom.clone(), @@ -825,7 +867,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, timeout: Duration, ) -> Result<(String, Raw), TransactionErr> { // As there wouldn't be enough time to process the data, to mitigate potential edge problems (such as attempting to send transaction @@ -856,7 +898,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> Result<(String, Raw), TransactionErr> { let mut account_info = try_tx_s!(self.account_info(&self.account_id).await); let (tx_id, tx_raw) = loop { @@ -866,7 +908,7 @@ impl TendermintCoin { tx_payload.clone(), fee.clone(), timeout_height, - memo.clone(), + memo, )); match self.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await { @@ -891,7 +933,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, timeout: Duration, ) -> Result<(String, Raw), TransactionErr> { #[derive(Deserialize)] @@ -935,10 +977,12 @@ impl TendermintCoin { &self, msg: Any, timeout_height: u64, - memo: String, + memo: &str, withdraw_fee: Option, ) -> MmResult { - let Ok(activated_priv_key) = self.activation_policy.activated_key_or_err() else { + let activated_priv_key = if let Ok(activated_priv_key) = self.activation_policy.activated_key_or_err() { + activated_priv_key + } else { let (gas_price, gas_limit) = self.gas_info_for_withdraw(&withdraw_fee, GAS_LIMIT_DEFAULT); let amount = ((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil(); @@ -953,13 +997,7 @@ impl TendermintCoin { let mut account_info = self.account_info(&self.account_id).await?; let (response, raw_response) = loop { let tx_bytes = self - .gen_simulated_tx( - &account_info, - activated_priv_key, - msg.clone(), - timeout_height, - memo.clone(), - ) + .gen_simulated_tx(&account_info, activated_priv_key, msg.clone(), timeout_height, memo) .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( @@ -1020,10 +1058,12 @@ impl TendermintCoin { priv_key: Option, msg: Any, timeout_height: u64, - memo: String, + memo: &str, withdraw_fee: Option, ) -> MmResult { - let Some(priv_key) = priv_key else { + let priv_key = if let Some(priv_key) = priv_key { + priv_key + } else { let (gas_price, _) = self.gas_info_for_withdraw(&withdraw_fee, 0); return Ok(((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil() as u64); }; @@ -1031,7 +1071,7 @@ impl TendermintCoin { let mut account_info = self.account_info(account_id).await?; let (response, raw_response) = loop { let tx_bytes = self - .gen_simulated_tx(&account_info, &priv_key, msg.clone(), timeout_height, memo.clone()) + .gen_simulated_tx(&account_info, &priv_key, msg.clone(), timeout_height, memo) .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( @@ -1095,9 +1135,10 @@ impl TendermintCoin { .account .or_mm_err(|| TendermintCoinRpcError::InvalidResponse("Account is None".into()))?; + let account_prefix = self.account_prefix.clone(); let base_account = match BaseAccount::decode(account.value.as_slice()) { Ok(account) => account, - Err(err) if &self.account_prefix == "iaa" => { + Err(err) if account_prefix.as_str() == "iaa" => { let ethermint_account = EthermintAccount::decode(account.value.as_slice())?; ethermint_account @@ -1138,11 +1179,10 @@ impl TendermintCoin { .map_to_mm(|e| TendermintCoinRpcError::InvalidResponse(format!("balance is not u64, err {}", e))) } - #[allow(clippy::result_large_err)] - pub(super) fn account_id_and_pk_for_withdraw( + pub(super) fn extract_account_id_and_private_key( &self, withdraw_from: Option, - ) -> Result<(AccountId, Option), WithdrawError> { + ) -> Result<(AccountId, Option), io::Error> { if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { return Ok((self.account_id.clone(), None)); } @@ -1152,28 +1192,28 @@ impl TendermintCoin { let path_to_coin = self .activation_policy .path_to_coin_or_err() - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; let path_to_address = from .to_address_path(path_to_coin.coin_type()) - .map_err(|e| WithdrawError::InternalError(e.to_string()))? + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))? .to_derivation_path(path_to_coin) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; let priv_key = self .activation_policy .hd_wallet_derived_priv_key_or_err(&path_to_address) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; let account_id = account_id_from_privkey(priv_key.as_slice(), &self.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?; Ok((account_id, Some(priv_key))) }, None => { let activated_key = self .activation_policy .activated_key_or_err() - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; Ok((self.account_id.clone(), Some(*activated_key))) }, @@ -1182,14 +1222,14 @@ impl TendermintCoin { pub(super) fn any_to_transaction_data( &self, - maybe_pk: Option, + maybe_priv_key: Option, message: Any, account_info: &BaseAccount, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> Result { - if let Some(priv_key) = maybe_pk { + if let Some(priv_key) = maybe_priv_key { let tx_raw = self.any_to_signed_raw_tx(&priv_key, account_info, message, fee, timeout_height, memo)?; let tx_bytes = tx_raw.to_bytes()?; let hash = sha256(&tx_bytes); @@ -1272,7 +1312,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> cosmrs::Result { let signkey = SigningKey::from_slice(priv_key.as_slice())?; let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); @@ -1287,7 +1327,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> cosmrs::Result { let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let pubkey = self.activation_policy.public_key()?.into(); @@ -1319,7 +1359,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, ) -> cosmrs::Result { const MSG_SEND_TYPE_URL: &str = "/cosmos.bank.v1beta1.MsgSend"; const LEDGER_MSG_SEND_TYPE_URL: &str = "cosmos-sdk/MsgSend"; @@ -1338,7 +1378,7 @@ impl TendermintCoin { let msg_send = MsgSend::from_any(&tx_payload)?; let timeout_height = u32::try_from(timeout_height)?; let original_tx_type_url = tx_payload.type_url.clone(); - let body_bytes = tx::Body::new(vec![tx_payload], &memo, timeout_height).into_bytes()?; + let body_bytes = tx::Body::new(vec![tx_payload], memo, timeout_height).into_bytes()?; let amount: Vec = msg_send .amount @@ -1393,6 +1433,7 @@ impl TendermintCoin { Ok(SerializedUnsignedTx { tx_json, body_bytes }) } + #[allow(clippy::let_unit_value)] // for mockable pub fn add_activated_token_info(&self, ticker: String, decimals: u8, denom: Denom) { self.tokens_info .lock() @@ -1515,7 +1556,7 @@ impl TendermintCoin { coin.calculate_fee( create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None ) .await @@ -1526,7 +1567,7 @@ impl TendermintCoin { create_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(time_lock_duration), ) .await @@ -1542,8 +1583,7 @@ impl TendermintCoin { pub(super) fn send_taker_fee_for_denom( &self, - fee_addr: &[u8], - amount: BigDecimal, + dex_fee: &DexFee, denom: Denom, decimals: u8, uuid: &[u8], @@ -1551,20 +1591,56 @@ impl TendermintCoin { ) -> TransactionFut { let memo = try_tx_fus!(Uuid::from_slice(uuid)).to_string(); let from_address = self.account_id.clone(); - let pubkey_hash = dhash160(fee_addr); - let to_address = try_tx_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); - - let amount_as_u64 = try_tx_fus!(sat_from_big_decimal(&amount, decimals)); - let amount = cosmrs::Amount::from(amount_as_u64); + let dex_pubkey_hash = dhash160(self.dex_pubkey()); + let burn_pubkey_hash = dhash160(self.burn_pubkey()); + let dex_address = try_tx_fus!(AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice())); + let burn_address = try_tx_fus!(AccountId::new(&self.account_prefix, burn_pubkey_hash.as_slice())); - let amount = vec![Coin { denom, amount }]; + let fee_amount_as_u64 = try_tx_fus!(dex_fee.fee_amount_as_u64(decimals)); + let fee_amount = vec![Coin { + denom: denom.clone(), + amount: cosmrs::Amount::from(fee_amount_as_u64), + }]; - let tx_payload = try_tx_fus!(MsgSend { - from_address, - to_address, - amount, - } - .to_any()); + let tx_result = match dex_fee { + DexFee::NoFee => try_tx_fus!(Err("Unexpected DexFee::NoFee".to_owned())), + DexFee::Standard(_) => MsgSend { + from_address, + to_address: dex_address, + amount: fee_amount, + } + .to_any(), + DexFee::WithBurn { .. } => { + let burn_amount_as_u64 = try_tx_fus!(dex_fee.burn_amount_as_u64(decimals)).unwrap_or_default(); + let burn_amount = vec![Coin { + denom: denom.clone(), + amount: cosmrs::Amount::from(burn_amount_as_u64), + }]; + let total_amount_as_u64 = fee_amount_as_u64 + burn_amount_as_u64; + let total_amount = vec![Coin { + denom, + amount: cosmrs::Amount::from(total_amount_as_u64), + }]; + MsgMultiSend { + inputs: vec![MultiSendIo { + address: from_address, + coins: total_amount, + }], + outputs: vec![ + MultiSendIo { + address: dex_address, + coins: fee_amount, + }, + MultiSendIo { + address: burn_address, + coins: burn_amount, + }, + ], + } + .to_any() + }, + }; + let tx_payload = try_tx_fus!(tx_result); let coin = self.clone(); let fut = async move { @@ -1572,7 +1648,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(), None) + coin.calculate_fee(tx_payload.clone(), timeout_height, TX_DEFAULT_MEMO, None) .await ); @@ -1582,7 +1658,7 @@ impl TendermintCoin { tx_payload.clone(), fee.clone(), timeout_height, - memo.clone(), + &memo, Duration::from_secs(timeout) ) .await @@ -1601,8 +1677,7 @@ impl TendermintCoin { &self, fee_tx: &TransactionEnum, expected_sender: &[u8], - fee_addr: &[u8], - amount: &BigDecimal, + dex_fee: &DexFee, decimals: u8, uuid: &[u8], denom: String, @@ -1621,66 +1696,40 @@ impl TendermintCoin { let sender_pubkey_hash = dhash160(expected_sender); let expected_sender_address = try_f!(AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice()) - .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))) - .to_string(); - - let dex_fee_addr_pubkey_hash = dhash160(fee_addr); - let expected_dex_fee_address = try_f!(AccountId::new( - &self.account_prefix, - dex_fee_addr_pubkey_hash.as_slice() - ) - .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))) - .to_string(); - - let expected_amount = try_f!(sat_from_big_decimal(amount, decimals)); - let expected_amount = CoinProto { - denom, - amount: expected_amount.to_string(), - }; + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))); let coin = self.clone(); + let dex_fee = dex_fee.clone(); let fut = async move { let tx_body = TxBody::decode(tx.data.body_bytes.as_slice()) .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; - if tx_body.messages.len() != 1 { - return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Tx body must have exactly one message".to_string(), - )); - } - - let msg = MsgSendProto::decode(tx_body.messages[0].value.as_slice()) - .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; - if msg.to_address != expected_dex_fee_address { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Dex fee is sent to wrong address: {}, expected {}", - msg.to_address, expected_dex_fee_address - ))); - } - - if msg.amount.len() != 1 { - return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Msg must have exactly one Coin".to_string(), - )); - } - if msg.amount[0] != expected_amount { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Invalid amount {:?}, expected {:?}", - msg.amount[0], expected_amount - ))); - } - - if msg.from_address != expected_sender_address { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Invalid sender: {}, expected {}", - msg.from_address, expected_sender_address - ))); + match dex_fee { + DexFee::NoFee => { + return MmError::err(ValidatePaymentError::InternalError( + "unexpected DexFee::NoFee".to_string(), + )) + }, + DexFee::Standard(_) => coin.validate_standard_dex_fee( + &tx_body, + &expected_sender_address, + &dex_fee, + decimals, + denom.clone(), + )?, + DexFee::WithBurn { .. } => coin.validate_with_burn_dex_fee( + &tx_body, + &expected_sender_address, + &dex_fee, + decimals, + denom.clone(), + )?, } if tx_body.memo != uuid { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Invalid memo: {}, expected {}", - msg.from_address, uuid + tx_body.memo, uuid ))); } @@ -1780,6 +1829,152 @@ impl TendermintCoin { } } + fn validate_standard_dex_fee( + &self, + tx_body: &TxBody, + expected_sender_address: &AccountId, + dex_fee: &DexFee, + decimals: u8, + denom: String, + ) -> MmResult<(), ValidatePaymentError> { + if tx_body.messages.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Tx body must have exactly one message".to_string(), + )); + } + + let dex_pubkey_hash = dhash160(self.dex_pubkey()); + let expected_dex_address = AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice()) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; + + let fee_amount_as_u64 = dex_fee.fee_amount_as_u64(decimals)?; + let expected_dex_amount = CoinProto { + denom, + amount: fee_amount_as_u64.to_string(), + }; + + let msg = MsgSendProto::decode(tx_body.messages[0].value.as_slice()) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + if msg.to_address != expected_dex_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Dex fee is sent to wrong address: {}, expected {}", + msg.to_address, expected_dex_address + ))); + } + if msg.amount.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Msg must have exactly one Coin".to_string(), + )); + } + if msg.amount[0] != expected_dex_amount { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid amount {:?}, expected {:?}", + msg.amount[0], expected_dex_amount + ))); + } + if msg.from_address != expected_sender_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid sender: {}, expected {}", + msg.from_address, expected_sender_address + ))); + } + Ok(()) + } + + fn validate_with_burn_dex_fee( + &self, + tx_body: &TxBody, + expected_sender_address: &AccountId, + dex_fee: &DexFee, + decimals: u8, + denom: String, + ) -> MmResult<(), ValidatePaymentError> { + if tx_body.messages.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Tx body must have exactly one message".to_string(), + )); + } + + let dex_pubkey_hash = dhash160(self.dex_pubkey()); + let expected_dex_address = AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice()) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; + + let burn_pubkey_hash = dhash160(self.burn_pubkey()); + let expected_burn_address = AccountId::new(&self.account_prefix, burn_pubkey_hash.as_slice()) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; + + let fee_amount_as_u64 = dex_fee.fee_amount_as_u64(decimals)?; + let expected_dex_amount = CoinProto { + denom: denom.clone(), + amount: fee_amount_as_u64.to_string(), + }; + let burn_amount_as_u64 = dex_fee.burn_amount_as_u64(decimals)?.unwrap_or_default(); + let expected_burn_amount = CoinProto { + denom, + amount: burn_amount_as_u64.to_string(), + }; + + let msg = MsgMultiSendProto::decode(tx_body.messages[0].value.as_slice()) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + if msg.outputs.len() != 2 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Msg must have exactly two outputs".to_string(), + )); + } + + // Validate dex fee output + if msg.outputs[0].address != expected_dex_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Dex fee is sent to wrong address: {}, expected {}", + msg.outputs[0].address, expected_dex_address + ))); + } + if msg.outputs[0].coins.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Dex fee output must have exactly one Coin".to_string(), + )); + } + if msg.outputs[0].coins[0] != expected_dex_amount { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid dex fee amount {:?}, expected {:?}", + msg.outputs[0].coins[0], expected_dex_amount + ))); + } + + // Validate burn output + if msg.outputs[1].address != expected_burn_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Burn fee is sent to wrong address: {}, expected {}", + msg.outputs[1].address, expected_burn_address + ))); + } + if msg.outputs[1].coins.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Burn fee output must have exactly one Coin".to_string(), + )); + } + if msg.outputs[1].coins[0] != expected_burn_amount { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid burn amount {:?}, expected {:?}", + msg.outputs[1].coins[0], expected_burn_amount + ))); + } + if msg.inputs.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Msg must have exactly one input".to_string(), + )); + } + + // validate input + if msg.inputs[0].address != expected_sender_address.as_ref() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid sender: {}, expected {}", + msg.inputs[0].address, expected_sender_address + ))); + } + Ok(()) + } + pub(super) async fn get_sender_trade_fee_for_denom( &self, ticker: String, @@ -1822,7 +2017,7 @@ impl TendermintCoin { self.activation_policy.activated_key(), create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None, ) .await?; @@ -1873,7 +2068,7 @@ impl TendermintCoin { self.activation_policy.activated_key(), msg_send, timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None, ) .await?; @@ -1978,9 +2173,9 @@ impl TendermintCoin { amount >= &min_tx_amount } - async fn search_for_swap_tx_spend( + async fn search_for_swap_tx_spend<'l>( &self, - input: SearchForSwapTxSpendInput<'_>, + input: SearchForSwapTxSpendInput<'l>, ) -> MmResult, SearchForSwapTxSpendErr> { let tx = cosmrs::Tx::from_bytes(input.tx)?; let first_message = tx @@ -2008,37 +2203,29 @@ impl TendermintCoin { match htlc_state { HTLC_STATE_OPEN => Ok(None), HTLC_STATE_COMPLETED => { - let events_string = format!("claim_htlc.id='{}'", htlc_id); - // TODO: Remove deprecated attribute when new version of tendermint-rs is released - #[allow(deprecated)] - let request = GetTxsEventRequest { - events: vec![events_string], - order_by: TendermintResultOrder::Ascending as i32, + let query = format!("claim_htlc.id='{}'", htlc_id); + let request = TxSearchRequest { + query, + order_by: TendermintResultOrder::Ascending.into(), page: 1, - limit: 1, - pagination: None, + per_page: 1, + prove: false, }; - let encoded_request = request.encode_to_vec(); let response = self .rpc_client() .await? - .abci_query( - Some(ABCI_GET_TXS_EVENT_PATH.to_string()), - encoded_request.as_slice(), - ABCI_REQUEST_HEIGHT, - ABCI_REQUEST_PROVE, - ) + .perform(request) .await .map_to_mm(TendermintCoinRpcError::from)?; - let response = GetTxsEventResponse::decode(response.value.as_slice())?; match response.txs.first() { - Some(tx) => { + Some(raw_tx) => { + let tx = cosmrs::Tx::from_bytes(&raw_tx.tx)?; let tx = TransactionEnum::CosmosTransaction(CosmosTransaction { data: TxRaw { - body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), - auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), - signatures: tx.signatures.clone(), + body_bytes: tx.body.into_bytes()?, + auth_info_bytes: tx.auth_info.into_bytes()?, + signatures: tx.signatures, }, }); Ok(Some(FoundSwapTxSpend::Spent(tx))) @@ -2080,126 +2267,781 @@ impl TendermintCoin { None } -} - -fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { - if nodes.is_empty() { - return MmError::err(TendermintInitErrorKind::EmptyRpcUrls); - } - - let p2p_keypair = if nodes.iter().any(|n| n.komodo_proxy) { - let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); - Some(p2p_ctx.keypair().clone()) - } else { - None - }; - let mut clients = Vec::new(); - let mut errors = Vec::new(); + pub(crate) async fn validators_list( + &self, + filter_status: ValidatorStatus, + paging: PagingOptions, + ) -> MmResult, TendermintCoinRpcError> { + let request = QueryValidatorsRequest { + status: filter_status.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; - // check that all urls are valid - // keep all invalid urls in one vector to show all of them in error - for node in nodes.iter() { - let proxy_sign_keypair = if node.komodo_proxy { p2p_keypair.clone() } else { None }; - match HttpClient::new(node.url.as_str(), proxy_sign_keypair) { - Ok(client) => clients.push(client), - Err(e) => errors.push(format!("Url {} is invalid, got error {}", node.url, e)), - } - } - drop_mutability!(clients); - drop_mutability!(errors); - if !errors.is_empty() { - let errors: String = errors.into_iter().join(", "); - return MmError::err(TendermintInitErrorKind::RpcClientInitError(errors)); - } - Ok(clients) -} + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_VALIDATORS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; -pub async fn get_ibc_chain_list() -> IBCChainRegistriesResult { - fn map_metadata_to_chain_registry_name(metadata: &FileMetadata) -> Result> { - let split_filename_by_dash: Vec<&str> = metadata.name.split('-').collect(); - let chain_registry_name = split_filename_by_dash - .first() - .or_mm_err(|| { - IBCChainsRequestError::InternalError(format!( - "Could not read chain registry name from '{}'", - metadata.name - )) - })? - .to_string(); + let decoded_proto = QueryValidatorsResponseProto::decode(raw_response.value.as_slice())?; + let typed_response = QueryValidatorsResponse::try_from(decoded_proto) + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; - Ok(chain_registry_name) + Ok(typed_response.validators) } - let git_controller: GitController = GitController::new(GITHUB_API_URI); + pub(crate) async fn delegate(&self, req: DelegationPayload) -> MmResult { + fn generate_message( + delegator_address: AccountId, + validator_address: AccountId, + denom: Denom, + amount: u128, + ) -> Result { + MsgDelegate { + delegator_address, + validator_address, + amount: Coin { denom, amount }, + } + .to_any() + } - let metadata_list = git_controller - .client - .get_file_metadata_list( - CHAIN_REGISTRY_REPO_OWNER, - CHAIN_REGISTRY_REPO_NAME, - CHAIN_REGISTRY_BRANCH, - CHAIN_REGISTRY_IBC_DIR_NAME, - ) - .await - .map_err(|e| IBCChainsRequestError::Transport(format!("{:?}", e)))?; + /// Calculates the send and total amounts. + /// + /// The send amount is what the receiver receives, while the total amount is what sender + /// pays including the transaction fee. + fn calc_send_and_total_amount( + coin: &TendermintCoin, + balance_u64: u64, + balance_decimal: BigDecimal, + fee_u64: u64, + fee_decimal: BigDecimal, + request_amount: BigDecimal, + is_max: bool, + ) -> Result<(u64, BigDecimal), DelegationError> { + let not_sufficient = |required| DelegationError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_decimal.clone(), + required, + }; - let chain_list: Result, MmError> = - metadata_list.iter().map(map_metadata_to_chain_registry_name).collect(); + if is_max { + if balance_u64 < fee_u64 { + return Err(not_sufficient(fee_decimal)); + } - let mut distinct_chain_list = chain_list?; - distinct_chain_list.dedup(); + let amount_u64 = balance_u64 - fee_u64; + return Ok((amount_u64, balance_decimal)); + } - Ok(IBCChainRegistriesResponse { - chain_registry_list: distinct_chain_list, - }) -} + let total = &request_amount + &fee_decimal; + if balance_decimal < total { + return Err(not_sufficient(total)); + } -#[async_trait] -#[allow(unused_variables)] -impl MmCoin for TendermintCoin { - fn is_asset_chain(&self) -> bool { false } + let amount_u64 = sat_from_big_decimal(&request_amount, coin.decimals) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; - fn wallet_only(&self, ctx: &MmArc) -> bool { - let coin_conf = crate::coin_conf(ctx, self.ticker()); - // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only - if coin_conf.is_null() { - return true; + Ok((amount_u64, total)) } - let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); - wallet_only_conf || self.is_keplr_from_ledger - } + let validator_address = + AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + let (delegator_address, maybe_priv_key) = self + .extract_account_id_and_private_key(req.withdraw_from) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; - fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { - let coin = self.clone(); - let fut = async move { - let to_address = - AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + let (balance_u64, balance_dec) = self + .get_balance_as_unsigned_and_decimal(&delegator_address, &self.denom, self.decimals()) + .await?; - let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); + let amount_u64 = if req.max { + balance_u64 + } else { + sat_from_big_decimal(&req.amount, self.decimals) + .map_err(|e| DelegationError::InternalError(e.to_string()))? + }; - let (account_id, maybe_pk) = coin.account_id_and_pk_for_withdraw(req.from)?; + // This is used for transaction simulation so we can predict the best possible fee amount. + let msg_for_fee_prediction = generate_message( + delegator_address.clone(), + validator_address.clone(), + self.denom.clone(), + amount_u64.into(), + ) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; - let (balance_denom, balance_dec) = coin - .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) - .await?; + let timeout_height = self + .current_block() + .compat() + .await + .map_to_mm(DelegationError::Transport)? + + TIMEOUT_HEIGHT_DELTA; - let (amount_denom, amount_dec) = if req.max { - let amount_denom = balance_denom; - (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) - } else { - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) - }; + // `delegate` uses more gas than the regular transactions + let gas_limit_default = (GAS_LIMIT_DEFAULT * 3) / 2; + let (_, gas_limit) = self.gas_info_for_withdraw(&req.fee, gas_limit_default); - if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { - return MmError::err(WithdrawError::AmountTooLow { - amount: amount_dec, - threshold: coin.min_tx_amount(), - }); - } + let fee_amount_u64 = self + .calculate_account_fee_amount_as_u64( + &delegator_address, + maybe_priv_key, + msg_for_fee_prediction, + timeout_height, + &req.memo, + req.fee, + ) + .await?; + + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, self.decimals()); + + let fee = Fee::from_amount_and_gas( + Coin { + denom: self.denom.clone(), + amount: fee_amount_u64.into(), + }, + gas_limit, + ); + + let (amount_u64, total_amount) = calc_send_and_total_amount( + self, + balance_u64, + balance_dec, + fee_amount_u64, + fee_amount_dec.clone(), + req.amount, + req.max, + )?; + + let msg_for_actual_tx = generate_message( + delegator_address.clone(), + validator_address.clone(), + self.denom.clone(), + amount_u64.into(), + ) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let account_info = self.account_info(&delegator_address).await?; + + let tx = self + .any_to_transaction_data( + maybe_priv_key, + msg_for_actual_tx, + &account_info, + fee, + timeout_height, + &req.memo, + ) + .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; + + let internal_id = { + let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + sha256(&hex_vec).to_vec().into() + }; + + Ok(TransactionDetails { + tx, + from: vec![delegator_address.to_string()], + to: vec![req.validator_address], + my_balance_change: &BigDecimal::default() - &total_amount, + spent_by_me: total_amount.clone(), + total_amount, + received_by_me: BigDecimal::default(), + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: self.ticker.clone(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit, + })), + coin: self.ticker.to_string(), + internal_id, + kmd_rewards: None, + transaction_type: TransactionType::StakingDelegation, + memo: Some(req.memo), + }) + } + + pub(crate) async fn undelegate(&self, req: DelegationPayload) -> MmResult { + fn generate_message( + delegator_address: AccountId, + validator_address: AccountId, + denom: Denom, + amount: u128, + ) -> Result { + MsgUndelegate { + delegator_address, + validator_address, + amount: Coin { denom, amount }, + } + .to_any() + } + + let (delegator_address, maybe_priv_key) = self + .extract_account_id_and_private_key(None) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let validator_address = + AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; + + let (total_delegated_amount, total_delegated_uamount) = self.get_delegated_amount(&validator_address).await?; + + let uamount_to_undelegate = if req.max { + total_delegated_uamount + } else { + if req.amount > total_delegated_amount { + return MmError::err(DelegationError::TooMuchToUndelegate { + available: total_delegated_amount, + requested: req.amount, + }); + }; + + sat_from_big_decimal(&req.amount, self.decimals) + .map_err(|e| DelegationError::InternalError(e.to_string()))? + }; + + let undelegate_msg = generate_message( + delegator_address.clone(), + validator_address.clone(), + self.denom.clone(), + uamount_to_undelegate.into(), + ) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let timeout_height = self + .current_block() + .compat() + .await + .map_to_mm(DelegationError::Transport)? + + TIMEOUT_HEIGHT_DELTA; + + // This uses more gas than any other transactions + let gas_limit_default = GAS_LIMIT_DEFAULT * 2; + let (_, gas_limit) = self.gas_info_for_withdraw(&req.fee, gas_limit_default); + + let fee_amount_u64 = self + .calculate_account_fee_amount_as_u64( + &delegator_address, + maybe_priv_key, + undelegate_msg.clone(), + timeout_height, + &req.memo, + req.fee, + ) + .await?; + + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, self.decimals()); + + let my_balance = self.my_balance().compat().await?.spendable; + + if fee_amount_dec > my_balance { + return MmError::err(DelegationError::NotSufficientBalance { + coin: self.ticker.clone(), + available: my_balance, + required: fee_amount_dec, + }); + } + + let fee = Fee::from_amount_and_gas( + Coin { + denom: self.denom.clone(), + amount: fee_amount_u64.into(), + }, + gas_limit, + ); + + let account_info = self.account_info(&delegator_address).await?; + + let tx = self + .any_to_transaction_data( + maybe_priv_key, + undelegate_msg, + &account_info, + fee, + timeout_height, + &req.memo, + ) + .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; + + let internal_id = { + let hex_vec = tx.tx_hex().map_or_else(Vec::new, |h| h.to_vec()); + sha256(&hex_vec).to_vec().into() + }; + + Ok(TransactionDetails { + tx, + from: vec![delegator_address.to_string()], + to: vec![], // We just pay the transaction fee for undelegation + my_balance_change: &BigDecimal::default() - &fee_amount_dec, + spent_by_me: fee_amount_dec.clone(), + total_amount: fee_amount_dec.clone(), + received_by_me: BigDecimal::default(), + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: self.ticker.clone(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit, + })), + coin: self.ticker.to_string(), + internal_id, + kmd_rewards: None, + transaction_type: TransactionType::RemoveDelegation, + memo: Some(req.memo), + }) + } + + async fn get_delegated_amount( + &self, + validator_addr: &AccountId, // keep this as `AccountId` to make it pre-validated + ) -> MmResult<(BigDecimal, u64), DelegationError> { + let delegator_addr = self + .my_address() + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + let validator_addr = validator_addr.to_string(); + + let request = QueryDelegationRequest { + delegator_addr, + validator_addr, + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATION_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .map_err(|e| DelegationError::Transport(e.to_string())) + .await?; + + let decoded_response = QueryDelegationResponse::decode(raw_response.value.as_slice()) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let Some(delegation_response) = decoded_response.delegation_response else { + return MmError::err(DelegationError::CanNotUndelegate { + delegator_addr: request.delegator_addr, + validator_addr: request.validator_addr, + }); + }; + + let Some(balance) = delegation_response.balance else { + return MmError::err(DelegationError::Transport( + format!("Unexpected response from '{ABCI_DELEGATION_PATH}' with {request:?} request; balance field should not be empty.") + )); + }; + + let uamount = u64::from_str(&balance.amount).map_err(|e| DelegationError::InternalError(e.to_string()))?; + + Ok((big_decimal_from_sat_unsigned(uamount, self.decimals()), uamount)) + } + + async fn get_delegation_reward_amount( + &self, + validator_addr: &AccountId, // keep this as `AccountId` to make it pre-validated + ) -> MmResult { + let delegator_address = self + .my_address() + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + let validator_address = validator_addr.to_string(); + + let query_payload = QueryDelegationRewardsRequest { + delegator_address, + validator_address, + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATION_REWARDS_PATH.to_owned()), + query_payload.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .map_err(|e| DelegationError::Transport(e.to_string())) + .await?; + + let decoded_response = QueryDelegationRewardsResponse::decode(raw_response.value.as_slice()) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + match decoded_response + .rewards + .iter() + .find(|t| t.denom == self.denom.to_string()) + { + Some(dec_coin) => extract_big_decimal_from_dec_coin(dec_coin, self.decimals as u32) + .map_to_mm(|e| DelegationError::InternalError(e.to_string())), + None => MmError::err(DelegationError::NothingToClaim { + coin: self.ticker.clone(), + }), + } + } + + pub(crate) async fn claim_staking_rewards( + &self, + req: ClaimRewardsPayload, + ) -> MmResult { + let (delegator_address, maybe_priv_key) = self + .extract_account_id_and_private_key(None) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let validator_address = + AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; + + let msg = MsgWithdrawDelegatorReward { + delegator_address: delegator_address.clone(), + validator_address: validator_address.clone(), + } + .to_any() + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let reward_amount = self.get_delegation_reward_amount(&validator_address).await?; + + if reward_amount.is_zero() { + return MmError::err(DelegationError::NothingToClaim { + coin: self.ticker.clone(), + }); + } + + let timeout_height = self + .current_block() + .compat() + .await + .map_to_mm(DelegationError::Transport)? + + TIMEOUT_HEIGHT_DELTA; + + // This uses more gas than the regular transactions + let gas_limit_default = (GAS_LIMIT_DEFAULT * 3) / 2; + let (_, gas_limit) = self.gas_info_for_withdraw(&req.fee, gas_limit_default); + + let fee_amount_u64 = self + .calculate_account_fee_amount_as_u64( + &delegator_address, + maybe_priv_key, + msg.clone(), + timeout_height, + &req.memo, + req.fee, + ) + .await?; + + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, self.decimals()); + + let my_balance = self.my_balance().compat().await?.spendable; + + if fee_amount_dec > my_balance { + return MmError::err(DelegationError::NotSufficientBalance { + coin: self.ticker.clone(), + available: my_balance, + required: fee_amount_dec, + }); + } + + if !req.force && fee_amount_dec > reward_amount { + return MmError::err(DelegationError::UnprofitableReward { + reward: reward_amount.clone(), + fee: fee_amount_dec.clone(), + }); + } + + let fee = Fee::from_amount_and_gas( + Coin { + denom: self.denom.clone(), + amount: fee_amount_u64.into(), + }, + gas_limit, + ); + + let account_info = self.account_info(&delegator_address).await?; + + let tx = self + .any_to_transaction_data(maybe_priv_key, msg, &account_info, fee, timeout_height, &req.memo) + .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; + + let internal_id = { + let hex_vec = tx.tx_hex().map_or_else(Vec::new, |h| h.to_vec()); + sha256(&hex_vec).to_vec().into() + }; + + Ok(TransactionDetails { + tx, + from: vec![validator_address.to_string()], + to: vec![delegator_address.to_string()], + my_balance_change: &reward_amount - &fee_amount_dec, + spent_by_me: fee_amount_dec.clone(), + total_amount: reward_amount.clone(), + received_by_me: reward_amount, + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: self.ticker.clone(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit, + })), + coin: self.ticker.to_string(), + internal_id, + kmd_rewards: None, + transaction_type: TransactionType::ClaimDelegationRewards, + memo: Some(req.memo), + }) + } + + pub(crate) async fn delegations_list( + &self, + paging: PagingOptions, + ) -> MmResult { + let request = QueryDelegatorDelegationsRequest { + delegator_addr: self.account_id.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATOR_DELEGATIONS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let decoded_proto = QueryDelegatorDelegationsResponse::decode(raw_response.value.as_slice())?; + + let mut delegations = Vec::new(); + let selfi = self.clone(); + for response in decoded_proto.delegation_responses { + let Some(delegation) = response.delegation else { + continue; + }; + let Some(balance) = response.balance else { continue }; + + let account_id = AccountId::from_str(&delegation.validator_address) + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + let reward_amount = match selfi.get_delegation_reward_amount(&account_id).await { + Ok(reward) => reward, + Err(e) => match e.get_inner() { + DelegationError::NothingToClaim { .. } => BigDecimal::zero(), + _ => return MmError::err(TendermintCoinRpcError::InvalidResponse(e.to_string())), + }, + }; + + let amount = balance + .amount + .parse::() + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + delegations.push(Delegation { + validator_address: delegation.validator_address, + delegated_amount: big_decimal_from_sat_unsigned(amount, selfi.decimals()), + reward_amount, + }); + } + + Ok(DelegationsQueryResponse { delegations }) + } + + pub(crate) async fn ongoing_undelegations_list( + &self, + paging: PagingOptions, + ) -> MmResult { + let request = QueryDelegatorUnbondingDelegationsRequest { + delegator_addr: self.account_id.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_DELEGATOR_UNDELEGATIONS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let decoded_proto = QueryDelegatorUnbondingDelegationsResponse::decode(raw_response.value.as_slice())?; + let ongoing_undelegations = decoded_proto + .unbonding_responses + .into_iter() + .map(|r| { + let entries = r + .entries + .into_iter() + .filter_map(|e| { + let balance: u64 = e.balance.parse().ok()?; + + Some(UndelegationEntry { + creation_height: e.creation_height, + completion_datetime: e.completion_time?.to_string(), + balance: big_decimal_from_sat_unsigned(balance, self.decimals()), + }) + }) + .collect(); + + Undelegation { + validator_address: r.validator_address, + entries, + } + }) + .collect(); + + Ok(UndelegationsQueryResponse { ongoing_undelegations }) + } +} + +fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { + if nodes.is_empty() { + return MmError::err(TendermintInitErrorKind::EmptyRpcUrls); + } + + let p2p_keypair = if nodes.iter().any(|n| n.komodo_proxy) { + let p2p_ctx = P2PContext::fetch_from_mm_arc(ctx); + Some(p2p_ctx.keypair().clone()) + } else { + None + }; + + let mut clients = Vec::new(); + let mut errors = Vec::new(); + + // check that all urls are valid + // keep all invalid urls in one vector to show all of them in error + for node in nodes.iter() { + let proxy_sign_keypair = if node.komodo_proxy { p2p_keypair.clone() } else { None }; + match HttpClient::new(node.url.as_str(), proxy_sign_keypair) { + Ok(client) => clients.push(client), + Err(e) => errors.push(format!("Url {} is invalid, got error {}", node.url, e)), + } + } + drop_mutability!(clients); + drop_mutability!(errors); + if !errors.is_empty() { + let errors: String = errors.into_iter().join(", "); + return MmError::err(TendermintInitErrorKind::RpcClientInitError(errors)); + } + Ok(clients) +} + +pub async fn get_ibc_chain_list() -> IBCChainRegistriesResult { + fn map_metadata_to_chain_registry_name(metadata: &FileMetadata) -> Result> { + let split_filename_by_dash: Vec<&str> = metadata.name.split('-').collect(); + let chain_registry_name = split_filename_by_dash + .first() + .or_mm_err(|| { + IBCChainsRequestError::InternalError(format!( + "Could not read chain registry name from '{}'", + metadata.name + )) + })? + .to_string(); + + Ok(chain_registry_name) + } + + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = git_controller + .client + .get_file_metadata_list( + CHAIN_REGISTRY_REPO_OWNER, + CHAIN_REGISTRY_REPO_NAME, + CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, + ) + .await + .map_err(|e| IBCChainsRequestError::Transport(format!("{:?}", e)))?; + + let chain_list: Result, MmError> = + metadata_list.iter().map(map_metadata_to_chain_registry_name).collect(); + + let mut distinct_chain_list = chain_list?; + distinct_chain_list.dedup(); + + Ok(IBCChainRegistriesResponse { + chain_registry_list: distinct_chain_list, + }) +} + +#[async_trait] +#[allow(unused_variables)] +impl MmCoin for TendermintCoin { + fn is_asset_chain(&self) -> bool { false } + + fn wallet_only(&self, ctx: &MmArc) -> bool { + let coin_conf = crate::coin_conf(ctx, self.ticker()); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } + let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); + + wallet_only_conf || self.is_keplr_from_ledger + } + + fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } + + fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { + let coin = self.clone(); + let fut = async move { + let to_address = + AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + + let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); + + let (account_id, maybe_priv_key) = coin + .extract_account_id_and_private_key(req.from) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let (balance_denom, balance_dec) = coin + .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) + .await?; + + let (amount_denom, amount_dec) = if req.max { + let amount_denom = balance_denom; + (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) + } else { + (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) + }; + + if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { + return MmError::err(WithdrawError::AmountTooLow { + amount: amount_dec, + threshold: coin.min_tx_amount(), + }); + } let received_by_me = if to_address == account_id { amount_dec @@ -2244,10 +3086,10 @@ impl MmCoin for TendermintCoin { let fee_amount_u64 = coin .calculate_account_fee_amount_as_u64( &account_id, - maybe_pk, + maybe_priv_key, msg_payload.clone(), timeout_height, - memo.clone(), + &memo, req.fee, ) .await?; @@ -2307,7 +3149,7 @@ impl MmCoin for TendermintCoin { let account_info = coin.account_info(&account_id).await?; let tx = coin - .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) + .any_to_transaction_data(maybe_priv_key, msg_payload, &account_info, fee, timeout_height, &memo) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { @@ -2335,7 +3177,7 @@ impl MmCoin for TendermintCoin { internal_id, kmd_rewards: None, transaction_type: if is_ibc_transfer { - TransactionType::TendermintIBCTransfer + TransactionType::TendermintIBCTransfer { token_id: None } } else { TransactionType::StandardTransfer }, @@ -2359,8 +3201,12 @@ impl MmCoin for TendermintCoin { fn get_tx_hex_by_hash(&self, tx_hash: Vec) -> RawTransactionFut { let coin = self.clone(); - let hash = hex::encode_upper(H256::from(tx_hash.as_slice())); let fut = async move { + let len = tx_hash.len(); + let hash: [u8; 32] = tx_hash.try_into().map_to_mm(|_| { + RawTransactionError::InvalidHashError(format!("Invalid hash length: expected 32, got {}", len)) + })?; + let hash = hex::encode_upper(H256::from(hash)); let tx_from_rpc = coin.request_tx(hash).await?; Ok(RawTransactionRes { tx_hex: tx_from_rpc.encode_to_vec().into(), @@ -2610,36 +3456,26 @@ impl MarketCoinOps for TendermintCoin { let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), htlc.amount(), args.secret_hash); - let events_string = format!("claim_htlc.id='{}'", htlc_id); - // TODO: Remove deprecated attribute when new version of tendermint-rs is released - #[allow(deprecated)] - let request = GetTxsEventRequest { - events: vec![events_string], - order_by: TendermintResultOrder::Ascending as i32, + let query = format!("claim_htlc.id='{}'", htlc_id); + let request = TxSearchRequest { + query, + order_by: TendermintResultOrder::Ascending.into(), page: 1, - limit: 1, - pagination: None, + per_page: 1, + prove: false, }; - let encoded_request = request.encode_to_vec(); loop { - let response = try_tx_s!( - try_tx_s!(self.rpc_client().await) - .abci_query( - Some(ABCI_GET_TXS_EVENT_PATH.to_string()), - encoded_request.as_slice(), - ABCI_REQUEST_HEIGHT, - ABCI_REQUEST_PROVE - ) - .await - ); - let response = try_tx_s!(GetTxsEventResponse::decode(response.value.as_slice())); - if let Some(tx) = response.txs.first() { + let response = try_tx_s!(try_tx_s!(self.rpc_client().await).perform(request.clone()).await); + + if let Some(raw_tx) = response.txs.first() { + let tx = try_tx_s!(cosmrs::Tx::from_bytes(&raw_tx.tx)); + return Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { data: TxRaw { - body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), - auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), - signatures: tx.signatures.clone(), + body_bytes: try_tx_s!(tx.body.into_bytes()), + auth_info_bytes: try_tx_s!(tx.auth_info.into_bytes()), + signatures: tx.signatures, }, })); } @@ -2678,6 +3514,9 @@ impl MarketCoinOps for TendermintCoin { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + #[inline] + fn should_burn_dex_fee(&self) -> bool { false } // TODO: fix back to true when negotiation version added + fn is_trezor(&self) -> bool { match &self.activation_policy { TendermintActivationPolicy::PrivateKey(pk) => pk.is_trezor(), @@ -2689,17 +3528,10 @@ impl MarketCoinOps for TendermintCoin { #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintCoin { - async fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { - self.send_taker_fee_for_denom( - fee_addr, - dex_fee.fee_amount().into(), - self.denom.clone(), - self.decimals, - uuid, - expire_at, - ) - .compat() - .await + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { + self.send_taker_fee_for_denom(&dex_fee, self.denom.clone(), self.decimals, uuid, expire_at) + .compat() + .await } async fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { @@ -2728,6 +3560,11 @@ impl SwapOps for TendermintCoin { .await } + // TODO: release this function once watchers are supported + // fn is_supported_by_watchers(&self) -> bool { + // !matches!(self.activation_policy, TendermintActivationPolicy::PublicKey(_)) + // } + async fn send_maker_spends_taker_payment( &self, maker_spends_payment_args: SpendPaymentArgs<'_>, @@ -2764,13 +3601,8 @@ impl SwapOps for TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee = try_tx_s!( - self.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.to_owned(), - None - ) - .await + self.calculate_fee(claim_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO, None) + .await ); let (_tx_id, tx_raw) = try_tx_s!( @@ -2778,7 +3610,7 @@ impl SwapOps for TendermintCoin { claim_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(timeout), ) .await @@ -2825,13 +3657,8 @@ impl SwapOps for TendermintCoin { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee = try_tx_s!( - self.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - None - ) - .await + self.calculate_fee(claim_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO, None) + .await ); let (tx_id, tx_raw) = try_tx_s!( @@ -2839,7 +3666,7 @@ impl SwapOps for TendermintCoin { claim_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(timeout), ) .await @@ -2866,8 +3693,7 @@ impl SwapOps for TendermintCoin { self.validate_fee_for_denom( validate_fee_args.fee_tx, validate_fee_args.expected_sender, - validate_fee_args.fee_addr, - &validate_fee_args.dex_fee.fee_amount().into(), + validate_fee_args.dex_fee, self.decimals, validate_fee_args.uuid, self.denom.to_string(), @@ -2920,7 +3746,7 @@ impl SwapOps for TendermintCoin { secret_hash: &[u8], spend_tx: &[u8], watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { let tx = try_s!(cosmrs::Tx::from_bytes(spend_tx)); let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); @@ -2930,21 +3756,7 @@ impl SwapOps for TendermintCoin { )); let htlc = try_s!(ClaimHtlcMsg::try_from(htlc_proto)); - Ok(try_s!(hex::decode(htlc.secret()))) - } - - fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - - // Todo - fn is_auto_refundable(&self) -> bool { false } - - // Todo - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) + Ok(try_s!(try_s!(hex::decode(htlc.secret())).as_slice().try_into())) } fn negotiate_swap_contract_addr( @@ -2957,17 +3769,20 @@ impl SwapOps for TendermintCoin { #[inline] fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { key_pair_from_secret( - self.activation_policy + &self + .activation_policy .activated_key_or_err() .expect("valid priv key") - .as_ref(), + .take(), ) .expect("valid priv key") } #[inline] - fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { - self.activation_policy.public_key().expect("valid pubkey").to_bytes() + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> [u8; 33] { + let mut res = [0u8; 33]; + res.copy_from_slice(&self.activation_policy.public_key().expect("valid pubkey").to_bytes()); + res } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { @@ -2975,124 +3790,10 @@ impl SwapOps for TendermintCoin { .or_mm_err(|| ValidateOtherPubKeyErr::InvalidPubKey(hex::encode(raw_pubkey)))?; Ok(()) } - - async fn maker_payment_instructions( - &self, - args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - async fn taker_payment_instructions( - &self, - args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } } #[async_trait] -impl TakerSwapMakerCoin for TendermintCoin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for TendermintCoin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl WatcherOps for TendermintCoin { - fn create_maker_payment_spend_preimage( - &self, - _maker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - unimplemented!(); - } - - fn create_taker_payment_refund_preimage( - &self, - _taker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_contract_address: &Option, - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - unimplemented!(); - } - - fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - async fn watcher_search_for_swap_tx_spend( - &self, - _input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String> { - unimplemented!(); - } - - async fn get_taker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _coin_amount: Option, - _other_coin_amount: Option, - _reward_amount: Option, - _wait_until: u64, - ) -> Result> { - unimplemented!() - } - - async fn get_maker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _reward_amount: Option, - _wait_until: u64, - ) -> Result, MmError> { - unimplemented!() - } -} +impl WatcherOps for TendermintCoin {} /// Processes the given `priv_key_build_policy` and returns corresponding `TendermintPrivKeyPolicy`. /// This function expects either [`PrivKeyBuildPolicy::IguanaPrivKey`] @@ -3105,7 +3806,7 @@ pub fn tendermint_priv_key_policy( ) -> MmResult { match priv_key_build_policy { PrivKeyBuildPolicy::IguanaPrivKey(iguana) => { - let mm2_internal_key_pair = key_pair_from_secret(iguana.as_ref()).mm_err(|e| TendermintInitError { + let mm2_internal_key_pair = key_pair_from_secret(&iguana.take()).mm_err(|e| TendermintInitError { ticker: ticker.to_string(), kind: TendermintInitErrorKind::Internal(e.to_string()), })?; @@ -3303,6 +4004,13 @@ pub async fn get_ibc_transfer_channels( }) } +fn extract_big_decimal_from_dec_coin(dec_coin: &DecCoin, decimals: u32) -> Result { + let raw = BigDecimal::from_str(&dec_coin.amount)?; + // `DecCoin` represents decimal numbers as integer-like strings where the last 18 digits are the decimal part. + let scale = BigDecimal::from(1_000_000_000_000_000_000u64) * BigDecimal::from(10u64.pow(decimals)); + Ok(raw / scale) +} + fn parse_expected_sequence_number(e: &str) -> MmResult { if let Some(sequence) = SEQUENCE_PARSER_REGEX.captures(e).and_then(|c| c.get(1)) { let account_sequence = @@ -3320,11 +4028,13 @@ fn parse_expected_sequence_number(e: &str) -> MmResult TendermintProtocolInfo { + TendermintProtocolInfo { + decimals: 6, + denom: String::from("ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C"), + account_prefix: String::from("nuc"), + chain_id: String::from("nucleus-testnet"), + gas_price: None, + chain_registry_name: None, + } + } + + fn get_tx_signer_pubkey_unprefixed(tx: &Tx, i: usize) -> Vec { + tx.auth_info.as_ref().unwrap().signer_infos[i] + .public_key + .as_ref() + .unwrap() + .value[2..] + .to_vec() + } + #[test] fn test_tx_hash_str_from_bytes() { let tx_hex = "0a97010a8f010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126f0a2d636f736d6f7331737661773061716334353834783832356a753775613033673578747877643061686c3836687a122d636f736d6f7331737661773061716334353834783832356a753775613033673578747877643061686c3836687a1a0f0a057561746f6d120631303030303018d998bf0512670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2102000eef4ab169e7b26a4a16c47420c4176ab702119ba57a8820fb3e53c8e7506212040a020801180312130a0d0a057561746f6d12043130303010a08d061a4093e5aec96f7d311d129f5ec8714b21ad06a75e483ba32afab86354400b2ac8350bfc98731bbb05934bf138282750d71aadbe08ceb6bb195f2b55e1bbfdddaaad"; @@ -3454,7 +4184,7 @@ pub mod tendermint_coin_tests { coin.calculate_fee( create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None, ) .await @@ -3465,7 +4195,7 @@ pub mod tendermint_coin_tests { create_htlc_tx.msg_payload.clone(), fee, timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(20), ); block_on(async { @@ -3494,21 +4224,16 @@ pub mod tendermint_coin_tests { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; let fee = block_on(async { - coin.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.to_owned(), - None, - ) - .await - .unwrap() + coin.calculate_fee(claim_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO, None) + .await + .unwrap() }); let send_tx_fut = coin.common_send_raw_tx_bytes( claim_htlc_tx.msg_payload, fee, timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO, Duration::from_secs(30), ); @@ -3548,30 +4273,21 @@ pub mod tendermint_coin_tests { )) .unwrap(); - let events = "claim_htlc.id='2B925FC83A106CC81590B3DB108AC2AE496FFA912F368FE5E29BC1ED2B754F2C'"; - // TODO: Remove deprecated attribute when new version of tendermint-rs is released - #[allow(deprecated)] - let request = GetTxsEventRequest { - events: vec![events.into()], - order_by: TendermintResultOrder::Ascending as i32, + let query = "claim_htlc.id='2B925FC83A106CC81590B3DB108AC2AE496FFA912F368FE5E29BC1ED2B754F2C'".to_owned(); + let request = TxSearchRequest { + query, + order_by: TendermintResultOrder::Ascending.into(), page: 1, - limit: 1, - pagination: None, + per_page: 1, + prove: false, }; - let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( - Some(ABCI_GET_TXS_EVENT_PATH.to_string()), - request.encode_to_vec(), - ABCI_REQUEST_HEIGHT, - ABCI_REQUEST_PROVE, - )) - .unwrap(); + let response = block_on(block_on(coin.rpc_client()).unwrap().perform(request)).unwrap(); println!("{:?}", response); - let response = GetTxsEventResponse::decode(response.value.as_slice()).unwrap(); - let tx = response.txs.first().unwrap(); + let tx = cosmrs::Tx::from_bytes(&response.txs.first().unwrap().tx).unwrap(); println!("{:?}", tx); - let first_msg = tx.body.as_ref().unwrap().messages.first().unwrap(); + let first_msg = tx.body.messages.first().unwrap(); println!("{:?}", first_msg); let claim_htlc = ClaimHtlcProto::decode(HtlcType::Iris, first_msg.value.as_slice()).unwrap(); @@ -3684,19 +4400,20 @@ pub mod tendermint_coin_tests { // CreateHtlc tx, validation should fail because first message of dex fee tx must be MsgSend // https://nyancat.iobscan.io/#/tx?txHash=2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727 - let create_htlc_tx_hash = "2DB382CE3D9953E4A94957B475B0E8A98F5B6DDB32D6BF0F6A765D949CF4A727"; - let create_htlc_tx_bytes = block_on(coin.request_tx(create_htlc_tx_hash.into())) - .unwrap() - .encode_to_vec(); + let create_htlc_tx_response = GetTxResponse::decode(hex::decode("0ac4030a96020a8e020a1b2f697269736d6f642e68746c632e4d736743726561746548544c4312ee010a2a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866321a40623736353830316334303930363762623837396565326563666665363138623931643734346663343030303030303030303030303030303030303030303030302a0d0a036e696d120631303030303032403063333463373165626132613531373338363939663966336436646166666231356265353736653865643534333230333438353739316235646133396431306440ea3c18afaba80212670a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a02080118a50312120a0c0a05756e79616e120332303010a08d061a4029dfbe5fc6ec9ed257e0f3a86542cb9da0d6047620274f22265c4fb8221ed45830236adef675f76962f74e4cfcc7a10e1390f4d2071bc7dd07838e300381952612882208ccaaa8021240324442333832434533443939353345344139343935374234373542304538413938463542364444423332443642463046364137363544393439434634413732372ac60130413631304131423246363937323639373336443646363432453638373436433633324534443733363734333732363536313734363534383534344334333132343230413430343634333339343433383433333033353336343233363339343233323436333433313331333734353332343134333433333533323337343133343339333933303435333734353434333234323336343533323432343634313334343333323334333533373335333034343339333333353434333833313332333434333330333832cc095b7b226576656e7473223a5b7b2274797065223a22636f696e5f7265636569766564222c2261747472696275746573223a5b7b226b6579223a227265636569766572222c2276616c7565223a2269616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130303030306e696d227d5d7d2c7b2274797065223a22636f696e5f7370656e74222c2261747472696275746573223a5b7b226b6579223a227370656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130303030306e696d227d5d7d2c7b2274797065223a226372656174655f68746c63222c2261747472696275746573223a5b7b226b6579223a226964222c2276616c7565223a2246433944384330353642363942324634313137453241434335323741343939304537454432423645324246413443323435373530443933354438313234433038227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a227265636569766572222c2276616c7565223a22696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632227d2c7b226b6579223a2272656365697665725f6f6e5f6f746865725f636861696e222c2276616c7565223a2262373635383031633430393036376262383739656532656366666536313862393164373434666334303030303030303030303030303030303030303030303030227d2c7b226b6579223a2273656e6465725f6f6e5f6f746865725f636861696e227d2c7b226b6579223a227472616e73666572222c2276616c7565223a2266616c7365227d5d7d2c7b2274797065223a226d657373616765222c2261747472696275746573223a5b7b226b6579223a22616374696f6e222c2276616c7565223a222f697269736d6f642e68746c632e4d736743726561746548544c43227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a226d6f64756c65222c2276616c7565223a2268746c63227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d5d7d2c7b2274797065223a227472616e73666572222c2261747472696275746573223a5b7b226b6579223a22726563697069656e74222c2276616c7565223a2269616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130303030306e696d227d5d7d5d7d5d3ac7061a5c0a0d636f696e5f726563656976656412360a087265636569766572122a69616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d12130a06616d6f756e7412093130303030306e696d1a580a0a636f696e5f7370656e7412350a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612130a06616d6f756e7412093130303030306e696d1acc020a0b6372656174655f68746c6312460a02696412404643394438433035364236394232463431313745324143433532374134393930453745443242364532424641344332343537353044393335443831323443303812340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612360a087265636569766572122a696161316530727838376d646a37397a656a65777563346a6737716c3975643232383667327573386632125b0a1772656365697665725f6f6e5f6f746865725f636861696e12406237363538303163343039303637626238373965653265636666653631386239316437343466633430303030303030303030303030303030303030303030303012170a1573656e6465725f6f6e5f6f746865725f636861696e12110a087472616e73666572120566616c73651aac010a076d65737361676512250a06616374696f6e121b2f697269736d6f642e68746c632e4d736743726561746548544c4312340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76120e0a066d6f64756c65120468746c6312340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a761a8e010a087472616e7366657212370a09726563697069656e74122a69616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d12340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612130a06616d6f756e7412093130303030306e696d48a08d06509bd3045ade030a152f636f736d6f732e74782e763162657461312e547812c4030a96020a8e020a1b2f697269736d6f642e68746c632e4d736743726561746548544c4312ee010a2a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866321a40623736353830316334303930363762623837396565326563666665363138623931643734346663343030303030303030303030303030303030303030303030302a0d0a036e696d120631303030303032403063333463373165626132613531373338363939663966336436646166666231356265353736653865643534333230333438353739316235646133396431306440ea3c18afaba80212670a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a02080118a50312120a0c0a05756e79616e120332303010a08d061a4029dfbe5fc6ec9ed257e0f3a86542cb9da0d6047620274f22265c4fb8221ed45830236adef675f76962f74e4cfcc7a10e1390f4d2071bc7dd07838e30038195266214323032322d30392d31355432333a30343a35355a6a410a027478123b0a076163635f736571122e696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a762f34323118016a6d0a02747812670a097369676e617475726512584b642b2b583862736e744a5834504f6f5a554c4c6e614457424859674a3038694a6c7850754349653146677749327265396e583361574c33546b7a387836454f45354430306763627839304867343477413447564a673d3d18016a5b0a0a636f696e5f7370656e7412370a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112140a06616d6f756e741208323030756e79616e18016a5f0a0d636f696e5f726563656976656412380a087265636569766572122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112140a06616d6f756e741208323030756e79616e18016a93010a087472616e7366657212390a09726563697069656e74122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112140a06616d6f756e741208323030756e79616e18016a410a076d65737361676512360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7618016a170a02747812110a036665651208323030756e79616e18016a320a076d65737361676512270a06616374696f6e121b2f697269736d6f642e68746c632e4d736743726561746548544c4318016a5c0a0a636f696e5f7370656e7412370a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112150a06616d6f756e7412093130303030306e696d18016a600a0d636f696e5f726563656976656412380a087265636569766572122a69616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d180112150a06616d6f756e7412093130303030306e696d18016a94010a087472616e7366657212390a09726563697069656e74122a69616131613778796e6a3463656674386b67646a72366b637130733037793363637961366d65707a646d180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112150a06616d6f756e7412093130303030306e696d18016a410a076d65737361676512360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7618016ad8020a0b6372656174655f68746c6312480a026964124046433944384330353642363942324634313137453241434335323741343939304537454432423645324246413443323435373530443933354438313234433038180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112380a087265636569766572122a696161316530727838376d646a37397a656a65777563346a6737716c39756432323836673275733866321801125d0a1772656365697665725f6f6e5f6f746865725f636861696e124062373635383031633430393036376262383739656532656366666536313862393164373434666334303030303030303030303030303030303030303030303030180112190a1573656e6465725f6f6e5f6f746865725f636861696e180112130a087472616e73666572120566616c736518016a530a076d65737361676512100a066d6f64756c65120468746c63180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a761801").unwrap().as_slice()).unwrap(); + let mock_tx = create_htlc_tx_response.tx.as_ref().unwrap().clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); let create_htlc_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw::decode(create_htlc_tx_bytes.as_slice()).unwrap(), + data: TxRaw::decode(create_htlc_tx_response.tx.as_ref().unwrap().encode_to_vec().as_slice()).unwrap(), }); let invalid_amount: MmNumber = 1.into(); let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &create_htlc_tx, expected_sender: &[], - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(invalid_amount.clone()), min_block_number: 0, uuid: &[1; 16], @@ -3713,22 +4430,31 @@ pub mod tendermint_coin_tests { error ), } + TendermintCoin::request_tx.clear_mock(); // just a random transfer tx not related to AtomicDEX, should fail on recipient address check // https://nyancat.iobscan.io/#/tx?txHash=65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF - let random_transfer_tx_hash = "65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF"; - let random_transfer_tx_bytes = block_on(coin.request_tx(random_transfer_tx_hash.into())) - .unwrap() - .encode_to_vec(); - + let random_transfer_tx_response = GetTxResponse::decode(hex::decode("0ac6020a95010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a791a120a05756e79616e1209313030303030303030120474657374126a0a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103327a4866304ead15d941dbbdf2d2563514fcc94d25e4af897a71681a02b637b212040a02080118880212150a0f0a05756e79616e120632303030303010c09a0c1a402d1c8c1e1a44bd56fe24947d6ed6cae27c6f8a46e3e9beaaad9798dc842ae4ea0c0a20f33144c8fad3490638455b65f63decdb74c347a7c97d0469f5de453fe312a41608febfba021240363538313538313445374437343833324438373935363134344331453834383031444339344645394135303944323037413041424333463137373735453544462a403041314530413143324636333646373336443646373332453632363136453642324537363331363236353734363133313245344437333637353336353645363432da055b7b226576656e7473223a5b7b2274797065223a22636f696e5f7265636569766564222c2261747472696275746573223a5b7b226b6579223a227265636569766572222c2276616c7565223a22696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a79227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030303030303030756e79616e227d5d7d2c7b2274797065223a22636f696e5f7370656e74222c2261747472696275746573223a5b7b226b6579223a227370656e646572222c2276616c7565223a22696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030303030303030756e79616e227d5d7d2c7b2274797065223a226d657373616765222c2261747472696275746573223a5b7b226b6579223a22616374696f6e222c2276616c7565223a222f636f736d6f732e62616e6b2e763162657461312e4d736753656e64227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538227d2c7b226b6579223a226d6f64756c65222c2276616c7565223a2262616e6b227d5d7d2c7b2274797065223a227472616e73666572222c2261747472696275746573223a5b7b226b6579223a22726563697069656e74222c2276616c7565223a22696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a79227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030303030303030756e79616e227d5d7d5d7d5d3ad1031a610a0d636f696e5f726563656976656412360a087265636569766572122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a7912180a06616d6f756e74120e313030303030303030756e79616e1a5d0a0a636f696e5f7370656e7412350a077370656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e777472753812180a06616d6f756e74120e313030303030303030756e79616e1a770a076d65737361676512260a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412340a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538120e0a066d6f64756c65120462616e6b1a93010a087472616e7366657212370a09726563697069656e74122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a7912340a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e777472753812180a06616d6f756e74120e313030303030303030756e79616e48c09a0c5092e5035ae0020a152f636f736d6f732e74782e763162657461312e547812c6020a95010a8c010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126c0a2a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a791a120a05756e79616e1209313030303030303030120474657374126a0a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103327a4866304ead15d941dbbdf2d2563514fcc94d25e4af897a71681a02b637b212040a02080118880212150a0f0a05756e79616e120632303030303010c09a0c1a402d1c8c1e1a44bd56fe24947d6ed6cae27c6f8a46e3e9beaaad9798dc842ae4ea0c0a20f33144c8fad3490638455b65f63decdb74c347a7c97d0469f5de453fe36214323032322d31302d30335430363a35313a31375a6a410a027478123b0a076163635f736571122e696161317039703230667468306c7665647634736d7733327339377079386e74657230716e77747275382f32363418016a6d0a02747812670a097369676e617475726512584c52794d486870457656622b4a4a52396274624b346e7876696b626a36623671725a655933495171354f6f4d4369447a4d5554492b744e4a426a68465732583250657a62644d4e4870386c3942476e31336b552f34773d3d18016a5e0a0a636f696e5f7370656e7412370a077370656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538180112170a06616d6f756e74120b323030303030756e79616e18016a620a0d636f696e5f726563656976656412380a087265636569766572122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112170a06616d6f756e74120b323030303030756e79616e18016a96010a087472616e7366657212390a09726563697069656e74122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112360a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e7774727538180112170a06616d6f756e74120b323030303030756e79616e18016a410a076d65737361676512360a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e777472753818016a1a0a02747812140a03666565120b323030303030756e79616e18016a330a076d65737361676512280a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6418016a610a0a636f696e5f7370656e7412370a077370656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e77747275381801121a0a06616d6f756e74120e313030303030303030756e79616e18016a650a0d636f696e5f726563656976656412380a087265636569766572122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a791801121a0a06616d6f756e74120e313030303030303030756e79616e18016a99010a087472616e7366657212390a09726563697069656e74122a696161316b36636d636b7875757732647a7a6b76747a7239776c7467356c3633747361746b6c71357a79180112360a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e77747275381801121a0a06616d6f756e74120e313030303030303030756e79616e18016a410a076d65737361676512360a0673656e646572122a696161317039703230667468306c7665647634736d7733327339377079386e74657230716e777472753818016a1b0a076d65737361676512100a066d6f64756c65120462616e6b1801").unwrap().as_slice()).unwrap(); + let mock_tx = random_transfer_tx_response.tx.as_ref().unwrap().clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); let random_transfer_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw::decode(random_transfer_tx_bytes.as_slice()).unwrap(), + data: TxRaw::decode( + random_transfer_tx_response + .tx + .as_ref() + .unwrap() + .encode_to_vec() + .as_slice(), + ) + .unwrap(), }); let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &random_transfer_tx, expected_sender: &[], - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(invalid_amount.clone()), min_block_number: 0, uuid: &[1; 16], @@ -3740,26 +4466,25 @@ pub mod tendermint_coin_tests { ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("sent to wrong address")), _ => panic!("Expected `WrongPaymentTx` wrong address, found {:?}", error), } + TendermintCoin::request_tx.clear_mock(); // dex fee tx sent during real swap // https://nyancat.iobscan.io/#/tx?txHash=8AA6B9591FE1EE93C8B89DE4F2C59B2F5D3473BD9FB5F3CFF6A5442BEDC881D7 - let dex_fee_hash = "8AA6B9591FE1EE93C8B89DE4F2C59B2F5D3473BD9FB5F3CFF6A5442BEDC881D7"; - let dex_fee_tx = block_on(coin.request_tx(dex_fee_hash.into())).unwrap(); + let dex_fee_tx_response = GetTxResponse::decode(hex::decode("0abc020a8e010a86010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412660a2a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0c0a05756e79616e120331303018a89bb00212670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103d4f75874e5f2a51d9d22f747ebd94da63207b08c7b023b09865051f074eb7ea412040a020801180612130a0d0a05756e79616e12043130303010a08d061a40784831c62a96658e9b0c484bbf684465788701c4fbd46c744f20f4ade3dbba1152f279c8afb118ae500ed9dc1260a8125a0f173c91ea408a3a3e0bd42b226ae012da1508c59ab0021240384141364239353931464531454539334338423839444534463243353942324635443334373342443946423546334346463641353434324245444338383144372a403041314530413143324636333646373336443646373332453632363136453642324537363331363236353734363133313245344437333637353336353645363432c8055b7b226576656e7473223a5b7b2274797065223a22636f696e5f7265636569766564222c2261747472696275746573223a5b7b226b6579223a227265636569766572222c2276616c7565223a22696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030756e79616e227d5d7d2c7b2274797065223a22636f696e5f7370656e74222c2261747472696275746573223a5b7b226b6579223a227370656e646572222c2276616c7565223a2269616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030756e79616e227d5d7d2c7b2274797065223a226d657373616765222c2261747472696275746573223a5b7b226b6579223a22616374696f6e222c2276616c7565223a222f636f736d6f732e62616e6b2e763162657461312e4d736753656e64227d2c7b226b6579223a2273656e646572222c2276616c7565223a2269616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038227d2c7b226b6579223a226d6f64756c65222c2276616c7565223a2262616e6b227d5d7d2c7b2274797065223a227472616e73666572222c2261747472696275746573223a5b7b226b6579223a22726563697069656e74222c2276616c7565223a22696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a227d2c7b226b6579223a2273656e646572222c2276616c7565223a2269616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a22313030756e79616e227d5d7d5d7d5d3abf031a5b0a0d636f696e5f726563656976656412360a087265636569766572122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a12120a06616d6f756e741208313030756e79616e1a570a0a636f696e5f7370656e7412350a077370656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b703812120a06616d6f756e741208313030756e79616e1a770a076d65737361676512260a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412340a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038120e0a066d6f64756c65120462616e6b1a8d010a087472616e7366657212370a09726563697069656e74122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a12340a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b703812120a06616d6f756e741208313030756e79616e48a08d0650acdf035ad6020a152f636f736d6f732e74782e763162657461312e547812bc020a8e010a86010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412660a2a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0c0a05756e79616e120331303018a89bb00212670a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103d4f75874e5f2a51d9d22f747ebd94da63207b08c7b023b09865051f074eb7ea412040a020801180612130a0d0a05756e79616e12043130303010a08d061a40784831c62a96658e9b0c484bbf684465788701c4fbd46c744f20f4ade3dbba1152f279c8afb118ae500ed9dc1260a8125a0f173c91ea408a3a3e0bd42b226ae06214323032322d30392d32335431313a31313a35395a6a3f0a02747812390a076163635f736571122c69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b70382f3618016a6d0a02747812670a097369676e6174757265125865456778786971575a5936624445684c763268455a5869484163543731477830547944307265506275684653386e6e4972374559726c414f32647753594b675357673858504a487151496f36506776554b794a7134413d3d18016a5c0a0a636f696e5f7370656e7412370a077370656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038180112150a06616d6f756e74120931303030756e79616e18016a600a0d636f696e5f726563656976656412380a087265636569766572122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112150a06616d6f756e74120931303030756e79616e18016a94010a087472616e7366657212390a09726563697069656e74122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112360a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038180112150a06616d6f756e74120931303030756e79616e18016a410a076d65737361676512360a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b703818016a180a02747812120a03666565120931303030756e79616e18016a330a076d65737361676512280a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6418016a5b0a0a636f696e5f7370656e7412370a077370656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038180112140a06616d6f756e741208313030756e79616e18016a5f0a0d636f696e5f726563656976656412380a087265636569766572122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a180112140a06616d6f756e741208313030756e79616e18016a93010a087472616e7366657212390a09726563697069656e74122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a180112360a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b7038180112140a06616d6f756e741208313030756e79616e18016a410a076d65737361676512360a0673656e646572122a69616131647863376c64676b336e666e356b373671706c75703967397868786e7966346d6570396b703818016a1b0a076d65737361676512100a066d6f64756c65120462616e6b1801").unwrap().as_slice()).unwrap(); + let mock_tx = dex_fee_tx_response.tx.as_ref().unwrap().clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); - let pubkey = dex_fee_tx.auth_info.as_ref().unwrap().signer_infos[0] - .public_key - .as_ref() - .unwrap() - .value[2..] - .to_vec(); + let pubkey = get_tx_signer_pubkey_unprefixed(dex_fee_tx_response.tx.as_ref().unwrap(), 0); let dex_fee_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw::decode(dex_fee_tx.encode_to_vec().as_slice()).unwrap(), + data: TxRaw::decode(dex_fee_tx_response.tx.as_ref().unwrap().encode_to_vec().as_slice()).unwrap(), }); let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &[], - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(invalid_amount), min_block_number: 0, uuid: &[1; 16], @@ -3777,7 +4502,6 @@ pub mod tendermint_coin_tests { let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(valid_amount.clone().into()), min_block_number: 0, uuid: &[1; 16], @@ -3794,7 +4518,6 @@ pub mod tendermint_coin_tests { let error = block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &pubkey, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(valid_amount.into()), min_block_number: 0, uuid: &[1; 16], @@ -3808,31 +4531,93 @@ pub mod tendermint_coin_tests { } // https://nyancat.iobscan.io/#/tx?txHash=5939A9D1AF57BB828714E0C4C4D7F2AEE349BB719B0A1F25F8FBCC3BB227C5F9 - let fee_with_memo_hash = "5939A9D1AF57BB828714E0C4C4D7F2AEE349BB719B0A1F25F8FBCC3BB227C5F9"; - let fee_with_memo_tx = block_on(coin.request_tx(fee_with_memo_hash.into())).unwrap(); - - let pubkey = fee_with_memo_tx.auth_info.as_ref().unwrap().signer_infos[0] - .public_key - .as_ref() - .unwrap() - .value[2..] - .to_vec(); + let fee_with_memo_tx_response = GetTxResponse::decode(hex::decode("0ae2020ab2010a84010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412640a2a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0a0a036e696d1203313030122463616536303131622d393831302d343731302d623738342d31653564643062336130643018dbe0bb0212690a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a02080118a50412140a0e0a05756e79616e1205353030303010a08d061a4078295295db2e305b7b53c6b7154f1d6b1c311fd10aaf56ad96840e59f403bae045f2ca5920e7bef679eacd200d6f30eca7d3571b93dcde38c8c130e1c1d9e4c712f41508f8dfbb021240353933394139443141463537424238323837313445304334433444374632414545333439424237313942304131463235463846424343334242323237433546392a403041314530413143324636333646373336443646373332453632363136453642324537363331363236353734363133313245344437333637353336353645363432c2055b7b226576656e7473223a5b7b2274797065223a22636f696e5f7265636569766564222c2261747472696275746573223a5b7b226b6579223a227265636569766572222c2276616c7565223a22696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130306e696d227d5d7d2c7b2274797065223a22636f696e5f7370656e74222c2261747472696275746573223a5b7b226b6579223a227370656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130306e696d227d5d7d2c7b2274797065223a226d657373616765222c2261747472696275746573223a5b7b226b6579223a22616374696f6e222c2276616c7565223a222f636f736d6f732e62616e6b2e763162657461312e4d736753656e64227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a226d6f64756c65222c2276616c7565223a2262616e6b227d5d7d2c7b2274797065223a227472616e73666572222c2261747472696275746573223a5b7b226b6579223a22726563697069656e74222c2276616c7565223a22696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a227d2c7b226b6579223a2273656e646572222c2276616c7565223a22696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76227d2c7b226b6579223a22616d6f756e74222c2276616c7565223a223130306e696d227d5d7d5d7d5d3ab9031a590a0d636f696e5f726563656976656412360a087265636569766572122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a12100a06616d6f756e7412063130306e696d1a550a0a636f696e5f7370656e7412350a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612100a06616d6f756e7412063130306e696d1a770a076d65737361676512260a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76120e0a066d6f64756c65120462616e6b1a8b010a087472616e7366657212370a09726563697069656e74122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a12340a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7612100a06616d6f756e7412063130306e696d48a08d0650d4e1035afc020a152f636f736d6f732e74782e763162657461312e547812e2020ab2010a84010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412640a2a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a1a0a0a036e696d1203313030122463616536303131622d393831302d343731302d623738342d31653564643062336130643018dbe0bb0212690a510a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a02080118a50412140a0e0a05756e79616e1205353030303010a08d061a4078295295db2e305b7b53c6b7154f1d6b1c311fd10aaf56ad96840e59f403bae045f2ca5920e7bef679eacd200d6f30eca7d3571b93dcde38c8c130e1c1d9e4c76214323032322d31302d30345431313a33343a35355a6a410a027478123b0a076163635f736571122e696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a762f35343918016a6d0a02747812670a097369676e6174757265125865436c536c6473754d4674375538613346553864617877784839454b723161746c6f514f57665144757542463873705a494f652b396e6e717a53414e627a447370394e5847355063336a6a497754446877646e6b78773d3d18016a5d0a0a636f696e5f7370656e7412370a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112160a06616d6f756e74120a3530303030756e79616e18016a610a0d636f696e5f726563656976656412380a087265636569766572122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112160a06616d6f756e74120a3530303030756e79616e18016a95010a087472616e7366657212390a09726563697069656e74122a696161313778706676616b6d32616d67393632796c73366638347a336b656c6c3863356c396d72336676180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112160a06616d6f756e74120a3530303030756e79616e18016a410a076d65737361676512360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7618016a190a02747812130a03666565120a3530303030756e79616e18016a330a076d65737361676512280a06616374696f6e121c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6418016a590a0a636f696e5f7370656e7412370a077370656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112120a06616d6f756e7412063130306e696d18016a5d0a0d636f696e5f726563656976656412380a087265636569766572122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a180112120a06616d6f756e7412063130306e696d18016a91010a087472616e7366657212390a09726563697069656e74122a696161316567307167617a37336a737676727676747a713478383233686d7a387161706c64643078347a180112360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a76180112120a06616d6f756e7412063130306e696d18016a410a076d65737361676512360a0673656e646572122a696161316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c6468306b6a7618016a1b0a076d65737361676512100a066d6f64756c65120462616e6b1801").unwrap().as_slice()).unwrap(); + let mock_tx = fee_with_memo_tx_response.tx.as_ref().unwrap().clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); + let pubkey = get_tx_signer_pubkey_unprefixed(fee_with_memo_tx_response.tx.as_ref().unwrap(), 0); let fee_with_memo_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw::decode(fee_with_memo_tx.encode_to_vec().as_slice()).unwrap(), + data: TxRaw::decode( + fee_with_memo_tx_response + .tx + .as_ref() + .unwrap() + .encode_to_vec() + .as_slice(), + ) + .unwrap(), }); let uuid: Uuid = "cae6011b-9810-4710-b784-1e5dd0b3a0d0".parse().unwrap(); - let amount: BigDecimal = "0.0001".parse().unwrap(); + let dex_fee = DexFee::Standard(MmNumber::from("0.0001")); + block_on( + coin.validate_fee_for_denom(&fee_with_memo_tx, &pubkey, &dex_fee, 6, uuid.as_bytes(), "nim".into()) + .compat(), + ) + .unwrap(); + TendermintCoin::request_tx.clear_mock(); + } + + #[test] + fn validate_taker_fee_with_burn_test() { + const NUCLEUS_TEST_SEED: &str = "nucleus test seed"; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(NUCLEUS_TEST_SEED).unwrap(); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + let nucleus_nodes = vec![RpcNode::for_test("http://localhost:26657")]; + let iris_ibc_nucleus_protocol = get_iris_ibc_nucleus_protocol(); + let iris_ibc_nucleus_denom = + String::from("ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C"); + let coin = block_on(TendermintCoin::init( + &ctx, + "NUCLEUS-TEST".to_string(), + conf, + iris_ibc_nucleus_protocol, + nucleus_nodes, + false, + activation_policy, + false, + )) + .unwrap(); + + // tx from docker test (no real swaps yet) + let fee_with_burn_tx = Tx::decode(hex::decode("0abd030a91030a212f636f736d6f732e62616e6b2e763162657461312e4d73674d756c746953656e6412eb020a770a2a6e7563316572666e6b6a736d616c6b7774766a3434716e6672326472667a6474346e396c65647736337912490a446962632f4637463238464633433039303234413032323545444242444232303745353837324432423445463246423837344645343742303545463943394137443231314312013912770a2a6e7563316567307167617a37336a737676727676747a713478383233686d7a387161706c656877326b3212490a446962632f4637463238464633433039303234413032323545444242444232303745353837324432423445463246423837344645343742303545463943394137443231314312013712770a2a6e756331797937346b393278707437367a616e6c3276363837636175393861666d70363071723564743712490a446962632f46374632384646334330393032344130323235454442424442323037453538373244324234454632464238373446453437423035454639433941374432313143120132122433656338646436352d313036342d346630362d626166332d66373265623563396230346418b50a12680a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21025a37975c079a7543603fcab24e2565a4adee3cf9af8934690e103282fa40251112040a020801180312140a0e0a05756e75636c1205333338383510c8d0071a40852793cb49aeaff1f895fa18a4fc0a63a5c54813fd57b3f5a2af9d0d849a04cb4abe81bc8feb4178603e1c9eed4e4464157f0bffb7cf51ef3beb80f48cd73b91").unwrap().as_slice()).unwrap(); + let mock_tx = fee_with_burn_tx.clone(); + TendermintCoin::request_tx.mock_safe(move |_, _| { + let mock_tx = mock_tx.clone(); + MockResult::Return(Box::pin(async move { Ok(mock_tx) })) + }); + + let pubkey = get_tx_signer_pubkey_unprefixed(&fee_with_burn_tx, 0); + let fee_with_burn_cosmos_tx = TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw::decode(fee_with_burn_tx.encode_to_vec().as_slice()).unwrap(), + }); + + let uuid: Uuid = "3ec8dd65-1064-4f06-baf3-f72eb5c9b04d".parse().unwrap(); + let dex_fee = DexFee::WithBurn { + fee_amount: MmNumber::from("0.000007"), // Amount is 0.008, both dex and burn fees rounded down + burn_amount: MmNumber::from("0.000002"), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; block_on( coin.validate_fee_for_denom( - &fee_with_memo_tx, + &fee_with_burn_cosmos_tx, &pubkey, - &DEX_FEE_ADDR_RAW_PUBKEY, - &amount, + &dex_fee, 6, uuid.as_bytes(), - "nim".into(), + iris_ibc_nucleus_denom, ) .compat(), ) @@ -4225,4 +5010,127 @@ pub mod tendermint_coin_tests { assert!(parse_expected_sequence_number("").is_err()); assert!(parse_expected_sequence_number("check_tx log: account sequence mismatch, expected").is_err()); } + + #[test] + fn test_extract_big_decimal_from_dec_coin() { + let dec_coin = DecCoin { + denom: "".into(), + amount: "232503485176823921544000".into(), + }; + + let expected = BigDecimal::from_str("0.232503485176823921544").unwrap(); + let actual = extract_big_decimal_from_dec_coin(&dec_coin, 6).unwrap(); + assert_eq!(expected, actual); + + let dec_coin = DecCoin { + denom: "".into(), + amount: "1000000000000000000000000".into(), + }; + + let expected = BigDecimal::from(1); + let actual = extract_big_decimal_from_dec_coin(&dec_coin, 6).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn test_claim_staking_rewards() { + let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; + let protocol_conf = get_iris_protocol(); + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + nodes, + false, + activation_policy, + false, + )) + .unwrap(); + + let validator_address = "iva1svannhv2zaxefq83m7treg078udfk37lpjufkw"; + let memo = "test".to_owned(); + let req = ClaimRewardsPayload { + validator_address: validator_address.to_owned(), + fee: None, + memo: memo.clone(), + force: false, + }; + let reward_amount = + block_on(coin.get_delegation_reward_amount(&AccountId::from_str(validator_address).unwrap())).unwrap(); + let res = block_on(coin.claim_staking_rewards(req)).unwrap(); + + assert_eq!(vec![validator_address], res.from); + assert_eq!(vec![coin.account_id.to_string()], res.to); + assert_eq!(TransactionType::ClaimDelegationRewards, res.transaction_type); + assert_eq!(Some(memo), res.memo); + // Rewards can increase during our tests, so round the first 4 digits. + assert_eq!(reward_amount.round(4), res.total_amount.round(4)); + assert_eq!(reward_amount.round(4), res.received_by_me.round(4)); + // tx fee must be taken into account + assert!(reward_amount > res.my_balance_change); + } + + #[test] + fn test_delegations_list() { + let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; + let protocol_conf = get_iris_protocol(); + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + nodes, + false, + activation_policy, + false, + )) + .unwrap(); + + let validator_address = "iva1svannhv2zaxefq83m7treg078udfk37lpjufkw"; + let reward_amount = + block_on(coin.get_delegation_reward_amount(&AccountId::from_str(validator_address).unwrap())).unwrap(); + + let expected_list = DelegationsQueryResponse { + delegations: vec![Delegation { + validator_address: validator_address.to_owned(), + delegated_amount: BigDecimal::from_str("1.98").unwrap(), + reward_amount: reward_amount.round(4), + }], + }; + + let mut actual_list = block_on(coin.delegations_list(PagingOptions { + limit: 0, + page_number: NonZeroUsize::new(1).unwrap(), + from_uuid: None, + })) + .unwrap(); + for delegation in &mut actual_list.delegations { + delegation.reward_amount = delegation.reward_amount.round(4); + } + + assert_eq!(expected_list, actual_list); + } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index e5cc90f895..589220b555 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -6,19 +6,15 @@ use super::{create_withdraw_msg_as_any, TendermintCoin, TendermintFeeDetails, GA use crate::coin_errors::ValidatePaymentResult; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, - CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, - FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, - NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, RawTransactionError, - RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionErr, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; -use crate::{DexFee, MmCoinEnum, PaymentInstructionArgs, ValidateWatcherSpendInput, WatcherReward, WatcherRewardError}; + CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, + HistorySyncState, MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, + RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, + SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, + TransactionEnum, TransactionErr, TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, + ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawError, + WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcrypto::sha256; use common::executor::abortable_queue::AbortableQueue; @@ -32,6 +28,7 @@ use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::MmNumber; +use primitives::hash::H256; use rpc::v1::types::Bytes as BytesJson; use serde_json::Value as Json; use std::ops::Deref; @@ -99,21 +96,19 @@ impl TendermintToken { }; Ok(TendermintToken(Arc::new(token_impl))) } + + fn token_id(&self) -> BytesJson { + let denom_hash = sha256(self.denom.as_ref().to_lowercase().as_bytes()); + H256::from(denom_hash.take()).to_vec().into() + } } #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintToken { - async fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { self.platform_coin - .send_taker_fee_for_denom( - fee_addr, - dex_fee.fee_amount().into(), - self.denom.clone(), - self.decimals, - uuid, - expire_at, - ) + .send_taker_fee_for_denom(&dex_fee, self.denom.clone(), self.decimals, uuid, expire_at) .compat() .await } @@ -181,8 +176,7 @@ impl SwapOps for TendermintToken { .validate_fee_for_denom( validate_fee_args.fee_tx, validate_fee_args.expected_sender, - validate_fee_args.fee_addr, - &validate_fee_args.dex_fee.fee_amount().into(), + validate_fee_args.dex_fee, self.decimals, validate_fee_args.uuid, self.denom.to_string(), @@ -238,26 +232,12 @@ impl SwapOps for TendermintToken { secret_hash: &[u8], spend_tx: &[u8], watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { self.platform_coin .extract_secret(secret_hash, spend_tx, watcher_reward) .await } - fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - - // Todo - fn is_auto_refundable(&self) -> bool { false } - - // Todo - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) - } - fn negotiate_swap_contract_addr( &self, other_side_address: Option<&[u8]>, @@ -271,131 +251,17 @@ impl SwapOps for TendermintToken { } #[inline] - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> [u8; 33] { self.platform_coin.derive_htlc_pubkey(swap_unique_data) } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { self.platform_coin.validate_other_pubkey(raw_pubkey) } - - async fn maker_payment_instructions( - &self, - args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - async fn taker_payment_instructions( - &self, - args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } } #[async_trait] -impl TakerSwapMakerCoin for TendermintToken { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for TendermintToken { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl WatcherOps for TendermintToken { - fn create_maker_payment_spend_preimage( - &self, - _maker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - unimplemented!(); - } - - fn create_taker_payment_refund_preimage( - &self, - _taker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_contract_address: &Option, - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - unimplemented!(); - } - - fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - async fn watcher_search_for_swap_tx_spend( - &self, - _input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String> { - unimplemented!(); - } - - async fn get_taker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _coin_amount: Option, - _other_coin_amount: Option, - _reward_amount: Option, - _wait_until: u64, - ) -> Result> { - unimplemented!() - } - - async fn get_maker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _reward_amount: Option, - _wait_until: u64, - ) -> Result, MmError> { - unimplemented!() - } -} +impl WatcherOps for TendermintToken {} #[async_trait] impl MarketCoinOps for TendermintToken { @@ -481,6 +347,9 @@ impl MarketCoinOps for TendermintToken { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + #[inline] + fn should_burn_dex_fee(&self) -> bool { false } // TODO: fix back to true when negotiation version added + fn is_trezor(&self) -> bool { self.platform_coin.is_trezor() } } @@ -500,7 +369,7 @@ impl MmCoin for TendermintToken { wallet_only_conf || self.platform_coin.is_keplr_from_ledger } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { let platform = self.platform_coin.clone(); @@ -511,7 +380,9 @@ impl MmCoin for TendermintToken { let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some(); - let (account_id, maybe_pk) = platform.account_id_and_pk_for_withdraw(req.from)?; + let (account_id, maybe_priv_key) = platform + .extract_account_id_and_private_key(req.from) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; let (base_denom_balance, base_denom_balance_dec) = platform .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) @@ -592,10 +463,10 @@ impl MmCoin for TendermintToken { let fee_amount_u64 = platform .calculate_account_fee_amount_as_u64( &account_id, - maybe_pk, + maybe_priv_key, msg_payload.clone(), timeout_height, - memo.clone(), + &memo, req.fee, ) .await?; @@ -620,7 +491,7 @@ impl MmCoin for TendermintToken { let account_info = platform.account_info(&account_id).await?; let tx = platform - .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) + .any_to_transaction_data(maybe_priv_key, msg_payload, &account_info, fee, timeout_height, &memo) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let internal_id = { @@ -648,9 +519,11 @@ impl MmCoin for TendermintToken { internal_id, kmd_rewards: None, transaction_type: if is_ibc_transfer { - TransactionType::TendermintIBCTransfer + TransactionType::TendermintIBCTransfer { + token_id: Some(token.token_id()), + } } else { - TransactionType::StandardTransfer + TransactionType::TokenTransfer(token.token_id()) }, memo: Some(memo), }) @@ -677,7 +550,7 @@ impl MmCoin for TendermintToken { Box::new(futures01::future::err(())) } - fn history_sync_status(&self) -> HistorySyncState { HistorySyncState::NotEnabled } + fn history_sync_status(&self) -> HistorySyncState { self.platform_coin.history_sync_status() } fn get_trade_fee(&self) -> Box + Send> { Box::new(futures01::future::err("Not implemented".into())) diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 3dc95e7443..252b9ad4cd 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -4,6 +4,7 @@ use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHisto use crate::tendermint::htlc::CustomTendermintMsgType; use crate::tendermint::TendermintFeeDetails; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::tx_history_events::TxHistoryEventStreamer; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionData, TransactionDetails, TransactionType, TxFeeDetails}; @@ -17,13 +18,14 @@ use cosmrs::tendermint::abci::{Code as TxCode, EventAttribute}; use cosmrs::tx::Fee; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; +use mm2_event_stream::StreamingManager; use mm2_number::BigDecimal; use mm2_state_machine::prelude::*; use mm2_state_machine::state_machine::StateMachineTrait; use primitives::hash::H256; use rpc::v1::types::Bytes as BytesJson; use std::cmp; -use std::convert::Infallible; +use std::convert::{Infallible, TryInto}; const TX_PAGE_SIZE: u8 = 50; @@ -38,6 +40,10 @@ const IBC_SEND_EVENT: &str = "ibc_transfer"; const IBC_RECEIVE_EVENT: &str = "fungible_token_packet"; const IBC_NFT_RECEIVE_EVENT: &str = "non_fungible_token_packet"; +const DELEGATE_EVENT: &str = "delegate"; +const UNDELEGATE_EVENT: &str = "unbond"; +const WITHDRAW_REWARDS_EVENT: &str = "withdraw_rewards"; + const ACCEPTED_EVENTS: &[&str] = &[ TRANSFER_EVENT, CREATE_HTLC_EVENT, @@ -45,6 +51,9 @@ const ACCEPTED_EVENTS: &[&str] = &[ IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT, + DELEGATE_EVENT, + UNDELEGATE_EVENT, + WITHDRAW_REWARDS_EVENT, ]; const RECEIVER_TAG_KEY: &str = "receiver"; @@ -56,6 +65,12 @@ const RECIPIENT_TAG_KEY_BASE64: &str = "cmVjaXBpZW50"; const SENDER_TAG_KEY: &str = "sender"; const SENDER_TAG_KEY_BASE64: &str = "c2VuZGVy"; +const DELEGATOR_TAG_KEY: &str = "delegator"; +const DELEGATOR_TAG_KEY_BASE64: &str = "ZGVsZWdhdG9y"; + +const VALIDATOR_TAG_KEY: &str = "validator"; +const VALIDATOR_TAG_KEY_BASE64: &str = "dmFsaWRhdG9y"; + const AMOUNT_TAG_KEY: &str = "amount"; const AMOUNT_TAG_KEY_BASE64: &str = "YW1vdW50"; @@ -124,22 +139,23 @@ impl CoinWithTxHistoryV2 for TendermintCoin { #[async_trait] impl CoinWithTxHistoryV2 for TendermintToken { - fn history_wallet_id(&self) -> WalletId { WalletId::new(self.platform_ticker().into()) } + fn history_wallet_id(&self) -> WalletId { WalletId::new(self.platform_ticker().to_owned()) } async fn get_tx_history_filters( &self, _target: MyTxHistoryTarget, ) -> MmResult { - let denom_hash = sha256(self.denom.to_string().as_bytes()); - let id = H256::from(denom_hash.as_slice()); + let denom_hash = sha256(self.denom.as_ref().to_lowercase().as_bytes()); + let token_id = H256::from(denom_hash.take()).to_string(); - Ok(GetTxHistoryFilters::for_address(self.platform_coin.account_id.to_string()).with_token_id(id.to_string())) + Ok(GetTxHistoryFilters::for_address(self.platform_coin.account_id.to_string()).with_token_id(token_id)) } } struct TendermintTxHistoryStateMachine { coin: Coin, storage: Storage, + streaming_manager: StreamingManager, balances: AllBalancesResult, last_received_page: u32, last_spent_page: u32, @@ -403,167 +419,298 @@ where ClaimHtlc, IBCSend, IBCReceive, + Delegate, + Undelegate, + ClaimRewards, } #[derive(Clone)] struct TransferDetails { + ticker: String, + denom: String, from: String, to: String, - denom: String, amount: u64, transfer_event_type: TransferEventType, } /// Reads sender and receiver addresses properly from an IBC event. fn read_real_ibc_addresses(transfer_details: &mut TransferDetails, msg_event: &Event) { - transfer_details.transfer_event_type = match msg_event.kind.as_str() { + let event_type = match msg_event.kind.as_str() { IBC_SEND_EVENT => TransferEventType::IBCSend, IBC_RECEIVE_EVENT | IBC_NFT_RECEIVE_EVENT => TransferEventType::IBCReceive, _ => unreachable!("`read_real_ibc_addresses` shouldn't be called for non-IBC events."), }; - transfer_details.from = some_or_return!(get_value_from_event_attributes( + let from = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64 )); - transfer_details.to = some_or_return!(get_value_from_event_attributes( + let to = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, RECEIVER_TAG_KEY, RECEIVER_TAG_KEY_BASE64, )); + + transfer_details.from = from; + transfer_details.to = to; + transfer_details.transfer_event_type = event_type; } /// Reads sender and receiver addresses properly from an HTLC event. - fn read_real_htlc_addresses(transfer_details: &mut TransferDetails, msg_event: &&Event) { + fn read_real_htlc_addresses(transfer_details: &mut TransferDetails, msg_event: &Event) { match msg_event.kind.as_str() { CREATE_HTLC_EVENT => { - transfer_details.from = some_or_return!(get_value_from_event_attributes( + let from = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64 )); - transfer_details.to = some_or_return!(get_value_from_event_attributes( + let to = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, RECEIVER_TAG_KEY, RECEIVER_TAG_KEY_BASE64, )); + transfer_details.from = from; + transfer_details.to = to; transfer_details.transfer_event_type = TransferEventType::CreateHtlc; }, CLAIM_HTLC_EVENT => { - transfer_details.from = some_or_return!(get_value_from_event_attributes( + let from = some_or_return!(get_value_from_event_attributes( &msg_event.attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64 )); + transfer_details.from = from; transfer_details.transfer_event_type = TransferEventType::ClaimHtlc; }, _ => unreachable!("`read_real_htlc_addresses` shouldn't be called for non-HTLC events."), } } - fn parse_transfer_values_from_events(tx_events: Vec<&Event>) -> Vec { + fn parse_transfer_values_from_events(coin: &Coin, mut tx_events: Vec<&Event>) -> Vec + where + Coin: CoinCapabilities, + { let mut transfer_details_list: Vec = vec![]; - for event in tx_events.iter() { - if event.kind.as_str() == TRANSFER_EVENT { - let amount_with_denoms = some_or_continue!(get_value_from_event_attributes( - &event.attributes, - AMOUNT_TAG_KEY, - AMOUNT_TAG_KEY_BASE64 - )); - - let amount_with_denoms = amount_with_denoms.split(','); + for i in 0..tx_events.len() { + // Avoid out-of-bounds exceptions after removing HTLC and IBC elements below. + if i >= tx_events.len() { + break; + } - for amount_with_denom in amount_with_denoms { - let extracted_amount: String = - amount_with_denom.chars().take_while(|c| c.is_numeric()).collect(); - let denom = &amount_with_denom[extracted_amount.len()..]; - let amount = some_or_continue!(extracted_amount.parse().ok()); + let event = tx_events[i]; - let from = some_or_continue!(get_value_from_event_attributes( - &event.attributes, - SENDER_TAG_KEY, - SENDER_TAG_KEY_BASE64 - )); + let amount_with_denoms = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + AMOUNT_TAG_KEY, + AMOUNT_TAG_KEY_BASE64 + )); - let to = some_or_continue!(get_value_from_event_attributes( - &event.attributes, - RECIPIENT_TAG_KEY, - RECIPIENT_TAG_KEY_BASE64, - )); + let amount_with_denoms = amount_with_denoms.split(','); + for amount_with_denom in amount_with_denoms { + let extracted_amount: String = amount_with_denom.chars().take_while(|c| c.is_numeric()).collect(); + let denom = amount_with_denom[extracted_amount.len()..].to_owned(); + let ticker = some_or_continue!(coin.denom_to_ticker(&denom)); + let amount = some_or_continue!(extracted_amount.parse().ok()); + + match event.kind.as_str() { + TRANSFER_EVENT => { + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); + + let to = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + RECIPIENT_TAG_KEY, + RECIPIENT_TAG_KEY_BASE64, + )); + + let mut tx_details = TransferDetails { + ticker, + denom, + from, + to, + amount, + // Default is Standard, can be changed later in read_real_htlc_addresses + transfer_event_type: TransferEventType::default(), + }; + + // For HTLC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. + // Use `read_real_htlc_addresses` to handle them properly. + if let Some(htlc_event_index) = tx_events + .iter() + .position(|e| [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&e.kind.as_str())) + { + read_real_htlc_addresses(&mut tx_details, tx_events[htlc_event_index]); + tx_events.remove(htlc_event_index); + } + // For IBC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. + // Use `read_real_ibc_addresses` to handle them properly. + else if let Some(ibc_event_index) = tx_events.iter().position(|e| { + [IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT].contains(&e.kind.as_str()) + }) { + read_real_ibc_addresses(&mut tx_details, tx_events[ibc_event_index]); + tx_events.remove(ibc_event_index); + } + + handle_new_transfer_event(&mut transfer_details_list, tx_details); + }, - let mut tx_details = TransferDetails { - from, - to, - denom: denom.to_owned(), - amount, - // Default is Standard, can be changed later in read_real_htlc_addresses - transfer_event_type: TransferEventType::default(), - }; + DELEGATE_EVENT => { + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + DELEGATOR_TAG_KEY, + DELEGATOR_TAG_KEY_BASE64, + )); + + let to = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + VALIDATOR_TAG_KEY, + VALIDATOR_TAG_KEY_BASE64, + )); + + let tx_details = TransferDetails { + ticker, + denom, + from, + to, + amount, + transfer_event_type: TransferEventType::Delegate, + }; + + handle_new_transfer_event(&mut transfer_details_list, tx_details); + }, - // For HTLC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. - // Use `read_real_htlc_addresses` to handle them properly. - if let Some(htlc_event) = tx_events - .iter() - .find(|e| [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&e.kind.as_str())) - { - read_real_htlc_addresses(&mut tx_details, htlc_event); - } - // For IBC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. - // Use `read_real_ibc_addresses` to handle them properly. - else if let Some(ibc_event) = tx_events.iter().find(|e| { - [IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT].contains(&e.kind.as_str()) - }) { - read_real_ibc_addresses(&mut tx_details, ibc_event); - } + UNDELEGATE_EVENT => { + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + DELEGATOR_TAG_KEY, + DELEGATOR_TAG_KEY_BASE64, + )); + + let tx_details = TransferDetails { + ticker, + denom, + from, + to: String::default(), + amount: 0, + transfer_event_type: TransferEventType::Undelegate, + }; + + handle_new_transfer_event(&mut transfer_details_list, tx_details); + }, - // sum the amounts coins and pairs are same - let mut duplicated_details = transfer_details_list.iter_mut().find(|details| { - details.from == tx_details.from - && details.to == tx_details.to - && details.denom == tx_details.denom - }); + WITHDRAW_REWARDS_EVENT => { + let to = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + DELEGATOR_TAG_KEY, + DELEGATOR_TAG_KEY_BASE64, + )); + + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + VALIDATOR_TAG_KEY, + VALIDATOR_TAG_KEY_BASE64, + )); + + let tx_details = TransferDetails { + ticker, + denom, + from, + to, + amount, + transfer_event_type: TransferEventType::ClaimRewards, + }; + + handle_new_transfer_event(&mut transfer_details_list, tx_details); + }, - if let Some(duplicated_details) = &mut duplicated_details { - duplicated_details.amount += tx_details.amount; - } else { - transfer_details_list.push(tx_details); - } - } + unrecognized => { + covered_warn!( + "Found an unrecognized event '{unrecognized}' in transaction history processing." + ); + }, + }; } } transfer_details_list } - fn get_transfer_details(tx_events: Vec, fee_amount_with_denom: String) -> Vec { + fn handle_new_transfer_event(transfer_details_list: &mut Vec, new_transfer: TransferDetails) { + let mut existing_transfer = transfer_details_list.iter_mut().find(|details| { + details.from == new_transfer.from + && details.to == new_transfer.to + && details.denom == new_transfer.denom + }); + + if let Some(existing_transfer) = &mut existing_transfer { + // Handle multi-amount transfer events + existing_transfer.amount += new_transfer.amount; + } else { + transfer_details_list.push(new_transfer); + } + } + + fn get_transfer_details( + coin: &Coin, + mut tx_events: Vec, + fee_amount_with_denom: String, + ) -> Vec + where + Coin: CoinCapabilities, + { + tx_events.sort_by(|a, b| a.kind.cmp(&b.kind)); + tx_events.dedup(); + + // We are only interested `DELEGATE_EVENT` events for delegation transactions. + if let Some(delegate_event) = tx_events.iter().find(|e| e.kind == DELEGATE_EVENT) { + return parse_transfer_values_from_events(coin, vec![delegate_event]); + }; + + // We are only interested `UNDELEGATE_EVENT` events for undelegation transactions. + if let Some(undelegate_event) = tx_events.iter().find(|e| e.kind == UNDELEGATE_EVENT) { + return parse_transfer_values_from_events(coin, vec![undelegate_event]); + }; + + // We are only interested `WITHDRAW_REWARDS_EVENT` events for withdraw reward transactions. + if let Some(withdraw_rewards_event) = tx_events.iter().find(|e| e.kind == WITHDRAW_REWARDS_EVENT) { + return parse_transfer_values_from_events(coin, vec![withdraw_rewards_event]); + }; + // Filter out irrelevant events let mut events: Vec<&Event> = tx_events .iter() .filter(|event| ACCEPTED_EVENTS.contains(&event.kind.as_str())) + .rev() .collect(); - events.reverse(); - if events.len() > DEFAULT_TRANSFER_EVENT_COUNT { - // Retain fee related events events.retain(|event| { + // Fees are included in `TRANSFER_EVENT` events, but since we handle fees + // separately, drop them from this list as we use them to extract the user + // amounts. if event.kind == TRANSFER_EVENT { let amount_with_denom = get_value_from_event_attributes(&event.attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64); - amount_with_denom != Some(fee_amount_with_denom.clone()) - } else { - true + + return amount_with_denom.as_deref() != Some(&fee_amount_with_denom); } + + true }); } - parse_transfer_values_from_events(events) + parse_transfer_values_from_events(coin, events) } fn get_transaction_type( @@ -584,8 +731,14 @@ where }, token_id, }, - (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), - _ => TransactionType::StandardTransfer, + (TransferEventType::IBCSend, token_id) | (TransferEventType::IBCReceive, token_id) => { + TransactionType::TendermintIBCTransfer { token_id } + }, + (TransferEventType::Delegate, _) => TransactionType::StakingDelegation, + (TransferEventType::Undelegate, _) => TransactionType::RemoveDelegation, + (TransferEventType::ClaimRewards, _) => TransactionType::ClaimDelegationRewards, + (TransferEventType::Standard, Some(token_id)) => TransactionType::TokenTransfer(token_id), + (TransferEventType::Standard, None) => TransactionType::StandardTransfer, } } @@ -604,9 +757,14 @@ where } }, TransferEventType::ClaimHtlc => Some((vec![my_address], vec![])), - TransferEventType::Standard | TransferEventType::IBCSend | TransferEventType::IBCReceive => { + TransferEventType::Standard + | TransferEventType::IBCSend + | TransferEventType::IBCReceive + | TransferEventType::Delegate + | TransferEventType::ClaimRewards => { Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()])) }, + TransferEventType::Undelegate => Some((vec![my_address], vec![])), } } @@ -614,6 +772,7 @@ where address: String, coin: &Coin, storage: &Storage, + streaming_manager: &StreamingManager, query: String, from_height: u64, page: &mut u32, @@ -683,7 +842,7 @@ where let fee_amount_with_denom = format!("{}{}", fee_data.amount, fee_data.denom); - let transfer_details_list = get_transfer_details(tx.tx_result.events, fee_amount_with_denom); + let transfer_details_list = get_transfer_details(coin, tx.tx_result.events, fee_amount_with_denom); if transfer_details_list.is_empty() { log::debug!( @@ -699,12 +858,50 @@ where ); let mut fee_added = false; + let wallet_id = coin.history_wallet_id(); for (index, transfer_details) in transfer_details_list.iter().enumerate() { let mut internal_id_hash = index.to_le_bytes().to_vec(); internal_id_hash.extend_from_slice(tx_hash.as_bytes()); - drop_mutability!(internal_id_hash); - let internal_id = H256::from(internal_id_hash.as_slice()).reversed().to_vec().into(); + let len = internal_id_hash.len(); + + // TODO: Remove this block at Q3 2025. + { + let old_internal_id_hash: [u8; 32] = internal_id_hash + .get(..32) + .and_then(|slice| slice.try_into().ok()) + .unwrap_or_default(); + + let old_internal_id: BytesJson = + H256::from(old_internal_id_hash).reversed().to_vec().into(); + let old_internal_id_for_fees: BytesJson = H256::from(old_internal_id_hash).to_vec().into(); + + for id in [old_internal_id, old_internal_id_for_fees] { + if let Ok(Some(_)) = storage.get_tx_from_history(&wallet_id, &id).await { + if let Err(e) = storage.remove_tx_from_history(&wallet_id, &id).await { + log::debug!("Failed to remove old transaction history record. {e:?}"); + }; + } + } + } + + let internal_id_hash: [u8; 33] = match internal_id_hash + .get(..33) + .and_then(|slice| slice.try_into().ok()) + { + Some(hash) => hash, + None => { + log::debug!( + "Invalid internal_id_hash length for tx '{}' at index {}: expected 32 bytes, got {} bytes.", + tx_hash, + index, + len + ); + continue; + }, + }; + + let internal_id = internal_id_hash.iter().rev().copied().collect::>().into(); if let Ok(Some(_)) = storage .get_tx_from_history(&coin.history_wallet_id(), &internal_id) @@ -715,7 +912,7 @@ where } let tx_sent_by_me = address == transfer_details.from; - let is_platform_coin_tx = transfer_details.denom == coin.platform_denom().to_string(); + let is_platform_coin_tx = transfer_details.ticker == *coin.platform_ticker(); let is_self_tx = transfer_details.to == transfer_details.from && tx_sent_by_me; let is_sign_claim_htlc = tx_sent_by_me && matches!(transfer_details.transfer_event_type, TransferEventType::ClaimHtlc); @@ -743,8 +940,8 @@ where let token_id: Option = match !is_platform_coin_tx { true => { - let denom_hash = sha256(transfer_details.denom.clone().as_bytes()); - Some(H256::from(denom_hash.as_slice()).to_vec().into()) + let denom_hash = sha256(transfer_details.denom.to_lowercase().as_bytes()); + Some(H256::from(denom_hash.take()).to_vec().into()) }, false => None, }; @@ -766,7 +963,7 @@ where tx: TransactionData::new_signed(msg.into(), tx_hash.to_string()), fee_details: Some(TxFeeDetails::Tendermint(fee_details.clone())), block_height: tx.height.into(), - coin: transfer_details.denom.clone(), + coin: transfer_details.ticker.clone(), internal_id, timestamp, kmd_rewards: None, @@ -784,9 +981,9 @@ where fee_tx_details.spent_by_me = fee_details.amount.clone(); fee_tx_details.received_by_me = BigDecimal::default(); fee_tx_details.my_balance_change = BigDecimal::default() - &fee_details.amount; - fee_tx_details.coin = coin.platform_ticker().to_string(); + fee_tx_details.coin = fee_details.coin.clone(); // Non-reversed version of original internal id - fee_tx_details.internal_id = H256::from(internal_id_hash.as_slice()).to_vec().into(); + fee_tx_details.internal_id = internal_id_hash.to_vec().into(); fee_tx_details.transaction_type = TransactionType::FeeForTokenTx; tx_details.push(fee_tx_details); @@ -797,6 +994,12 @@ where log::debug!("Tx '{}' successfully parsed.", tx.hash); } + streaming_manager + .send_fn(&TxHistoryEventStreamer::derive_streamer_id(coin.ticker()), || { + tx_details.clone() + }) + .ok(); + try_or_return_stopped_as_err!( storage .add_transactions_to_history(&coin.history_wallet_id(), tx_details) @@ -822,6 +1025,7 @@ where self.address.clone(), &ctx.coin, &ctx.storage, + &ctx.streaming_manager, q, self.from_block_height, &mut ctx.last_spent_page, @@ -844,6 +1048,7 @@ where self.address.clone(), &ctx.coin, &ctx.storage, + &ctx.streaming_manager, q, self.from_block_height, &mut ctx.last_received_page, @@ -957,7 +1162,7 @@ fn get_value_from_event_attributes(events: &[EventAttribute], tag: &str, base64_ pub async fn tendermint_history_loop( coin: TendermintCoin, storage: impl TxHistoryStorage, - _ctx: MmArc, + ctx: MmArc, _current_balance: Option, ) { let balances = match coin.get_all_balances().await { @@ -971,6 +1176,7 @@ pub async fn tendermint_history_loop( let mut state_machine = TendermintTxHistoryStateMachine { coin, storage, + streaming_manager: ctx.event_stream_manager.clone(), balances, last_received_page: 1, last_spent_page: 1, diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 43765ab0ba..ce2511b5b0 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -1,23 +1,23 @@ #![allow(clippy::all)] -use super::{CoinBalance, CommonSwapOpsV2, FundingTxSpend, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, - RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee, - TransactionEnum, TransactionFut, WaitForPaymentSpendError}; +use super::{CoinBalance, CommonSwapOpsV2, FindPaymentSpendError, FundingTxSpend, HistorySyncState, MarketCoinOps, + MmCoin, RawTransactionFut, RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, + SwapOps, TradeFee, TransactionEnum, TransactionFut}; use crate::coin_errors::ValidatePaymentResult; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, - ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, - GenTakerPaymentSpendArgs, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - ParseCoinAssocTypes, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - RawTransactionResult, RefundFundingSecretArgs, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, - SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, - SignatureResult, SpendPaymentArgs, TakerCoinSwapOpsV2, TakerSwapMakerCoin, TradePreimageFut, - TradePreimageResult, TradePreimageValue, Transaction, TransactionErr, TransactionResult, TxMarshalingErr, - TxPreimageWithSig, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, +use crate::hd_wallet::AddrToString; +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, + FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + MmCoinEnum, NegotiateSwapContractAddrErr, ParseCoinAssocTypes, PaymentInstructionArgs, + PaymentInstructions, PaymentInstructionsErr, RawTransactionResult, RefundFundingSecretArgs, + RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, + TakerCoinSwapOpsV2, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, + TransactionErr, TransactionResult, TxMarshalingErr, TxPreimageWithSig, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawFut, WithdrawRequest}; use crate::{DexFee, ToBytes, ValidateWatcherSpendInput}; use async_trait::async_trait; use common::executor::AbortedError; @@ -26,9 +26,11 @@ use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; +#[cfg(any(test, feature = "for-tests"))] use mocktopus::macros::*; use rpc::v1::types::Bytes as BytesJson; use serde_json::Value as Json; +use std::fmt::{Display, Formatter}; use std::ops::Deref; use std::sync::Arc; @@ -57,8 +59,7 @@ impl TestCoin { } #[async_trait] -#[mockable] -#[async_trait] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl MarketCoinOps for TestCoin { fn ticker(&self) -> &str { &self.ticker } @@ -108,13 +109,17 @@ impl MarketCoinOps for TestCoin { fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + fn is_kmd(&self) -> bool { &self.ticker == "KMD" } + + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { unimplemented!() } } #[async_trait] -#[mockable] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl SwapOps for TestCoin { - async fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { unimplemented!() } @@ -187,16 +192,12 @@ impl SwapOps for TestCoin { unimplemented!() } - fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - async fn extract_secret( &self, secret_hash: &[u8], spend_tx: &[u8], watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { unimplemented!() } @@ -213,7 +214,7 @@ impl SwapOps for TestCoin { fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } - fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { unimplemented!() } + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> [u8; 33] { unimplemented!() } async fn can_refund_htlc(&self, locktime: u64) -> Result { unimplemented!() } @@ -248,24 +249,18 @@ impl SwapOps for TestCoin { ) -> Result> { unimplemented!() } -} -#[async_trait] -impl TakerSwapMakerCoin for TestCoin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { unimplemented!() } - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { unimplemented!() } -#[async_trait] -impl MakerSwapTakerCoin for TestCoin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { unimplemented!() } - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { unimplemented!() } } #[async_trait] -#[mockable] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl WatcherOps for TestCoin { fn create_maker_payment_spend_preimage( &self, @@ -339,11 +334,11 @@ impl WatcherOps for TestCoin { } #[async_trait] -#[mockable] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl MmCoin for TestCoin { fn is_asset_chain(&self) -> bool { unimplemented!() } - fn spawner(&self) -> CoinFutSpawner { unimplemented!() } + fn spawner(&self) -> WeakSpawner { unimplemented!() } fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } @@ -441,9 +436,19 @@ impl ToBytes for TestSig { fn to_bytes(&self) -> Vec { vec![] } } +pub struct TestAddress {} + +impl AddrToString for TestAddress { + fn addr_to_string(&self) -> String { unimplemented!() } +} + +impl Display for TestAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { unimplemented!() } +} + #[async_trait] impl ParseCoinAssocTypes for TestCoin { - type Address = String; + type Address = TestAddress; type AddressParseError = String; type Pubkey = TestPubkey; type PubkeyParseError = String; @@ -468,7 +473,7 @@ impl ParseCoinAssocTypes for TestCoin { } #[async_trait] -#[mockable] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl TakerCoinSwapOpsV2 for TestCoin { async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { todo!() } @@ -549,7 +554,7 @@ impl TakerCoinSwapOpsV2 for TestCoin { async fn sign_and_broadcast_taker_payment_spend( &self, - preimage: &TxPreimageWithSig, + preimage: Option<&TxPreimageWithSig>, gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], @@ -557,12 +562,16 @@ impl TakerCoinSwapOpsV2 for TestCoin { unimplemented!() } - async fn wait_for_taker_payment_spend( + async fn find_taker_payment_spend_tx( &self, taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult { + ) -> MmResult { + unimplemented!() + } + + async fn extract_secret_v2(&self, secret_hash: &[u8], spend_tx: &Self::Tx) -> Result<[u8; 32], String> { unimplemented!() } } @@ -571,4 +580,7 @@ impl CommonSwapOpsV2 for TestCoin { fn derive_htlc_pubkey_v2(&self, _swap_unique_data: &[u8]) -> Self::Pubkey { todo!() } fn derive_htlc_pubkey_v2_bytes(&self, _swap_unique_data: &[u8]) -> Vec { todo!() } + + #[inline(always)] + fn taker_pubkey_bytes(&self) -> Option> { todo!() } } diff --git a/mm2src/coins/tx_history_storage/mod.rs b/mm2src/coins/tx_history_storage/mod.rs index 1f0ca4f8d8..d440c98098 100644 --- a/mm2src/coins/tx_history_storage/mod.rs +++ b/mm2src/coins/tx_history_storage/mod.rs @@ -23,16 +23,16 @@ mod tx_history_v2_tests; #[inline] pub fn token_id_from_tx_type(tx_type: &TransactionType) -> String { match tx_type { - TransactionType::TokenTransfer(token_id) => { + TransactionType::TokenTransfer(token_id) + | TransactionType::TendermintIBCTransfer { + token_id: Some(token_id), + } + | TransactionType::CustomTendermintMsg { + token_id: Some(token_id), + .. + } => { format!("{:02x}", token_id) }, - TransactionType::CustomTendermintMsg { token_id, .. } => { - if let Some(token_id) = token_id { - format!("{:02x}", token_id) - } else { - String::new() - } - }, _ => String::new(), } } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 6d98451c7f..35e7281d4e 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -32,6 +32,7 @@ pub mod rpc_clients; pub mod slp; pub mod spv; pub mod swap_proto_v2_scripts; +pub mod tx_history_events; pub mod utxo_balance_events; pub mod utxo_block_header_storage; pub mod utxo_builder; @@ -56,7 +57,7 @@ use common::{now_sec, now_sec_u32}; use crypto::{DerivationPath, HDPathToCoin, Secp256k1ExtendedPublicKey}; use derive_more::Display; #[cfg(not(target_arch = "wasm32"))] use dirs::home_dir; -use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender, UnboundedReceiver, UnboundedSender}; +use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures01::Future; @@ -102,14 +103,15 @@ use utxo_signer::{TxProvider, TxProviderError, UtxoSignTxError, UtxoSignTxResult use self::rpc_clients::{electrum_script_hash, ElectrumClient, ElectrumConnectionSettings, EstimateFeeMethod, EstimateFeeMode, NativeClient, UnspentInfo, UnspentMap, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; -use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinFutSpawner, - CoinsContext, DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, - MarketCoinOps, MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyPolicy, +use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinsContext, + DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, MarketCoinOps, + MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, Transaction, TransactionDetails, TransactionEnum, TransactionErr, - UnexpectedDerivationMethod, VerificationError, WithdrawError, WithdrawRequest}; + UnexpectedDerivationMethod, VerificationError, WeakSpawner, WithdrawError, WithdrawRequest}; use crate::coin_balance::{EnableCoinScanPolicy, EnabledCoinBalanceParams, HDAddressBalanceScanner}; -use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDPathAccountToAddressId, HDWalletCoinOps, HDWalletOps}; +use crate::hd_wallet::{AddrToString, HDAccountOps, HDAddressOps, HDPathAccountToAddressId, HDWalletCoinOps, + HDWalletOps}; use crate::utxo::tx_cache::UtxoVerboseCacheShared; use crate::{ParseCoinAssocTypes, ToBytes}; @@ -144,9 +146,6 @@ pub enum ScripthashNotification { SubscribeToAddresses(HashSet
), } -pub type ScripthashNotificationSender = Option>; -type ScripthashNotificationHandler = Option>>>; - #[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] fn get_special_folder_path() -> PathBuf { @@ -604,14 +603,13 @@ pub struct UtxoCoinFields { /// The watcher/receiver of the block headers synchronization status, /// initialized only for non-native mode if spv is enabled for the coin. pub block_headers_status_watcher: Option>>, + /// A weak reference to the MM context we are running on top of. + /// + /// This faciliates access to global MM state and fields (e.g. event streaming manager). + pub ctx: MmWeak, /// This abortable system is used to spawn coin's related futures that should be aborted on coin deactivation /// and on [`MmArc::stop`]. pub abortable_system: AbortableQueue, - pub(crate) ctx: MmWeak, - /// This is used for balance event streaming implementation for UTXOs. - /// If balance event streaming isn't enabled, this value will always be `None`; otherwise, - /// it will be used for receiving scripthash notifications to re-fetch balances. - scripthash_notification_handler: ScripthashNotificationHandler, } #[derive(Debug, Display)] @@ -814,6 +812,7 @@ impl UtxoCoinFields { posv: self.conf.is_posv, str_d_zeel, hash_algo: self.tx_hash_algo.into(), + v_extra_payload: None, } } } @@ -1028,6 +1027,10 @@ impl ToBytes for Signature { fn to_bytes(&self) -> Vec { self.to_vec() } } +impl AddrToString for Address { + fn addr_to_string(&self) -> String { self.to_string() } +} + #[async_trait] impl ParseCoinAssocTypes for T { type Address = Address; @@ -1158,7 +1161,7 @@ pub trait UtxoStandardOps { /// * `input_transactions` - the cache of the already requested transactions. async fn tx_details_by_hash( &self, - hash: &[u8], + hash: &H256Json, input_transactions: &mut HistoryUtxoTxMap, ) -> Result; @@ -1305,11 +1308,11 @@ impl VerboseTransactionFrom { } } -pub fn compressed_key_pair_from_bytes(raw: &[u8], prefix: u8, checksum_type: ChecksumType) -> Result { - if raw.len() != 32 { - return ERR!("Invalid raw priv key len {}", raw.len()); - } - +pub fn compressed_key_pair_from_bytes( + raw: &[u8; 32], + prefix: u8, + checksum_type: ChecksumType, +) -> Result { let private = Private { prefix, compressed: true, @@ -1319,9 +1322,12 @@ pub fn compressed_key_pair_from_bytes(raw: &[u8], prefix: u8, checksum_type: Che Ok(try_s!(KeyPair::from_private(private))) } -pub fn compressed_pub_key_from_priv_raw(raw_priv: &[u8], sum_type: ChecksumType) -> Result { +pub fn compressed_pub_key_from_priv_raw(raw_priv: &[u8; 32], sum_type: ChecksumType) -> Result { let key_pair: KeyPair = try_s!(compressed_key_pair_from_bytes(raw_priv, 0, sum_type)); - Ok(H264::from(&**key_pair.public())) + match key_pair.public() { + Public::Compressed(pub_key) => Ok(*pub_key), + _ => ERR!("Invalid public key type"), + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index d71b6538e3..e38a59ce27 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -15,16 +15,14 @@ use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetails UtxoTxHistoryOps}; use crate::{coin_balance, BlockHeightAndTime, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinBalanceMap, CoinProtocol, CoinWithDerivationMethod, CoinWithPrivKeyPolicy, ConfirmPaymentInput, DexFee, - GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, - RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, - TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MmCoinEnum, NegotiateSwapContractAddrErr, + PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, + SignatureResult, SpendPaymentArgs, SwapOps, TradePreimageValue, TransactionFut, TransactionResult, + TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, + WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; use common::log::warn; @@ -45,8 +43,8 @@ pub type BchUnspentMap = HashMap; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BchActivationRequest { #[serde(default)] - allow_slp_unsafe_conf: bool, - bchd_urls: Vec, + pub allow_slp_unsafe_conf: bool, + pub bchd_urls: Vec, #[serde(flatten)] pub utxo_params: UtxoActivationParams, } @@ -83,6 +81,10 @@ pub struct BchCoin { slp_tokens_infos: Arc>>, } +impl From for UtxoArc { + fn from(coin: BchCoin) -> Self { coin.utxo_arc } +} + #[allow(clippy::large_enum_variant)] pub enum IsSlpUtxoError { Rpc(UtxoRpcError), @@ -158,6 +160,15 @@ impl From for IsSlpUtxoError { } impl BchCoin { + pub fn new(utxo_arc: UtxoArc, slp_addr_prefix: CashAddrPrefix, bchd_urls: Vec) -> Self { + BchCoin { + utxo_arc, + slp_addr_prefix, + bchd_urls, + slp_tokens_infos: Arc::new(Mutex::new(HashMap::new())), + } + } + pub fn slp_prefix(&self) -> &CashAddrPrefix { &self.slp_addr_prefix } pub fn slp_address(&self, address: &Address) -> Result { @@ -627,15 +638,7 @@ pub async fn bch_coin_with_policy( } let bchd_urls = params.bchd_urls; - let slp_tokens_infos = Arc::new(Mutex::new(HashMap::new())); - let constructor = { - move |utxo_arc| BchCoin { - utxo_arc, - slp_addr_prefix: slp_addr_prefix.clone(), - bchd_urls: bchd_urls.clone(), - slp_tokens_infos: slp_tokens_infos.clone(), - } - }; + let constructor = { move |utxo_arc| BchCoin::new(utxo_arc, slp_addr_prefix.clone(), bchd_urls.clone()) }; let coin = try_s!( UtxoArcBuilder::new(ctx, ticker, conf, ¶ms.utxo_params, priv_key_policy, constructor) @@ -870,16 +873,8 @@ impl UtxoCommonOps for BchCoin { #[async_trait] impl SwapOps for BchCoin { #[inline] - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) - .compat() - .await + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + utxo_common::send_taker_fee(self.clone(), dex_fee).compat().await } #[inline] @@ -937,9 +932,8 @@ impl SwapOps for BchCoin { tx, utxo_common::DEFAULT_FEE_VOUT, validate_fee_args.expected_sender, - validate_fee_args.dex_fee, + validate_fee_args.dex_fee.clone(), validate_fee_args.min_block_number, - validate_fee_args.fee_addr, ) .compat() .await @@ -991,29 +985,16 @@ impl SwapOps for BchCoin { utxo_common::search_for_swap_tx_spend_other(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } - #[inline] - fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { - utxo_common::check_all_inputs_signed_by_pub(tx, expected_pub) - } - #[inline] async fn extract_secret( &self, secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } - fn is_auto_refundable(&self) -> bool { false } - - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) - } - #[inline] async fn can_refund_htlc(&self, locktime: u64) -> Result { utxo_common::can_refund_htlc(self, locktime) @@ -1033,7 +1014,7 @@ impl SwapOps for BchCoin { utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) } - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> [u8; 33] { utxo_common::derive_htlc_pubkey(self, swap_unique_data) } @@ -1042,53 +1023,9 @@ impl SwapOps for BchCoin { utxo_common::validate_other_pubkey(raw_pubkey) } - async fn maker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - async fn taker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - fn is_supported_by_watchers(&self) -> bool { true } } -#[async_trait] -impl TakerSwapMakerCoin for BchCoin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for BchCoin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - fn total_unspent_value<'a>(unspents: impl IntoIterator) -> u64 { unspents.into_iter().fold(0, |cur, unspent| cur + unspent.value) } @@ -1277,6 +1214,8 @@ impl MarketCoinOps for BchCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } @@ -1284,7 +1223,7 @@ impl MarketCoinOps for BchCoin { impl MmCoin for BchCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) @@ -1410,9 +1349,12 @@ impl CoinWithDerivationMethod for BchCoin { #[async_trait] impl IguanaBalanceOps for BchCoin { - type BalanceObject = CoinBalance; + type BalanceObject = CoinBalanceMap; - async fn iguana_balances(&self) -> BalanceResult { self.my_balance().compat().await } + async fn iguana_balances(&self) -> BalanceResult { + let balance = self.my_balance().compat().await?; + Ok(HashMap::from([(self.ticker().to_string(), balance)])) + } } #[async_trait] diff --git a/mm2src/coins/utxo/bchd_grpc.rs b/mm2src/coins/utxo/bchd_grpc.rs index da240508c6..c7c01d073b 100644 --- a/mm2src/coins/utxo/bchd_grpc.rs +++ b/mm2src/coins/utxo/bchd_grpc.rs @@ -10,6 +10,7 @@ use get_slp_trusted_validation_response::validity_result::ValidityResultType; use keys::hash::H256; use mm2_err_handle::prelude::*; use mm2_net::grpc_web::{post_grpc_web, PostGrpcWebErr}; +use std::convert::TryInto; #[derive(Debug, Display)] #[display(fmt = "Error {:?} on request to the url {}", err, to_url)] @@ -132,7 +133,20 @@ pub async fn validate_slp_utxos( let responses: Vec<(_, GetSlpTrustedValidationResponse)> = grpc_web_multi_url_request(&urls, &request).await?; for (url, response) in responses { for validation_result in response.results { - let actual_token_id = validation_result.token_id.as_slice().into(); + let actual_token_id = { + let token_id_len = validation_result.token_id.len(); + let arr: [u8; 32] = validation_result + .token_id + .try_into() + .map_to_mm(|_| ValidateSlpUtxosErr { + to_url: url.clone(), + kind: ValidateSlpUtxosErrKind::InvalidSlpTxData(format!( + "Invalid token_id length: expected 32 bytes, got {}", + token_id_len + )), + })?; + arr.into() + }; if actual_token_id != *token_id { return MmError::err(ValidateSlpUtxosErr { to_url: url.clone(), @@ -143,8 +157,23 @@ pub async fn validate_slp_utxos( }); } + let prev_out_hash = { + let prev_out_hash_len = validation_result.prev_out_hash.len(); + let arr: [u8; 32] = validation_result + .prev_out_hash + .try_into() + .map_to_mm(|_| ValidateSlpUtxosErr { + to_url: url.clone(), + kind: ValidateSlpUtxosErrKind::InvalidSlpTxData(format!( + "Invalid prev_out_hash length: expected 32 bytes, got {}", + prev_out_hash_len + )), + })?; + arr.into() + }; + let outpoint = OutPoint { - hash: validation_result.prev_out_hash.as_slice().into(), + hash: prev_out_hash, index: validation_result.prev_out_vout, }; diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index c5fbc67293..82da94209b 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -25,13 +25,11 @@ use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetails UtxoTxHistoryOps}; use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinBalanceMap, CoinWithDerivationMethod, CoinWithPrivKeyPolicy, ConfirmPaymentInput, DelegationError, DelegationFut, DexFee, - GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, StakingInfosFut, SwapOps, - TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MmCoinEnum, NegotiateSwapContractAddrErr, + PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, + SignatureResult, SpendPaymentArgs, StakingInfosFut, SwapOps, TradePreimageValue, TransactionFut, + TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, @@ -291,7 +289,7 @@ impl QtumBasedCoin for QtumCoin {} #[derive(Clone, Debug, Deserialize)] pub struct QtumDelegationRequest { - pub address: String, + pub validator_address: String, pub fee: Option, } @@ -488,7 +486,7 @@ impl UtxoCommonOps for QtumCoin { impl UtxoStandardOps for QtumCoin { async fn tx_details_by_hash( &self, - hash: &[u8], + hash: &H256Json, input_transactions: &mut HistoryUtxoTxMap, ) -> Result { utxo_common::tx_details_by_hash(self, hash, input_transactions).await @@ -510,16 +508,8 @@ impl UtxoStandardOps for QtumCoin { #[async_trait] impl SwapOps for QtumCoin { #[inline] - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) - .compat() - .await + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + utxo_common::send_taker_fee(self.clone(), dex_fee).compat().await } #[inline] @@ -577,9 +567,8 @@ impl SwapOps for QtumCoin { tx, utxo_common::DEFAULT_FEE_VOUT, validate_fee_args.expected_sender, - validate_fee_args.dex_fee, + validate_fee_args.dex_fee.clone(), validate_fee_args.min_block_number, - validate_fee_args.fee_addr, ) .compat() .await @@ -631,29 +620,16 @@ impl SwapOps for QtumCoin { utxo_common::search_for_swap_tx_spend_other(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } - #[inline] - fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { - utxo_common::check_all_inputs_signed_by_pub(tx, expected_pub) - } - #[inline] async fn extract_secret( &self, secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } - fn is_auto_refundable(&self) -> bool { false } - - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) - } - #[inline] async fn can_refund_htlc(&self, locktime: u64) -> Result { utxo_common::can_refund_htlc(self, locktime) @@ -673,7 +649,7 @@ impl SwapOps for QtumCoin { utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) } - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> [u8; 33] { utxo_common::derive_htlc_pubkey(self, swap_unique_data) } @@ -682,53 +658,9 @@ impl SwapOps for QtumCoin { utxo_common::validate_other_pubkey(raw_pubkey) } - async fn maker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - async fn taker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - fn is_supported_by_watchers(&self) -> bool { true } } -#[async_trait] -impl TakerSwapMakerCoin for QtumCoin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for QtumCoin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - #[async_trait] impl WatcherOps for QtumCoin { #[inline] @@ -898,13 +830,15 @@ impl MarketCoinOps for QtumCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } + + fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } } #[async_trait] impl MmCoin for QtumCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index f7573fafbf..b83289d97c 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -7,8 +7,8 @@ use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; use crate::utxo::{qtum, utxo_common, Address, GetUtxoListOps, UtxoCommonOps}; use crate::utxo::{PrivKeyPolicyNotAllowed, UTXO_LOCK}; -use crate::{DelegationError, DelegationFut, DelegationResult, MarketCoinOps, StakingInfos, StakingInfosError, - StakingInfosFut, StakingInfosResult, TransactionData, TransactionDetails, TransactionType}; +use crate::{DelegationError, DelegationFut, DelegationResult, MarketCoinOps, StakingInfoError, StakingInfos, + StakingInfosFut, TransactionData, TransactionDetails, TransactionType}; use bitcrypto::dhash256; use common::now_sec; use derive_more::Display; @@ -39,6 +39,7 @@ lazy_static! { } pub type QtumStakingAbiResult = Result>; +type StakingInfosResult = Result>; #[derive(Debug, Display)] pub enum QtumStakingAbiError { @@ -132,12 +133,12 @@ impl QtumCoin { .await } - async fn am_i_currently_staking(&self) -> Result, MmError> { + async fn am_i_currently_staking(&self) -> Result, MmError> { let utxo = self.as_ref(); let contract_address = contract_addr_into_rpc_format(&QTUM_DELEGATE_CONTRACT_ADDRESS); let client = match &utxo.rpc_client { UtxoRpcClientEnum::Native(_) => { - return MmError::err(StakingInfosError::Internal("Native not supported".to_string())) + return MmError::err(StakingInfoError::Internal("Native not supported".to_string())) }, UtxoRpcClientEnum::Electrum(electrum) => electrum, }; @@ -147,12 +148,12 @@ impl QtumCoin { .blockchain_contract_event_get_history(&address_rpc, &contract_address, QTUM_ADD_DELEGATION_TOPIC) .compat() .await - .map_to_mm(|e| StakingInfosError::Transport(e.to_string()))?; + .map_to_mm(|e| StakingInfoError::Transport(e.to_string()))?; let remove_delegation_history = client .blockchain_contract_event_get_history(&address_rpc, &contract_address, QTUM_REMOVE_DELEGATION_TOPIC) .compat() .await - .map_to_mm(|e| StakingInfosError::Transport(e.to_string()))?; + .map_to_mm(|e| StakingInfoError::Transport(e.to_string()))?; let am_i_staking = add_delegation_history.len() > remove_delegation_history.len(); if am_i_staking { let last_tx_add = some_or_return_ok_none!(add_delegation_history.last()); @@ -160,7 +161,7 @@ impl QtumCoin { .blockchain_transaction_get_receipt(&last_tx_add.tx_hash) .compat() .await - .map_to_mm(|e| StakingInfosError::Transport(e.to_string()))?; + .map_to_mm(|e| StakingInfoError::Transport(e.to_string()))?; // there is only 3 topics for an add_delegation // the first entry is the operation (add_delegation / remove_delegation), // the second entry is always the staker as hexadecimal 32 byte padded @@ -185,7 +186,7 @@ impl QtumCoin { .and_then(|log_entry| log_entry.topics.get(1)) .map(|padded_staker_address_hex| padded_staker_address_hex.trim_start_matches('0')) }) { - let hash = H160::from_str(raw).map_to_mm(|e| StakingInfosError::Internal(e.to_string()))?; + let hash = H160::from_str(raw).map_to_mm(|e| StakingInfoError::Internal(e.to_string()))?; let address = self.utxo_addr_from_contract_addr(hash); Ok(Some(address.to_string())) } else { @@ -234,8 +235,9 @@ impl QtumCoin { if let Some(staking_addr) = self.am_i_currently_staking().await? { return MmError::err(DelegationError::AlreadyDelegating(staking_addr)); } - let to_addr = Address::from_legacyaddress(request.address.as_str(), &self.as_ref().conf.address_prefixes) - .map_to_mm(DelegationError::AddressError)?; + let to_addr = + Address::from_legacyaddress(request.validator_address.as_str(), &self.as_ref().conf.address_prefixes) + .map_to_mm(DelegationError::AddressError)?; let fee = request.fee.unwrap_or(QTUM_DELEGATION_STANDARD_FEE); let _utxo_lock = UTXO_LOCK.lock(); let staker_address_hex = qtum::contract_addr_from_utxo_addr(to_addr.clone())?; diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs index ce0498cc31..e7c5311db2 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs @@ -5,7 +5,7 @@ use super::connection_manager::ConnectionManager; use super::constants::{BLOCKCHAIN_HEADERS_SUB_ID, BLOCKCHAIN_SCRIPTHASH_SUB_ID, ELECTRUM_REQUEST_TIMEOUT, NO_FORCE_CONNECT_METHODS, SEND_TO_ALL_METHODS}; use super::electrum_script_hash; -use super::event_handlers::{ElectrumConnectionManagerNotifier, ElectrumScriptHashNotificationBridge}; +use super::event_handlers::ElectrumConnectionManagerNotifier; use super::rpc_responses::*; use crate::utxo::rpc_clients::ConcurrentRequestMap; @@ -43,14 +43,15 @@ use std::ops::Deref; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::Arc; +use crate::utxo::utxo_balance_events::UtxoBalanceEventStreamer; use async_trait::async_trait; -use futures::channel::mpsc::UnboundedSender; use futures::compat::Future01CompatExt; use futures::future::{join_all, FutureExt, TryFutureExt}; use futures::stream::FuturesUnordered; use futures::StreamExt; use futures01::Future; use itertools::Itertools; +use mm2_event_stream::{StreamingManager, StreamingManagerError}; use serde_json::{self as json, Value as Json}; type ElectrumTxHistory = Vec; @@ -85,7 +86,8 @@ pub struct ElectrumClientImpl { /// in an `Arc` since they are shared outside `ElectrumClientImpl`. They are handed to each active /// `ElectrumConnection` to notify them about the events. event_handlers: Arc>>, - pub scripthash_notification_sender: Option>, + /// A streaming manager instance used to notify for Utxo balance events streamer. + streaming_manager: StreamingManager, abortable_system: AbortableQueue, } @@ -98,18 +100,10 @@ impl ElectrumClientImpl { fn try_new( client_settings: ElectrumClientSettings, block_headers_storage: BlockHeaderStorage, + streaming_manager: StreamingManager, abortable_system: AbortableQueue, mut event_handlers: Vec>, - scripthash_notification_sender: Option>, ) -> Result { - // This is used for balance event streaming implementation for UTXOs. - // Will be used for sending scripthash messages to trigger re-connections, re-fetching the balances, etc. - if let Some(scripthash_notification_sender) = scripthash_notification_sender.clone() { - event_handlers.push(Box::new(ElectrumScriptHashNotificationBridge { - scripthash_notification_sender, - })); - } - let connection_manager = ConnectionManager::try_new( client_settings.servers, client_settings.spawn_ping, @@ -132,7 +126,7 @@ impl ElectrumClientImpl { list_unspent_concurrent_map: ConcurrentRequestMap::new(), block_headers_storage, abortable_system, - scripthash_notification_sender, + streaming_manager, event_handlers: Arc::new(event_handlers), }) } @@ -142,16 +136,16 @@ impl ElectrumClientImpl { pub fn try_new_arc( client_settings: ElectrumClientSettings, block_headers_storage: BlockHeaderStorage, + streaming_manager: StreamingManager, abortable_system: AbortableQueue, event_handlers: Vec>, - scripthash_notification_sender: Option>, ) -> Result, String> { let client_impl = Arc::new(ElectrumClientImpl::try_new( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, )?); // Initialize the connection manager. client_impl @@ -185,13 +179,28 @@ impl ElectrumClientImpl { /// Sends a list of addresses through the scripthash notification sender to subscribe to their scripthash notifications. pub fn subscribe_addresses(&self, addresses: HashSet
) -> Result<(), String> { - if let Some(sender) = &self.scripthash_notification_sender { - sender - .unbounded_send(ScripthashNotification::SubscribeToAddresses(addresses)) - .map_err(|e| ERRL!("Failed sending scripthash message. {}", e))?; + match self.streaming_manager.send( + &UtxoBalanceEventStreamer::derive_streamer_id(&self.coin_ticker), + ScripthashNotification::SubscribeToAddresses(addresses), + ) { + // Don't error if the streamer isn't found/enabled. + Err(StreamingManagerError::StreamerNotFound) | Ok(()) => Ok(()), + Err(e) => Err(format!("Failed sending scripthash message. {:?}", e)), } + } - Ok(()) + /// Notifies the Utxo balance streamer of a new script hash balance change. + /// + /// The streamer will figure out which address this scripthash belongs to and will broadcast an notification to clients. + pub fn notify_triggered_hash(&self, script_hash: String) -> Result<(), String> { + match self.streaming_manager.send( + &UtxoBalanceEventStreamer::derive_streamer_id(&self.coin_ticker), + ScripthashNotification::Triggered(script_hash), + ) { + // Don't error if the streamer isn't found/enabled. + Err(StreamingManagerError::StreamerNotFound) | Ok(()) => Ok(()), + Err(e) => Err(format!("Failed sending scripthash message. {:?}", e)), + } } /// Get block headers storage. @@ -203,9 +212,9 @@ impl ElectrumClientImpl { pub fn with_protocol_version( client_settings: ElectrumClientSettings, block_headers_storage: BlockHeaderStorage, + streaming_manager: StreamingManager, abortable_system: AbortableQueue, event_handlers: Vec>, - scripthash_notification_sender: Option>, protocol_version: OrdRange, ) -> Result, String> { let client_impl = Arc::new(ElectrumClientImpl { @@ -213,9 +222,9 @@ impl ElectrumClientImpl { ..ElectrumClientImpl::try_new( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, )? }); // Initialize the connection manager. @@ -272,15 +281,15 @@ impl ElectrumClient { client_settings: ElectrumClientSettings, event_handlers: Vec>, block_headers_storage: BlockHeaderStorage, + streaming_manager: StreamingManager, abortable_system: AbortableQueue, - scripthash_notification_sender: Option>, ) -> Result { let client = ElectrumClient(ElectrumClientImpl::try_new_arc( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, )?); Ok(client) diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs index ca7a4bac60..3b234dc51f 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs @@ -6,18 +6,19 @@ use crate::{RpcTransportEventHandler, SharableRpcTransportEventHandler}; use common::custom_futures::timeout::FutureTimerExt; use common::executor::{abortable_queue::AbortableQueue, abortable_queue::WeakSpawner, AbortableSystem, SpawnFuture, Timer}; -use common::expirable_map::ExpirableMap; use common::jsonrpc_client::{JsonRpcBatchResponse, JsonRpcErrorType, JsonRpcId, JsonRpcRequest, JsonRpcResponse, JsonRpcResponseEnum}; use common::log::{error, info}; use common::{now_float, now_ms}; use mm2_rpc::data::legacy::ElectrumProtocol; +use timed_map::{MapKind, TimedMap}; use std::io; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::{Arc, Mutex}; use std::time::Duration; +use compatible_time::Instant; use futures::channel::oneshot as async_oneshot; use futures::compat::{Future01CompatExt, Stream01CompatExt}; use futures::future::FutureExt; @@ -27,7 +28,6 @@ use futures::stream::StreamExt; use futures01::sync::mpsc; use futures01::{Sink, Stream}; use http::Uri; -use instant::Instant; use serde::Serialize; cfg_native! { @@ -48,7 +48,7 @@ cfg_wasm32! { use std::sync::atomic::AtomicUsize; } -pub type JsonRpcPendingRequests = ExpirableMap>; +pub type JsonRpcPendingRequests = TimedMap>; macro_rules! disconnect_and_return { ($typ:tt, $err:expr, $conn:expr, $handlers:expr) => {{ @@ -177,7 +177,7 @@ impl ElectrumConnection { settings, tx: Mutex::new(None), establishing_connection: AsyncMutex::new(()), - responses: Mutex::new(JsonRpcPendingRequests::new()), + responses: Mutex::new(JsonRpcPendingRequests::new_with_map_kind(MapKind::BTreeMap).expiration_tick_cap(50)), protocol_version: Mutex::new(None), last_error: Mutex::new(None), abortable_system, @@ -251,7 +251,7 @@ impl ElectrumConnection { self.responses .lock() .unwrap() - .insert(rpc_id, req_tx, Duration::from_secs_f64(timeout)); + .insert_expirable(rpc_id, req_tx, Duration::from_secs_f64(timeout)); let tx = self .tx .lock() @@ -275,9 +275,9 @@ impl ElectrumConnection { } /// Process an incoming JSONRPC response from the electrum server. - fn process_electrum_response(&self, bytes: &[u8], event_handlers: &Vec>) { + fn process_electrum_response(&self, bytes: &[u8], client: &ElectrumClient) { // Inform the event handlers. - event_handlers.on_incoming_response(bytes); + client.event_handlers().on_incoming_response(bytes); // detect if we got standard JSONRPC response or subscription response as JSONRPC request #[derive(Deserialize)] @@ -308,8 +308,14 @@ impl ElectrumConnection { ElectrumRpcResponseEnum::BatchResponses(batch) => JsonRpcResponseEnum::Batch(batch), ElectrumRpcResponseEnum::SubscriptionNotification(req) => { match req.method.as_str() { - // NOTE: Sending a script hash notification is handled in it's own event handler. - BLOCKCHAIN_SCRIPTHASH_SUB_ID | BLOCKCHAIN_HEADERS_SUB_ID => {}, + BLOCKCHAIN_SCRIPTHASH_SUB_ID => { + if let Some(scripthash) = req.params.first().and_then(|s| s.as_str()) { + client.notify_triggered_hash(scripthash.to_string()).ok(); + } else { + error!("Notification must contain the script hash value, got: {req:?}"); + } + }, + BLOCKCHAIN_HEADERS_SUB_ID => {}, _ => { error!("Unexpected notification method: {}", req.method); }, @@ -329,18 +335,14 @@ impl ElectrumConnection { /// Process a bulk response from the electrum server. /// /// A bulk response is a response that contains multiple JSONRPC responses. - fn process_electrum_bulk_response( - &self, - bulk_response: &[u8], - event_handlers: &Vec>, - ) { + fn process_electrum_bulk_response(&self, bulk_response: &[u8], client: &ElectrumClient) { // We should split the received response because we can get several responses in bulk. let responses = bulk_response.split(|item| *item == b'\n'); for response in responses { // `split` returns empty slice if it ends with separator which is our case. if !response.is_empty() { - self.process_electrum_response(response, event_handlers) + self.process_electrum_response(response, client) } } } @@ -540,7 +542,7 @@ impl ElectrumConnection { #[cfg(not(target_arch = "wasm32"))] async fn recv_loop( connection: Arc, - event_handlers: Arc>>, + client: ElectrumClient, read: ReadHalf, last_response: Arc, ) -> ElectrumConnectionErr { @@ -559,7 +561,7 @@ impl ElectrumConnection { }; last_response.store(now_ms(), AtomicOrdering::Relaxed); - connection.process_electrum_bulk_response(buffer.as_bytes(), &event_handlers); + connection.process_electrum_bulk_response(buffer.as_bytes(), &client); buffer.clear(); } } @@ -567,7 +569,7 @@ impl ElectrumConnection { #[cfg(target_arch = "wasm32")] async fn recv_loop( connection: Arc, - event_handlers: Arc>>, + client: ElectrumClient, mut read: WsIncomingReceiver, last_response: Arc, ) -> ElectrumConnectionErr { @@ -576,7 +578,7 @@ impl ElectrumConnection { match response { Ok(bytes) => { last_response.store(now_ms(), AtomicOrdering::Relaxed); - connection.process_electrum_response(&bytes, &event_handlers); + connection.process_electrum_response(&bytes, &client); }, Err(e) => { error!("{address} error: {e:?}"); @@ -678,7 +680,7 @@ impl ElectrumConnection { let (read, write) = tokio::io::split(stream); #[cfg(target_arch = "wasm32")] let (read, write) = stream; - let recv_branch = Self::recv_loop(connection.clone(), event_handlers.clone(), read, last_response).boxed(); + let recv_branch = Self::recv_loop(connection.clone(), client.clone(), read, last_response).boxed(); // Branch 3: Send outgoing requests to the server. let (tx, rx) = mpsc::channel(0); diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection_manager/manager.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection_manager/manager.rs index b06628fd60..a3b792e9f6 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection_manager/manager.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection_manager/manager.rs @@ -9,7 +9,7 @@ use super::connection_context::ConnectionContext; use crate::utxo::rpc_clients::UtxoRpcClientOps; use common::executor::abortable_queue::AbortableQueue; use common::executor::{AbortableSystem, SpawnFuture, Timer}; -use common::log::{debug, error}; +use common::log::{debug, error, LogOnError}; use common::notifier::{Notifiee, Notifier}; use common::now_ms; use keys::Address; @@ -277,7 +277,7 @@ impl ConnectionManager { let abandoned_subs = connection_ctx.disconnected(); // Re-subscribe the abandoned addresses using the client. let client = unwrap_or_return!(self.get_client()); - client.subscribe_addresses(abandoned_subs).ok(); + client.subscribe_addresses(abandoned_subs).error_log(); } /// A method that should be called after using a specific server for some request. diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/event_handlers.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/event_handlers.rs index 27bd74b4d9..9db2ab93ec 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/event_handlers.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/event_handlers.rs @@ -1,51 +1,6 @@ use super::connection_manager::ConnectionManager; -use super::constants::BLOCKCHAIN_SCRIPTHASH_SUB_ID; -use crate::utxo::ScripthashNotification; use crate::RpcTransportEventHandler; -use common::jsonrpc_client::JsonRpcRequest; -use common::log::{error, warn}; - -use futures::channel::mpsc::UnboundedSender; -use serde_json::{self as json, Value as Json}; - -/// An `RpcTransportEventHandler` that forwards `ScripthashNotification`s to trigger balance updates. -/// -/// This handler hooks in `on_incoming_response` and looks for an electrum script hash notification to forward it. -pub struct ElectrumScriptHashNotificationBridge { - pub scripthash_notification_sender: UnboundedSender, -} - -impl RpcTransportEventHandler for ElectrumScriptHashNotificationBridge { - fn debug_info(&self) -> String { "ElectrumScriptHashNotificationBridge".into() } - - fn on_incoming_response(&self, data: &[u8]) { - if let Ok(raw_json) = json::from_slice::(data) { - // Try to parse the notification. A notification is sent as a JSON-RPC request. - if let Ok(notification) = json::from_value::(raw_json) { - // Only care about `BLOCKCHAIN_SCRIPTHASH_SUB_ID` notifications. - if notification.method.as_str() == BLOCKCHAIN_SCRIPTHASH_SUB_ID { - if let Some(scripthash) = notification.params.first().and_then(|s| s.as_str()) { - if let Err(e) = self - .scripthash_notification_sender - .unbounded_send(ScripthashNotification::Triggered(scripthash.to_string())) - { - error!("Failed sending script hash message. {e:?}"); - } - } else { - warn!("Notification must contain the script hash value, got: {notification:?}"); - } - }; - } - } - } - - fn on_connected(&self, _address: &str) -> Result<(), String> { Ok(()) } - - fn on_disconnected(&self, _address: &str) -> Result<(), String> { Ok(()) } - - fn on_outgoing_request(&self, _data: &[u8]) {} -} /// An `RpcTransportEventHandler` that notifies the `ConnectionManager` upon connections and disconnections. /// diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/rpc_responses.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/rpc_responses.rs index 75daac6f35..c045de98a4 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/rpc_responses.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/rpc_responses.rs @@ -1,3 +1,4 @@ +use bitcrypto::dhash256; use chain::{BlockHeader, BlockHeaderBits, BlockHeaderNonce, Transaction as UtxoTx}; use mm2_number::{BigDecimal, BigInt}; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; @@ -103,7 +104,7 @@ pub struct ElectrumBlockHeaderV14 { } impl ElectrumBlockHeaderV14 { - pub fn hash(&self) -> H256Json { self.hex.clone().into_vec()[..].into() } + pub fn hash(&self) -> H256Json { dhash256(&self.hex.clone().into_vec()).into() } } #[derive(Clone, Debug, Deserialize)] diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index cbc7780a34..1ca37e3eba 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -3,7 +3,7 @@ //! Tracking issue: https://github.com/KomodoPlatform/atomicDEX-API/issues/701 //! More info about the protocol and implementation guides can be found at https://slp.dev/ -use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentResult}; +use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::bch::BchCoin; @@ -13,20 +13,16 @@ use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_scri use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, AdditionalTxData, BroadcastTxErr, FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; -use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DerivationMethod, - DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, - MmCoinEnum, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, - PaymentInstructionsErr, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, - RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, - SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, - SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TakerSwapMakerCoin, TradeFee, TradePreimageError, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateOtherPubKeyErr, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DerivationMethod, DexFee, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, + NumConversError, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, + RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, + SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TradeFee, TradePreimageError, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, + TransactionErr, TransactionResult, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, VerificationError, + VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawError, WithdrawFee, + WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use base64::engine::general_purpose::STANDARD; use base64::Engine; @@ -54,7 +50,7 @@ use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; use serialization::{deserialize, serialize, Deserializable, Error as SerError, Reader}; use serialization_derive::Deserializable; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; use std::num::TryFromIntError; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::Arc; @@ -711,6 +707,7 @@ impl SlpToken { posv: unsigned.posv, str_d_zeel: unsigned.str_d_zeel, tx_hash_algo: self.platform_coin.as_ref().tx_hash_algo, + v_extra_payload: None, }; let _broadcast = self @@ -725,7 +722,6 @@ impl SlpToken { &self, tx: UtxoTx, expected_sender: &[u8], - fee_addr: &[u8], amount: BigDecimal, min_block_number: u64, ) -> Result<(), MmError> { @@ -759,9 +755,8 @@ impl SlpToken { tx, SLP_FEE_VOUT, expected_sender, - &DexFee::Standard(self.platform_dust_dec().into()), + DexFee::Standard(self.platform_dust_dec().into()), min_block_number, - fee_addr, ); validate_fut @@ -922,7 +917,7 @@ impl Deserializable for SlpTransaction { let additional_token_quantity = u64::from_be_bytes(bytes.try_into().expect("length is 8 bytes")); Ok(SlpTransaction::Mint { - token_id: H256::from(maybe_id.as_slice()), + token_id: H256::from_slice(maybe_id.as_slice()).map_err(|_| SerError::MalformedData)?, mint_baton_vout, additional_token_quantity, }) @@ -936,7 +931,7 @@ impl Deserializable for SlpTransaction { ))); } - let token_id = H256::from(maybe_id.as_slice()); + let token_id = H256::from_slice(maybe_id.as_slice()).map_err(|_| SerError::MalformedData)?; let mut amounts = Vec::with_capacity(1); while !reader.is_finished() { let bytes: Vec = reader.read_list()?; @@ -1130,7 +1125,8 @@ impl MarketCoinOps for SlpToken { let message_hash = self .sign_message_hash(message) .ok_or(VerificationError::PrefixNotFound)?; - let signature = CompactSignature::from(STANDARD.decode(signature)?); + let signature = CompactSignature::try_from(STANDARD.decode(signature)?) + .map_to_mm(|err| VerificationError::SignatureDecodingError(err.to_string()))?; let pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; let address_from_pubkey = self.platform_coin.address_from_pubkey(&pubkey); let slp_address = self @@ -1213,21 +1209,17 @@ impl MarketCoinOps for SlpToken { fn min_trading_vol(&self) -> MmNumber { big_decimal_from_sat_unsigned(1, self.decimals()).into() } + fn should_burn_dex_fee(&self) -> bool { false } + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] impl SwapOps for SlpToken { - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - let fee_pubkey = try_tx_s!(Public::from_slice(fee_addr)); + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + let fee_pubkey = try_tx_s!(Public::from_slice(self.dex_pubkey())); let script_pubkey = ScriptBuilder::build_p2pkh(&fee_pubkey.address_hash().into()).into(); - let amount = try_tx_s!(dex_fee.fee_uamount(self.decimals())); + let amount = try_tx_s!(dex_fee.fee_amount_as_u64(self.decimals())); let slp_out = SlpOutput { amount, script_pubkey }; let (preimage, recently_spent) = try_tx_s!(self.generate_slp_tx_preimage(vec![slp_out]).await); @@ -1353,7 +1345,6 @@ impl SwapOps for SlpToken { self.validate_dex_fee( tx, validate_fee_args.expected_sender, - validate_fee_args.fee_addr, amount.into(), validate_fee_args.min_block_number, ) @@ -1405,28 +1396,16 @@ impl SwapOps for SlpToken { utxo_common::search_for_swap_tx_spend_other(&self.platform_coin, input, SLP_SWAP_VOUT).await } - fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - #[inline] async fn extract_secret( &self, secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } - fn is_auto_refundable(&self) -> bool { false } - - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) - } - #[inline] fn negotiate_swap_contract_addr( &self, @@ -1439,7 +1418,7 @@ impl SwapOps for SlpToken { utxo_common::derive_htlc_key_pair(self.platform_coin.as_ref(), swap_unique_data) } - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> [u8; 33] { utxo_common::derive_htlc_pubkey(self, swap_unique_data) } @@ -1447,124 +1426,10 @@ impl SwapOps for SlpToken { fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } - - async fn maker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - async fn taker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } -} - -#[async_trait] -impl TakerSwapMakerCoin for SlpToken { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for SlpToken { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } } #[async_trait] -impl WatcherOps for SlpToken { - fn create_maker_payment_spend_preimage( - &self, - _maker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - unimplemented!(); - } - - fn create_taker_payment_refund_preimage( - &self, - _taker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_contract_address: &Option, - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - unimplemented!(); - } - - fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { - unimplemented!() - } - - async fn watcher_search_for_swap_tx_spend( - &self, - _input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String> { - unimplemented!(); - } - - async fn get_taker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _coin_amount: Option, - _other_coin_amount: Option, - _reward_amount: Option, - _wait_until: u64, - ) -> Result> { - unimplemented!() - } - - async fn get_maker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _reward_amount: Option, - _wait_until: u64, - ) -> Result, MmError> { - unimplemented!() - } -} +impl WatcherOps for SlpToken {} impl From for TradePreimageError { fn from(slp: GenSlpSpendErr) -> TradePreimageError { @@ -1601,7 +1466,7 @@ impl From for TxFeeDetails { impl MmCoin for SlpToken { fn is_asset_chain(&self) -> bool { false } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.conf.abortable_system) } + fn spawner(&self) -> WeakSpawner { self.conf.abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new( @@ -1644,17 +1509,19 @@ impl MmCoin for SlpToken { sat_from_big_decimal(&req.amount, coin.decimals())? }; - if address.hash.len() != 20 { - return MmError::err(WithdrawError::InvalidAddress(format!( - "Expected 20 address hash len, not {}", - address.hash.len() - ))); - } + let address_hash = address.hash.clone(); + let address_hash = { + let address_hash_len = address_hash.len(); + let address_hash: [u8; 20] = address_hash.try_into().map_err(|_| { + WithdrawError::InvalidAddress(format!("Expected 20 address hash len, not {}", address_hash_len)) + })?; + address_hash.into() + }; // TODO clarify with community whether we should support withdrawal to SLP P2SH addresses let script_pubkey = match address.address_type { CashAddrType::P2PKH => { - ScriptBuilder::build_p2pkh(&AddressHashEnum::AddressHash(address.hash.as_slice().into())).to_bytes() + ScriptBuilder::build_p2pkh(&AddressHashEnum::AddressHash(address_hash)).to_bytes() }, CashAddrType::P2SH => { return MmError::err(WithdrawError::InvalidAddress( @@ -1861,6 +1728,7 @@ impl MmCoin for SlpToken { &stage, ) .await?; + Ok(TradeFee { coin: self.platform_coin.ticker().into(), amount: fee.into(), diff --git a/mm2src/coins/utxo/tx_history_events.rs b/mm2src/coins/utxo/tx_history_events.rs new file mode 100644 index 0000000000..c336e6fbb0 --- /dev/null +++ b/mm2src/coins/utxo/tx_history_events.rs @@ -0,0 +1,43 @@ +use crate::TransactionDetails; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::StreamExt; + +pub struct TxHistoryEventStreamer { + coin: String, +} + +impl TxHistoryEventStreamer { + #[inline(always)] + pub fn new(coin: String) -> Self { Self { coin } } + + #[inline(always)] + pub fn derive_streamer_id(coin: &str) -> String { format!("TX_HISTORY:{coin}") } +} + +#[async_trait] +impl EventStreamer for TxHistoryEventStreamer { + type DataInType = Vec; + + fn streamer_id(&self) -> String { Self::derive_streamer_id(&self.coin) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + while let Some(new_txs) = data_rx.next().await { + for new_tx in new_txs { + let tx_details = serde_json::to_value(new_tx).expect("Serialization should't fail."); + broadcaster.broadcast(Event::new(self.streamer_id(), tx_details)); + } + } + } +} diff --git a/mm2src/coins/utxo/utxo_balance_events.rs b/mm2src/coins/utxo/utxo_balance_events.rs index ec1de7aa40..8fdde86ab7 100644 --- a/mm2src/coins/utxo/utxo_balance_events.rs +++ b/mm2src/coins/utxo/utxo_balance_events.rs @@ -1,20 +1,19 @@ -use super::utxo_standard::UtxoStandardCoin; +use super::{utxo_standard::UtxoStandardCoin, UtxoArc}; + use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::{utxo::{output_script, rpc_clients::electrum_script_hash, utxo_common::{address_balance, address_to_scripthash}, ScripthashNotification, UtxoCoinFields}, - CoinWithDerivationMethod, MarketCoinOps, MmCoin}; + CoinWithDerivationMethod, MarketCoinOps}; + use async_trait::async_trait; -use common::{executor::{AbortSettings, SpawnAbortable}, - log}; -use futures::channel::oneshot::{self, Receiver, Sender}; -use futures_util::StreamExt; +use common::log; +use futures::channel::oneshot; +use futures::StreamExt; use keys::Address; -use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - ErrorEventName, Event, EventName, EventStreamConfiguration}; -use std::collections::{BTreeMap, HashMap, HashSet}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use std::collections::{HashMap, HashSet}; macro_rules! try_or_continue { ($exp:expr) => { @@ -28,92 +27,73 @@ macro_rules! try_or_continue { }; } -#[async_trait] -impl EventBehaviour for UtxoStandardCoin { - fn event_name() -> EventName { EventName::CoinBalance } +pub struct UtxoBalanceEventStreamer { + coin: UtxoStandardCoin, +} - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } +impl UtxoBalanceEventStreamer { + pub fn new(utxo_arc: UtxoArc) -> Self { + Self { + // We wrap the UtxoArc in a UtxoStandardCoin for easier method accessibility. + // The UtxoArc might belong to a different coin type though. + coin: UtxoStandardCoin::from(utxo_arc), + } + } - async fn handle(self, _interval: f64, tx: oneshot::Sender) { - const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; + pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } +} - async fn subscribe_to_addresses( - utxo: &UtxoCoinFields, - addresses: HashSet
, - ) -> Result, String> { - match utxo.rpc_client.clone() { - UtxoRpcClientEnum::Electrum(client) => { - // Collect the scrpithash for every address into a map. - let scripthash_to_address_map = addresses - .into_iter() - .map(|address| { - let scripthash = address_to_scripthash(&address).map_err(|e| e.to_string())?; - Ok((scripthash, address)) - }) - .collect::, String>>()?; - // Add these subscriptions to the connection manager. It will choose whatever connections - // it sees fit to subscribe each of these addresses to. - client - .connection_manager - .add_subscriptions(&scripthash_to_address_map) - .await; - // Convert the hashmap back to btreemap. - Ok(scripthash_to_address_map.into_iter().map(|(k, v)| (k, v)).collect()) - }, - UtxoRpcClientEnum::Native(_) => { - Err("Balance streaming is currently not supported for native client.".to_owned()) - }, - } - } +#[async_trait] +impl EventStreamer for UtxoBalanceEventStreamer { + type DataInType = ScripthashNotification; - let ctx = match MmArc::from_weak(&self.as_ref().ctx) { - Some(ctx) => ctx, - None => { - let msg = "MM context must have been initialized already."; - tx.send(EventInitStatus::Failed(msg.to_owned())) - .expect(RECEIVER_DROPPED_MSG); - panic!("{}", msg); - }, - }; + fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } - let scripthash_notification_handler = match self.as_ref().scripthash_notification_handler.as_ref() { - Some(t) => t, - None => { - let e = "Scripthash notification receiver can not be empty."; - tx.send(EventInitStatus::Failed(e.to_string())) - .expect(RECEIVER_DROPPED_MSG); - panic!("{}", e); + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; + let streamer_id = self.streamer_id(); + let coin = self.coin; + let mut scripthash_to_address_map = HashMap::new(); + + // Make sure the RPC client is not native. That doesn't support balance streaming. + if coin.as_ref().rpc_client.is_native() { + let msg = "Balance streaming is not supported for native RPC client."; + ready_tx.send(Err(msg.to_string())).expect(RECEIVER_DROPPED_MSG); + panic!("{}", msg); + }; + // Get all the addresses to subscribe to their balance updates. + let all_addresses = match coin.all_addresses().await { + Ok(addresses) => addresses, + Err(e) => { + let msg = format!("Failed to get all addresses: {e}"); + ready_tx.send(Err(msg.clone())).expect(RECEIVER_DROPPED_MSG); + panic!("{}", msg); }, }; + ready_tx.send(Ok(())).expect(RECEIVER_DROPPED_MSG); - tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + // Initially, subscribe to all the addresses we currently have. + let tracking_list = subscribe_to_addresses(coin.as_ref(), all_addresses).await; + scripthash_to_address_map.extend(tracking_list); - let mut scripthash_to_address_map = BTreeMap::default(); - while let Some(message) = scripthash_notification_handler.lock().await.next().await { + while let Some(message) = data_rx.next().await { let notified_scripthash = match message { ScripthashNotification::Triggered(t) => t, ScripthashNotification::SubscribeToAddresses(addresses) => { - match subscribe_to_addresses(self.as_ref(), addresses).await { - Ok(map) => scripthash_to_address_map.extend(map), - Err(e) => { - log::error!("{e}"); - - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", Self::error_event_name(), self.ticker()), - json!({ "error": e }).to_string(), - )) - .await; - }, - }; - + let tracking_list = subscribe_to_addresses(coin.as_ref(), addresses).await; + scripthash_to_address_map.extend(tracking_list); continue; }, }; let address = match scripthash_to_address_map.get(¬ified_scripthash) { Some(t) => Some(t.clone()), - None => try_or_continue!(self.all_addresses().await) + None => try_or_continue!(coin.all_addresses().await) .into_iter() .find_map(|addr| { let script = match output_script(&addr) { @@ -146,62 +126,54 @@ impl EventBehaviour for UtxoStandardCoin { }, }; - let balance = match address_balance(&self, &address).await { + let balance = match address_balance(&coin, &address).await { Ok(t) => t, Err(e) => { - let ticker = self.ticker(); + let ticker = coin.ticker(); log::error!("Failed getting balance for '{ticker}'. Error: {e}"); let e = serde_json::to_value(e).expect("Serialization should't fail."); - ctx.stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", Self::error_event_name(), ticker), - e.to_string(), - )) - .await; + broadcaster.broadcast(Event::err(streamer_id.clone(), e)); continue; }, }; let payload = json!({ - "ticker": self.ticker(), + "ticker": coin.ticker(), "address": address.to_string(), "balance": { "spendable": balance.spendable, "unspendable": balance.unspendable } }); - ctx.stream_channel_controller - .broadcast(Event::new( - Self::event_name().to_string(), - json!(vec![payload]).to_string(), - )) - .await; + broadcaster.broadcast(Event::new(streamer_id.clone(), json!(vec![payload]))); } } +} - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - log::info!( - "{} event is activated for {}. `stream_interval_seconds`({}) has no effect on this.", - Self::event_name(), - self.ticker(), - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = AbortSettings::info_on_abort(format!( - "{} event is stopped for {}.", - Self::event_name(), - self.ticker() - )); - self.spawner().spawn_with_settings(fut, settings); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive - } +async fn subscribe_to_addresses(utxo: &UtxoCoinFields, addresses: HashSet
) -> HashMap { + match utxo.rpc_client.clone() { + UtxoRpcClientEnum::Electrum(client) => { + // Collect the scripthash for every address into a map. + let scripthash_to_address_map = addresses + .into_iter() + .filter_map(|address| { + let scripthash = address_to_scripthash(&address) + .map_err(|e| log::error!("Failed to get scripthash for address {address}: {e}")) + .ok()?; + Some((scripthash, address)) + }) + .collect(); + // Add these subscriptions to the connection manager. It will choose whatever connections + // it sees fit to subscribe each of these addresses to. + client + .connection_manager + .add_subscriptions(&scripthash_to_address_map) + .await; + scripthash_to_address_map + }, + UtxoRpcClientEnum::Native(_) => { + // Unreachable: The caller should have checked that the RPC client isn't native. + HashMap::new() + }, } } diff --git a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs index c065016176..7ebc87d70a 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs @@ -27,8 +27,12 @@ impl Debug for BlockHeaderStorage { impl BlockHeaderStorage { #[cfg(all(not(test), not(target_arch = "wasm32")))] pub(crate) fn new_from_ctx(ctx: MmArc, ticker: String) -> Result { - let sqlite_connection = ctx.sqlite_connection.get().ok_or(BlockHeaderStorageError::Internal( - "sqlite_connection is not initialized".to_owned(), + #[cfg(not(feature = "new-db-arch"))] + let maybe_sqlite_connection = ctx.sqlite_connection.get(); + #[cfg(feature = "new-db-arch")] + let maybe_sqlite_connection = ctx.global_db_conn.get(); + let sqlite_connection = maybe_sqlite_connection.ok_or(BlockHeaderStorageError::Internal( + "BlockHeaderStorage's SQL DB is not initialized".to_owned(), ))?; Ok(BlockHeaderStorage { inner: Box::new(SqliteBlockHeadersStorage { diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs index f8e16a6089..65bd34103f 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -1,9 +1,9 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientImpl, UtxoJsonRpcClientInfo, UtxoRpcClientEnum}; + use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; -use crate::utxo::utxo_standard::UtxoStandardCoin; use crate::utxo::{generate_and_send_tx, FeePolicy, GetUtxoListOps, UtxoArc, UtxoCommonOps, UtxoSyncStatusLoopHandle, UtxoWeak}; use crate::{DerivationMethod, PrivKeyBuildPolicy, UtxoActivationParams}; @@ -14,7 +14,6 @@ use common::log::{debug, error, info, warn}; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; #[cfg(test)] use mocktopus::macros::*; use rand::Rng; use script::Builder; @@ -109,7 +108,6 @@ where let utxo = self.build_utxo_fields().await?; let sync_status_loop_handle = utxo.block_headers_status_notifier.clone(); let spv_conf = utxo.conf.spv_conf.clone(); - let (is_native_mode, mode) = (utxo.rpc_client.is_native(), utxo.rpc_client.to_string()); let utxo_arc = UtxoArc::new(utxo); self.spawn_merge_utxo_loop_if_required(&utxo_arc, self.constructor.clone()); @@ -121,18 +119,6 @@ where spawn_block_header_utxo_loop(self.ticker, &utxo_arc, sync_handle, spv_conf); } - if let Some(stream_config) = &self.ctx().event_stream_configuration { - if is_native_mode { - return MmError::err(UtxoCoinBuildError::UnsupportedModeForBalanceEvents { mode }); - } - - if let EventInitStatus::Failed(err) = - EventBehaviour::spawn_if_active(UtxoStandardCoin::from(utxo_arc), stream_config).await - { - return MmError::err(UtxoCoinBuildError::FailedSpawningBalanceEvents(err)); - } - } - Ok(result_coin) } } diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 15a699c2f1..b916afc232 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -5,9 +5,8 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumC use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError}; -use crate::utxo::{output_script, ElectrumBuilderArgs, RecentlySpentOutPoints, ScripthashNotification, - ScripthashNotificationSender, TxFee, UtxoCoinConf, UtxoCoinFields, UtxoHDWallet, UtxoRpcMode, - UtxoSyncStatus, UtxoSyncStatusLoopHandle, UTXO_DUST_AMOUNT}; +use crate::utxo::{output_script, ElectrumBuilderArgs, RecentlySpentOutPoints, TxFee, UtxoCoinConf, UtxoCoinFields, + UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, UTXO_DUST_AMOUNT}; use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, SharableRpcTransportEventHandler, UtxoActivationParams}; @@ -17,7 +16,7 @@ use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, Aborted use common::now_sec; use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError}; use derive_more::Display; -use futures::channel::mpsc::{channel, Receiver as AsyncReceiver, UnboundedReceiver, UnboundedSender}; +use futures::channel::mpsc::{channel, Receiver as AsyncReceiver}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use keys::bytes::Bytes; @@ -30,7 +29,6 @@ use serde_json::{self as json, Value as Json}; use spv_validation::conf::SPVConf; use spv_validation::helpers_validation::SPVError; use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; -use std::sync::Arc; use std::sync::Mutex; cfg_native! { @@ -38,6 +36,7 @@ cfg_native! { use crate::utxo::rpc_clients::{ConcurrentRequestMap, NativeClient, NativeClientImpl}; use dirs::home_dir; use std::path::{Path, PathBuf}; + use std::sync::Arc; } /// Number of seconds in a day (24 hours * 60 * 60) @@ -227,22 +226,6 @@ pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } } -// The return type is one-time used only. No need to create a type for it. -#[allow(clippy::type_complexity)] -fn get_scripthash_notification_handlers( - ctx: &MmArc, -) -> Option<( - UnboundedSender, - Arc>>, -)> { - if ctx.event_stream_configuration.is_some() { - let (sender, receiver) = futures::channel::mpsc::unbounded(); - Some((sender, Arc::new(AsyncMutex::new(receiver)))) - } else { - None - } -} - async fn build_utxo_coin_fields_with_conf_and_policy( builder: &Builder, conf: UtxoCoinConf, @@ -266,19 +249,11 @@ where let my_script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; - let (scripthash_notification_sender, scripthash_notification_handler) = - match get_scripthash_notification_handlers(builder.ctx()) { - Some((sender, receiver)) => (Some(sender), Some(receiver)), - None => (None, None), - }; - // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. let abortable_system: AbortableQueue = builder.ctx().abortable_system.create_subsystem()?; - let rpc_client = builder - .rpc_client(scripthash_notification_sender, abortable_system.create_subsystem()?) - .await?; + let rpc_client = builder.rpc_client(abortable_system.create_subsystem()?).await?; let tx_fee = builder.tx_fee(&rpc_client).await?; let decimals = builder.decimals(&rpc_client).await?; let dust_amount = builder.dust_amount(); @@ -305,9 +280,8 @@ where check_utxo_maturity, block_headers_status_notifier, block_headers_status_watcher, + ctx: builder.ctx().clone().weak(), abortable_system, - scripthash_notification_handler, - ctx: builder.ctx().weak(), }; Ok(coin) @@ -353,19 +327,11 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { address_format, }; - let (scripthash_notification_sender, scripthash_notification_handler) = - match get_scripthash_notification_handlers(self.ctx()) { - Some((sender, receiver)) => (Some(sender), Some(receiver)), - None => (None, None), - }; - // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. let abortable_system: AbortableQueue = self.ctx().abortable_system.create_subsystem()?; - let rpc_client = self - .rpc_client(scripthash_notification_sender, abortable_system.create_subsystem()?) - .await?; + let rpc_client = self.rpc_client(abortable_system.create_subsystem()?).await?; let tx_fee = self.tx_fee(&rpc_client).await?; let decimals = self.decimals(&rpc_client).await?; let dust_amount = self.dust_amount(); @@ -392,9 +358,8 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { check_utxo_maturity, block_headers_status_notifier, block_headers_status_watcher, + ctx: self.ctx().clone().weak(), abortable_system, - scripthash_notification_handler, - ctx: self.ctx().weak(), }; Ok(coin) } @@ -529,11 +494,7 @@ pub trait UtxoCoinBuilderCommonOps { } } - async fn rpc_client( - &self, - scripthash_notification_sender: ScripthashNotificationSender, - abortable_system: AbortableQueue, - ) -> UtxoCoinBuildResult { + async fn rpc_client(&self, abortable_system: AbortableQueue) -> UtxoCoinBuildResult { match self.activation_params().mode.clone() { UtxoRpcMode::Native => { #[cfg(target_arch = "wasm32")] @@ -557,7 +518,6 @@ pub trait UtxoCoinBuilderCommonOps { ElectrumBuilderArgs::default(), servers, (min_connected, max_connected), - scripthash_notification_sender, ) .await?; Ok(UtxoRpcClientEnum::Electrum(electrum)) @@ -573,7 +533,6 @@ pub trait UtxoCoinBuilderCommonOps { args: ElectrumBuilderArgs, servers: Vec, (min_connected, max_connected): (Option, Option), - scripthash_notification_sender: ScripthashNotificationSender, ) -> UtxoCoinBuildResult { let coin_ticker = self.ticker().to_owned(); let ctx = self.ctx(); @@ -610,8 +569,8 @@ pub trait UtxoCoinBuilderCommonOps { client_settings, event_handlers, block_headers_storage, + ctx.event_stream_manager.clone(), abortable_system, - scripthash_notification_sender, ) .map_to_mm(UtxoCoinBuildError::Internal) } @@ -768,7 +727,7 @@ pub trait UtxoCoinBuilderCommonOps { }; let secs_since_date = current_time_sec - date_s; - let days_since_date = (secs_since_date / DAY_IN_SECONDS) - 1; + let days_since_date = (secs_since_date / DAY_IN_SECONDS).max(1) - 1; let blocks_to_sync = (days_since_date * blocks_per_day) + blocks_per_day; if current_block_height < blocks_to_sync { diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 70c8522b58..db7ad1f65a 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -13,13 +13,13 @@ use crate::utxo::utxo_hd_wallet::UtxoHDAddress; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; use crate::watcher_common::validate_watcher_reward; use crate::{scan_for_new_addresses_impl, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, - DexFee, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, - RawTransactionError, RawTransactionRequest, RawTransactionRes, RawTransactionResult, - RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundPaymentArgs, RewardTarget, - SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SendTakerFundingArgs, SignRawTransactionEnum, SignRawTransactionRequest, SignUtxoTransactionParams, - SignatureError, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, TradePreimageValue, TransactionData, TransactionFut, TransactionResult, + DexFee, DexFeeBurnDestination, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + GetWithdrawSenderAddress, RawTransactionError, RawTransactionRequest, RawTransactionRes, + RawTransactionResult, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundPaymentArgs, + RewardTarget, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionEnum, SignRawTransactionRequest, + SignUtxoTransactionParams, SignatureError, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, + SwapOps, SwapTxTypeWithSecretHash, TradePreimageValue, TransactionData, TransactionFut, TransactionResult, TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageError, @@ -53,6 +53,7 @@ use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, MmNumber}; use primitives::hash::H512; use rpc::v1::types::{Bytes as BytesJson, ToTxHash, TransactionInputEnum, H256 as H256Json}; +#[cfg(test)] use rpc_clients::NativeClientImpl; use script::{Builder, Opcode, Script, ScriptAddress, TransactionInputSigner, UnsignedTransactionInput}; use secp256k1::{PublicKey, Signature as SecpSignature}; use serde_json::{self as json}; @@ -60,8 +61,11 @@ use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, C SERIALIZE_TRANSACTION_WITNESS}; use std::cmp::Ordering; use std::collections::hash_map::{Entry, HashMap}; +use std::convert::TryFrom; use std::str::FromStr; use std::sync::atomic::Ordering as AtomicOrdering; +#[cfg(test)] +use utxo_common_tests::{utxo_coin_fields_for_test, utxo_coin_from_fields}; use utxo_signer::with_key_pair::{calc_and_sign_sighash, p2sh_spend, signature_hash_to_sign, SIGHASH_ALL, SIGHASH_SINGLE}; use utxo_signer::UtxoSignerOps; @@ -69,7 +73,7 @@ use utxo_signer::UtxoSignerOps; pub mod utxo_tx_history_v2_common; pub const DEFAULT_FEE_VOUT: usize = 0; -pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 305; +pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 496; // TODO: checking with komodo-like tx size, included the burn output pub const DEFAULT_SWAP_VOUT: usize = 0; pub const DEFAULT_SWAP_VIN: usize = 0; const MIN_BTC_TRADING_VOL: &str = "0.00777"; @@ -934,6 +938,7 @@ async fn p2sh_spending_tx_preimage( posv: coin.as_ref().conf.is_posv, str_d_zeel, hash_algo, + v_extra_payload: None, }) } @@ -978,6 +983,7 @@ pub async fn p2sh_spending_tx(coin: &T, input: P2SHSpendingTxI posv: coin.as_ref().conf.is_posv, str_d_zeel: unsigned.str_d_zeel, tx_hash_algo: unsigned.hash_algo.into(), + v_extra_payload: None, }) } @@ -1238,43 +1244,77 @@ pub async fn sign_and_send_taker_funding_spend( Ok(final_tx) } -async fn gen_taker_payment_spend_preimage( +/// Make tx preimage to spend taker payment for swaps V2 +async fn gen_taker_payment_spend_preimage( coin: &T, args: &GenTakerPaymentSpendArgs<'_, T>, n_time: NTimeSetting, ) -> GenPreimageResInner { - let dex_fee_address = address_from_raw_pubkey( - args.dex_fee_pub, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.bech32_hrp.clone(), - coin.addr_format().clone(), - ) - .map_to_mm(|e| TxGenError::AddressDerivation(format!("Failed to derive dex_fee_address: {}", e)))?; - - let mut outputs = generate_taker_fee_tx_outputs(coin.as_ref().decimals, dex_fee_address.hash(), args.dex_fee)?; - if let DexFee::WithBurn { .. } = args.dex_fee { - let script = output_script(args.maker_address).map_to_mm(|e| { - TxGenError::Other(format!( - "Couldn't generate output script for maker address {}, error {}", - args.maker_address, e - )) - })?; - let tx_fee = coin - .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await?; - let maker_value = args - .taker_tx - .first_output() - .map_to_mm(|e| TxGenError::PrevTxIsNotValid(e.to_string()))? - .value - - outputs[0].value - - outputs[1].value - - tx_fee; - outputs.push(TransactionOutput { - value: maker_value, - script_pubkey: script.to_bytes(), - }) + let mut outputs = generate_taker_fee_tx_outputs(coin, args.dex_fee).map_err(TxGenError::Other)?; + match args.dex_fee { + &DexFee::WithBurn { .. } | &DexFee::NoFee => { + let script = output_script(args.maker_address).map_to_mm(|e| { + TxGenError::Other(format!( + "Couldn't generate output script for maker address {}, error {}", + args.maker_address, e + )) + })?; + let tx_fee = coin + .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await?; + let dex_fee_value = if matches!(args.dex_fee, &DexFee::WithBurn { .. }) { + outputs[0].value + outputs[1].value + } else { + 0 + }; + let prev_value = args + .taker_tx + .first_output() + .map_to_mm(|e| TxGenError::PrevTxIsNotValid(e.to_string()))? + .value; + let maker_value = prev_value + .checked_sub(dex_fee_value) + .ok_or(TxGenError::PrevOutputTooLow(format!( + "taker value too low: {}", + prev_value + )))? + .checked_sub(tx_fee) + .ok_or(TxGenError::PrevOutputTooLow(format!( + "taker value too low: {}", + prev_value + )))?; + // taker also adds maker output as we can't use SIGHASH_SINGLE with two outputs, dex fee and burn, + // and both the maker and taker sign all outputs: + outputs.push(TransactionOutput { + value: maker_value, + script_pubkey: script.to_bytes(), + }) + }, + &DexFee::Standard(..) => {}, // We do not add maker output here, only the single dex fee output (signed with SIGHASH_SINGLE) is created by the taker or validated by the maker + } + + #[cfg(feature = "run-docker-tests")] + { + match *args.dex_fee { + DexFee::NoFee => { + if args.taker_pub.to_vec().as_slice() != coin.burn_pubkey() { + panic!("taker pubkey must be equal to burn pubkey for DexFee::NoFee"); + } + assert_eq!(outputs.len(), 1); // only the maker output + }, + DexFee::Standard(..) => { + if args.taker_pub.to_vec().as_slice() == coin.burn_pubkey() { + panic!("taker pubkey must NOT be equal to burn pubkey for DexFee::Standard"); + } + assert_eq!(outputs.len(), 1); // only the dex fee output (maker output will be added later) + }, + DexFee::WithBurn { .. } => { + if args.taker_pub.to_vec().as_slice() == coin.burn_pubkey() { + panic!("taker pubkey must NOT be equal to burn pubkey for DexFee::WithBurn"); + } + assert_eq!(outputs.len(), 3); // dex fee, burn and maker outputs + }, + } } p2sh_spending_tx_preimage( @@ -1289,7 +1329,7 @@ async fn gen_taker_payment_spend_preimage( .map_to_mm(TxGenError::Legacy) } -pub async fn gen_and_sign_taker_payment_spend_preimage( +pub async fn gen_and_sign_taker_payment_spend_preimage( coin: &T, args: &GenTakerPaymentSpendArgs<'_, T>, htlc_keypair: &KeyPair, @@ -1306,7 +1346,7 @@ pub async fn gen_and_sign_taker_payment_spend_preimage( let sig_hash_type = match args.dex_fee { DexFee::Standard(_) => SIGHASH_SINGLE, - DexFee::WithBurn { .. } => SIGHASH_ALL, + DexFee::WithBurn { .. } | DexFee::NoFee => SIGHASH_ALL, }; let signature = calc_and_sign_sighash( @@ -1349,7 +1389,7 @@ pub async fn validate_taker_payment_spend_preimage( let sig_hash_type = match gen_args.dex_fee { DexFee::Standard(_) => SIGHASH_SINGLE, - DexFee::WithBurn { .. } => SIGHASH_ALL, + DexFee::WithBurn { .. } | DexFee::NoFee => SIGHASH_ALL, }; let sig_hash = signature_hash_to_sign( @@ -1400,6 +1440,8 @@ pub async fn sign_and_broadcast_taker_payment_spend( payment_input.amount = payment_output.value; signer.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; + // Add the maker output if DexFee is Standard (when the single dex fee output is signed with SIGHASH_SINGLE) + // (in other DexFee options the make output is added in gen_taker_payment_spend_preimage fn) if let DexFee::Standard(dex_fee) = gen_args.dex_fee { let dex_fee_sat = try_tx_s!(sat_from_big_decimal(&dex_fee.to_decimal(), coin.as_ref().decimals)); @@ -1433,7 +1475,7 @@ pub async fn sign_and_broadcast_taker_payment_spend( let mut taker_signature_with_sighash = preimage.signature.to_vec(); let taker_sig_hash = match gen_args.dex_fee { DexFee::Standard(_) => (SIGHASH_SINGLE | coin.as_ref().conf.fork_id) as u8, - DexFee::WithBurn { .. } => (SIGHASH_ALL | coin.as_ref().conf.fork_id) as u8, + DexFee::WithBurn { .. } | DexFee::NoFee => (SIGHASH_ALL | coin.as_ref().conf.fork_id) as u8, }; taker_signature_with_sighash.push(taker_sig_hash); @@ -1460,47 +1502,80 @@ pub async fn sign_and_broadcast_taker_payment_spend( Ok(final_tx) } -pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], dex_fee: DexFee) -> TransactionFut +pub fn send_taker_fee(coin: T, dex_fee: DexFee) -> TransactionFut where - T: UtxoCommonOps + GetUtxoListOps, + T: UtxoCommonOps + GetUtxoListOps + SwapOps, { - let address = try_tx_fus!(address_from_raw_pubkey( - fee_pub_key, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.bech32_hrp.clone(), - coin.addr_format().clone(), - )); + let outputs = try_tx_fus!(generate_taker_fee_tx_outputs(&coin, &dex_fee,)); - let outputs = try_tx_fus!(generate_taker_fee_tx_outputs( - coin.as_ref().decimals, - address.hash(), - &dex_fee, - )); + #[cfg(feature = "run-docker-tests")] + { + let taker_pub = coin.derive_htlc_pubkey(&[]); + match dex_fee { + DexFee::NoFee => { + panic!("should not send dex fee for DexFee::NoFee"); + }, + DexFee::Standard(..) => { + if taker_pub.as_slice() == coin.burn_pubkey() { + panic!("taker pubkey must NOT be equal to burn pubkey for DexFee::Standard"); + } + assert_eq!(outputs.len(), 1); + }, + DexFee::WithBurn { .. } => { + if taker_pub.as_slice() == coin.burn_pubkey() { + panic!("taker pubkey must NOT be equal to burn pubkey for DexFee::WithBurn"); + } + assert_eq!(outputs.len(), 2); + }, + } + } send_outputs_from_my_address(coin, outputs) } -fn generate_taker_fee_tx_outputs( - decimals: u8, - address_hash: &AddressHashEnum, - dex_fee: &DexFee, -) -> Result, MmError> { - let fee_amount = dex_fee.fee_uamount(decimals)?; - - let mut outputs = vec![TransactionOutput { - value: fee_amount, - script_pubkey: Builder::build_p2pkh(address_hash).to_bytes(), - }]; - - if let Some(burn_amount) = dex_fee.burn_uamount(decimals)? { - outputs.push(TransactionOutput { - value: burn_amount, - script_pubkey: Builder::default().push_opcode(Opcode::OP_RETURN).into_bytes(), - }); +/// Create dex fee (and optionally burn fee) outputs +fn generate_taker_fee_tx_outputs(coin: &T, dex_fee: &DexFee) -> Result, String> +where + T: UtxoCommonOps + SwapOps, +{ + match dex_fee { + DexFee::NoFee => Ok(vec![]), + // TODO: return an error for DexFee::Standard like 'dex fee must contain burn amount' when nodes upgraded to this code + DexFee::Standard(_) | DexFee::WithBurn { .. } => { + let dex_address = dex_address(coin)?; + let burn_address = burn_address(coin)?; + let fee_amount = dex_fee + .fee_amount_as_u64(coin.as_ref().decimals) + .map_err(|err| err.to_string())?; + + let mut outputs = vec![TransactionOutput { + value: fee_amount, + script_pubkey: Builder::build_p2pkh(dex_address.hash()).to_bytes(), + }]; + + if let DexFee::WithBurn { + fee_amount: _, + burn_amount, + burn_destination, + } = dex_fee + { + let burn_amount_u64 = sat_from_big_decimal(&burn_amount.to_decimal(), coin.as_ref().decimals) + .map_err(|err| err.to_string())?; + let burn_output = match burn_destination { + DexFeeBurnDestination::KmdOpReturn => TransactionOutput { + value: burn_amount_u64, + script_pubkey: Builder::default().push_opcode(Opcode::OP_RETURN).into_bytes(), + }, + DexFeeBurnDestination::PreBurnAccount => TransactionOutput { + value: burn_amount_u64, + script_pubkey: Builder::build_p2pkh(burn_address.hash()).to_bytes(), + }, + }; + outputs.push(burn_output); + } + Ok(outputs) + }, } - - Ok(outputs) } pub fn send_maker_payment(coin: T, args: SendPaymentArgs) -> TransactionFut @@ -2012,11 +2087,6 @@ where } } -pub fn check_all_inputs_signed_by_pub(tx: &[u8], expected_pub: &[u8]) -> Result> { - let tx: UtxoTx = deserialize(tx)?; - check_all_utxo_inputs_signed_by_pub(&tx, expected_pub) -} - pub fn check_all_utxo_inputs_signed_by_pub( tx: &UtxoTx, expected_pub: &[u8], @@ -2036,25 +2106,30 @@ pub fn check_all_utxo_inputs_signed_by_pub( Ok(true) } -pub fn watcher_validate_taker_fee( +pub fn watcher_validate_taker_fee( coin: &T, input: WatcherValidateTakerFeeInput, output_index: usize, ) -> ValidatePaymentFut<()> { let coin = coin.clone(); - let sender_pubkey = input.sender_pubkey; - let taker_fee_hash = input.taker_fee_hash; + let sender_pubkey = input.sender_pubkey.clone(); let min_block_number = input.min_block_number; let lock_duration = input.lock_duration; - let fee_addr = input.fee_addr.to_vec(); - let fut = async move { + let taker_fee_hash_len = input.taker_fee_hash.len(); + let taker_fee_hash_array: [u8; 32] = input.taker_fee_hash.try_into().map_to_mm(|_| { + ValidatePaymentError::InternalError(format!( + "Invalid taker_fee_hash length: expected 32 bytes, got {} bytes", + taker_fee_hash_len + )) + })?; + let taker_fee_hash = taker_fee_hash_array.into(); let mut attempts = 0; loop { let tx_from_rpc = match coin .as_ref() .rpc_client - .get_verbose_transaction(&H256Json::from(taker_fee_hash.as_slice())) + .get_verbose_transaction(&taker_fee_hash) .compat() .await { @@ -2096,18 +2171,10 @@ pub fn watcher_validate_taker_fee( ))); } - let address = address_from_raw_pubkey( - &fee_addr, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.bech32_hrp.clone(), - coin.addr_format().clone(), - ) - .map_to_mm(ValidatePaymentError::TxDeserializationError)?; - + let dex_address = dex_address(&coin).map_to_mm(ValidatePaymentError::TxDeserializationError)?; match taker_fee_tx.outputs.get(output_index) { Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); + let expected_script_pubkey = Builder::build_p2pkh(dex_address.hash()).to_bytes(); if out.script_pubkey != expected_script_pubkey { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", @@ -2129,24 +2196,88 @@ pub fn watcher_validate_taker_fee( Box::new(fut.boxed().compat()) } -pub fn validate_fee( +/// Helper fn to validate taker tx output to dex address +fn validate_dex_output( + coin: &T, + tx: &UtxoTx, + output_index: usize, + dex_address: &Address, + fee_amount: &MmNumber, +) -> MmResult<(), ValidatePaymentError> { + let fee_amount_u64 = sat_from_big_decimal(&fee_amount.to_decimal(), coin.as_ref().decimals)?; + match tx.outputs.get(output_index) { + Some(out) => { + let expected_script_pubkey = Builder::build_p2pkh(dex_address.hash()).to_bytes(); + if out.script_pubkey != expected_script_pubkey { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", + INVALID_RECEIVER_ERR_LOG, out.script_pubkey, expected_script_pubkey + ))); + } + if out.value < fee_amount_u64 { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided dex fee tx output value is less than expected {:?} {:?}", + out.value, fee_amount_u64 + ))); + } + }, + None => { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided dex fee tx {:?} does not have output {}", + tx, output_index + ))) + }, + } + Ok(()) +} + +/// Helper fn to validate taker tx output burning coins +fn validate_burn_output( + coin: &T, + tx: &UtxoTx, + output_index: usize, + burn_script_pubkey: &Script, + burn_amount: &MmNumber, +) -> MmResult<(), ValidatePaymentError> { + let burn_amount_u64 = sat_from_big_decimal(&burn_amount.to_decimal(), coin.as_ref().decimals)?; + match tx.outputs.get(output_index) { + Some(out) => { + if out.script_pubkey != burn_script_pubkey.to_bytes() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Provided burn tx output script_pubkey {:?} doesn't match expected {:?}", + INVALID_RECEIVER_ERR_LOG, + out.script_pubkey, + burn_script_pubkey.to_bytes() + ))); + } + + if out.value < burn_amount_u64 { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided burn tx output value {:?} is less than expected: {:?}", + out.value, burn_amount + ))); + } + }, + None => { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided burn tx {:?} does not have output {}", + tx, output_index + ))) + }, + } + Ok(()) +} + +pub fn validate_fee( coin: T, tx: UtxoTx, output_index: usize, sender_pubkey: &[u8], - dex_amount: &DexFee, + dex_fee: DexFee, min_block_number: u64, - fee_addr: &[u8], ) -> ValidatePaymentFut<()> { - let address = try_f!(address_from_raw_pubkey( - fee_addr, - coin.as_ref().conf.address_prefixes.clone(), - coin.as_ref().conf.checksum_type, - coin.as_ref().conf.bech32_hrp.clone(), - coin.addr_format().clone(), - ) - .map_to_mm(ValidatePaymentError::TxDeserializationError)); - + let dex_address = try_f!(dex_address(&coin).map_to_mm(ValidatePaymentError::InternalError)); + let burn_address = try_f!(burn_address(&coin).map_to_mm(ValidatePaymentError::InternalError)); let inputs_signed_by_pub = try_f!(check_all_utxo_inputs_signed_by_pub(&tx, sender_pubkey)); if !inputs_signed_by_pub { return Box::new(futures01::future::err( @@ -2157,9 +2288,6 @@ pub fn validate_fee( )); } - let fee_amount = try_f!(dex_amount.fee_uamount(coin.as_ref().decimals)); - let burn_amount = try_f!(dex_amount.burn_uamount(coin.as_ref().decimals)); - let fut = async move { let tx_from_rpc = coin .as_ref() @@ -2187,58 +2315,28 @@ pub fn validate_fee( ))); } - match tx.outputs.get(output_index) { - Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); - if out.script_pubkey != expected_script_pubkey { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", - INVALID_RECEIVER_ERR_LOG, out.script_pubkey, expected_script_pubkey - ))); - } - if out.value < fee_amount { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided dex fee tx output value is less than expected {:?} {:?}", - out.value, fee_amount - ))); - } - }, - None => { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided dex fee tx {:?} does not have output {}", - tx, output_index - ))) + match dex_fee { + DexFee::NoFee => {}, + DexFee::Standard(fee_amount) => { + validate_dex_output(&coin, &tx, output_index, &dex_address, &fee_amount)?; }, - } - - if let Some(burn_amount) = burn_amount { - match tx.outputs.get(output_index + 1) { - Some(out) => { - let expected_script_pubkey = Builder::default().push_opcode(Opcode::OP_RETURN).into_bytes(); - - if out.script_pubkey != expected_script_pubkey { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: Provided burn tx output script_pubkey doesn't match expected {:?} {:?}", - INVALID_RECEIVER_ERR_LOG, out.script_pubkey, expected_script_pubkey - ))); - } - - if out.value < burn_amount { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided burn tx output value is less than expected {:?} {:?}", - out.value, burn_amount - ))); - } + DexFee::WithBurn { + fee_amount, + burn_amount, + burn_destination, + } => match burn_destination { + DexFeeBurnDestination::KmdOpReturn => { + validate_dex_output(&coin, &tx, output_index, &dex_address, &fee_amount)?; + let burn_script_pubkey = Builder::default().push_opcode(Opcode::OP_RETURN).into_script(); + validate_burn_output(&coin, &tx, output_index + 1, &burn_script_pubkey, &burn_amount)?; }, - None => { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided burn tx output {:?} does not have output {}", - tx, output_index - ))) + DexFeeBurnDestination::PreBurnAccount => { + let burn_script_pubkey = Builder::build_p2pkh(burn_address.hash()); + validate_dex_output(&coin, &tx, output_index, &dex_address, &fee_amount)?; + validate_burn_output(&coin, &tx, output_index + 1, &burn_script_pubkey, &burn_amount)?; }, - } - } - + }, + }; Ok(()) }; Box::new(fut.boxed().compat()) @@ -2582,21 +2680,28 @@ pub async fn get_taker_watcher_reward Result, String> { +pub fn extract_secret(secret_hash: &[u8], spend_tx: &[u8]) -> Result<[u8; 32], String> { let spend_tx: UtxoTx = try_s!(deserialize(spend_tx).map_err(|e| ERRL!("{:?}", e))); + extract_secret_v2(secret_hash, &spend_tx) +} + +/// Extract a secret from the `spend_tx`. +/// Note spender could generate the spend with several inputs where the only one input is the p2sh script. +pub fn extract_secret_v2(secret_hash: &[u8], spend_tx: &UtxoTx) -> Result<[u8; 32], String> { let expected_secret_hash = if secret_hash.len() == 32 { ripemd160(secret_hash) } else { + let secret_hash: [u8; 20] = try_s!(secret_hash.try_into()); H160::from(secret_hash) }; - for input in spend_tx.inputs.into_iter() { + for input in spend_tx.inputs.iter() { let script: Script = input.script_sig.clone().into(); for instruction in script.iter().flatten() { if instruction.opcode == Opcode::OP_PUSHBYTES_32 { if let Some(secret) = instruction.data { let actual_secret_hash = dhash160(secret); if actual_secret_hash == expected_secret_hash { - return Ok(secret.to_vec()); + return Ok(try_s!(secret.try_into())); } } } @@ -2616,6 +2721,26 @@ pub fn my_address(coin: &T) -> MmResult(coin: &T) -> Result { + address_from_raw_pubkey( + coin.dex_pubkey(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.bech32_hrp.clone(), + coin.addr_format().clone(), + ) +} + +pub fn burn_address(coin: &T) -> Result { + address_from_raw_pubkey( + coin.burn_pubkey(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.bech32_hrp.clone(), + coin.addr_format().clone(), + ) +} + /// Hash message for signature using Bitcoin's message signing format. /// sha256(sha256(PREFIX_LENGTH + PREFIX + MESSAGE_LENGTH + MESSAGE)) pub fn sign_message_hash(coin: &UtxoCoinFields, message: &str) -> Option<[u8; 32]> { @@ -2644,7 +2769,8 @@ pub fn verify_message( address: &str, ) -> VerificationResult { let message_hash = sign_message_hash(coin.as_ref(), message).ok_or(VerificationError::PrefixNotFound)?; - let signature = CompactSignature::from(STANDARD.decode(signature_base64)?); + let signature = CompactSignature::try_from(STANDARD.decode(signature_base64)?) + .map_to_mm(|err| VerificationError::SignatureDecodingError(err.to_string()))?; let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; let received_address = checked_address_from_str(coin, address)?; Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == *received_address.hash()) @@ -2788,10 +2914,20 @@ async fn sign_raw_utxo_tx + UtxoTxGenerationOps>( let prev_hash = hex::decode(prev_utxo.tx_hash.as_bytes()) .map_to_mm(|e| RawTransactionError::DecodeError(e.to_string()))?; + let prev_hash = { + let prev_hash_len = prev_hash.len(); + let arr: [u8; 32] = prev_hash.try_into().map_to_mm(|_| { + RawTransactionError::DecodeError(format!( + "Invalid prev_out_hash length: expected 32 bytes, got {}", + prev_hash_len + )) + })?; + arr.into() + }; unspents.push(UnspentInfo { outpoint: OutPoint { - hash: prev_hash.as_slice().into(), + hash: prev_hash, index: prev_utxo.index, }, value: sat_from_big_decimal(&prev_utxo.amount, coin.as_ref().decimals) @@ -2968,6 +3104,8 @@ pub fn min_trading_vol(coin: &UtxoCoinFields) -> MmNumber { pub fn is_asset_chain(coin: &UtxoCoinFields) -> bool { coin.conf.asset_chain } +pub const fn should_burn_dex_fee() -> bool { false } // TODO: fix back to true when negotiation version added + pub async fn get_raw_transaction(coin: &UtxoCoinFields, req: RawTransactionRequest) -> RawTransactionResult { let hash = H256Json::from_str(&req.tx_hash).map_to_mm(|e| RawTransactionError::InvalidHashError(e.to_string()))?; let hex = coin @@ -2980,9 +3118,14 @@ pub async fn get_raw_transaction(coin: &UtxoCoinFields, req: RawTransactionReque } pub async fn get_tx_hex_by_hash(coin: &UtxoCoinFields, tx_hash: Vec) -> RawTransactionResult { + let len = tx_hash.len(); + let hash: [u8; 32] = tx_hash.try_into().map_to_mm(|_| { + RawTransactionError::InvalidHashError(format!("Invalid hash length: expected 32, got {}", len)) + })?; + let hex = coin .rpc_client - .get_transaction_bytes(&H256Json::from(tx_hash.as_slice())) + .get_transaction_bytes(&H256Json::from(hash)) .compat() .await .map_err(|e| RawTransactionError::Transport(e.to_string()))?; @@ -3332,7 +3475,7 @@ where Entry::Vacant(e) => { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => coin.as_ref().conf.ticker.clone(), "method" => "tx_detail_by_hash"); - match coin.tx_details_by_hash(&txid.0, &mut input_transactions).await { + match coin.tx_details_by_hash(&txid, &mut input_transactions).await { Ok(mut tx_details) => { mm_counter!(ctx.metrics, "tx.history.response.count", 1, "coin" => coin.as_ref().conf.ticker.clone(), "method" => "tx_detail_by_hash"); @@ -3366,7 +3509,7 @@ 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"); - match coin.tx_details_by_hash(&txid.0, &mut input_transactions).await { + match coin.tx_details_by_hash(&txid, &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 @@ -3530,17 +3673,16 @@ where pub async fn tx_details_by_hash( coin: &T, - hash: &[u8], + hash: &H256Json, input_transactions: &mut HistoryUtxoTxMap, ) -> Result { let ticker = &coin.as_ref().conf.ticker; - let hash = H256Json::from(hash); - let verbose_tx = try_s!(coin.as_ref().rpc_client.get_verbose_transaction(&hash).compat().await); + let verbose_tx = try_s!(coin.as_ref().rpc_client.get_verbose_transaction(hash).compat().await); let mut tx: UtxoTx = try_s!(deserialize(verbose_tx.hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let my_address = try_s!(coin.as_ref().derivation_method.single_addr_or_err().await); - input_transactions.insert(hash, HistoryUtxoTx { + input_transactions.insert(*hash, HistoryUtxoTx { tx: tx.clone(), height: verbose_tx.height, }); @@ -3628,6 +3770,7 @@ pub async fn tx_details_by_hash( let fee = verbose_tx.vin.iter().fold(0., |cur, input| { let fee = match input { TransactionInputEnum::Lelantus(lelantus) => lelantus.n_fees, + TransactionInputEnum::Spark(spark) => spark.n_fees, _ => 0., }; cur + fee @@ -3971,11 +4114,9 @@ pub async fn get_fee_to_send_taker_fee( stage: FeeApproxStage, ) -> TradePreimageResult where - T: MarketCoinOps + UtxoCommonOps, + T: MarketCoinOps + UtxoCommonOps + SwapOps, { - let decimals = coin.as_ref().decimals; - - let outputs = generate_taker_fee_tx_outputs(decimals, &AddressHashEnum::default_address_hash(), &dex_fee)?; + let outputs = generate_taker_fee_tx_outputs(coin, &dex_fee).map_err(TradePreimageError::InternalError)?; let gas_fee = None; let fee_amount = coin @@ -4748,8 +4889,12 @@ pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> } #[inline] -pub fn derive_htlc_pubkey(coin: &dyn SwapOps, swap_unique_data: &[u8]) -> Vec { - coin.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() +pub fn derive_htlc_pubkey(coin: &dyn SwapOps, swap_unique_data: &[u8]) -> [u8; 33] { + coin.derive_htlc_key_pair(swap_unique_data) + .public_slice() + .to_vec() + .try_into() + .expect("valid pubkey length") } pub fn validate_other_pubkey(raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { @@ -5009,7 +5154,7 @@ pub async fn spend_maker_payment_v2( let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default() - .push_data(args.maker_secret) + .push_data(&args.maker_secret) .push_opcode(Opcode::OP_1) .push_opcode(Opcode::OP_0) .into_script(); @@ -5182,42 +5327,104 @@ fn test_tx_v_size() { } #[test] -fn test_generate_taker_fee_tx_outputs() { - let amount = BigDecimal::from(6150); - let fee_amount = sat_from_big_decimal(&amount, 8).unwrap(); +fn test_generate_taker_fee_tx_outputs_with_standard_dex_fee() { + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(NativeClientImpl::default()))); + let mut fields = utxo_coin_fields_for_test(client, None, false); + fields.conf.ticker = "MYCOIN1".to_owned(); + let coin = utxo_coin_from_fields(fields); + + let fee_amount = BigDecimal::from(6150); + let fee_uamount = sat_from_big_decimal(&fee_amount, 8).unwrap(); + // TODO: replace with error result ('dex fee must contain burn amount') when nodes are upgraded let outputs = generate_taker_fee_tx_outputs( - 8, - &AddressHashEnum::default_address_hash(), - &DexFee::Standard(amount.into()), + &coin, + &DexFee::create_from_fields(fee_amount.into(), 0.into(), "MYCOIN1"), ) .unwrap(); - assert_eq!(outputs.len(), 1); + let dex_address = dex_address(&coin).unwrap(); - assert_eq!(outputs[0].value, fee_amount); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].value, fee_uamount); + assert_eq!( + outputs[0].script_pubkey, + Builder::build_p2pkh(dex_address.hash()).to_bytes() + ); } #[test] -fn test_generate_taker_fee_tx_outputs_with_burn() { +fn test_generate_taker_fee_tx_outputs_with_non_kmd_burn() { + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(NativeClientImpl::default()))); + let mut fields = utxo_coin_fields_for_test(client, None, false); + fields.conf.ticker = "MYCOIN1".to_owned(); + let coin = utxo_coin_from_fields(fields); + let fee_amount = BigDecimal::from(6150); let burn_amount = &(&fee_amount / &BigDecimal::from_str("0.75").unwrap()) - &fee_amount; - let fee_uamount = sat_from_big_decimal(&fee_amount, 8).unwrap(); let burn_uamount = sat_from_big_decimal(&burn_amount, 8).unwrap(); let outputs = generate_taker_fee_tx_outputs( - 8, - &AddressHashEnum::default_address_hash(), - &DexFee::with_burn(fee_amount.into(), burn_amount.into()), + &coin, + &DexFee::create_from_fields(fee_amount.into(), burn_amount.into(), "MYCOIN1"), ) .unwrap(); - assert_eq!(outputs.len(), 2); + let dex_address = dex_address(&coin).unwrap(); + let burn_address = burn_address(&coin).unwrap(); + assert_eq!(outputs.len(), 2); assert_eq!(outputs[0].value, fee_uamount); + assert_eq!( + outputs[0].script_pubkey, + Builder::build_p2pkh(dex_address.hash()).to_bytes() + ); + assert_eq!(outputs[1].value, burn_uamount); + assert_eq!( + outputs[1].script_pubkey, + Builder::build_p2pkh(burn_address.hash()).to_bytes() + ); +} + +#[test] +fn test_generate_taker_fee_tx_outputs_with_kmd_burn() { + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(NativeClientImpl::default()))); + let mut fields = utxo_coin_fields_for_test(client, None, false); + fields.conf.ticker = "KMD".to_owned(); + let coin = utxo_coin_from_fields(fields); + let fee_amount = BigDecimal::from(6150); + let burn_amount = &(&fee_amount / &BigDecimal::from_str("0.75").unwrap()) - &fee_amount; + let fee_uamount = sat_from_big_decimal(&fee_amount, 8).unwrap(); + let burn_uamount = sat_from_big_decimal(&burn_amount, 8).unwrap(); + + let outputs = generate_taker_fee_tx_outputs( + &coin, + &DexFee::create_from_fields(fee_amount.into(), burn_amount.into(), "KMD"), + ) + .unwrap(); + + let dex_address = address_from_raw_pubkey( + coin.dex_pubkey(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.bech32_hrp.clone(), + coin.addr_format().clone(), + ) + .unwrap(); + + assert_eq!(outputs.len(), 2); + assert_eq!(outputs[0].value, fee_uamount); + assert_eq!( + outputs[0].script_pubkey, + Builder::build_p2pkh(dex_address.hash()).to_bytes() + ); assert_eq!(outputs[1].value, burn_uamount); + assert_eq!( + outputs[1].script_pubkey, + Builder::default().push_opcode(Opcode::OP_RETURN).into_bytes() + ); } #[test] 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 32577de810..01f9750b03 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 @@ -1,7 +1,7 @@ use crate::coin_balance::CoinBalanceReportOps; -use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps, HDWalletOps}; -use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, MyTxHistoryErrorV2, MyTxHistoryTarget, - TxDetailsBuilder, TxHistoryStorage}; +use crate::hd_wallet::{DisplayAddress, HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps, HDWalletOps}; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, + TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::rpc_clients::{electrum_script_hash, ElectrumClient, NativeClient, UtxoRpcClientEnum}; use crate::utxo::utxo_common::{big_decimal_from_sat, HISTORY_TOO_LARGE_ERROR}; @@ -51,10 +51,6 @@ where (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressId(hd_address_id)) => { get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await }, - (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressDerivationPath(derivation_path)) => { - let hd_address_id = HDPathAccountToAddressId::from(derivation_path); - get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await - }, (DerivationMethod::HDWallet(_), target) => MmError::err(MyTxHistoryErrorV2::with_expected_target( target, "an HD account/address", @@ -199,6 +195,7 @@ where let fee = verbose_tx.vin.iter().fold(0., |cur, input| { let fee = match input { TransactionInputEnum::Lelantus(lelantus) => lelantus.n_fees, + TransactionInputEnum::Spark(spark) => spark.n_fees, _ => 0., }; cur + fee diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 03fea53699..d432207d8e 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -147,9 +147,8 @@ pub(super) fn utxo_coin_fields_for_test( check_utxo_maturity: false, block_headers_status_notifier: None, block_headers_status_watcher: None, + ctx: MmWeak::default(), abortable_system: AbortableQueue::default(), - scripthash_notification_handler: None, - ctx: Default::default(), } } @@ -271,6 +270,7 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { hd_accounts.insert(0, hd_account_for_test); let mut fields = utxo_coin_fields_for_test(rpc_client.into(), None, false); + fields.ctx = ctx.weak(); fields.conf.ticker = "DOC".to_string(); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { inner: HDWallet { @@ -291,6 +291,7 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { coin.clone(), storage, ctx.metrics.clone(), + ctx.event_stream_manager.clone(), current_balances.clone(), )); @@ -316,6 +317,7 @@ pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { coin.clone(), storage, ctx.metrics.clone(), + ctx.event_stream_manager.clone(), current_balances, )); diff --git a/mm2src/coins/utxo/utxo_hd_wallet.rs b/mm2src/coins/utxo/utxo_hd_wallet.rs index 5297157e33..93a9cad110 100644 --- a/mm2src/coins/utxo/utxo_hd_wallet.rs +++ b/mm2src/coins/utxo/utxo_hd_wallet.rs @@ -1,9 +1,9 @@ -use crate::hd_wallet::{HDAccount, HDAccountMut, HDAccountOps, HDAccountsMap, HDAccountsMut, HDAccountsMutex, - HDAddress, HDWallet, HDWalletCoinStorage, HDWalletOps, HDWalletStorageOps, +use crate::hd_wallet::{DisplayAddress, HDAccount, HDAccountMut, HDAccountOps, HDAccountsMap, HDAccountsMut, + HDAccountsMutex, HDAddress, HDWallet, HDWalletCoinStorage, HDWalletOps, HDWalletStorageOps, WithdrawSenderAddress}; use async_trait::async_trait; use crypto::{Bip44Chain, HDPathToCoin, Secp256k1ExtendedPublicKey}; -use keys::{Address, AddressFormat as UtxoAddressFormat, Public}; +use keys::{Address, AddressFormat as UtxoAddressFormat, CashAddress, Public}; pub type UtxoHDAddress = HDAddress; pub type UtxoHDAccount = HDAccount; @@ -56,3 +56,11 @@ impl HDWalletOps for UtxoHDWallet { self.inner.get_enabled_address().await } } + +impl DisplayAddress for Address { + fn display_address(&self) -> String { self.to_string() } +} + +impl DisplayAddress for CashAddress { + fn display_address(&self) -> String { self.encode().expect("A valid cash address") } +} diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index f5a02f5095..99a97d846f 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -23,27 +23,26 @@ use crate::utxo::utxo_hd_wallet::{UtxoHDAccount, UtxoHDAddress}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinBalanceMap, CoinWithDerivationMethod, - CoinWithPrivKeyPolicy, CommonSwapOpsV2, ConfirmPaymentInput, DexFee, FundingTxSpend, GenPreimageResult, - GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, IguanaBalanceOps, - IguanaPrivKey, MakerCoinSwapOpsV2, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, - RawTransactionRequest, RawTransactionResult, RefundError, RefundFundingSecretArgs, - RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundPaymentArgs, RefundResult, + CoinWithPrivKeyPolicy, CommonSwapOpsV2, ConfirmPaymentInput, DexFee, FindPaymentSpendError, + FundingTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MakerCoinSwapOpsV2, MmCoinEnum, + NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, + RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundPaymentArgs, RefundTakerPaymentArgs, SearchForFundingSpendErr, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, - TakerCoinSwapOpsV2, TakerSwapMakerCoin, ToBytes, TradePreimageValue, TransactionFut, TransactionResult, - TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateMakerPaymentArgs, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, - ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, - ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WaitForPaymentSpendError, - WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + TakerCoinSwapOpsV2, ToBytes, TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, + TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateMakerPaymentArgs, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageResult, + ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; use futures::{FutureExt, TryFutureExt}; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; +#[cfg(test)] use mocktopus::macros::*; use script::Opcode; use utxo_signer::UtxoSignerOps; @@ -281,7 +280,7 @@ impl UtxoCommonOps for UtxoStandardCoin { impl UtxoStandardOps for UtxoStandardCoin { async fn tx_details_by_hash( &self, - hash: &[u8], + hash: &H256Json, input_transactions: &mut HistoryUtxoTxMap, ) -> Result { utxo_common::tx_details_by_hash(self, hash, input_transactions).await @@ -301,18 +300,11 @@ impl UtxoStandardOps for UtxoStandardCoin { } #[async_trait] +#[cfg_attr(test, mockable)] impl SwapOps for UtxoStandardCoin { #[inline] - async fn send_taker_fee( - &self, - fee_addr: &[u8], - dex_fee: DexFee, - _uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { - utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) - .compat() - .await + async fn send_taker_fee(&self, dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionResult { + utxo_common::send_taker_fee(self.clone(), dex_fee).compat().await } #[inline] @@ -370,9 +362,8 @@ impl SwapOps for UtxoStandardCoin { tx, utxo_common::DEFAULT_FEE_VOUT, validate_fee_args.expected_sender, - validate_fee_args.dex_fee, + validate_fee_args.dex_fee.clone(), validate_fee_args.min_block_number, - validate_fee_args.fee_addr, ) .compat() .await @@ -424,18 +415,13 @@ impl SwapOps for UtxoStandardCoin { utxo_common::search_for_swap_tx_spend_other(self, input, utxo_common::DEFAULT_SWAP_VOUT).await } - #[inline] - fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { - utxo_common::check_all_inputs_signed_by_pub(tx, expected_pub) - } - #[inline] async fn extract_secret( &self, secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -446,14 +432,6 @@ impl SwapOps for UtxoStandardCoin { .map_err(|e| ERRL!("{}", e)) } - fn is_auto_refundable(&self) -> bool { false } - - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) - } - #[inline] fn negotiate_swap_contract_addr( &self, @@ -466,7 +444,7 @@ impl SwapOps for UtxoStandardCoin { utxo_common::derive_htlc_key_pair(self.as_ref(), swap_unique_data) } - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> [u8; 33] { utxo_common::derive_htlc_pubkey(self, swap_unique_data) } @@ -475,53 +453,9 @@ impl SwapOps for UtxoStandardCoin { utxo_common::validate_other_pubkey(raw_pubkey) } - async fn maker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - async fn taker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - fn is_supported_by_watchers(&self) -> bool { true } } -#[async_trait] -impl TakerSwapMakerCoin for UtxoStandardCoin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for UtxoStandardCoin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - #[async_trait] impl WatcherOps for UtxoStandardCoin { #[inline] @@ -857,21 +791,23 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { async fn sign_and_broadcast_taker_payment_spend( &self, - preimage: &TxPreimageWithSig, + preimage: Option<&TxPreimageWithSig>, gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], ) -> Result { + let preimage = preimage + .ok_or_else(|| TransactionErr::Plain(ERRL!("taker_payment_spend_preimage must be Some for UTXO coin")))?; let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); utxo_common::sign_and_broadcast_taker_payment_spend(self, preimage, gen_args, secret, &htlc_keypair).await } - async fn wait_for_taker_payment_spend( + async fn find_taker_payment_spend_tx( &self, taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult { + ) -> MmResult { let res = utxo_common::wait_for_output_spend_impl( self.as_ref(), taker_payment, @@ -883,6 +819,10 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { .await?; Ok(res) } + + async fn extract_secret_v2(&self, secret_hash: &[u8], spend_tx: &Self::Tx) -> Result<[u8; 32], String> { + utxo_common::extract_secret_v2(secret_hash, spend_tx) + } } impl CommonSwapOpsV2 for UtxoStandardCoin { @@ -894,6 +834,11 @@ impl CommonSwapOpsV2 for UtxoStandardCoin { fn derive_htlc_pubkey_v2_bytes(&self, swap_unique_data: &[u8]) -> Vec { self.derive_htlc_pubkey_v2(swap_unique_data).to_bytes() } + + #[inline(always)] + fn taker_pubkey_bytes(&self) -> Option> { + Some(self.derive_htlc_pubkey_v2(&[]).to_bytes()) // unique_data not used for non-private coins + } } #[async_trait] @@ -970,6 +915,10 @@ impl MarketCoinOps for UtxoStandardCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + fn is_kmd(&self) -> bool { &self.utxo_arc.conf.ticker == "KMD" } + + fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } @@ -977,7 +926,7 @@ impl MarketCoinOps for UtxoStandardCoin { impl MmCoin for UtxoStandardCoin { fn is_asset_chain(&self) -> bool { utxo_common::is_asset_chain(&self.utxo_arc) } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut { Box::new(utxo_common::get_raw_transaction(&self.utxo_arc, req).boxed().compat()) diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index bcd7cc991f..7b9d39d38b 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -13,9 +13,9 @@ use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, S use crate::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin, QtumDelegationOps, QtumDelegationRequest}; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::rpc_clients::{BlockHashOrHeight, NativeUnspent}; -use crate::utxo::rpc_clients::{ElectrumBalance, ElectrumClient, ElectrumClientImpl, ElectrumClientSettings, - GetAddressInfoRes, ListSinceBlockRes, NativeClient, NativeClientImpl, NetworkInfo, - UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; +use crate::utxo::rpc_clients::{ElectrumBalance, ElectrumBlockHeader, ElectrumClient, ElectrumClientImpl, + ElectrumClientSettings, GetAddressInfoRes, ListSinceBlockRes, NativeClient, + NativeClientImpl, NetworkInfo, UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; use crate::utxo::spv::SimplePaymentVerification; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::utxo_block_header_storage::{BlockHeaderStorage, SqliteBlockHeadersStorage}; @@ -40,15 +40,18 @@ use crypto::{privkey::key_pair_from_seed, Bip44Chain, HDPathToAccount, RpcDeriva use db_common::sqlite::rusqlite::Connection; use futures::channel::mpsc::channel; use futures::future::{join_all, Either, FutureExt, TryFutureExt}; +use hex::FromHex; use keys::prefixes::*; use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_event_stream::StreamingManager; use mm2_number::bigdecimal::{BigDecimal, Signed}; +use mm2_number::MmNumber; use mm2_test_helpers::electrums::doc_electrums; use mm2_test_helpers::for_tests::{electrum_servers_rpc, mm_ctx_with_custom_db, DOC_ELECTRUM_ADDRS, MARTY_ELECTRUM_ADDRS, T_BCH_ELECTRUMS}; use mocktopus::mocking::*; use rpc::v1::types::H256 as H256Json; -use serialization::{deserialize, CoinVariant}; +use serialization::{deserialize, CoinVariant, CompactInteger, Reader}; use spv_validation::conf::{BlockHeaderValidationParams, SPVBlockHeader}; use spv_validation::storage::BlockHeaderStorageOps; use spv_validation::work::DifficultyAlgorithm; @@ -85,7 +88,7 @@ pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); let abortable_system = AbortableQueue::default(); - block_on(builder.electrum_client(abortable_system, args, servers, (None, None), None)).unwrap() + block_on(builder.electrum_client(abortable_system, args, servers, (None, None))).unwrap() } /// Returned client won't work by default, requires some mocks to be usable @@ -103,7 +106,7 @@ fn utxo_coin_for_test( /// Returns `TransactionDetails` of the given `tx_hash` via [`UtxoStandardOps::tx_details_by_hash`]. #[track_caller] fn get_tx_details_by_hash(coin: &Coin, tx_hash: &str) -> TransactionDetails { - let hash = hex::decode(tx_hash).unwrap(); + let hash = <[u8; 32]>::from_hex(tx_hash).unwrap().into(); let mut input_transactions = HistoryUtxoTxMap::new(); block_on(UtxoStandardOps::tx_details_by_hash( @@ -122,7 +125,7 @@ where let my_addresses = block_on(coin.my_addresses()).unwrap(); let (_ctx, storage) = init_storage_for(coin); let params = UtxoTxDetailsParams { - hash: &hex::decode(tx_hash).unwrap().as_slice().into(), + hash: &<[u8; 32]>::from_hex(tx_hash).unwrap().into(), block_height_and_time: Some(BlockHeightAndTime { height, timestamp }), storage: &storage, my_addresses: &my_addresses, @@ -151,7 +154,8 @@ fn test_extract_secret() { let coin = utxo_coin_for_test(client.into(), None, false); let tx_hex = hex::decode("0400008085202f890125236f423b7f585e6a86d8a6c45c6805bbd5823851a57a00f6dcd3a41dc7487500000000d8483045022100ce7246314170b7c84df41a9d987dad5b572cfca5c27ee738d2682ce147c460a402206fa477fc27bec62600b13ea8a3f81fbad1fa9adad28bc1fa5c212a12ecdccd7f01205c62072b57b6473aeee6d35270c8b56d86975e6d6d4245b25425d771239fae32004c6b630476ac3765b1752103242d9cb2168968d785f6914c494c303ff1c27ba0ad882dbc3c15cfa773ea953cac6782012088a914f95ae6f5fb6a4c4e69b00b4c1dbc0698746c0f0288210210e0f210673a2024d4021270bb711664a637bb542317ed9be5ad592475320c0cac68ffffffff0128230000000000001976a9142c445a7af3da3feb2ba7d5f2a32002c772acc1e188ac76ac3765000000000000000000000000000000").unwrap(); - let expected_secret = hex::decode("5c62072b57b6473aeee6d35270c8b56d86975e6d6d4245b25425d771239fae32").unwrap(); + let expected_secret = + <[u8; 32]>::from_hex("5c62072b57b6473aeee6d35270c8b56d86975e6d6d4245b25425d771239fae32").unwrap(); let secret_hash = &*dhash160(&expected_secret); let secret = block_on(coin.extract_secret(secret_hash, &tx_hex, false)).unwrap(); assert_eq!(secret, expected_secret); @@ -481,8 +485,8 @@ fn test_wait_for_payment_spend_timeout_electrum() { client_settings, Default::default(), block_headers_storage, + StreamingManager::default(), abortable_system, - None, ) .expect("Expected electrum_client_impl constructed without a problem"); let client = UtxoRpcClientEnum::Electrum(client); @@ -603,15 +607,12 @@ fn test_withdraw_impl_set_fixed_fee() { let withdraw_req = WithdrawRequest { amount: 1u64.into(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoFixed { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; let expected = Some( UtxoFeeDetails { @@ -652,15 +653,12 @@ fn test_withdraw_impl_sat_per_kb_fee() { let withdraw_req = WithdrawRequest { amount: 1u64.into(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; // The resulting transaction size might be 244 or 245 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -704,15 +702,12 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { let withdraw_req = WithdrawRequest { amount: "9.9789".parse().unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -758,15 +753,12 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() let withdraw_req = WithdrawRequest { amount: "9.9789".parse().unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.09999999".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -812,15 +804,12 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { let withdraw_req = WithdrawRequest { amount: "9.97939455".parse().unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), - max: false, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; block_on_f01(coin.withdraw(withdraw_req)).unwrap_err(); } @@ -853,15 +842,13 @@ fn test_withdraw_impl_sat_per_kb_fee_max() { let withdraw_req = WithdrawRequest { amount: 0u64.into(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: TEST_COIN_NAME.into(), max: true, fee: Some(WithdrawFee::UtxoPerKbyte { amount: "0.1".parse().unwrap(), }), - memo: None, - ibc_source_channel: None, + ..Default::default() }; // The resulting transaction size might be 210 or 211 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -909,7 +896,7 @@ fn test_withdraw_kmd_rewards_impl( UtxoStandardCoin::get_current_mtp .mock_safe(move |_fields| MockResult::Return(Box::pin(futures::future::ok(current_mtp)))); NativeClient::get_verbose_transaction.mock_safe(move |_coin, txid| { - let expected: H256Json = hex::decode(tx_hash).unwrap().as_slice().into(); + let expected: H256Json = <[u8; 32]>::from_hex(tx_hash).unwrap().into(); assert_eq!(*txid, expected); MockResult::Return(Box::new(futures01::future::ok(verbose.clone()))) }); @@ -922,13 +909,9 @@ fn test_withdraw_kmd_rewards_impl( let withdraw_req = WithdrawRequest { amount: BigDecimal::from_str("0.00001").unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: "KMD".to_owned(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -1004,13 +987,9 @@ fn test_withdraw_rick_rewards_none() { let withdraw_req = WithdrawRequest { amount: BigDecimal::from_str("0.00001").unwrap(), - from: None, to: "RQq6fWoy8aGGMLjvRfMY5mBNVm2RQxJyLa".to_string(), coin: "RICK".to_owned(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -1543,13 +1522,13 @@ fn test_network_info_negative_time_offset() { #[test] fn test_unavailable_electrum_proto_version() { ElectrumClientImpl::try_new_arc.mock_safe( - |client_settings, block_headers_storage, abortable_system, event_handlers, scripthash_notification_sender| { + |client_settings, block_headers_storage, streaming_manager, abortable_system, event_handlers| { MockResult::Return(ElectrumClientImpl::with_protocol_version( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, OrdRange::new(1.8, 1.9).unwrap(), )) }, @@ -1622,25 +1601,26 @@ fn test_spam_rick() { #[test] fn test_one_unavailable_electrum_proto_version() { - // Patch the electurm client construct to require protocol version 1.4 only. + // First mock with an unrealistically high version requirement that no server would support ElectrumClientImpl::try_new_arc.mock_safe( - |client_settings, block_headers_storage, abortable_system, event_handlers, scripthash_notification_sender| { + |client_settings, block_headers_storage, streaming_manager, abortable_system, event_handlers| { MockResult::Return(ElectrumClientImpl::with_protocol_version( client_settings, block_headers_storage, + streaming_manager, abortable_system, event_handlers, - scripthash_notification_sender, - OrdRange::new(1.4, 1.4).unwrap(), + OrdRange::new(7.4, 7.4).unwrap(), )) }, ); - // check if the electrum-mona.bitbank.cc:50001 doesn't support the protocol version 1.4 - let client = electrum_client_for_test(&["electrum-mona.bitbank.cc:50001"]); + + // Try to connect with the high version requirement - should fail + let client = electrum_client_for_test(&["electrum1.cipig.net:10000"]); // When an electrum server doesn't support our protocol version range, it gets removed by the client, // wait a little bit to make sure this is the case. block_on(Timer::sleep(2.)); - let error = block_on_f01(client.get_block_count_from("electrum-mona.bitbank.cc:50001")) + let error = block_on_f01(client.get_block_count_from("electrum1.cipig.net:10000")) .unwrap_err() .to_string(); log!("{}", error); @@ -1649,11 +1629,24 @@ fn test_one_unavailable_electrum_proto_version() { drop(client); log!("Run BTC coin to test the server.version loop"); + // Now reset the mock to a supported version + ElectrumClientImpl::try_new_arc.mock_safe( + |client_settings, block_headers_storage, streaming_manager, abortable_system, event_handlers| { + MockResult::Return(ElectrumClientImpl::with_protocol_version( + client_settings, + block_headers_storage, + streaming_manager, + abortable_system, + event_handlers, + OrdRange::new(1.4, 1.4).unwrap(), + )) + }, + ); + let conf = json!({"coin":"BTC","asset":"BTC","rpcport":8332}); let req = json!({ "method": "electrum", - // electrum-mona.bitbank.cc:50001 supports only 1.2 protocol version - "servers": [{"url":"electrum1.cipig.net:10000"},{"url":"electrum-mona.bitbank.cc:50001"}], + "servers": [{"url":"electrum1.cipig.net:10000"}], }); let ctx = MmCtxBuilder::new().into_mm_arc(); @@ -1718,7 +1711,7 @@ fn test_qtum_add_delegation() { ) .unwrap(); let request = QtumDelegationRequest { - address: address.to_string(), + validator_address: address.to_string(), fee: Some(10), }; let res = block_on_f01(coin.add_delegation(request)).unwrap(); @@ -1728,7 +1721,7 @@ fn test_qtum_add_delegation() { assert!(res.spent_by_me > res.received_by_me); let request = QtumDelegationRequest { - address: "fake_address".to_string(), + validator_address: "fake_address".to_string(), fee: Some(10), }; let res = block_on_f01(coin.add_delegation(request)); @@ -1761,7 +1754,7 @@ fn test_qtum_add_delegation_on_already_delegating() { ) .unwrap(); let request = QtumDelegationRequest { - address: address.to_string(), + validator_address: address.to_string(), fee: Some(10), }; let res = block_on_f01(coin.add_delegation(request)); @@ -1933,7 +1926,7 @@ fn test_get_mature_unspent_ordered_map_from_cache_impl( expected_confs: u32, ) { const TX_HASH: &str = "b43f9ed47f7b97d4766b6f1614136fa0c55b9a52c97342428333521fa13ad714"; - let tx_hash: H256Json = hex::decode(TX_HASH).unwrap().as_slice().into(); + let tx_hash: H256Json = <[u8; 32]>::from_hex(TX_HASH).unwrap().into(); let client = electrum_client_for_test(DOC_ELECTRUM_ADDRS); let mut verbose = block_on_f01(client.get_verbose_transaction(&tx_hash)).unwrap(); verbose.confirmations = cached_confs; @@ -2522,14 +2515,13 @@ fn test_find_output_spend_skips_conflicting_transactions() { const LIST_SINCE_BLOCK_JSON: &str = r#"{"transactions":[{"involvesWatchonly":true,"account":"","address":"RAsbVN52LC2hEp3UWWSLbV8pJ8CneKjW9F","category":"send","amount":-0.01537462,"vout":0,"fee":-0.00001000,"rawconfirmations":-1,"confirmations":-1,"txid":"220c337006b2581c3da734ef9f1106601e8538ebab823d0dd6719a4d4580fd04","walletconflicts":["a2144bee4eac4b41ab1aed2dd8f854785b3ddebd617d48696dd84e62d129544b"],"time":1607831631,"timereceived":1607831631,"vjoinsplit":[],"size":320},{"involvesWatchonly":true,"account":"","address":"RAsbVN52LC2hEp3UWWSLbV8pJ8CneKjW9F","category":"send","amount":-0.01537462,"vout":0,"fee":-0.00001000,"rawconfirmations":-1,"confirmations":-1,"txid":"6fb83afb1bf309515fa429814bf07552eea951656fdee913f3aa687d513cd720","walletconflicts":["4aad6471f59e5912349cd7679bc029bfbd5da54d34c235d20500249f98f549e4"],"time":1607831556,"timereceived":1607831556,"vjoinsplit":[],"size":320},{"account":"","address":"RT9MpMyucqXiX8bZLimXBnrrn2ofmdGNKd","category":"receive","amount":0.54623851,"vout":2,"rawconfirmations":1617,"confirmations":1617,"blockhash":"000000000c33a387d73180220a5a8f2fe6081bad9bdfc0dba5a9985abcee8294","blockindex":7,"blocktime":1607957613,"expiryheight":0,"txid":"45e4900a2b330800a356a74ce2a97370596ad3a25e689e3ed5c36e421d12bbf7","walletconflicts":[],"time":1607957175,"timereceived":1607957175,"vjoinsplit":[],"size":567},{"involvesWatchonly":true,"account":"","address":"RT9MpMyucqXiX8bZLimXBnrrn2ofmdGNKd","category":"send","amount":-0.00797200,"vout":0,"fee":-0.00001000,"rawconfirmations":-1,"confirmations":-1,"txid":"bfc99c06d1a060cdbeba05620dc1c6fdb7351eb4c04b7aae578688ca6aeaeafd","walletconflicts":[],"time":1607957792,"timereceived":1607957792,"vjoinsplit":[],"size":286}],"lastblock":"06082d363f78174fd13b126994210d3c3ad9d073ee3983ad59fe8b76e6e3e071"}"#; // in the json above this transaction is only one not conflicting const NON_CONFLICTING_TXID: &str = "45e4900a2b330800a356a74ce2a97370596ad3a25e689e3ed5c36e421d12bbf7"; - let expected_txid: H256Json = hex::decode(NON_CONFLICTING_TXID).unwrap().as_slice().into(); - + let expected_txid: H256Json = <[u8; 32]>::from_hex(NON_CONFLICTING_TXID).unwrap().into(); NativeClientImpl::get_block_hash.mock_safe(|_, _| { // no matter what we return here - let blockhash: H256Json = hex::decode("000000000c33a387d73180220a5a8f2fe6081bad9bdfc0dba5a9985abcee8294") - .unwrap() - .as_slice() - .into(); + let blockhash: H256Json = + <[u8; 32]>::from_hex("000000000c33a387d73180220a5a8f2fe6081bad9bdfc0dba5a9985abcee8294") + .unwrap() + .into(); MockResult::Return(Box::new(futures01::future::ok(blockhash))) }); @@ -2675,6 +2667,28 @@ fn test_get_sender_trade_fee_dynamic_tx_fee() { assert_eq!(fee1, fee3); } +// validate an old tx with no output with the burn account +// TODO: remove when we disable such old style txns +#[test] +fn test_validate_old_fee_tx() { + let rpc_client = electrum_client_for_test(MARTY_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Electrum(rpc_client), None, false); + let tx_bytes = hex::decode("0400008085202f8901033aedb3c3c02fc76c15b393c7b1f638cfa6b4a1d502e00d57ad5b5305f12221000000006a473044022074879aabf38ef943eba7e4ce54c444d2d6aa93ac3e60ea1d7d288d7f17231c5002205e1671a62d8c031ac15e0e8456357e54865b7acbf49c7ebcba78058fd886b4bd012103242d9cb2168968d785f6914c494c303ff1c27ba0ad882dbc3c15cfa773ea953cffffffff0210270000000000001976a914ca1e04745e8ca0c60d8c5881531d51bec470743f88ac4802d913000000001976a914902053231ef0541a7628c11acac40d30f2a127bd88ac008e3765000000000000000000000000000000").unwrap(); + let taker_fee_tx = coin.tx_enum_from_bytes(&tx_bytes).unwrap(); + let amount: MmNumber = "0.0001".parse::().unwrap().into(); + let dex_fee = DexFee::Standard(amount); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &taker_fee_tx, + expected_sender: &hex::decode("03242d9cb2168968d785f6914c494c303ff1c27ba0ad882dbc3c15cfa773ea953c").unwrap(), + dex_fee: &dex_fee, + min_block_number: 0, + uuid: &[], + }; + let result = block_on(coin.validate_fee(validate_fee_args)); + log!("result: {:?}", result); + assert!(result.is_ok()); +} + #[test] fn test_validate_fee_wrong_sender() { let rpc_client = electrum_client_for_test(MARTY_ELECTRUM_ADDRS); @@ -2686,7 +2700,6 @@ fn test_validate_fee_wrong_sender() { let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], @@ -2711,7 +2724,6 @@ fn test_validate_fee_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 278455, uuid: &[], @@ -2740,7 +2752,6 @@ fn test_validate_fee_bch_70_bytes_signature() { let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, expected_sender: &sender_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], @@ -2836,6 +2847,51 @@ fn firo_lelantus_tx_details() { assert_eq!(Some(expected_fee), tx_details.fee_details); } +#[test] +fn firo_spark_tx() { + // https://explorer.firo.org/tx/c50e5a3f16744ac86bacae28d9251a29bf754d250592bce16a953cd961b584d5 + let tx_hash = "c50e5a3f16744ac86bacae28d9251a29bf754d250592bce16a953cd961b584d5".into(); + let electrum = electrum_client_for_test(&[ + "electrumx01.firo.org:50001", + "electrumx02.firo.org:50001", + "electrumx03.firo.org:50001", + ]); + let _tx = block_on_f01(electrum.get_verbose_transaction(&tx_hash)).unwrap(); +} + +#[test] +fn firo_spark_tx_details() { + // https://explorer.firo.org/tx/c50e5a3f16744ac86bacae28d9251a29bf754d250592bce16a953cd961b584d5 + let electrum = electrum_client_for_test(&[ + "electrumx01.firo.org:50001", + "electrumx02.firo.org:50001", + "electrumx03.firo.org:50001", + ]); + let coin = utxo_coin_for_test(electrum.into(), None, false); + + let tx_details = get_tx_details_eq_for_both_versions( + &coin, + "c50e5a3f16744ac86bacae28d9251a29bf754d250592bce16a953cd961b584d5", + ); + + let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), + amount: "0.00003603".parse().unwrap(), + }); + assert_eq!(Some(expected_fee), tx_details.fee_details); + + let tx_details = get_tx_details_eq_for_both_versions( + &coin, + "3b3da29d2ff910ce15e274355b12ff89917fb98a80f746e4a0bbb669ab732250", + ); + + let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { + coin: Some(TEST_COIN_NAME.into()), + amount: "0.00003603".parse().unwrap(), + }); + assert_eq!(Some(expected_fee), tx_details.fee_details); +} + #[test] fn test_generate_tx_doge_fee() { // A tx below 1kb is always 0,01 doge fee per kb. @@ -3204,13 +3260,9 @@ fn test_withdraw_to_p2pk_fails() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: "03f8f8fa2062590ba9a0a7a86f937de22f540c015864aad35a2a9f6766de906265".to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; assert!(matches!( @@ -3262,13 +3314,9 @@ fn test_withdraw_to_p2pkh() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: p2pkh_address.to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3322,13 +3370,9 @@ fn test_withdraw_to_p2sh() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: p2sh_address.to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3382,13 +3426,9 @@ fn test_withdraw_to_p2wpkh() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: p2wpkh_address.to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -3437,13 +3477,9 @@ fn test_withdraw_p2pk_balance() { let withdraw_req = WithdrawRequest { amount: 1.into(), - from: None, to: my_p2pkh_address.to_string(), coin: TEST_COIN_NAME.into(), - max: false, - fee: None, - memo: None, - ibc_source_channel: None, + ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); @@ -4080,8 +4116,6 @@ fn test_scan_for_new_addresses() { let client = NativeClient(Arc::new(NativeClientImpl::default())); let mut fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); - let ctx = MmCtxBuilder::new().into_mm_arc(); - fields.ctx = ctx.weak(); let mut hd_accounts = HDAccountsMap::new(); hd_accounts.insert(0, UtxoHDAccount { account_id: 0, @@ -4224,8 +4258,6 @@ fn test_get_new_address() { let client = NativeClient(Arc::new(NativeClientImpl::default())); let mut fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); - let ctx = MmCtxBuilder::new().into_mm_arc(); - fields.ctx = ctx.weak(); let mut hd_accounts = HDAccountsMap::new(); let hd_account_for_test = UtxoHDAccount { account_id: 0, @@ -4926,3 +4958,57 @@ fn test_block_header_utxo_loop_with_reorg() { panic!("Loop shouldn't stop") }; } + +#[test] +fn test_electrum_v14_block_hash() { + let client = electrum_client_for_test(DOC_ELECTRUM_ADDRS); + + // First verify BlockHeader hash implementation works correctly with a known reference block + let headers = + block_on_f01(client.blockchain_block_headers(841548, NonZeroU64::new(1).expect("Failed to create NonZeroU64"))) + .expect("Failed to fetch block headers"); + + // Deserialize the reference block header + let serialized = serialize(&CompactInteger::from(headers.count)) + .take() + .into_iter() + .chain(headers.hex.0) + .collect::>(); + let headers = Reader::new_with_coin_variant(&serialized, CoinVariant::RICK) + .read_list::() + .expect("Failed to deserialize headers"); + + // Confirm BlockHeader hash matches the known hash value + assert_eq!( + headers[0].hash().reversed().to_string(), + "0f0a6ce253b0536000636f85491db8030659064de8c27423b46ceef824d4ad28" + ); + + // Now get the latest block via V14 subscription to test its hash implementation + let header = + block_on_f01(client.blockchain_headers_subscribe()).expect("Failed to subscribe to blockchain headers"); + + // Extract hash and height from V14 header + let (hash, height) = match header { + ElectrumBlockHeader::V14(header) => (header.hash(), header.height), + _ => panic!("Expected ElectrumBlockHeader::V14"), + }; + + // Get the same block data to create a BlockHeader for comparison + let headers = + block_on_f01(client.blockchain_block_headers(height, NonZeroU64::new(1).expect("Failed to create NonZeroU64"))) + .expect("Failed to fetch block headers"); + + // Create BlockHeader from the same block (using the implementation we just verified) + let serialized = serialize(&CompactInteger::from(headers.count)) + .take() + .into_iter() + .chain(headers.hex.0) + .collect::>(); + let headers = Reader::new_with_coin_variant(&serialized, CoinVariant::RICK) + .read_list::() + .expect("Failed to deserialize headers"); + + // Verify V14 header produces the same hash as our verified BlockHeader implementation + assert_eq!(hash, headers[0].hash().into()); +} diff --git a/mm2src/coins/utxo/utxo_tx_history_v2.rs b/mm2src/coins/utxo/utxo_tx_history_v2.rs index 698a9bf0b6..0231bdc4c7 100644 --- a/mm2src/coins/utxo/utxo_tx_history_v2.rs +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -1,19 +1,21 @@ use super::RequestTxHistoryResult; -use crate::hd_wallet::AddressDerivingError; -use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, TxHistoryStorage, TxHistoryStorageError}; +use crate::hd_wallet::{AddressDerivingError, DisplayAddress}; +use crate::my_tx_history_v2::{CoinWithTxHistoryV2, TxHistoryStorage, TxHistoryStorageError}; use crate::tx_history_storage::FilteringAddresses; use crate::utxo::bch::BchCoin; use crate::utxo::slp::ParseSlpScriptError; +use crate::utxo::tx_history_events::TxHistoryEventStreamer; use crate::utxo::{utxo_common, AddrFromStrError, GetBlockHeaderError}; use crate::{BalanceError, BalanceResult, BlockHeightAndTime, CoinWithDerivationMethod, HistorySyncState, - MarketCoinOps, NumConversError, ParseBigDecimalError, TransactionDetails, UnexpectedDerivationMethod, - UtxoRpcError, UtxoTx}; + MarketCoinOps, MmCoin, NumConversError, ParseBigDecimalError, TransactionDetails, + UnexpectedDerivationMethod, UtxoRpcError, UtxoTx}; use async_trait::async_trait; use common::executor::Timer; use common::log::{error, info}; use derive_more::Display; use keys::Address; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use mm2_state_machine::prelude::*; @@ -104,7 +106,7 @@ pub struct UtxoTxDetailsParams<'a, Storage> { #[async_trait] pub trait UtxoTxHistoryOps: - CoinWithTxHistoryV2 + CoinWithDerivationMethod + MarketCoinOps + Send + Sync + 'static + CoinWithTxHistoryV2 + CoinWithDerivationMethod + MarketCoinOps + MmCoin + Send + Sync + 'static { /// Returns addresses for those we need to request Transaction history. async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError>; @@ -145,6 +147,8 @@ struct UtxoTxHistoryStateMachine`] everywhere. balances: HashMap, @@ -620,6 +624,12 @@ where }, }; + ctx.streaming_manager + .send_fn(&TxHistoryEventStreamer::derive_streamer_id(ctx.coin.ticker()), || { + tx_details.clone() + }) + .ok(); + if let Err(e) = ctx.storage.add_transactions_to_history(&wallet_id, tx_details).await { return Self::change_state(Stopped::storage_error(e)); } @@ -707,6 +717,7 @@ pub async fn bch_and_slp_history_loop( coin: BchCoin, storage: impl TxHistoryStorage, metrics: MetricsArc, + streaming_manager: StreamingManager, current_balance: Option, ) { let balances = match current_balance { @@ -743,6 +754,7 @@ pub async fn bch_and_slp_history_loop( coin, storage, metrics, + streaming_manager, balances, }; state_machine @@ -755,6 +767,7 @@ pub async fn utxo_history_loop( coin: Coin, storage: Storage, metrics: MetricsArc, + streaming_manager: StreamingManager, current_balances: HashMap, ) where Coin: UtxoTxHistoryOps, @@ -764,6 +777,7 @@ pub async fn utxo_history_loop( coin, storage, metrics, + streaming_manager, balances: current_balances, }; state_machine diff --git a/mm2src/coins/utxo/utxo_wasm_tests.rs b/mm2src/coins/utxo/utxo_wasm_tests.rs index bd059c8627..f6b1306fdd 100644 --- a/mm2src/coins/utxo/utxo_wasm_tests.rs +++ b/mm2src/coins/utxo/utxo_wasm_tests.rs @@ -4,6 +4,7 @@ use super::utxo_standard::UtxoStandardCoin; use super::*; use crate::utxo::utxo_common_tests; use crate::{IguanaPrivKey, PrivKeyBuildPolicy}; +use hex::FromHex; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::DOC_ELECTRUM_ADDRS; use serialization::deserialize; @@ -42,7 +43,7 @@ pub async fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); let abortable_system = AbortableQueue::default(); builder - .electrum_client(abortable_system, args, servers, (None, None), None) + .electrum_client(abortable_system, args, servers, (None, None)) .await .unwrap() } @@ -51,9 +52,8 @@ pub async fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { async fn test_electrum_rpc_client() { let client = electrum_client_for_test(DOC_ELECTRUM_ADDRS).await; - let tx_hash: H256Json = hex::decode("a3ebedbe20f82e43708f276152cf7dfb03a6050921c8f266e48c00ab66e891fb") + let tx_hash: H256Json = <[u8; 32]>::from_hex("a3ebedbe20f82e43708f276152cf7dfb03a6050921c8f266e48c00ab66e891fb") .unwrap() - .as_slice() .into(); let verbose_tx = client .get_verbose_transaction(&tx_hash) diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 8721a6a433..29ef2f5963 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -3,7 +3,7 @@ use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionData, TransactionDetails, - WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; + UnexpectedDerivationMethod, WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use chain::TransactionOutput; use common::log::info; @@ -91,6 +91,28 @@ impl From for WithdrawError { fn from(e: keys::Error) -> Self { WithdrawError::InternalError(e.to_string()) } } +fn derive_hd_key_pair( + coin: &Coin, + derivation_path: &DerivationPath, +) -> Result> +where + Coin: AsRef, +{ + let secret = coin + .as_ref() + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(derivation_path)?; + + let private = Private { + prefix: coin.as_ref().conf.wif_prefix, + secret, + compressed: true, + checksum_type: coin.as_ref().conf.checksum_type, + }; + + KeyPair::from_private(private).map_to_mm(|err| UnexpectedDerivationMethod::InternalError(err.to_string())) +} + #[async_trait] pub trait UtxoWithdraw where @@ -312,18 +334,18 @@ where .with_unsigned_tx(unsigned_tx); let sign_params = sign_params.build()?; - let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; - let hw_ctx = crypto_ctx - .hw_ctx() - .or_mm_err(|| WithdrawError::HwError(HwRpcError::NoTrezorDeviceAvailable))?; - - let sign_policy = match self.coin.as_ref().priv_key_policy { - PrivKeyPolicy::Iguana(ref key_pair) => SignPolicy::WithKeyPair(key_pair), - // InitUtxoWithdraw works only for hardware wallets so it's ok to use signing with activated keypair here as a placeholder. - PrivKeyPolicy::HDWallet { - activated_key: ref activated_key_pair, - .. - } => SignPolicy::WithKeyPair(activated_key_pair), + let signed = match self.coin.as_ref().priv_key_policy { + PrivKeyPolicy::Iguana(ref key_pair) => { + self.coin + .sign_tx(sign_params, SignPolicy::WithKeyPair(key_pair)) + .await? + }, + PrivKeyPolicy::HDWallet { .. } => { + let from_key_pair = derive_hd_key_pair(self.coin(), &self.from_derivation_path)?; + self.coin() + .sign_tx(sign_params, SignPolicy::WithKeyPair(&from_key_pair)) + .await? + }, PrivKeyPolicy::Trezor => { let trezor_statuses = TrezorRequestStatuses { on_button_request: WithdrawInProgressStatus::FollowHwDeviceInstructions, @@ -333,8 +355,16 @@ where }; let sign_processor = TrezorRpcTaskProcessor::new(self.task_handle.clone(), trezor_statuses); let sign_processor = Arc::new(sign_processor); + let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| WithdrawError::HwError(HwRpcError::NoTrezorDeviceAvailable))?; let trezor_session = hw_ctx.trezor(sign_processor).await?; - SignPolicy::WithTrezor(trezor_session) + self.task_handle + .update_in_progress_status(WithdrawInProgressStatus::WaitingForUserToConfirmSigning)?; + self.coin + .sign_tx(sign_params, SignPolicy::WithTrezor(trezor_session)) + .await? }, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => { @@ -344,10 +374,6 @@ where }, }; - self.task_handle - .update_in_progress_status(WithdrawInProgressStatus::WaitingForUserToConfirmSigning)?; - let signed = self.coin.sign_tx(sign_params, sign_policy).await?; - Ok(signed) } } @@ -437,19 +463,7 @@ where let from_address_string = from.address.display_address().map_to_mm(WithdrawError::InternalError)?; let key_pair = match from.derivation_path { - Some(der_path) => { - let secret = coin - .as_ref() - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&der_path)?; - let private = Private { - prefix: coin.as_ref().conf.wif_prefix, - secret, - compressed: true, - checksum_type: coin.as_ref().conf.checksum_type, - }; - KeyPair::from_private(private).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))? - }, + Some(der_path) => derive_hd_key_pair(&coin, &der_path)?, // [`WithdrawSenderAddress::derivation_path`] is not set, but the coin is initialized with an HD wallet derivation method. None if coin.has_hd_wallet_derivation_method() => { let error = "Cannot determine 'from' address derivation path".to_owned(); diff --git a/mm2src/coins/utxo_signer/src/sign_common.rs b/mm2src/coins/utxo_signer/src/sign_common.rs index 1ba966aa6b..aa2ab355ef 100644 --- a/mm2src/coins/utxo_signer/src/sign_common.rs +++ b/mm2src/coins/utxo_signer/src/sign_common.rs @@ -26,6 +26,7 @@ pub(crate) fn complete_tx(unsigned: TransactionInputSigner, signed_inputs: Vec, - z_balance_event_handler: Option, } impl Transaction for ZTransaction { @@ -281,12 +279,19 @@ impl ZCoin { /// Asynchronously checks the synchronization status and returns `true` if /// the Sapling state has finished synchronizing, meaning that the block number is available. /// Otherwise, it returns `false`. + #[cfg(any(test, feature = "run-docker-tests"))] #[inline] pub async fn is_sapling_state_synced(&self) -> bool { - matches!( - self.sync_status().await, - Ok(SyncStatus::Finished { block_number: _, .. }) - ) + use futures::StreamExt; + + let mut watcher = self.z_fields.sync_state_connector.lock().await; + while let Some(sync) = watcher.sync_watcher.next().await { + if matches!(sync, SyncStatus::Finished { .. }) { + return true; + } + } + + false } #[inline] @@ -520,7 +525,7 @@ impl ZCoin { fn tx_details_from_db_item( &self, tx_item: ZCoinTxHistoryItem, - transactions: &mut HashMap, + transactions: &HashMap, prev_transactions: &HashMap, current_block: u64, ) -> Result> { @@ -532,8 +537,8 @@ impl ZCoin { } let mut transparent_input_amount = Amount::zero(); - let hash = H256Json::from(tx_item.tx_hash.as_slice()); - let z_tx = transactions.remove(&hash).or_mm_err(|| NoInfoAboutTx(hash))?; + let hash = H256Json::from(tx_item.tx_hash); + let z_tx = transactions.get(&hash).or_mm_err(|| NoInfoAboutTx(hash))?; for input in z_tx.vin.iter() { let mut hash = H256Json::from(*input.prevout.hash()); hash.0.reverse(); @@ -622,9 +627,9 @@ impl ZCoin { let hashes_for_verbose = req_result .transactions .iter() - .map(|item| H256Json::from(item.tx_hash.as_slice())) + .map(|item| H256Json::from(item.tx_hash)) .collect(); - let mut transactions = self.z_transactions_from_cache_or_rpc(hashes_for_verbose).await?; + let transactions = self.z_transactions_from_cache_or_rpc(hashes_for_verbose).await?; let prev_tx_hashes: HashSet<_> = transactions .iter() @@ -641,9 +646,7 @@ impl ZCoin { let transactions = req_result .transactions .into_iter() - .map(|sql_item| { - self.tx_details_from_db_item(sql_item, &mut transactions, &prev_transactions, current_block) - }) + .map(|sql_item| self.tx_details_from_db_item(sql_item, &transactions, &prev_transactions, current_block)) .collect::>()?; Ok(MyTxHistoryResponseV2 { @@ -661,15 +664,32 @@ impl ZCoin { }) } - async fn spawn_balance_stream_if_enabled(&self, ctx: &MmArc) -> Result<(), String> { - let coin = self.clone(); - if let Some(stream_config) = &ctx.event_stream_configuration { - if let EventInitStatus::Failed(err) = EventBehaviour::spawn_if_active(coin, stream_config).await { - return ERR!("Failed spawning zcoin balance event with error: {}", err); - } + /// Validates dex fee output or burn output + /// Returns true if the output valid or error if not valid. Returns false if could not decrypt output (some other output) + fn validate_dex_fee_output( + &self, + shielded_out: &OutputDescription, + ovk: &OutgoingViewingKey, + expected_address: &PaymentAddress, + block_height: BlockHeight, + amount_sat: u64, + expected_memo: &MemoBytes, + ) -> Result { + let Some((note, address, memo)) = + try_sapling_output_recovery(self.consensus_params_ref(), block_height, ovk, shielded_out) + else { + return Ok(false); + }; + if &address == expected_address { + return Ok(false); } - - Ok(()) + if note.value != amount_sat { + return Err(format!("invalid amount {}, expected {}", note.value, amount_sat)); + } + if &memo != expected_memo { + return Err(format!("invalid memo {:?}, expected {:?}", memo, expected_memo)); + } + Ok(true) } } @@ -768,19 +788,38 @@ pub enum ZcoinRpcMode { } #[derive(Clone, Deserialize)] +#[serde(default)] pub struct ZcoinActivationParams { pub mode: ZcoinRpcMode, pub required_confirmations: Option, pub requires_notarization: Option, pub zcash_params_path: Option, - #[serde(default = "one_thousand_u32")] - pub scan_blocks_per_iteration: u32, - #[serde(default)] + pub scan_blocks_per_iteration: NonZeroU32, pub scan_interval_ms: u64, - #[serde(default)] pub account: u32, } +impl Default for ZcoinActivationParams { + fn default() -> Self { + Self { + mode: ZcoinRpcMode::Light { + electrum_servers: Vec::new(), + min_connected: None, + max_connected: None, + light_wallet_d_servers: Vec::new(), + sync_params: None, + skip_sync_params: None, + }, + required_confirmations: None, + requires_notarization: None, + zcash_params_path: None, + scan_blocks_per_iteration: NonZeroU32::new(1000).expect("1000 is a valid value"), + scan_interval_ms: Default::default(), + account: Default::default(), + } + } +} + pub async fn z_coin_from_conf_and_params( ctx: &MmArc, ticker: &str, @@ -835,6 +874,7 @@ pub struct ZCoinBuilder<'a> { z_coin_params: &'a ZcoinActivationParams, utxo_params: UtxoActivationParams, priv_key_policy: PrivKeyBuildPolicy, + #[cfg_attr(target_arch = "wasm32", allow(unused))] db_dir_path: PathBuf, /// `Some` if `ZCoin` should be initialized with a forced spending key. z_spending_key: Option, @@ -890,6 +930,13 @@ 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 dex_burn_addr = decode_payment_address( + self.protocol_info.consensus_params.hrp_sapling_payment_address(), + DEX_BURN_Z_ADDR, + ) + .expect("DEX_BURN_Z_ADDR is a valid z-address") + .expect("DEX_BURN_Z_ADDR is a valid z-address"); + 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(), @@ -897,24 +944,11 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { ); let blocks_db = self.init_blocks_db().await?; - let (z_balance_event_sender, z_balance_event_handler) = if self.ctx.event_stream_configuration.is_some() { - let (sender, receiver) = futures::channel::mpsc::unbounded(); - (Some(sender), Some(Arc::new(AsyncMutex::new(receiver)))) - } else { - (None, None) - }; let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => { - init_native_client( - &self, - self.native_client()?, - blocks_db, - &z_spending_key, - z_balance_event_sender, - ) - .await? + init_native_client(&self, self.native_client()?, blocks_db, &z_spending_key).await? }, ZcoinRpcMode::Light { light_wallet_d_servers, @@ -929,7 +963,6 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { sync_params, skip_sync_params.unwrap_or_default(), &z_spending_key, - z_balance_event_sender, ) .await? }, @@ -937,6 +970,7 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { let z_fields = Arc::new(ZCoinFields { dex_fee_addr, + dex_burn_addr, my_z_addr, my_z_addr_encoded, evk: ExtendedFullViewingKey::from(&z_spending_key), @@ -945,16 +979,9 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { light_wallet_db, consensus_params: self.protocol_info.consensus_params, sync_state_connector, - z_balance_event_handler, }); - let zcoin = ZCoin { utxo_arc, z_fields }; - zcoin - .spawn_balance_stream_if_enabled(self.ctx) - .await - .map_to_mm(ZCoinBuildError::FailedSpawningBalanceEvents)?; - - Ok(zcoin) + Ok(ZCoin { utxo_arc, z_fields }) } } @@ -1050,7 +1077,6 @@ impl<'a> ZCoinBuilder<'a> { .await .mm_err(|err| ZCoinBuildError::ZCashParamsError(err.to_string()))? { - // save params params_db .download_and_save_params() .await @@ -1067,19 +1093,27 @@ impl<'a> ZCoinBuilder<'a> { } } -/// Initialize `ZCoin` with a forced `z_spending_key`. -#[cfg(all(test, feature = "zhtlc-native-tests"))] +/// Initialize `ZCoin` with a forced `z_spending_key` for dockerized tests. +/// db_dir_path is where ZOMBIE_wallet.db located +/// Note that ZOMBIE_cache.db (db where blocks are downloaded to create ZOMBIE_wallet.db) is created in-memory (see BlockDbImpl::new fn) +#[cfg(any(test, feature = "run-docker-tests"))] #[allow(clippy::too_many_arguments)] -async fn z_coin_from_conf_and_params_with_z_key( +pub async fn z_coin_from_conf_and_params_with_docker( ctx: &MmArc, ticker: &str, conf: &Json, params: &ZcoinActivationParams, priv_key_policy: PrivKeyBuildPolicy, db_dir_path: PathBuf, - z_spending_key: ExtendedSpendingKey, protocol_info: ZcoinProtocolInfo, + spending_key: &str, ) -> Result> { + use zcash_client_backend::encoding::decode_extended_spending_key; + let z_spending_key = + decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, spending_key) + .unwrap() + .unwrap(); + let builder = ZCoinBuilder::new( ctx, ticker, @@ -1090,6 +1124,9 @@ async fn z_coin_from_conf_and_params_with_z_key( Some(z_spending_key), protocol_info, ); + + println!("ZOMBIE_wallet.db will be synch'ed with the chain, this may take a while for the first time."); + println!("You may also run prepare_zombie_sapling_cache test to update ZOMBIE_wallet.db before running tests."); builder.build().await } @@ -1209,20 +1246,16 @@ impl MarketCoinOps for ZCoin { fn is_privacy(&self) -> bool { true } + fn should_burn_dex_fee(&self) -> bool { false } // TODO: enable when burn z_address fixed + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] impl SwapOps for ZCoin { - async fn send_taker_fee( - &self, - _fee_addr: &[u8], - dex_fee: DexFee, - uuid: &[u8], - _expire_at: u64, - ) -> TransactionResult { + async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], _expire_at: u64) -> TransactionResult { let uuid = uuid.to_owned(); - let tx = try_tx_s!(z_send_dex_fee(self, dex_fee.fee_amount().into(), &uuid).await); + let tx = try_tx_s!(z_send_dex_fee(self, dex_fee, &uuid).await); Ok(tx.into()) } @@ -1377,6 +1410,8 @@ impl SwapOps for ZCoin { Ok(tx.into()) } + /// Currently validates both Standard and WithBurn options for DexFee + /// TODO: when all mm2 nodes upgrade to support the burn account then disable validation of the Standard option async fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentResult<()> { let z_tx = match validate_fee_args.fee_tx { TransactionEnum::ZTransaction(t) => t.clone(), @@ -1387,7 +1422,8 @@ impl SwapOps for ZCoin { ))) }, }; - let amount_sat = validate_fee_args.dex_fee.fee_uamount(self.utxo_arc.decimals)?; + let fee_amount_sat = validate_fee_args.dex_fee.fee_amount_as_u64(self.utxo_arc.decimals)?; + let burn_amount_sat = validate_fee_args.dex_fee.burn_amount_as_u64(self.utxo_arc.decimals)?; let expected_memo = MemoBytes::from_bytes(validate_fee_args.uuid).expect("Uuid length < 512"); let tx_hash = H256::from(z_tx.txid().0).reversed(); @@ -1421,40 +1457,53 @@ impl SwapOps for ZCoin { None => H0, }; + let mut fee_output_valid = false; + let mut burn_output_valid = false; for shielded_out in z_tx.shielded_outputs.iter() { - if let Some((note, address, memo)) = - try_sapling_output_recovery(self.consensus_params_ref(), block_height, &DEX_FEE_OVK, shielded_out) + if self + .validate_dex_fee_output( + shielded_out, + &DEX_FEE_OVK, + &self.z_fields.dex_fee_addr, + block_height, + fee_amount_sat, + &expected_memo, + ) + .map_err(|err| { + MmError::new(ValidatePaymentError::WrongPaymentTx(format!( + "Bad dex fee output: {}", + err + ))) + })? { - if address != self.z_fields.dex_fee_addr { - let encoded = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); - let expected = encode_payment_address( - z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, - &self.z_fields.dex_fee_addr, - ); - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Dex fee was sent to the invalid address {}, expected {}", - encoded, expected - ))); - } - - if note.value != amount_sat { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Dex fee has invalid amount {}, expected {}", - note.value, amount_sat - ))); - } - - if memo != expected_memo { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Dex fee has invalid memo {:?}, expected {:?}", - memo, expected_memo - ))); + fee_output_valid = true; + } + if let Some(burn_amount_sat) = burn_amount_sat { + if self + .validate_dex_fee_output( + shielded_out, + &DEX_FEE_OVK, + &self.z_fields.dex_burn_addr, + block_height, + burn_amount_sat, + &expected_memo, + ) + .map_err(|err| { + MmError::new(ValidatePaymentError::WrongPaymentTx(format!( + "Bad burn output: {}", + err + ))) + })? + { + burn_output_valid = true; } - - return Ok(()); } } + if fee_output_valid && (burn_amount_sat.is_none() || burn_output_valid) { + return Ok(()); + } + MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "The dex fee tx {:?} has no shielded outputs or outputs decryption failed", z_tx @@ -1513,22 +1562,10 @@ impl SwapOps for ZCoin { secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } - fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { - unimplemented!(); - } - - fn is_auto_refundable(&self) -> bool { false } - - async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { - MmError::err(RefundError::Internal( - "wait_for_htlc_refund is not supported for this coin!".into(), - )) - } - #[inline] fn negotiate_swap_contract_addr( &self, @@ -1542,142 +1579,32 @@ impl SwapOps for ZCoin { let signature = self.secp_keypair().private().sign(&message).expect("valid privkey"); let key = secp_privkey_from_hash(dhash256(&signature)); - key_pair_from_secret(key.as_slice()).expect("valid privkey") + key_pair_from_secret(&key.take()).expect("valid privkey") } #[inline] - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { - self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> [u8; 33] { + self.derive_htlc_key_pair(swap_unique_data) + .public_slice() + .to_vec() + .try_into() + .expect("valid pubkey length") } #[inline] fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { utxo_common::validate_other_pubkey(raw_pubkey) } - - async fn maker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - async fn taker_payment_instructions( - &self, - _args: PaymentInstructionArgs<'_>, - ) -> Result>, MmError> { - Ok(None) - } - - fn validate_maker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } - - fn validate_taker_payment_instructions( - &self, - _instructions: &[u8], - _args: PaymentInstructionArgs, - ) -> Result> { - MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) - } -} - -#[async_trait] -impl TakerSwapMakerCoin for ZCoin { - async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } -} - -#[async_trait] -impl MakerSwapTakerCoin for ZCoin { - async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } - - async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } } #[async_trait] -impl WatcherOps for ZCoin { - fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - unimplemented!(); - } - - fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { - unimplemented!(); - } - - fn create_taker_payment_refund_preimage( - &self, - _taker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_contract_address: &Option, - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn create_maker_payment_spend_preimage( - &self, - _maker_payment_tx: &[u8], - _time_lock: u64, - _maker_pub: &[u8], - _secret_hash: &[u8], - _swap_unique_data: &[u8], - ) -> TransactionFut { - unimplemented!(); - } - - fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - unimplemented!(); - } - - fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { - unimplemented!() - } - - async fn watcher_search_for_swap_tx_spend( - &self, - _input: WatcherSearchForSwapTxSpendInput<'_>, - ) -> Result, String> { - unimplemented!(); - } - - async fn get_taker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _coin_amount: Option, - _other_coin_amount: Option, - _reward_amount: Option, - _wait_until: u64, - ) -> Result> { - unimplemented!() - } - - async fn get_maker_watcher_reward( - &self, - _other_coin: &MmCoinEnum, - _reward_amount: Option, - _wait_until: u64, - ) -> Result, MmError> { - unimplemented!() - } -} +impl WatcherOps for ZCoin {} #[async_trait] impl MmCoin for ZCoin { fn is_asset_chain(&self) -> bool { self.utxo_arc.conf.asset_chain } - fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.as_ref().abortable_system) } + fn spawner(&self) -> WeakSpawner { self.as_ref().abortable_system.weak_spawner() } fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { Box::new(futures01::future::err(MmError::new(WithdrawError::InternalError( diff --git a/mm2src/coins/z_coin/storage.rs b/mm2src/coins/z_coin/storage.rs index b3c2c108c4..e2534281b7 100644 --- a/mm2src/coins/z_coin/storage.rs +++ b/mm2src/coins/z_coin/storage.rs @@ -1,4 +1,5 @@ use crate::z_coin::{ValidateBlocksError, ZcoinConsensusParams, ZcoinStorageError}; +use mm2_event_stream::StreamingManager; pub mod blockdb; pub use blockdb::*; @@ -10,7 +11,6 @@ pub(crate) use z_params::ZcashParamsWasmImpl; pub use walletdb::*; -use crate::z_coin::z_balance_streaming::ZBalanceEventSender; use mm2_err_handle::mm_error::MmResult; #[cfg(target_arch = "wasm32")] use walletdb::wasm::storage::DataConnStmtCacheWasm; @@ -60,7 +60,7 @@ pub struct CompactBlockRow { #[derive(Clone)] pub enum BlockProcessingMode { Validate, - Scan(DataConnStmtCacheWrapper, Option), + Scan(DataConnStmtCacheWrapper, StreamingManager), } /// Checks that the scanned blocks in the data database, when combined with the recent @@ -119,7 +119,7 @@ pub async fn scan_cached_block( params: &ZcoinConsensusParams, block: &CompactBlock, last_height: &mut BlockHeight, -) -> Result { +) -> Result>, ValidateBlocksError> { let mut data_guard = data.inner().clone(); // Fetch the ExtendedFullViewingKeys we are tracking let extfvks = data_guard.get_extended_full_viewing_keys().await?; @@ -184,9 +184,7 @@ pub async fn scan_cached_block( ); witnesses.extend(new_witnesses); - *last_height = current_height; - // If there are any transactions in the block, return the transaction count - Ok(txs.len()) + Ok(txs) } diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs index cccf8cc0a9..826ed52bdd 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs @@ -1,9 +1,10 @@ use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, ZcoinConsensusParams, ZcoinStorageRes}; +use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use crate::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; use crate::z_coin::z_coin_errors::ZcoinStorageError; use async_trait::async_trait; -use futures_util::SinkExt; use mm2_core::mm_ctx::MmArc; use mm2_db::indexed_db::{BeBigUint, ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, IndexedDbBuilder, InitDbResult, MultiIndex, OnUpgradeResult, TableSignature}; @@ -221,6 +222,7 @@ impl BlockDbImpl { validate_from: Option<(BlockHeight, BlockHash)>, limit: Option, ) -> ZcoinStorageRes<()> { + let ticker = self.ticker.to_owned(); let mut from_height = match &mode { BlockProcessingMode::Validate => validate_from .map(|(height, _)| height) @@ -241,7 +243,7 @@ impl BlockDbImpl { if block.height() != cbr.height { return MmError::err(ZcoinStorageError::CorruptedData(format!( - "Block height {} did not match row's height field value {}", + "{ticker}, Block height {} did not match row's height field value {}", block.height(), cbr.height ))); @@ -251,14 +253,17 @@ impl BlockDbImpl { BlockProcessingMode::Validate => { validate_chain(block, &mut prev_height, &mut prev_hash).await?; }, - BlockProcessingMode::Scan(data, z_balance_change_sender) => { - let tx_size = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; - // If there is/are transactions present in the current scanned block(s), - // we trigger a `Triggered` event to update the balance change. - if tx_size > 0 { - if let Some(mut sender) = z_balance_change_sender.clone() { - sender.send(()).await.expect("No receiver is available/dropped"); - }; + BlockProcessingMode::Scan(data, streaming_manager) => { + let txs = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + if !txs.is_empty() { + // Stream out the new transactions. + streaming_manager + .send(&ZCoinTxHistoryEventStreamer::derive_streamer_id(&ticker), txs) + .ok(); + // And also stream balance changes. + streaming_manager + .send(&ZCoinBalanceEventStreamer::derive_streamer_id(&ticker), ()) + .ok(); }; }, } diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs index fea9a93277..44721b4364 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs @@ -1,12 +1,13 @@ use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, ZcoinStorageRes}; +use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use crate::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; use crate::z_coin::z_coin_errors::ZcoinStorageError; use crate::z_coin::ZcoinConsensusParams; use common::async_blocking; use db_common::sqlite::rusqlite::{params, Connection}; use db_common::sqlite::{query_single_row, run_optimization_pragmas, rusqlite}; -use futures_util::SinkExt; use itertools::Itertools; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -227,14 +228,17 @@ impl BlockDbImpl { BlockProcessingMode::Validate => { validate_chain(block, &mut prev_height, &mut prev_hash).await?; }, - BlockProcessingMode::Scan(data, z_balance_change_sender) => { - let tx_size = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; - // If there are transactions present in the current scanned block, - // we send a `Triggered` event to update the balance change. - if tx_size > 0 { - if let Some(mut sender) = z_balance_change_sender.clone() { - sender.send(()).await.expect("No receiver is available/dropped"); - }; + BlockProcessingMode::Scan(data, streaming_manager) => { + let txs = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + if !txs.is_empty() { + // Stream out the new transactions. + streaming_manager + .send(&ZCoinTxHistoryEventStreamer::derive_streamer_id(&ticker), txs) + .ok(); + // And also stream balance changes. + streaming_manager + .send(&ZCoinBalanceEventStreamer::derive_streamer_id(&ticker), ()) + .ok(); }; }, } diff --git a/mm2src/coins/z_coin/storage/blockdb/mod.rs b/mm2src/coins/z_coin/storage/blockdb/mod.rs index 7e2ef49fe7..bc41c4de00 100644 --- a/mm2src/coins/z_coin/storage/blockdb/mod.rs +++ b/mm2src/coins/z_coin/storage/blockdb/mod.rs @@ -18,7 +18,6 @@ pub struct BlockDbImpl { pub db: Arc>, #[cfg(target_arch = "wasm32")] pub db: SharedDb, - #[allow(unused)] ticker: String, } @@ -110,9 +109,10 @@ mod native_tests { mod wasm_tests { use crate::z_coin::storage::blockdb::block_db_storage_tests::{test_insert_block_and_get_latest_block_impl, test_rewind_to_height_impl}; - use crate::z_coin::z_rpc::{LightRpcClient, ZRpcOps}; - use common::log::info; - use common::log::wasm_log::register_wasm_log; + // use crate::z_coin::z_rpc::{LightRpcClient, ZRpcOps}; + // use common::log::info; + // use common::log::wasm_log::register_wasm_log; + use common::log::warn; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -125,13 +125,14 @@ mod wasm_tests { #[wasm_bindgen_test] async fn test_transport() { - register_wasm_log(); - let client = LightRpcClient::new(vec!["https://pirate.battlefield.earth:8581".to_string()]) - .await - .unwrap(); - let latest_height = client.get_block_height().await; - - assert!(latest_height.is_ok()); - info!("LATEST BLOCK: {latest_height:?}"); + warn!("Skipping test_transport since it's failing, check https://github.com/KomodoPlatform/komodo-defi-framework/issues/2366"); + // register_wasm_log(); + // let client = LightRpcClient::new(vec!["https://pirate.battlefield.earth:8581".to_string()]) + // .await + // .unwrap(); + // let latest_height = client.get_block_height().await; + // + // assert!(latest_height.is_ok()); + // info!("LATEST BLOCK: {latest_height:?}"); } } diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs index 4c68fec22c..c1ffdfb0a2 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs @@ -71,6 +71,7 @@ mod wasm_test { use crate::z_coin::{ValidateBlocksError, ZcoinConsensusParams, ZcoinStorageError}; use crate::ZcoinProtocolInfo; use mm2_core::mm_ctx::MmArc; + use mm2_event_stream::StreamingManager; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; use protobuf::Message; use std::path::PathBuf; @@ -255,7 +256,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -300,7 +301,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -359,7 +360,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -453,7 +454,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -542,7 +543,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -572,7 +573,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -611,7 +612,7 @@ mod wasm_test { blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -629,7 +630,7 @@ mod wasm_test { let scan = blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -653,7 +654,7 @@ mod wasm_test { assert!(blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None ) @@ -697,7 +698,7 @@ mod wasm_test { assert!(blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None ) @@ -718,7 +719,7 @@ mod wasm_test { assert!(blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None ) @@ -760,7 +761,7 @@ mod wasm_test { assert!(blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None ) @@ -790,7 +791,7 @@ mod wasm_test { let scan = blockdb .process_blocks_with_mode( consensus_params.clone(), - BlockProcessingMode::Scan(scan, None), + BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, ) @@ -832,7 +833,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .is_ok()); // @@ -852,7 +853,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .is_ok()); // @@ -897,7 +898,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .is_ok()); // @@ -928,7 +929,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .is_ok()); // @@ -1098,7 +1099,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .unwrap(); // assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); @@ -1155,7 +1156,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .unwrap(); // @@ -1191,7 +1192,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, None), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) // .await // .unwrap(); // diff --git a/mm2src/coins/z_coin/tx_history_events.rs b/mm2src/coins/z_coin/tx_history_events.rs new file mode 100644 index 0000000000..f374bc22b1 --- /dev/null +++ b/mm2src/coins/z_coin/tx_history_events.rs @@ -0,0 +1,118 @@ +use super::z_tx_history::fetch_txs_from_db; +use super::{NoInfoAboutTx, ZCoin, ZTxHistoryError, ZcoinTxDetails}; +use crate::utxo::rpc_clients::UtxoRpcError; +use crate::MarketCoinOps; +use common::log; +use mm2_err_handle::prelude::MmError; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use rpc::v1::types::H256 as H256Json; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::compat::Future01CompatExt; +use futures::StreamExt; +use zcash_client_backend::wallet::WalletTx; +use zcash_primitives::sapling::Nullifier; + +pub struct ZCoinTxHistoryEventStreamer { + coin: ZCoin, +} + +impl ZCoinTxHistoryEventStreamer { + #[inline(always)] + pub fn new(coin: ZCoin) -> Self { Self { coin } } + + #[inline(always)] + pub fn derive_streamer_id(coin: &str) -> String { format!("TX_HISTORY:{coin}") } +} + +#[async_trait] +impl EventStreamer for ZCoinTxHistoryEventStreamer { + type DataInType = Vec>; + + fn streamer_id(&self) -> String { Self::derive_streamer_id(self.coin.ticker()) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + while let Some(new_txs) = data_rx.next().await { + let new_txs_details = match get_tx_details(&self.coin, new_txs).await { + Ok(tx_details) => tx_details, + Err(e) => { + broadcaster.broadcast(Event::err(self.streamer_id(), json!({ "error": e.to_string() }))); + log::error!("Failed to get tx details in streamer {}: {e:?}", self.streamer_id()); + continue; + }, + }; + for tx_details in new_txs_details { + let tx_details = serde_json::to_value(tx_details).expect("Serialization should't fail."); + broadcaster.broadcast(Event::new(self.streamer_id(), tx_details)); + } + } + } +} + +/// Errors that can occur while getting transaction details for some tx hashes. +/// +/// The error implements `Display` trait, so it can be easily converted `.to_string`. +#[derive(Debug, derive_more::Display)] +enum GetTxDetailsError { + #[display(fmt = "RPC Error: {_0:?}")] + UtxoRpcError(UtxoRpcError), + #[display(fmt = "DB Error: {_0:?}")] + DbError(String), + #[display(fmt = "Internal Error: {_0:?}")] + Internal(NoInfoAboutTx), +} + +impl From> for GetTxDetailsError { + fn from(e: MmError) -> Self { GetTxDetailsError::UtxoRpcError(e.into_inner()) } +} + +impl From> for GetTxDetailsError { + fn from(e: MmError) -> Self { GetTxDetailsError::DbError(e.to_string()) } +} + +impl From> for GetTxDetailsError { + fn from(e: MmError) -> Self { GetTxDetailsError::Internal(e.into_inner()) } +} + +async fn get_tx_details(coin: &ZCoin, txs: Vec>) -> Result, GetTxDetailsError> { + let current_block = coin.utxo_rpc_client().get_block_count().compat().await?; + let txs_from_db = { + let tx_ids = txs.iter().map(|tx| tx.txid).collect(); + fetch_txs_from_db(coin, tx_ids).await? + }; + + let hashes_for_verbose = txs_from_db + .iter() + .map(|item| H256Json::from(item.tx_hash.take())) + .collect(); + let transactions = coin.z_transactions_from_cache_or_rpc(hashes_for_verbose).await?; + + let prev_tx_hashes = transactions + .iter() + .flat_map(|(_, tx)| { + tx.vin.iter().map(|vin| { + let mut hash = *vin.prevout.hash(); + hash.reverse(); + H256Json::from(hash) + }) + }) + .collect(); + let prev_transactions = coin.z_transactions_from_cache_or_rpc(prev_tx_hashes).await?; + + let txs_details = txs_from_db + .into_iter() + .map(|tx_item| coin.tx_details_from_db_item(tx_item, &transactions, &prev_transactions, current_block)) + .collect::>()?; + + Ok(txs_details) +} diff --git a/mm2src/coins/z_coin/tx_streaming_tests/mod.rs b/mm2src/coins/z_coin/tx_streaming_tests/mod.rs new file mode 100644 index 0000000000..852bbbb768 --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/mod.rs @@ -0,0 +1,31 @@ +#[cfg(not(target_arch = "wasm32"))] mod native; +#[cfg(target_arch = "wasm32")] mod wasm; + +use common::now_sec; +use mm2_test_helpers::for_tests::{PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS}; + +use crate::utxo::rpc_clients::ElectrumConnectionSettings; +use crate::z_coin::{ZcoinActivationParams, ZcoinRpcMode}; + +#[allow(dead_code)] +fn light_zcoin_activation_params() -> ZcoinActivationParams { + ZcoinActivationParams { + mode: ZcoinRpcMode::Light { + electrum_servers: PIRATE_ELECTRUMS + .iter() + .map(|s| ElectrumConnectionSettings { + url: s.to_string(), + protocol: Default::default(), + disable_cert_verification: Default::default(), + timeout_sec: None, + }) + .collect(), + min_connected: None, + max_connected: None, + light_wallet_d_servers: PIRATE_LIGHTWALLETD_URLS.iter().map(|s| s.to_string()).collect(), + sync_params: Some(crate::z_coin::SyncStartPoint::Date(now_sec() - 24 * 60 * 60)), + skip_sync_params: None, + }, + ..Default::default() + } +} diff --git a/mm2src/coins/z_coin/tx_streaming_tests/native.rs b/mm2src/coins/z_coin/tx_streaming_tests/native.rs new file mode 100644 index 0000000000..f4bc2849dc --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/native.rs @@ -0,0 +1,73 @@ +use super::light_zcoin_activation_params; +use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use crate::z_coin::z_coin_from_conf_and_params; +use crate::z_coin::z_htlc::z_send_dex_fee; +use crate::{CoinProtocol, DexFee, MarketCoinOps, MmCoin, PrivKeyBuildPolicy}; + +use common::custom_futures::timeout::FutureTimerExt; +use common::{block_on, Future01CompatExt}; +use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_test_helpers::for_tests::{pirate_conf, ARRR}; +use std::time::Duration; + +#[test] +#[ignore] // Ignored because we don't have zcash params in CI. TODO: Why not download them on demand like how we do in wasm (see download_and_save_params). +fn test_zcoin_tx_streaming() { + let ctx = MmCtxBuilder::default().into_mm_arc(); + let conf = pirate_conf(); + let params = light_zcoin_activation_params(); + // Address: RQX5MnqnxEk6P33LSEAxC2vqA7DfSdWVyH + // Or: zs1n2azlwcj9pvl2eh36qvzgeukt2cpzmw44hya8wyu52j663d0dfs4d5hjx6tr04trz34jxyy433j + let priv_key_policy = + PrivKeyBuildPolicy::IguanaPrivKey("6d862798ef956fb60fb17bcc417dd6d44bfff066a4a49301cd2528e41a4a3e45".into()); + let protocol_info = match serde_json::from_value::(conf["protocol"].clone()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; + + let coin = block_on(z_coin_from_conf_and_params( + &ctx, + ARRR, + &conf, + ¶ms, + protocol_info, + priv_key_policy, + )) + .unwrap(); + + // Wait till we are synced with the sapling state. + while !block_on(coin.is_sapling_state_synced()) { + std::thread::sleep(Duration::from_secs(1)); + } + + // Query the block height to make sure our electrums are actually connected. + log!("current block = {:?}", block_on(coin.current_block().compat()).unwrap()); + + // Add a new client to use it for listening to tx history events. + let client_id = 1; + let mut event_receiver = ctx.event_stream_manager.new_client(client_id).unwrap(); + // Add the streamer that will stream the tx history events. + let streamer = ZCoinTxHistoryEventStreamer::new(coin.clone()); + // Subscribe the client to the streamer. + block_on(ctx.event_stream_manager.add(client_id, streamer, coin.spawner())).unwrap(); + + // Send a tx to have it in the tx history. + let tx = block_on(z_send_dex_fee(&coin, DexFee::Standard("0.0001".into()), &[1; 16])).unwrap(); + + // Wait for the tx history event (should be streamed next block). + let event = block_on(Box::pin(event_receiver.recv()).timeout_secs(120.)) + .expect("timed out waiting for tx to showup") + .expect("tx history sender shutdown"); + + log!("{:?}", event.get()); + let (event_type, event_data) = event.get(); + // Make sure this is not an error event, + assert!(!event_type.starts_with("ERROR_")); + // from the expected streamer, + assert_eq!( + event_type, + ZCoinTxHistoryEventStreamer::derive_streamer_id(coin.ticker()) + ); + // and has the expected data. + assert_eq!(event_data["tx_hash"].as_str().unwrap(), tx.txid().to_string()); +} diff --git a/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs b/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs new file mode 100644 index 0000000000..c672ef8770 --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs @@ -0,0 +1,76 @@ +// use common::custom_futures::timeout::FutureTimerExt; +// use common::{executor::Timer, Future01CompatExt}; +// use mm2_core::mm_ctx::MmCtxBuilder; +// use mm2_test_helpers::for_tests::{pirate_conf, ARRR}; +use common::log::warn; +use wasm_bindgen_test::*; +// +// use super::light_zcoin_activation_params; +// use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +// use crate::z_coin::z_coin_from_conf_and_params; +// use crate::z_coin::z_htlc::z_send_dex_fee; +// use crate::PrivKeyBuildPolicy; +// use crate::{CoinProtocol, MarketCoinOps, MmCoin}; + +#[wasm_bindgen_test] +async fn test_zcoin_tx_streaming() { + warn!("Skipping test_zcoin_tx_streaming since it's failing, check https://github.com/KomodoPlatform/komodo-defi-framework/issues/2366"); + // let ctx = MmCtxBuilder::default().into_mm_arc(); + // let conf = pirate_conf(); + // let params = light_zcoin_activation_params(); + // // Address: RQX5MnqnxEk6P33LSEAxC2vqA7DfSdWVyH + // // Or: zs1n2azlwcj9pvl2eh36qvzgeukt2cpzmw44hya8wyu52j663d0dfs4d5hjx6tr04trz34jxyy433j + // let priv_key_policy = + // PrivKeyBuildPolicy::IguanaPrivKey("6d862798ef956fb60fb17bcc417dd6d44bfff066a4a49301cd2528e41a4a3e45".into()); + // let protocol_info = match serde_json::from_value::(conf["protocol"].clone()).unwrap() { + // CoinProtocol::ZHTLC(protocol_info) => protocol_info, + // other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + // }; + // + // let coin = z_coin_from_conf_and_params(&ctx, ARRR, &conf, ¶ms, protocol_info, priv_key_policy) + // .await + // .unwrap(); + // + // // Wait till we are synced with the sapling state. + // while !coin.is_sapling_state_synced().await { + // Timer::sleep(1.).await; + // } + // + // // Query the block height to make sure our electrums are actually connected. + // log!("current block = {:?}", coin.current_block().compat().await.unwrap()); + // + // // Add a new client to use it for listening to tx history events. + // let client_id = 1; + // let mut event_receiver = ctx.event_stream_manager.new_client(client_id).unwrap(); + // // Add the streamer that will stream the tx history events. + // let streamer = ZCoinTxHistoryEventStreamer::new(coin.clone()); + // // Subscribe the client to the streamer. + // ctx.event_stream_manager + // .add(client_id, streamer, coin.spawner()) + // .await + // .unwrap(); + // + // // Send a tx to have it in the tx history. + // let tx = z_send_dex_fee(&coin, "0.0001".parse().unwrap(), &[1; 16]) + // .await + // .unwrap(); + // + // // Wait for the tx history event (should be streamed next block). + // let event = Box::pin(event_receiver.recv()) + // .timeout_secs(120.) + // .await + // .expect("timed out waiting for tx to showup") + // .expect("tx history sender shutdown"); + // + // log!("{:?}", event.get()); + // let (event_type, event_data) = event.get(); + // // Make sure this is not an error event, + // assert!(!event_type.starts_with("ERROR_")); + // // from the expected streamer, + // assert_eq!( + // event_type, + // ZCoinTxHistoryEventStreamer::derive_streamer_id(coin.ticker()) + // ); + // // and has the expected data. + // assert_eq!(event_data["tx_hash"].as_str().unwrap(), tx.txid().to_string()); +} diff --git a/mm2src/coins/z_coin/z_balance_streaming.rs b/mm2src/coins/z_coin/z_balance_streaming.rs index 5f6d3e590a..0760bfc929 100644 --- a/mm2src/coins/z_coin/z_balance_streaming.rs +++ b/mm2src/coins/z_coin/z_balance_streaming.rs @@ -1,114 +1,63 @@ use crate::common::Future01CompatExt; use crate::z_coin::ZCoin; -use crate::{MarketCoinOps, MmCoin}; +use crate::MarketCoinOps; use async_trait::async_trait; -use common::executor::{AbortSettings, SpawnAbortable}; -use common::log::{error, info}; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use common::log::error; use futures::channel::oneshot; -use futures::channel::oneshot::{Receiver, Sender}; -use futures::lock::Mutex as AsyncMutex; use futures_util::StreamExt; -use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; -use mm2_event_stream::{ErrorEventName, Event, EventName, EventStreamConfiguration}; -use std::sync::Arc; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; -pub type ZBalanceEventSender = UnboundedSender<()>; -pub type ZBalanceEventHandler = Arc>>; - -#[async_trait] -impl EventBehaviour for ZCoin { - fn event_name() -> EventName { EventName::CoinBalance } - - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } - - async fn handle(self, _interval: f64, tx: Sender) { - const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; - - macro_rules! send_status_on_err { - ($match: expr, $sender: tt, $msg: literal) => { - match $match { - Some(t) => t, - None => { - $sender - .send(EventInitStatus::Failed($msg.to_owned())) - .expect(RECEIVER_DROPPED_MSG); - panic!("{}", $msg); - }, - } - }; - } +pub struct ZCoinBalanceEventStreamer { + coin: ZCoin, +} - let ctx = send_status_on_err!( - MmArc::from_weak(&self.as_ref().ctx), - tx, - "MM context must have been initialized already." - ); - let z_balance_change_handler = send_status_on_err!( - self.z_fields.z_balance_event_handler.as_ref(), - tx, - "Z balance change receiver can not be empty." - ); +impl ZCoinBalanceEventStreamer { + #[inline(always)] + pub fn new(coin: ZCoin) -> Self { Self { coin } } - tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + #[inline(always)] + pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } +} - // Locks the balance change handler, iterates through received events, and updates balance changes accordingly. - let mut bal = z_balance_change_handler.lock().await; - while (bal.next().await).is_some() { - match self.my_balance().compat().await { +#[async_trait] +impl EventStreamer for ZCoinBalanceEventStreamer { + type DataInType = (); + + fn streamer_id(&self) -> String { Self::derive_streamer_id(self.coin.ticker()) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput<()>, + ) { + let streamer_id = self.streamer_id(); + let coin = self.coin; + + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + // Iterates through received events, and updates balance changes accordingly. + while (data_rx.next().await).is_some() { + match coin.my_balance().compat().await { Ok(balance) => { let payload = json!({ - "ticker": self.ticker(), - "address": self.my_z_address_encoded(), + "ticker": coin.ticker(), + "address": coin.my_z_address_encoded(), "balance": { "spendable": balance.spendable, "unspendable": balance.unspendable } }); - ctx.stream_channel_controller - .broadcast(Event::new(Self::event_name().to_string(), payload.to_string())) - .await; + broadcaster.broadcast(Event::new(streamer_id.clone(), payload)); }, Err(err) => { - let ticker = self.ticker(); + let ticker = coin.ticker(); error!("Failed getting balance for '{ticker}'. Error: {err}"); let e = serde_json::to_value(err).expect("Serialization should't fail."); - return ctx - .stream_channel_controller - .broadcast(Event::new( - format!("{}:{}", Self::error_event_name(), ticker), - e.to_string(), - )) - .await; + return broadcaster.broadcast(Event::err(streamer_id.clone(), e)); }, }; } } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - info!( - "{} event is activated for {} address {}. `stream_interval_seconds`({}) has no effect on this.", - Self::event_name(), - self.ticker(), - self.my_z_address_encoded(), - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = AbortSettings::info_on_abort(format!( - "{} event is stopped for {}.", - Self::event_name(), - self.ticker() - )); - self.spawner().spawn_with_settings(fut, settings); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive - } - } } diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index 7fcf06cb12..3c6e44a32a 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -129,7 +129,6 @@ pub enum GenTxError { LightClientErr(String), FailedToCreateNote, SpendableNotesError(String), - #[cfg(target_arch = "wasm32")] Internal(String), } @@ -177,9 +176,8 @@ impl From for WithdrawError { | GenTxError::BlockchainScanStopped | GenTxError::LightClientErr(_) | GenTxError::SpendableNotesError(_) - | GenTxError::FailedToCreateNote => WithdrawError::InternalError(gen_tx.to_string()), - #[cfg(target_arch = "wasm32")] - GenTxError::Internal(_) => WithdrawError::InternalError(gen_tx.to_string()), + | GenTxError::FailedToCreateNote + | GenTxError::Internal(_) => WithdrawError::InternalError(gen_tx.to_string()), } } } @@ -301,6 +299,7 @@ impl From for ZTxHistoryError { fn from(err: CursorError) -> Self { ZTxHistoryError::IndexedDbError(err.to_string()) } } +#[derive(Debug)] pub(super) struct NoInfoAboutTx(pub(super) H256Json); impl From for MyTxHistoryErrorV2 { diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs index f554d7c5d3..4e5ffc4325 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -1,3 +1,27 @@ +//! Native tests for zcoin +//! +//! To run zcoin tests in this source you need `--features zhtlc-native-tests` +//! ZOMBIE chain must be running for zcoin tests: +//! komodod -ac_name=ZOMBIE -ac_supply=0 -ac_reward=25600000000 -ac_halving=388885 -ac_private=1 -ac_sapling=1 -testnode=1 -addnode=65.21.51.116 -addnode=116.203.120.163 -addnode=168.119.236.239 -addnode=65.109.1.121 -addnode=159.69.125.84 -addnode=159.69.10.44 +//! Also check the test z_key (spending key) has balance: +//! `komodo-cli -ac_name=ZOMBIE z_getbalance zs10hvyxf3ajm82e4gvxem3zjlf9xf3yxhjww9fvz3mfqza9zwumvluzy735e29c3x5aj2nu0ua6n0` +//! If no balance, you may mine some transparent coins and send to the test z_key. +//! When tests are run for the first time (or have not been run for a long) synching to fill ZOMBIE_wallet.db is started which may take hours. +//! So it is recommended to run prepare_zombie_sapling_cache to sync ZOMBIE_wallet.db before running zcoin tests: +//! cargo test -p coins --features zhtlc-native-tests -- --nocapture prepare_zombie_sapling_cache +//! If you did not run prepare_zombie_sapling_cache waiting for ZOMBIE_wallet.db sync will be done in the first call to ZCoin::gen_tx. +//! In tests, for ZOMBIE_wallet.db to be filled, another database ZOMBIE_cache.db is created in memory, +//! so if db sync in tests is cancelled and restarted this would cause restarting of building ZOMBIE_cache.db in memory +//! +//! Note that during the ZOMBIE_wallet.db sync an error may be reported: +//! 'error trying to connect: tcp connect error: Can't assign requested address (os error 49)'. +//! Also during the sync other apps like ssh or komodo-cli may return same error or even crash. TODO: fix this problem, maybe it is due to too much load on TCP stack +//! Errors like `No one seems interested in SyncStatus: send failed because channel is full` in the debug log may be ignored (means that update status is temporarily not watched) +//! +//! To monitor sync status in logs you may add logging support into the beginning of prepare_zombie_sapling_cache test (or other tests): +//! common::log::UnifiedLoggerBuilder::default().init(); +//! and run cargo test with var RUST_LOG=debug + use bitcrypto::dhash160; use common::{block_on, now_sec}; use mm2_core::mm_ctx::MmCtxBuilder; @@ -9,15 +33,22 @@ use zcash_client_backend::encoding::decode_extended_spending_key; use super::{z_coin_from_conf_and_params_with_z_key, z_mainnet_constants, PrivKeyBuildPolicy, RefundPaymentArgs, SendPaymentArgs, SpendPaymentArgs, SwapOps, ValidateFeeArgs, ValidatePaymentError, ZTransaction}; use crate::z_coin::{z_htlc::z_send_dex_fee, ZcoinActivationParams, ZcoinRpcMode}; -use crate::DexFee; use crate::{CoinProtocol, SwapTxTypeWithSecretHash}; +use crate::{DexFee, DexFeeBurnDestination}; use mm2_number::MmNumber; -#[test] -fn zombie_coin_send_and_refund_maker_payment() { +fn native_zcoin_activation_params() -> ZcoinActivationParams { + ZcoinActivationParams { + mode: ZcoinRpcMode::Native, + ..Default::default() + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_and_refund_maker_payment() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let pk_data = [1; 32]; let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -26,7 +57,7 @@ fn zombie_coin_send_and_refund_maker_payment() { other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = block_on(z_coin_from_conf_and_params_with_z_key( + let coin = z_coin_from_conf_and_params_with_z_key( &ctx, "ZOMBIE", &conf, @@ -35,11 +66,17 @@ fn zombie_coin_send_and_refund_maker_payment() { db_dir, z_key, protocol_info, - )) + ) + .await .unwrap(); let time_lock = now_sec() - 3600; - let taker_pub = coin.utxo_arc.priv_key_policy.activated_key_or_err().unwrap().public(); + let maker_uniq_data = [3; 32]; + + let taker_uniq_data = [5; 32]; + let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); + let taker_pub = taker_key_pair.public(); + let secret_hash = [0; 20]; let args = SendPaymentArgs { @@ -49,12 +86,12 @@ fn zombie_coin_send_and_refund_maker_payment() { secret_hash: &secret_hash, amount: "0.01".parse().unwrap(), swap_contract_address: &None, - swap_unique_data: &[], + swap_unique_data: maker_uniq_data.as_slice(), payment_instructions: &None, watcher_reward: None, wait_for_confirmation_until: 0, }; - let tx = block_on(coin.send_maker_payment(args)).unwrap(); + let tx = coin.send_maker_payment(args).await.unwrap(); log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); let refund_args = RefundPaymentArgs { @@ -65,18 +102,18 @@ fn zombie_coin_send_and_refund_maker_payment() { maker_secret_hash: &secret_hash, }, swap_contract_address: &None, - swap_unique_data: pk_data.as_slice(), + swap_unique_data: maker_uniq_data.as_slice(), watcher_reward: false, }; - let refund_tx = block_on(coin.send_maker_refunds_payment(refund_args)).unwrap(); + let refund_tx = coin.send_maker_refunds_payment(refund_args).await.unwrap(); log!("refund tx {}", hex::encode(refund_tx.tx_hash_as_bytes().0)); } -#[test] -fn zombie_coin_send_and_spend_maker_payment() { +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_and_spend_maker_payment() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let pk_data = [1; 32]; let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -85,7 +122,7 @@ fn zombie_coin_send_and_spend_maker_payment() { other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = block_on(z_coin_from_conf_and_params_with_z_key( + let coin = z_coin_from_conf_and_params_with_z_key( &ctx, "ZOMBIE", &conf, @@ -94,11 +131,20 @@ fn zombie_coin_send_and_spend_maker_payment() { db_dir, z_key, protocol_info, - )) + ) + .await .unwrap(); let lock_time = now_sec() - 1000; - let taker_pub = coin.utxo_arc.priv_key_policy.activated_key_or_err().unwrap().public(); + + let maker_uniq_data = [3; 32]; + let maker_key_pair = coin.derive_htlc_key_pair(maker_uniq_data.as_slice()); + let maker_pub = maker_key_pair.public(); + + let taker_uniq_data = [5; 32]; + let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); + let taker_pub = taker_key_pair.public(); + let secret = [0; 32]; let secret_hash = dhash160(&secret); @@ -109,36 +155,34 @@ fn zombie_coin_send_and_spend_maker_payment() { secret_hash: secret_hash.as_slice(), amount: "0.01".parse().unwrap(), swap_contract_address: &None, - swap_unique_data: &[], + swap_unique_data: maker_uniq_data.as_slice(), payment_instructions: &None, watcher_reward: None, wait_for_confirmation_until: 0, }; - let tx = block_on(coin.send_maker_payment(maker_payment_args)).unwrap(); + let tx = coin.send_maker_payment(maker_payment_args).await.unwrap(); log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); - let maker_pub = taker_pub; - let spends_payment_args = SpendPaymentArgs { other_payment_tx: &tx.tx_hex(), time_lock: lock_time, other_pubkey: maker_pub, secret: &secret, - secret_hash: &[], + secret_hash: secret_hash.as_slice(), swap_contract_address: &None, - swap_unique_data: pk_data.as_slice(), + swap_unique_data: taker_uniq_data.as_slice(), watcher_reward: false, }; - let spend_tx = block_on(coin.send_taker_spends_maker_payment(spends_payment_args)).unwrap(); + let spend_tx = coin.send_taker_spends_maker_payment(spends_payment_args).await.unwrap(); log!("spend tx {}", hex::encode(spend_tx.tx_hash_as_bytes().0)); } -#[test] -fn zombie_coin_send_dex_fee() { +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_dex_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -147,27 +191,49 @@ fn zombie_coin_send_dex_fee() { other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = block_on(z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - priv_key, - db_dir, - z_key, - protocol_info, - )) - .unwrap(); + let coin = + z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) + .await + .unwrap(); - let tx = block_on(z_send_dex_fee(&coin, "0.01".parse().unwrap(), &[1; 16])).unwrap(); + let dex_fee = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); log!("dex fee tx {}", tx.txid()); } +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_standard_dex_fee() { + let ctx = MmCtxBuilder::default().into_mm_arc(); + let mut conf = zombie_conf(); + let params = native_zcoin_activation_params(); + let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); + let db_dir = PathBuf::from("./for_tests"); + let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); + let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; + + let coin = + z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) + .await + .unwrap(); + + let dex_fee = DexFee::Standard("0.01".into()); + let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); + log!("dex fee tx {}", tx.txid()); +} + +/// Use to create ZOMBIE_wallet.db #[test] fn prepare_zombie_sapling_cache() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -193,11 +259,11 @@ fn prepare_zombie_sapling_cache() { } } -#[test] -fn zombie_coin_validate_dex_fee() { +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_validate_dex_fee() { let ctx = MmCtxBuilder::default().into_mm_arc(); let mut conf = zombie_conf(); - let params = default_zcoin_activation_params(); + let params = native_zcoin_activation_params(); let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); let db_dir = PathBuf::from("./for_tests"); let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); @@ -206,36 +272,34 @@ fn zombie_coin_validate_dex_fee() { other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), }; - let coin = block_on(z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - priv_key, - db_dir, - z_key, - protocol_info, - )) - .unwrap(); + let coin = + z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) + .await + .unwrap(); - // https://zombie.explorer.lordofthechains.com/tx/ec620194c33eba004904f34c93f4f005a7544988771af1c5a527f65c08e4a4aa - let tx_hex = "0400008085202f89000000000000af330000e803000000000000015c3fc69c0eb25dc2b75593464af5b937da35816a2ffeb9b79f3da865c2187083a0b143011810109ab0ed410896aff77bcfbc8a8f5b9bfe0d273716095cfe401cbd97c66a999384aa12a571abc39508b113de0ad0816630fea67f18d68572c52be4364f812f9796e1084ee6c28d1419dac4767d12a7a33662536c2c1ffa7e221d843c9f2bf2601f34cc71a1e1c42041fab87e617ae00b796aa070280060e9cdc30e69e80367e6105e792bbefcd93f00c48ce8278c4eb36c8846cb94d5adcb273ce91decf79196461f7969d6a7031878c6c8e81edd4532a5c57bbaeeea4ed5f4440cef90f19020079c69e05325e63350e9cb9eac44a3d4937111a3c6dc00c79d4dfe72c1e73a6e00ad0aa1aded83f0b778ab92319fcdae19c2946c50c370d243fe6dfa4f92803dcec1992af0d91f0cda8ccbee2a5321f708fc0156d29b51a015b3fb70f543c7713b8547d24e6916caefca17edf1f4109099177498cb30f9305b5169ab1f2e3e4a83e789b5687f3f5f5013d917e2e6babc8ca4507cb349d1e5a30602f557bcbd6574c7fcb5779ce286bdd10fe5db58abadcacf5eaa9e5d3575e30e439d0c62494bc045456e7b6b03f5304a8ff8878f01883f8c473e066f8159bdc111a03d96670f4b29acd919d8b9674897e056c7ac6ef4da155ce7d923f2bedcd51f2198c2be360e03ef2373df94d1e63ba507effc2f9b2f1ccfed09f2f26b8c619415d4a90f556e4b9350099f58fb10a33986945a1512879fdae66e9ef94671764ecdc558ed2d760f7bd3ce2dedfdb4fc7e3aa26903288e16f34214632d8727f68d47389ff687f681b3b285896d3214b9eb60271d87f3223f20e4ddf39513c07fe3420eefa9e7372fff51c83468161d9ffe745533b02917e4ccf87a213c884042938511bb7ccbe6b54392897b1ba111d127ec2c16ba167bb5a65d7819295ceedc5b8faf493c71ed722b72578c62be7d59449bd218196e1f43c3a8bb4875c3bcce1adcb6c4afa6398a7276583c60dbe609c9819bf66385e6cff4b27090aa1dccd0a2f86ca3b3871f2077db44c17d57bba98f9809e6000676600ad70560cbf285354f979d24a5de6e8b0c65ee1a89e28f58f430d20988cae8b0a9690cf79519efc227d54ca739ce3dcde73ac6e624c00b120d6955b40b854b00b1b53dc18cc35cd4792716f3e0bc6552bf0ba4616d1b22900cebede31fbe4b722de1f11c0577abe2ca0614c9d6f24cb56e2b4c840b8573c503ca1d4bf9e671a583b04dd51af10cfc709e89965c5150d7fb6b8c924812e6c9d31025d30e8367defb39e71fda095a16c0e1a70b528799d8c4852b3adb700b113bf5de1d6ec6c7742a1ef678228930ec767e406b36a55fe4a8108236cf0487901e35b50312facad257fd9ba2be154fbc674b33240fffaffc149f26238c5b188107df049cc615289ab8ee6f12a868379f6e362b059ba7c3dde3f02a91a08316c194ad7e556d390d38e6442212502f84cb22fc7dbab262984d2155ebeee3e4109033e57e761e9ab701512cf2635fe92f12d42953ce33f020ad4606125477318f88f673517831f43e548c5ef1d6d4aef7d850fdc0d35bc38a69ac02ccc7436eb711c6303cd306b34931bf1a4cbaed6940ede588e2abf7835718e4afed606d71cdb48146598db31d024347ba9eb289f714bfa7a3670392b3a5e35b6359f6626ed07cca451f0389e4423bb531baf409c48279df489d0073ccf17676eb5c5caa732b104894c2bcf311774f1f8c0b8b6fa313437bc1209f29ee64ccb40a07bb0cf928c77ca6b6a4fe287b1dc6df678a32b8dda35876211d5f929f90a6cc772bd171d15f50da9de8f11a241be98d205b2c53a78a5ba1bce0e782ee88512c3fc815fe843c6b5ffae1b80f1bdd5132b84a813e5157d3096034011fec2f0543f9c30a119d87e8b66e9a857d833d45fe55352871f68aaf8757c03f3b82f1cbd13c56d1843b9d2ebf7fe42f41ab0493dc9491813456fd1e0466bfdfb87a684cba8944df2fd8d3703617383137613a853a3725b366079c3760bbce60f2a88fa2cc579a6ddc9813185cb26873e6c09e43b6db73e4a44d30eebdae38bdaae9f6f1c38941b342ba67822b039f35878e54aadc4b1861df8803494f739d07b0d8b7815d1b55932bcdda80f612f97e0a0c288a7daf3aee1eb0db33fa030082b439a6d0c8d1043a718747acc398913f89e09cb0c95be96fdc9b8aa01f8eba0bd543528035fb7442ce9c6fa5e5539d4dfe29f2a9400d2d122d61037b9df584c5738b851a0d8f6bb6cf553efbdeefc3db3718681a75cb90398fa54c8dd1e696de8dba5ec977c4e2909f4977fde39847f2c0d8f9f9927e9a6cc9466b90d7745e678baa32100cb1ca7d2969c6ec9f35b222f3f4126a7965c40e5da75f183f73d33d325f25a371f5767c6b5bca141c30ed409ffce5f8e073bfb0a85512d0594c96b80cd5d7b73ac3dca494aa9dc7085ad594b46eb28fd1df84afc8a71dd63bc5d23eaf21238706a205d643bd238fe01b32dcd50c93047498ed54bb01cf2108d326f7e3c0538a9e6cc79090ccee6cf47e7fd3cc5cf41aad6905c5d099cea22effdcd4bb7b8d85ba3e3d703c34863d2540936976c774e5c4cb020873873a186c3bab67b1a47c4029f2880cbadd1cd7d82a6c649b073aa0c938b5f28e9173a64c72c81745bc8df6706bf6e320b5e96820970322f21d633a2c28b23d79b8edbc8a13eafa2a5241d7bb59b341779fe6f5db2994567caefaec23b7b7c55a73dbc6614bb958bc1d62838c56197a3eceefeb1dc4f505645548f2dd8848e4046aca421548235f1945725f82f03b0ba5c774ddea6f9524cdcc302ee4712ef7d4bc1c16d7aa578d8fd8ceb680c16fcc6ca6a40afdcef6f89e81bd92f7d1f6e39c9c57f3239a1fcb23d649f8757348214572e53bc2c2c7ff8bce6d48df6e3c53ab7014a55c9296d05998a0d1b53749d9561541eb0cf6e1bfa65141ce9b6c30fe4f68cd8e869feba82675ec43bf953ab2994533d6d1af1705130243d9b9ee4088b635d6b4db5603b8784f4fe77d4b0d8a7935c06198d12fa0fc6e1ad2ddef96e7f9ab6103a2a29739ca3af9fe1736cdf49162e77d6f17d063f04dc2e1358af3da993fb3824e59575a9f15c7c429efd059477429be0c2a5b126078a8f8b1088d35aae59eac0897dfa4d45179947bad401c7417df2fac46f8782a2069f83cc18eda4d0070167878ad72f5d255e300a6368e0d390d3d0206aba68772b1e9d73c97406a0a5d80b7b8360502a9e7cb471fb5bd49ce9eee3a16f82aadca47327ccaa00a0575ed7191ffb710dd1ab7f801"; + // https://zombie.explorer.lordofthechains.com/tx/9390a26810342151f48f455b09e5d087a5429cbba08f2381b02c43b76f813e29 + let tx_hex = "0400008085202f8900000000000001030c00e8030000000000000169e7017fbd969be53da2c1b8812002baaf59ce98b230a9c1001397ba7f4db8676bd77e8ea644b67067d1f996d8d81c279961343f00a10095bccbddc341c98539287c900cf969688ddc574786e0e34bd6d3ec2ffaab5e2d472848781b116906669786c14c5c608b20dc23c9566fd46861f6a258b5ffc6de73495b56f4823e098c8664eab895d5cd31c013428ae2cbe940dc236ca40465ea2b912ce6c36555b2affb1f38b99b28dc593d865b0b948d567f9315df666d2e65e666d829b9823154bae0410bd885582b4a8a6eb4b9ae214b59ffd9b1167b7cd48f48a11cbd67c08f4e01ed4fd78fc91d0c9e70baa4f25761ef6c78cd7268b307aaa6ece2b443937eb4beac2c8843279a8879adbe0b381e65d0b674f2feeb54b78f80b377f66baab72c4cf9f10dde48f343c001df91a1a6d252ad8eca26eea0fdee49ad7024b505e55b4e082e94616794ddd7c2b852594b4b7af2292f0aa9e34f38322f548f1a21c015e92dbfd239ce18144f3b8045e9efa3de6b4c6b338f01d0adeb26a088a3c8c00503b67b2980b7663e97541e2944e4ad3588554966b6a930d2dc01d9fc7f8a846583fcf3b721f979705eff5bb9bb1fb0cad9ad941ceb3f581710efd8c50713a53751a0a196322ef8618bf1e097383666e91b5133ba81645d2b542181476eba2326cd02fb29a9f09edc46ea04b32ed9243597318d23b955a2570d78cbfb46cc26c1807eddd1de4785b6e752f859f7e25fc67f9e8a00feafac6fd7781eb72a663d9b80c10e9c387abc4d41294b3573785fd53bc56ccac2edf5c7bbb99cb3bcf87161fa893d2e1aabfee75754767cef07a12e44bb707720e727e585a258356cc797ecee8263c0f61cfc8ffa0360c758f1348ac44c186e12ce0f4faad43b4638abd4a0bc9fd4a6fa4352c20cc771241f95c26f1671ca95c8f4a63a8318dc43299f54e8a899df78ccfd3112a0d5ea637847dd2e3b05be8c0658dd0d7d814473fa5369957c00e84df600df23faaee5faa17b9ededad4731e5e9c1099dfddf5264756800dcfcad4b006b736d1d47c59a019acde4dc22249fc40846b77b43294e32a21db745e1bec790324c3d505edc79388a6e44b02841b26306ed48cfce1e941642c30792315016dba03797c8e4e279eec5b78aad602620471f24c25aea3aaa57509aa9eef2057f11bc95bad708918f2f0df74ac179d7dffc772b2c603dd89e7aea0e8f94f1a8bab4a4fba10bf05c88fbe4b021b3faff3d558e32e4bc20be4bed62d653674ce697390e098e590a3e354cb4a1e703474de8aab30cd76cf7e237f2e66bf486c4fc6c22028764e95adf7d8fa018f44b51ae6acfa3bf80f14c45c06623b916d79649abe0a2b229f96e60e421f6e734160da37f01e915cf73d1cacd1eb7f06c26c33b4d8e4dde264f3cfe84bada0601d1c03aa31c5938750ca0b852f3177883cae9f285d582a4eb38c05f8ef6e5cff5be0745e1ec66e20752bfd5bd5a1590fa280ace3e9786e0022e7ae3c48bcca14e9c5513bc8b57e15820a685f8348159862be0579a35d8ac9d1abaf36d9274c7e750fd9ad265c0d8f08c95ed9ce69eef3a55aef05f2d5d601f80f472689f3428e4f0095829a459813d5dace7e6137a752ae5567982e67b2092afeba99561fbe4e716f67bd1b4e8de1f376dec30eed27371bcc42d7de2ea0f4288054618e9afa002a2d1996b7a70a9683229f28bab811b67629dad527f325c0f12e19d92bac51e5924f27048fa118673b52b296b3642ec946d9915ded0ae84e1a2236da65f672bdad75a22cc0ea751c07e56d2ec22caa41afc98ec6b37a8c1b6a5378a81f2cdb2228f4efb8d7f35c0086a955e1b04bd09bd7e056c949fab1805f733a8b2061adad0c2b7fae33d21363de911e517b21a1539dfa1b3cbb1ea0dbfa3ffff23bbac01183f852de41e798fca5a278b711893175aeaded90873574d8de30b360f39ea239492c630eda4a811d3bb7a125054d5ca74bb6698aeea1a417ad19415ca0e5ca36abc2f96725986f73bcbe3113e391010d08f58f05979c7cef26ff92506c5d1eb2a2f6f5689e9a39957f0723bef3262f5190de996234d4f00b73ed74d78fdf1e6bf31161e16bd083bc6fbddc4eba85c17067e15f08019e5ed943de8e23a974d516abc641e85e641b03779816c30b3449a16b142417c1ff93ab7fa8f96a175e9ef73b3f06ac76788c27889d426efa78d5b8ce35be4591902f7766fe579a0aa28229235a920d26264c09625dea807f619a040f08931d6e1fe57ff0c48ea476be93a16d1fc8de3617984eeebcf14b63c839b41f8f9305402d1288c8e481a4fa5c3302bb1f83e3f0dc8ff9550f9bacb44bccb58f3de152abef5d578afed1c29dc89495b9e54a0c6d00f1dba45a2cf68c9512d9a9ff0b2531e58e47428a99cb246ca23f867b660dc71785b57407cc292f735634c602409792c4640831809f1f1e51903273b623aa0ae0cdd335c7b9db360b0bceb0d15f2313e1944800f30f82ed5bb07cfa1c4740c2bf2806539a4afac1f79d779b923ad8dc2493ebb2d2fce9aea58a009d64e7d1b71ca6893b076e41f7e88a4b51b5402e3fa6c60fa65a686adea229f0164318c9fa1b6d2d2218e5ada710daffecb6b7dd8bf7447658795c4c7a0ad710c4f02fd19017a0575f9467600cdca019793f2f49d197dbfc937828e5790b90929e5ca16037ec79734b64feec36b36c220a2979c45dd51e24c9fb21d8634471aac20c6f179f90c0d61c7b3d89826d146b157bedd8f6b66f6edfabfe04b49f2f2d999fc2e578a440bafd524c82ae614dc8017e379cf926e042f4fbd6f0628fde52de18d764ba8385b77569eda30d5a3617fb0a0c7fd26c821308c3ae98498d33b974cb318a04af3ea3fbcb13fc62fc952aaef095423da9ec7bdc7b77adbd403931189ddc98fe19a06711415b40a9a68812bb7c5453b7b2377910c7b89c99b379e038a7940487c0fd2405456ee55ab6ead3ef25a8a5b1abcae479c24f5e6869057e0bdabcdf352b4a64a3e385171a6e14c8102b2a187034e21705e3a457167fe0dc0d63d6e8d489c9a18c9d84b541504d36b086c2c63cc1a34c0080122c5d60ca33ab60289d16f21e1ded753607267c2093b1c587b89da9df65584fbe3ff9eb7f91d64e33912b8e91adc27191d22f8e835be6bb24546f21488f7abcb29339c34058d4f4093096144b17b8ab76a346275b7e7c80bca59d20e0bb482bb2a9cc3c9515cc1b5be17348c65c73e9fb1ed77d423c509f7cff0e355a34d080d310f3b848dbc209bbba6b6b109fb8d9556dca0fab086e197327ab423d5d762b68961244d8d22c30a8a3a116770bb15b5a0a347091a843b68d6a8e0f1c79f12523a7561c1233cd44db90f6cd3c1ce5fc13f8382177b5522aae028379269b71ae2a42f41dff7374ed7e83c89566f57297b82478b04359a2c199ce8f842112b7450cc1e2e2e394cda4c67e0b2302e21f6af997607ceefd067f77be8900bb3ecb3e30782477aa76861b286b9ddc9e36fcebb50f04f9516e02da31e6219bb5bcb81ee673d95be14c1bd2be4909556d6dbca0365292c582dedcafcc60b255ab7bcd9d977a4139f394ca1da81040e784fd8e7534f230bc5201e7f1db47eadc30f37609d5bbaba624157d98d65029bbab766b6c23c3049a32b894c0cfcb40913ba1cd2d5acda7d2acc920fd01c36f28fc6b7ffd01a37b17fc3235d0dbe9b8098530bed6894b288604b8689f4aafc22cdf211fb95ef5c90cae62a250234e6f790e9a15012acac88305dc4f91fd564a9ab8bb27c057ec5dd46fe952a7be557caea9b7b1d6118aa42df79b8c207e2bae6c34d67dc32b4360ad20b3e609e9caeb7f432ad51cfce139f2d4eb9ed219f4323acd5685e0e0409939eb662175a83fa083f500516dbcb091a3448cb24c3198c8fc547fbda3cb0894edeceef7ccb4ad746aa06f4038b63ab4095a9c390656520561ba3763b1057b3af7cb548342a2bfc2ab725b01b12a7adfc30d7d9632acafd2595cde406b8637a911b7c86f7b09b11f58acec3f1a1bd7cf6853331b48d7907ed699d91fbdbcab8001e3d8d3a26b491b6e2d98c5e149847a07a2b7faa1f567cd4bc9c83ad553339632f3dcacb890c5222656b3349ddd5c8eacaa490ac0b2b38f8a26da9ce7789f5601769a7f10b93125cb93b589bda4ddb4e8795817b60cc149af7c0699b2bbbf655f2f5ec170d6af51213e8c725e699d181923ecf10c6f1069f46e6bc89c7a29d2ebe133b5c0c4b67826a93add7d4824e60b4c5f0cee358abedb50c54a59e95185d7a80081f2dddba5c7c7c637b2dfe8575ddaa71306a2725c9ec17b8e4e1f271a442f6798cc21bbd55c2d69819ddde37a8e8d6a812c41a3e58719b7c96e9375155c4a873ed698ad37144ef32e3fe41cce9c48bbe31441dbbeec7b97734769063d6d04cd8d4963f09f7101bf57cb97a83452cc5de873c5ac0ce001c471c9fcd3275d90a118dd4c25a525d9fb358ff85104b98136850786b387fa17cc1a1d128bc5f7c365ec7920ea677e4c8023071a958647d9fbd27e29d7d099b4dfbbac086ac2af00407fd12092ef1f4847bf8988d839e49a6b5b42482c3dde77022ace66e1ca15b46f2df88d053c1bc3623110b3be74b08749eba6d22f87a44cf7cc1997e7e45d0e"; let tx_bytes = hex::decode(tx_hex).unwrap(); let tx = ZTransaction::read(tx_bytes.as_slice()).unwrap(); let tx = tx.into(); + let expected_fee = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - fee_addr: &[], dex_fee: &DexFee::Standard(MmNumber::from("0.001")), min_block_number: 12000, uuid: &[1; 16], }; // Invalid amount should return an error - let err = block_on(coin.validate_fee(validate_fee_args)).unwrap_err().into_inner(); + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid amount")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } @@ -243,52 +307,73 @@ fn zombie_coin_validate_dex_fee() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - fee_addr: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), + dex_fee: &expected_fee, min_block_number: 12000, uuid: &[2; 16], }; - let err = block_on(coin.validate_fee(validate_fee_args)).unwrap_err().into_inner(); + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid memo")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid memo")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } + /* Fix realtime min_block_number to run this test: // Confirmed before min block + let min_block_number = 451208; let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - fee_addr: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), - min_block_number: 14000, + dex_fee: &expected_fee, + min_block_number: , uuid: &[1; 16], }; - let err = block_on(coin.validate_fee(validate_fee_args)).unwrap_err().into_inner(); + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min block")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } + } */ // Success validation let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - fee_addr: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), + dex_fee: &expected_fee, min_block_number: 12000, uuid: &[1; 16], }; - block_on(coin.validate_fee(validate_fee_args)).unwrap(); -} + coin.validate_fee(validate_fee_args).await.unwrap(); -fn default_zcoin_activation_params() -> ZcoinActivationParams { - ZcoinActivationParams { - mode: ZcoinRpcMode::Native, - required_confirmations: None, - requires_notarization: None, - zcash_params_path: None, - scan_blocks_per_iteration: 0, - scan_interval_ms: 0, - account: 0, + // Test old standard dex fee with no burn output + // TODO: disable when the upgrade transition period ends + + // https://zombie.explorer.lordofthechains.com/tx/9eb7fc697b280499df33e5838af6e67540d436fd8f565f47a7f03e6013e8342c + let tx_2_hex = "0400008085202f8900000000000006030c00e80300000000000001c167d6e78e09dfbac2973bfd8acac75fc603f6ffb481377e3ec790f1cc812a8a3979ecfb8a0c7c3a966d90675261568550f9363f9384a21390d7f58bde6f7b03270d88e1fa61d739c27d7f585c9bbc81a3d522fbb88fe8dc8567e27a048d475ce14fdfd11455fd54c577538438decbf6954f1ffba86c78896178ce514c5f1762a7de9e83552533eb4c558c4f9950b1806f266b25d6437f5aac08048d6f48100d49ecb2253e85c3b555a7cd84c9628ae58e5d68ddad61e69edfcdc0fa12170dd80340c417bff9e1711bf6e9728a6a52c42598d7ffd00c35679b1555cab075e54b134901d02ca9b07bb20c5719b2728faa020fb844c183c2ae649034a5476c4d129c3f97cd00a87be1ca7e73d027188cdab57fbb34b5addb7432f51454299b8cf47b389f98bad8abd42d82a2f8c2d11312e39272d44409540bcfa4c6b445e8e6dc63cc2fd5db1448875adb055ea8665c863bd07bf3aa8eb210f638287789957c96c54819061ee215eb7ba7b6048591a57f097a3e5da06b6359325d830d5b74c20c025996a113e4bb9fd2c853b7360d4961396cd99c23a13de972097eede3a955a5d5d8c8695a7290581a248fc03ea87606e71564d8e8fb00ebb8d5c10fc8fefe1660171524264060d15363fc2dc0ac0ab21fcbae1dc53786873cb9e8716f3ada651e79c3306ad49adeeb354213cc37499e217fa1c0f219e85bd22cf493f5e76f053543dd3b36bd180b1dcf17f781e35d6955c33c06426a885138f1e21b78ee87a27624f33b6567bfa6a0fe43e2d623578f6917d300a408c4dd48683213ffad453de1003e120fbfa74a6db4628af9d446e26492fde67bf52d034fcaf2b9b959472404fd631ef599815c6f190807b75f638e134148a5813424ba6cf59cf86ce515a14b95f7b8f80b1aa1b3cbbc091fa2a686277a9cc613e48b2c227aed7b4b093ac8b12a238bc99f9983c8bac21bb0f897eada35bf0e01b1436cf6d44b959595bdcdfd4676e28b500b9ad6b8a5825c3d3c0c38a4a5a2c3ded205584439621eaa7ee639b09aca1f533bb4892b29d761d94887fa78f605b9b8f5b3ab44ea578d9329bd78d7a6ae903f1960e16007a924be79ab31ea6ed7466485488b5c71eb02d6b99f345f2f61cb3cd994045c502d19f615233b3ebb263981de26674de082d384cc04c09a309567780f7f24298847fc2dff5f22082074684aa9efa260b8aaf4357bff2e9d32f8918b16876051b5459136dcc8788aba7b2ead435c3bf662f9f1acddd4a8a71b593e99ed50e158028946195ee991666bf88f4cf4d30a04c877ce8a9e6d224aed662e85a32f5cb9029a3dd4ba663b6f6314ef58fbce623171946d01d1ff456f90131159e5209cb41329061a0dd8a5fc35576108681e783fb173f67dda33134a9b1f07494a1d6273810fd77a25c92f7444d6226738d5c7161b7b198be069ac65d50a22d728292e95d1859e0c646db62aa3f401e55026a551b1edfe8fd5eca8e4c6836bd09429b5e22f64a09db4c6935b6febcbac6430f66dc0280c9be046133795f1f59ec32cbf4511749984f7b2ba131588f86f82322901ee7d709550ecadb5b915d5cfb2e950d2a8c5eda57da49d2ac9562b851f81e70a32178989e83807f04a6324cf7320a26a91b41e31a06c706431794ffb8b9ce5f3d853fb9106c8a98ea3b2948356948bfbbd63eb30e3cb68d7e373df80221d1b1211c717afe8b7b0b46a3208859254d9ae3517b8e031f413178c0fd408e76ccdc580a9a19edf4b3c70c273f4c8c626fad225e5aeee890c65328437b8bf316066e54a4741d8ac8ab9b5555f09b89b79165f9aa08a59be8f10c121b1b425bd5e3a64b6e4db3e1cacb00a5867fd05b454b75ff1eb8560770f21af7680107560a2209373d2999eb21bed2a10bafe1eaf5a31c18e69c63cce9b8c6cddcfc1088f956bcf3c9adeb77ef0589ab6405f0a9ba5650819a48fb42597fcd2f4ad67bdc89870d82eaa0d8dbd298a59ff552576dedb539834de725638e0f68307d4ac203d8e2e4649e31abc4e8748251c8fb6df3459300d1badfc19ad4d2f680f466b02680bb3e5a13c0c8a5db3665bc9fc2093c4d38acb176754db556ebd1663c23f284bec95279957b112131f8aa09af15ff26eebea3215c96b9df43c9fc9134d9db4e588aff293f3084db13e1d92bc33ca07a1b534b4a4e5fcbf098be7d26f9312db7f9d6b160318a4562c3c3b0c87688c59f402e0032242324339ef33713bf39c2110e7eb155bf926888385fe4b18bf3ef13dc2601b76def3d763f5b2ddea363f7e3697112194fb6332be96540a53a86e1e34fd70429dcfc39c5e2f68fa72e0045fe4ef12b965f0827c5bee9cd4f0c9b4cf6468316384fe33df5703c7742f9b409b9a508e94faa8be3c27ad75d21f85ee31753c96deb909221befd62bae084885c890d89f775dc0eee940ffbcad0aa65c08a71d09e234ad150e82610ba03deb608d44e9019d8579f9e9351daa6f3bcbbc8ec170c8b700bcb495c333b32136721f6417a3f3b12500641eb7af9e5813fafd27794a7b2476320fde18f3019302d49d77c3536af214e6c8357a36029a37a07011d1cdbe0db3fe7443a6908f5d3b6e08d61f33bad2a0bfbc9db86022d4f91b0ba6ef1b5ec30f0187f4c540eeb117c4d3d78659e46540df4b9301c6fce031d7e438abeb13a747be6ce9c0a33a2bd6f6092d0a26d5ba138bb6f2c3113ea6cff868853dacfc5df0433049a59d2b365e9a87ee6a6203e52121d60bc709feb1c1a30e95fbc600f648dfa5fadc8cf324a4c5d91e1f80501661aa51a518b381933932a1367e4369e07943f291012f5a9394692d9984fc2dc55c0ec4fe3d18a4a0b9f9d7c9d3f57b2e2a0c31f08f17ffe7355fec963b8ae364ed8cff046aa8220dc813f2dc78405069c707afadb77cfc8d64803a25eab7ebc74c738b41f9b3f2d881f1e2b77d37f38c1b5991daf5c911c04947891909f9c3e50e1314884207f0ea99d9310c9cfe93fea53fb57c93efbd412702e283e61196b9158de774333893b51c768ae48ec086e47b105d0b21357bd14f85b9f145fbfd63c0e998d6e54900915c8ffaf1234fa910ede3035e5e47ee9b22559459d0ea2b0f3242c5ec2782d09a7b477b560b1ecfd14d82f24600334d2c85dc2def0f457ea199e266c52fb9a596de02da05a9df8e4731cf941e1ada11c66d0954742745d5ef1b36dc7628614ed28ba9358ab38c2d007aa90147906270ab35ae26fa3473ec5881f8e6ed04c592a403386c4061becc70b5735531f8d249abb079317f43f111de58c6678e62a6d2dc83193acef928c906"; + let tx_2_bytes = hex::decode(tx_2_hex).unwrap(); + let tx_2 = ZTransaction::read(tx_2_bytes.as_slice()).unwrap(); + let tx_2 = tx_2.into(); + + // Success validation + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx_2, + expected_sender: &[], + dex_fee: &DexFee::Standard("0.00999999".into()), + min_block_number: 12000, + uuid: &[1; 16], + }; + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), + _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } + + // Success validation + let expected_std_fee = DexFee::Standard("0.01".into()); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx_2, + expected_sender: &[], + dex_fee: &expected_std_fee, + min_block_number: 12000, + uuid: &[1; 16], + }; + coin.validate_fee(validate_fee_args).await.unwrap(); } diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index c2fba37a88..ffda1aba42 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -11,7 +11,7 @@ use crate::utxo::utxo_common::payment_script; use crate::utxo::{sat_from_big_decimal, UtxoAddressFormat}; use crate::z_coin::SendOutputsErr; use crate::z_coin::{ZOutput, DEX_FEE_OVK}; -use crate::NumConversError; +use crate::{DexFee, NumConversError}; use crate::{PrivKeyPolicyNotAllowed, TransactionEnum}; use bitcrypto::dhash160; use derive_more::Display; @@ -85,19 +85,36 @@ pub async fn z_send_htlc( /// Sends HTLC output from the coin's my_z_addr pub async fn z_send_dex_fee( coin: &ZCoin, - amount: BigDecimal, + dex_fee: DexFee, uuid: &[u8], ) -> Result> { - let dex_fee_amount = sat_from_big_decimal(&amount, coin.utxo_arc.decimals)?; + if matches!(dex_fee, DexFee::NoFee) { + return MmError::err(SendOutputsErr::InternalError("unexpected DexFee::NoFee".to_string())); + } + let dex_fee_amount_sat = sat_from_big_decimal(&dex_fee.fee_amount().to_decimal(), coin.utxo_arc.decimals)?; + // add dex fee output let dex_fee_out = ZOutput { to_addr: coin.z_fields.dex_fee_addr.clone(), - amount: Amount::from_u64(dex_fee_amount).map_err(|_| NumConversError::new("Invalid ZCash amount".into()))?, + amount: Amount::from_u64(dex_fee_amount_sat) + .map_err(|_| NumConversError::new("Invalid ZCash amount".into()))?, viewing_key: Some(DEX_FEE_OVK), memo: Some(MemoBytes::from_bytes(uuid).expect("uuid length < 512")), }; + let mut outputs = vec![dex_fee_out]; + if let Some(dex_burn_amount) = dex_fee.burn_amount() { + let dex_burn_amount_sat = sat_from_big_decimal(&dex_burn_amount.to_decimal(), coin.utxo_arc.decimals)?; + // add output to the dex burn address: + let dex_burn_out = ZOutput { + to_addr: coin.z_fields.dex_burn_addr.clone(), + amount: Amount::from_u64(dex_burn_amount_sat) + .map_err(|_| NumConversError::new("Invalid ZCash amount".into()))?, + viewing_key: Some(DEX_FEE_OVK), + memo: Some(MemoBytes::from_bytes(uuid).expect("uuid length < 512")), + }; + outputs.push(dex_burn_out); + } - let tx = coin.send_outputs(vec![], vec![dex_fee_out]).await?; - + let tx = coin.send_outputs(vec![], outputs).await?; Ok(tx) } diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index bd71d554c6..40eef387c0 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -17,6 +17,7 @@ use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::StreamExt; use hex::{FromHex, FromHexError}; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use parking_lot::Mutex; use prost::Message; use rpc::v1::types::{Bytes, H256 as H256Json}; @@ -33,7 +34,6 @@ use zcash_primitives::zip32::ExtendedSpendingKey; pub(crate) mod z_coin_grpc { tonic::include_proto!("pirate.wallet.sdk.rpc"); } -use crate::z_coin::z_balance_streaming::ZBalanceEventSender; use z_coin_grpc::compact_tx_streamer_client::CompactTxStreamerClient; use z_coin_grpc::{ChainSpec, CompactBlock as TonicCompactBlock}; @@ -509,7 +509,6 @@ pub(super) async fn init_light_client<'a>( sync_params: &Option, skip_sync_params: bool, z_spending_key: &ExtendedSpendingKey, - z_balance_event_sender: Option, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -568,10 +567,10 @@ pub(super) async fn init_light_client<'a>( main_sync_state_finished: false, on_tx_gen_watcher, watch_for_tx: None, - scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration, + scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration.into(), scan_interval_ms: builder.z_coin_params.scan_interval_ms, first_sync_block: first_sync_block.clone(), - z_balance_event_sender, + streaming_manager: builder.ctx.event_stream_manager.clone(), }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(light_rpc_clients))); @@ -588,7 +587,6 @@ pub(super) async fn init_native_client<'a>( native_client: NativeClient, blocks_db: BlockDbImpl, z_spending_key: &ExtendedSpendingKey, - z_balance_event_sender: Option, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -616,10 +614,10 @@ pub(super) async fn init_native_client<'a>( main_sync_state_finished: false, on_tx_gen_watcher, watch_for_tx: None, - scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration, + scan_blocks_per_iteration: builder.z_coin_params.scan_blocks_per_iteration.into(), scan_interval_ms: builder.z_coin_params.scan_interval_ms, first_sync_block: first_sync_block.clone(), - z_balance_event_sender, + streaming_manager: builder.ctx.event_stream_manager.clone(), }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(native_client))); @@ -718,7 +716,8 @@ pub struct SaplingSyncLoopHandle { scan_blocks_per_iteration: u32, scan_interval_ms: u64, first_sync_block: FirstSyncBlock, - z_balance_event_sender: Option, + /// A copy of the streaming manager to send notifications to the streamers upon new txs, balance change, etc... + streaming_manager: StreamingManager, } impl SaplingSyncLoopHandle { @@ -832,17 +831,21 @@ impl SaplingSyncLoopHandle { if max_in_wallet >= current_block { break; } else { + debug!("Updating wallet.db from block {} to {}", max_in_wallet, current_block); self.notify_building_wallet_db(max_in_wallet.into(), current_block.into()); } }, - None => self.notify_building_wallet_db(0, current_block.into()), + None => { + debug!("Updating wallet.db from block {} to {}", 0, current_block); + self.notify_building_wallet_db(0, current_block.into()) + }, } let scan = DataConnStmtCacheWrapper::new(wallet_ops.clone()); blocks_db .process_blocks_with_mode( self.consensus_params.clone(), - BlockProcessingMode::Scan(scan, self.z_balance_event_sender.clone()), + BlockProcessingMode::Scan(scan, self.streaming_manager.clone()), None, Some(self.scan_blocks_per_iteration), ) @@ -941,7 +944,7 @@ type SyncWatcher = AsyncReceiver; type NewTxNotifier = AsyncSender)>>; pub(super) struct SaplingSyncConnector { - sync_watcher: SyncWatcher, + pub(super) sync_watcher: SyncWatcher, on_tx_gen_notifier: NewTxNotifier, abort_handle: Arc>, first_sync_block: FirstSyncBlock, diff --git a/mm2src/coins/z_coin/z_tx_history.rs b/mm2src/coins/z_coin/z_tx_history.rs index 57eb2fdb4c..7c442cc676 100644 --- a/mm2src/coins/z_coin/z_tx_history.rs +++ b/mm2src/coins/z_coin/z_tx_history.rs @@ -1,28 +1,36 @@ +use std::collections::HashSet; + use crate::z_coin::{ZCoin, ZTxHistoryError}; use common::PagingOptionsEnum; use mm2_err_handle::prelude::MmError; +use primitives::hash::H256; +use std::convert::TryInto; +use zcash_primitives::transaction::TxId; cfg_wasm32!( use crate::z_coin::storage::wasm::tables::{WalletDbBlocksTable, WalletDbReceivedNotesTable, WalletDbTransactionsTable}; use crate::MarketCoinOps; use mm2_number::BigInt; + use mm2_db::indexed_db::cursor_prelude::CursorError; + use mm2_err_handle::prelude::MapToMmResult; use num_traits::ToPrimitive; ); cfg_native!( use crate::z_coin::BLOCKS_TABLE; + use common::async_blocking; use db_common::sqlite::sql_builder::{name, SqlBuilder, SqlName}; use db_common::sqlite::rusqlite::Error as SqliteError; use db_common::sqlite::rusqlite::Row; use db_common::sqlite::offset_by_id; - use common::async_blocking; + use db_common::sqlite::rusqlite::types::Type; ); #[cfg(not(target_arch = "wasm32"))] const TRANSACTIONS_TABLE: &str = "transactions"; pub(crate) struct ZCoinTxHistoryItem { - pub(crate) tx_hash: Vec, + pub(crate) tx_hash: H256, pub(crate) internal_id: i64, pub(crate) height: i64, pub(crate) timestamp: i64, @@ -118,11 +126,14 @@ pub(crate) async fn fetch_tx_history_from_db( } } - let mut tx_hash = tx.txid; + let mut tx_hash: [u8; 32] = tx + .txid + .try_into() + .map_err(|_| ZTxHistoryError::IndexedDbError("Expected 32 bytes for transaction hash".to_string()))?; tx_hash.reverse(); tx_details.push(ZCoinTxHistoryItem { - tx_hash, + tx_hash: H256::from(tx_hash), internal_id: internal_id as i64, height: *height as i64, timestamp: *time as i64, @@ -142,10 +153,21 @@ pub(crate) async fn fetch_tx_history_from_db( #[cfg(not(target_arch = "wasm32"))] impl ZCoinTxHistoryItem { fn try_from_sql_row(row: &Row<'_>) -> Result { - let mut tx_hash: Vec = row.get(0)?; + let tx_bytes: Vec = row.get(0)?; + let mut tx_hash: [u8; 32] = tx_bytes.try_into().map_err(|_| { + SqliteError::FromSqlConversionFailure( + 0, + Type::Blob, + Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Expected 32 bytes for transaction hash", + )), + ) + })?; tx_hash.reverse(); + Ok(ZCoinTxHistoryItem { - tx_hash, + tx_hash: H256::from(tx_hash), internal_id: row.get(1)?, height: row.get(2)?, timestamp: row.get(3)?, @@ -220,3 +242,156 @@ pub(crate) async fn fetch_tx_history_from_db( }) .await } + +#[cfg(target_arch = "wasm32")] +pub(crate) async fn fetch_txs_from_db( + z: &ZCoin, + tx_hashes: HashSet, +) -> Result, MmError> { + let wallet_db = z.z_fields.light_wallet_db.clone(); + let wallet_db = wallet_db.db.lock_db().await.unwrap(); + let db_transaction = wallet_db.get_inner().transaction().await?; + let tx_table = db_transaction.table::().await?; + + let limit = tx_hashes.len(); + let condition = { + // Convert TxIds to Vecs for comparison. + let tx_hashes: HashSet<_> = tx_hashes.into_iter().map(|txid| txid.0.to_vec()).collect(); + move |tx| { + let tx = serde_json::from_value::(tx) + .map_to_mm(|err| CursorError::ErrorDeserializingItem(err.to_string()))?; + Ok(tx_hashes.contains(&tx.txid)) + } + }; + + // Fetch transactions + let txs = tx_table + .cursor_builder() + .only("ticker", z.ticker())? + // We need to explicitly set a limit since `where_` implicitly sets a limit of 1 if no limit is set. + // TODO: Remove when `where_` doesn't exhibit this behavior. + .limit(limit) + .where_(condition) + .reverse() + .open_cursor("ticker") + .await? + .collect() + .await?; + + // Fetch received notes + let rn_table = db_transaction.table::().await?; + let received_notes = rn_table + .cursor_builder() + .only("ticker", z.ticker())? + .open_cursor("ticker") + .await? + .collect() + .await?; + + // Fetch blocks + let blocks_table = db_transaction.table::().await?; + let blocks = blocks_table + .cursor_builder() + .only("ticker", z.ticker())? + .open_cursor("ticker") + .await? + .collect() + .await?; + + // Process transactions and construct tx_details + let mut transactions = Vec::new(); + for (tx_id, tx) in txs { + if let Some((_, WalletDbBlocksTable { height, time, .. })) = blocks + .iter() + .find(|(_, block)| tx.block.map(|b| b == block.height).unwrap_or_default()) + { + let internal_id = tx_id; + let mut received_amount = 0; + let mut spent_amount = 0; + + for (_, note) in &received_notes { + if internal_id == note.tx { + received_amount += note.value.to_u64().ok_or_else(|| { + ZTxHistoryError::IndexedDbError("Number is too large to fit in a u64".to_string()) + })? as i64; + } + + // detecting spent amount by "spent" field in received_notes table + if let Some(spent) = ¬e.spent { + if &BigInt::from(internal_id) == spent { + spent_amount += note.value.to_u64().ok_or_else(|| { + ZTxHistoryError::IndexedDbError("Number is too large to fit in a u64".to_string()) + })? as i64; + } + } + } + + let mut tx_hash: [u8; 32] = tx + .txid + .try_into() + .map_err(|_| ZTxHistoryError::IndexedDbError("Expected 32 bytes for transaction hash".to_string()))?; + tx_hash.reverse(); + + transactions.push(ZCoinTxHistoryItem { + tx_hash: H256::from(tx_hash), + internal_id: internal_id as i64, + height: *height as i64, + timestamp: *time as i64, + received_amount, + spent_amount, + }); + } + } + + Ok(transactions) +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) async fn fetch_txs_from_db( + z: &ZCoin, + tx_hashes: HashSet, +) -> Result, MmError> { + let wallet_db = z.z_fields.light_wallet_db.clone(); + async_blocking(move || { + let sql_query = SqlBuilder::select_from(name!(TRANSACTIONS_TABLE; "txes")) + .field("txes.txid as tx_hash") + .field("txes.id_tx as internal_id") + .field("txes.block as block") + .field("blocks.time") + .field("COALESCE(rn.received_amount, 0)") + .field("COALESCE(sn.sent_amount, 0)") + .and_where_in_quoted( + // Make sure the tx hash from the DB is lowercase, + "lower(hex(tx_hash))", + &tx_hashes + .iter() + // as well as the tx hashes we are looking for. + .map(|tx_hash| hex::encode(tx_hash.0).to_lowercase()) + .collect::>(), + ) + .left() + .join("(SELECT tx, SUM(value) as received_amount FROM received_notes GROUP BY tx) as rn") + .on("txes.id_tx = rn.tx") + .join("(SELECT spent, SUM(value) as sent_amount FROM received_notes GROUP BY spent) as sn") + .on("txes.id_tx = sn.spent") + .join(BLOCKS_TABLE) + .on("txes.block = blocks.height") + .group_by("internal_id") + .order_by("block", true) + .order_by("internal_id", false) + .sql() + .expect("valid query"); + + let txs = wallet_db + .db + .inner() + .lock() + .unwrap() + .sql_conn() + .prepare(&sql_query)? + .query_map([], ZCoinTxHistoryItem::try_from_sql_row)? + .collect::, _>>()?; + Ok(txs) + }) + .await +} diff --git a/mm2src/coins_activation/src/bch_with_tokens_activation.rs b/mm2src/coins_activation/src/bch_with_tokens_activation.rs index ebda8efcba..b38c1bee36 100644 --- a/mm2src/coins_activation/src/bch_with_tokens_activation.rs +++ b/mm2src/coins_activation/src/bch_with_tokens_activation.rs @@ -19,7 +19,6 @@ use common::{drop_mutability, true_f}; use crypto::CryptoCtxError; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; use rpc_task::RpcTaskHandleShared; use serde_derive::{Deserialize, Serialize}; @@ -347,19 +346,18 @@ impl PlatformCoinWithTokensActivationOps for BchCoin { storage: impl TxHistoryStorage + Send + 'static, initial_balance: Option, ) { - let fut = bch_and_slp_history_loop(self.clone(), storage, ctx.metrics.clone(), initial_balance); + let fut = bch_and_slp_history_loop( + self.clone(), + storage, + ctx.metrics.clone(), + ctx.event_stream_manager.clone(), + initial_balance, + ); let settings = AbortSettings::info_on_abort(format!("bch_and_slp_history_loop stopped for {}", self.ticker())); self.spawner().spawn_with_settings(fut, settings); } - async fn handle_balance_streaming( - &self, - _config: &EventStreamConfiguration, - ) -> Result<(), MmError> { - Ok(()) - } - fn rpc_task_manager( _activation_ctx: &CoinsActivationContext, ) -> &InitPlatformCoinWithTokensTaskManagerShared { diff --git a/mm2src/coins_activation/src/context.rs b/mm2src/coins_activation/src/context.rs index 5ae19eb60e..dca33f40c8 100644 --- a/mm2src/coins_activation/src/context.rs +++ b/mm2src/coins_activation/src/context.rs @@ -4,7 +4,8 @@ use crate::init_erc20_token_activation::Erc20TokenTaskManagerShared; use crate::lightning_activation::LightningTaskManagerShared; #[cfg(feature = "enable-sia")] use crate::sia_coin_activation::SiaCoinTaskManagerShared; -use crate::utxo_activation::{QtumTaskManagerShared, UtxoStandardTaskManagerShared}; +use crate::tendermint_with_assets_activation::TendermintCoinTaskManagerShared; +use crate::utxo_activation::{BchTaskManagerShared, QtumTaskManagerShared, UtxoStandardTaskManagerShared}; use crate::z_coin_activation::ZcoinTaskManagerShared; use mm2_core::mm_ctx::{from_ctx, MmArc}; use rpc_task::RpcTaskManager; @@ -12,12 +13,14 @@ use std::sync::Arc; pub struct CoinsActivationContext { pub(crate) init_utxo_standard_task_manager: UtxoStandardTaskManagerShared, + pub(crate) init_bch_task_manager: BchTaskManagerShared, pub(crate) init_qtum_task_manager: QtumTaskManagerShared, #[cfg(feature = "enable-sia")] pub(crate) init_sia_task_manager: SiaCoinTaskManagerShared, pub(crate) init_z_coin_task_manager: ZcoinTaskManagerShared, pub(crate) init_eth_task_manager: EthTaskManagerShared, pub(crate) init_erc20_token_task_manager: Erc20TokenTaskManagerShared, + pub(crate) init_tendermint_coin_task_manager: TendermintCoinTaskManagerShared, #[cfg(not(target_arch = "wasm32"))] pub(crate) init_lightning_task_manager: LightningTaskManagerShared, } @@ -28,14 +31,16 @@ impl CoinsActivationContext { from_ctx(&ctx.coins_activation_ctx, move || { Ok(CoinsActivationContext { #[cfg(feature = "enable-sia")] - init_sia_task_manager: RpcTaskManager::new_shared(), - init_utxo_standard_task_manager: RpcTaskManager::new_shared(), - init_qtum_task_manager: RpcTaskManager::new_shared(), - init_z_coin_task_manager: RpcTaskManager::new_shared(), - init_eth_task_manager: RpcTaskManager::new_shared(), - init_erc20_token_task_manager: RpcTaskManager::new_shared(), + init_sia_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_utxo_standard_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_bch_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_qtum_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_z_coin_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_eth_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_erc20_token_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), + init_tendermint_coin_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), #[cfg(not(target_arch = "wasm32"))] - init_lightning_task_manager: RpcTaskManager::new_shared(), + init_lightning_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), }) }) } diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs index 77970284b4..c73672d9e8 100644 --- a/mm2src/coins_activation/src/erc20_token_activation.rs +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -1,8 +1,8 @@ use crate::{prelude::{TryFromCoinProtocol, TryPlatformCoinFromMmCoinEnum}, token::{EnableTokenError, TokenActivationOps, TokenProtocolParams}}; use async_trait::async_trait; -use coins::eth::display_eth_address; use coins::eth::v2_activation::{EthTokenActivationParams, EthTokenProtocol, NftProtocol, NftProviderEnum}; +use coins::hd_wallet::DisplayAddress; use coins::nft::nft_structs::NftInfo; use coins::{eth::{v2_activation::{Erc20Protocol, EthTokenActivationError}, valid_addr_from_str, EthCoin}, @@ -152,7 +152,7 @@ impl TokenActivationOps for EthCoin { ) .await?; - let address = display_eth_address(&token.derivation_method().single_addr_or_err().await?); + let address = token.derivation_method().single_addr_or_err().await?.display_address(); let token_contract_address = token.erc20_token_address().ok_or_else(|| { EthTokenActivationError::InternalError("Token contract address is missing".to_string()) })?; diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 7bc62b444a..d8c4a0f49e 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -12,8 +12,8 @@ use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy}; use coins::eth::v2_activation::{EthTokenActivationError, NftActivationRequest, NftProviderEnum}; -use coins::eth::{display_eth_address, Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; -use coins::hd_wallet::RpcTaskXPubExtractor; +use coins::eth::{Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; +use coins::hd_wallet::{DisplayAddress, RpcTaskXPubExtractor}; use coins::my_tx_history_v2::TxHistoryStorage; use coins::nft::nft_structs::NftInfo; use coins::{CoinBalance, CoinBalanceMap, CoinProtocol, CoinWithDerivationMethod, DerivationMethod, MarketCoinOps, @@ -25,7 +25,6 @@ use common::{drop_mutability, true_f}; use crypto::HwRpcError; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::EventStreamConfiguration; #[cfg(target_arch = "wasm32")] use mm2_metamask::MetamaskRpcError; use mm2_number::BigDecimal; @@ -368,11 +367,8 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { return Ok(EthWithTokensActivationResult::Iguana( IguanaEthWithTokensActivationResult { current_block, - eth_addresses_infos: HashMap::from([(display_eth_address(my_address), eth_address_info)]), - erc20_addresses_infos: HashMap::from([( - display_eth_address(my_address), - erc20_address_info, - )]), + eth_addresses_infos: HashMap::from([(my_address.display_address(), eth_address_info)]), + erc20_addresses_infos: HashMap::from([(my_address.display_address(), erc20_address_info)]), nfts_infos: nfts_map, }, )); @@ -396,8 +392,8 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { Ok(EthWithTokensActivationResult::Iguana( IguanaEthWithTokensActivationResult { current_block, - eth_addresses_infos: HashMap::from([(display_eth_address(my_address), eth_address_info)]), - erc20_addresses_infos: HashMap::from([(display_eth_address(my_address), erc20_address_info)]), + eth_addresses_infos: HashMap::from([(my_address.display_address(), eth_address_info)]), + erc20_addresses_infos: HashMap::from([(my_address.display_address(), erc20_address_info)]), nfts_infos: nfts_map, }, )) @@ -449,13 +445,6 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { ) { } - async fn handle_balance_streaming( - &self, - _config: &EventStreamConfiguration, - ) -> Result<(), MmError> { - Ok(()) - } - fn rpc_task_manager( activation_ctx: &CoinsActivationContext, ) -> &InitPlatformCoinWithTokensTaskManagerShared { diff --git a/mm2src/coins_activation/src/init_token.rs b/mm2src/coins_activation/src/init_token.rs index 01d47b3656..6b32f83622 100644 --- a/mm2src/coins_activation/src/init_token.rs +++ b/mm2src/coins_activation/src/init_token.rs @@ -16,8 +16,8 @@ use mm2_err_handle::mm_error::{MmError, MmResult, NotEqual, NotMmError}; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError, RpcTaskUserActionRequest}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes, TaskId}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -48,7 +48,7 @@ pub struct InitTokenReq { pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sync + 'static { type ActivationRequest: Clone + Send + Sync; type ProtocolInfo: TokenProtocolParams + TryFromCoinProtocol + Clone + Send + Sync; - type ActivationResult: serde::Serialize + Clone + CurrentBlock + Send + Sync; + type ActivationResult: CurrentBlock + serde::Serialize + Clone + Send + Sync; type ActivationError: From + Into + NotEqual @@ -56,8 +56,8 @@ pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sy + Clone + Send + Sync; - type InProgressStatus: InitTokenInitialStatus + Clone + Send + Sync; - type AwaitingStatus: Clone + Send + Sync; + type InProgressStatus: InitTokenInitialStatus + serde::Serialize + Clone + Send + Sync; + type AwaitingStatus: serde::Serialize + Clone + Send + Sync; type UserAction: NotMmError + Send + Sync; /// Getter for the token initialization task manager. @@ -87,7 +87,7 @@ pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sy /// Implementation of the init token RPC command. pub async fn init_token( ctx: MmArc, - request: InitTokenReq, + request: RpcInitReq>, ) -> MmResult where Token: InitTokenActivationOps + Send + Sync + 'static, @@ -95,6 +95,7 @@ where InitTokenError: From, (Token::ActivationError, InitTokenError): NotEqual, { + let (client_id, request) = (request.client_id, request.inner); if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { return MmError::err(InitTokenError::TokenIsAlreadyActivated { ticker: request.ticker }); } @@ -123,7 +124,7 @@ where }; let task_manager = Token::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task, client_id) .mm_err(|e| InitTokenError::Internal(e.to_string()))?; Ok(InitTokenResponse { task_id }) diff --git a/mm2src/coins_activation/src/l2/init_l2.rs b/mm2src/coins_activation/src/l2/init_l2.rs index e6b0888700..3dd3276ec6 100644 --- a/mm2src/coins_activation/src/l2/init_l2.rs +++ b/mm2src/coins_activation/src/l2/init_l2.rs @@ -10,7 +10,8 @@ use common::SuccessResponse; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusRequest, RpcTaskUserActionRequest}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; use serde_derive::Deserialize; use serde_json::Value as Json; @@ -39,8 +40,8 @@ pub trait InitL2ActivationOps: Into + Send + Sync + 'static { type CoinConf: Clone + Send + Sync; type ActivationResult: serde::Serialize + Clone + Send + Sync; type ActivationError: From + NotEqual + SerMmErrorType + Clone + Send + Sync; - type InProgressStatus: InitL2InitialStatus + Clone + Send + Sync; - type AwaitingStatus: Clone + Send + Sync; + type InProgressStatus: InitL2InitialStatus + serde::Serialize + Clone + Send + Sync; + type AwaitingStatus: serde::Serialize + Clone + Send + Sync; type UserAction: NotMmError + Send + Sync; fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitL2TaskManagerShared; @@ -67,13 +68,14 @@ pub trait InitL2ActivationOps: Into + Send + Sync + 'static { pub async fn init_l2( ctx: MmArc, - req: InitL2Req, + req: RpcInitReq>, ) -> Result> where L2: InitL2ActivationOps, InitL2Error: From, (L2::ActivationError, InitL2Error): NotEqual, { + let (client_id, req) = (req.client_id, req.inner); let ticker = req.ticker.clone(); if let Ok(Some(_)) = lp_coinfind(&ctx, &ticker).await { return MmError::err(InitL2Error::L2IsAlreadyActivated(ticker)); @@ -108,7 +110,7 @@ where }; let task_manager = L2::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task, client_id) .mm_err(|e| InitL2Error::Internal(e.to_string()))?; Ok(InitL2Response { task_id }) diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index d5ee5cfbf0..5c1677865e 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -13,12 +13,11 @@ use crypto::CryptoCtxError; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError, RpcTaskUserActionRequest}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes, TaskId}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::Value as Json; @@ -179,8 +178,8 @@ pub trait PlatformCoinWithTokensActivationOps: Into + Clone + Send + + Send + Sync; - type InProgressStatus: InitPlatformCoinWithTokensInitialStatus + Clone + Send + Sync; - type AwaitingStatus: Clone + Send + Sync; + type InProgressStatus: InitPlatformCoinWithTokensInitialStatus + serde::Serialize + Clone + Send + Sync; + type AwaitingStatus: serde::Serialize + Clone + Send + Sync; type UserAction: NotMmError + Send + Sync; /// Initializes the platform coin itself @@ -221,11 +220,6 @@ pub trait PlatformCoinWithTokensActivationOps: Into + Clone + Send + initial_balance: Option, ); - async fn handle_balance_streaming( - &self, - config: &EventStreamConfiguration, - ) -> Result<(), MmError>; - fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &InitPlatformCoinWithTokensTaskManagerShared where EnablePlatformCoinWithTokensError: From; @@ -488,10 +482,6 @@ where ); } - if let Some(config) = &ctx.event_stream_configuration { - platform_coin.handle_balance_streaming(config).await?; - } - let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); coins_ctx .add_platform_with_tokens(platform_coin.into(), mm_tokens, nft_global) @@ -564,7 +554,7 @@ impl InitPlatformCoinWithTokensInitialStatus for InitPlatformCoinWithTokensInPro /// Implementation of the init platform coin with tokens RPC command. pub async fn init_platform_coin_with_tokens( ctx: MmArc, - request: EnablePlatformCoinWithTokensReq, + request: RpcInitReq>, ) -> MmResult where Platform: PlatformCoinWithTokensActivationOps + Send + Sync + 'static + Clone, @@ -572,6 +562,7 @@ where EnablePlatformCoinWithTokensError: From, (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, { + let (client_id, request) = (request.client_id, request.inner); if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { return MmError::err(EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated( request.ticker, @@ -584,7 +575,7 @@ where let task = InitPlatformCoinWithTokensTask:: { ctx, request }; let task_manager = Platform::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task, client_id) .mm_err(|e| EnablePlatformCoinWithTokensError::Internal(e.to_string()))?; Ok(EnablePlatformCoinWithTokensResponse { task_id }) @@ -668,7 +659,7 @@ pub mod for_tests { use common::{executor::Timer, now_ms, wait_until_ms}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; - use rpc_task::RpcTaskStatus; + use rpc_task::{RpcInitReq, RpcTaskStatus}; use super::{init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, EnablePlatformCoinWithTokensError, EnablePlatformCoinWithTokensReq, @@ -686,6 +677,10 @@ pub mod for_tests { EnablePlatformCoinWithTokensError: From, (Platform::ActivationError, EnablePlatformCoinWithTokensError): NotEqual, { + let request = RpcInitReq { + client_id: 0, + inner: request, + }; let init_result = init_platform_coin_with_tokens::(ctx.clone(), request) .await .unwrap(); diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index 42c93c1377..0816ecd7fe 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -1,5 +1,6 @@ #[cfg(feature = "enable-sia")] use coins::siacoin::SiaCoinActivationParams; +use coins::utxo::bch::BchActivationRequest; use coins::utxo::UtxoActivationParams; use coins::z_coin::ZcoinActivationParams; use coins::{coin_conf, CoinBalance, CoinProtocol, CustomTokenError, DerivationMethodResponse, MmCoinEnum}; @@ -22,6 +23,10 @@ impl TxHistory for UtxoActivationParams { fn tx_history(&self) -> bool { self.tx_history } } +impl TxHistory for BchActivationRequest { + fn tx_history(&self) -> bool { self.utxo_params.tx_history } +} + #[cfg(feature = "enable-sia")] impl TxHistory for SiaCoinActivationParams { fn tx_history(&self) -> bool { self.tx_history } diff --git a/mm2src/coins_activation/src/sia_coin_activation.rs b/mm2src/coins_activation/src/sia_coin_activation.rs index 11c72955ab..110f8bbb7b 100644 --- a/mm2src/coins_activation/src/sia_coin_activation.rs +++ b/mm2src/coins_activation/src/sia_coin_activation.rs @@ -17,6 +17,7 @@ use derive_more::Display; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use rpc_task::RpcTaskError; @@ -25,6 +26,7 @@ use serde_derive::Serialize; use serde_json::Value as Json; use std::collections::HashMap; use std::time::Duration; + pub type SiaCoinTaskManagerShared = InitStandaloneCoinTaskManagerShared; pub type SiaCoinRpcTaskHandleShared = InitStandaloneCoinTaskHandleShared; pub type SiaCoinAwaitingStatus = HwRpcTaskAwaitingStatus; @@ -237,6 +239,7 @@ impl InitStandaloneCoinActivationOps for SiaCoin { &self, _metrics: MetricsArc, _storage: impl TxHistoryStorage, + _streaming_manager: StreamingManager, _current_balances: HashMap, ) { } diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs index a90f53e968..002d87c872 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs @@ -10,10 +10,12 @@ use coins::{lp_coinfind, lp_register_coin, CoinsContext, MmCoinEnum, RegisterCoi use common::{log, SuccessResponse}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use rpc_task::rpc_common::{CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusRequest, RpcTaskUserActionRequest}; -use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; use serde_derive::Deserialize; use serde_json::Value as Json; use std::collections::HashMap; @@ -72,13 +74,14 @@ pub trait InitStandaloneCoinActivationOps: Into + Send + Sync + 'sta &self, metrics: MetricsArc, storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, current_balances: HashMap, ); } pub async fn init_standalone_coin( ctx: MmArc, - request: InitStandaloneCoinReq, + request: RpcInitReq>, ) -> MmResult where Standalone: InitStandaloneCoinActivationOps + Send + Sync + 'static, @@ -86,6 +89,7 @@ where InitStandaloneCoinError: From, (Standalone::ActivationError, InitStandaloneCoinError): NotEqual, { + let (client_id, request) = (request.client_id, request.inner); if let Ok(Some(_)) = lp_coinfind(&ctx, &request.ticker).await { return MmError::err(InitStandaloneCoinError::CoinIsAlreadyActivated { ticker: request.ticker }); } @@ -102,7 +106,7 @@ where }; let task_manager = Standalone::rpc_task_manager(&coins_act_ctx); - let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task) + let task_id = RpcTaskManager::spawn_rpc_task(task_manager, &spawner, task, client_id) .mm_err(|e| InitStandaloneCoinError::Internal(e.to_string()))?; Ok(InitStandaloneCoinResponse { task_id }) @@ -215,6 +219,7 @@ where coin.start_history_background_fetching( self.ctx.metrics.clone(), TxHistoryStorageBuilder::new(&self.ctx).build()?, + self.ctx.event_stream_manager.clone(), current_balances, ); } diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 349e37b23d..18cf645110 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -20,14 +20,14 @@ use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; -use mm2_event_stream::EventStreamConfiguration; use mm2_number::BigDecimal; use rpc_task::RpcTaskHandleShared; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; +pub type TendermintCoinTaskManagerShared = InitPlatformCoinWithTokensTaskManagerShared; + impl TokenOf for TendermintToken { type PlatformCoin = TendermintCoin; } @@ -235,17 +235,20 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { activation_request: Self::ActivationRequest, protocol_conf: Self::PlatformProtocolInfo, ) -> Result> { + if protocol_conf.decimals > 18 { + return MmError::err(TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::InvalidProtocolData(format!( + "'decimals' value is too high; it must be 18 or lower but the current value is {}", + protocol_conf.decimals + )), + }); + } + let conf = TendermintConf::try_from_json(&ticker, coin_conf)?; let is_keplr_from_ledger = activation_request.is_keplr_from_ledger && activation_request.with_pubkey.is_some(); let activation_policy = if let Some(pubkey) = activation_request.with_pubkey { - if ctx.is_watcher() || ctx.use_watchers() { - return MmError::err(TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::CantUseWatchersWithPubkeyPolicy, - }); - } - TendermintActivationPolicy::with_public_key(pubkey) } else { let private_key_policy = @@ -260,9 +263,9 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { TendermintActivationPolicy::with_private_key_policy(tendermint_private_key_policy) }; - TendermintCoin::init( + let coin = TendermintCoin::init( &ctx, - ticker, + ticker.clone(), conf, protocol_conf, activation_request.nodes, @@ -270,7 +273,9 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { activation_policy, is_keplr_from_ledger, ) - .await + .await?; + + Ok(coin) } async fn enable_global_nft( @@ -368,22 +373,7 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { self.spawner().spawn_with_settings(fut, settings); } - async fn handle_balance_streaming( - &self, - config: &EventStreamConfiguration, - ) -> Result<(), MmError> { - if let EventInitStatus::Failed(err) = EventBehaviour::spawn_if_active(self.clone(), config).await { - return MmError::err(TendermintInitError { - ticker: self.ticker().to_owned(), - kind: TendermintInitErrorKind::BalanceStreamInitError(err), - }); - } - Ok(()) - } - - fn rpc_task_manager( - _activation_ctx: &CoinsActivationContext, - ) -> &InitPlatformCoinWithTokensTaskManagerShared { - unimplemented!() + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &TendermintCoinTaskManagerShared { + &activation_ctx.init_tendermint_coin_task_manager } } diff --git a/mm2src/coins_activation/src/utxo_activation/common_impl.rs b/mm2src/coins_activation/src/utxo_activation/common_impl.rs index 43cc0e32d6..eba196c370 100644 --- a/mm2src/coins_activation/src/utxo_activation/common_impl.rs +++ b/mm2src/coins_activation/src/utxo_activation/common_impl.rs @@ -8,13 +8,14 @@ use coins::hd_wallet::RpcTaskXPubExtractor; use coins::my_tx_history_v2::TxHistoryStorage; use coins::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; use coins::utxo::{UtxoActivationParams, UtxoCoinFields}; -use coins::{CoinBalanceMap, CoinFutSpawner, MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; +use coins::{CoinBalanceMap, MarketCoinOps, PrivKeyActivationPolicy, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use crypto::hw_rpc_task::HwConnectStatuses; use crypto::{CryptoCtxError, HwRpcError}; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use std::collections::HashMap; @@ -99,14 +100,15 @@ pub(crate) fn start_history_background_fetching( coin: Coin, metrics: MetricsArc, storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, current_balances: HashMap, ) where Coin: AsRef + UtxoTxHistoryOps, { - let spawner = CoinFutSpawner::new(&coin.as_ref().abortable_system); + let spawner = coin.as_ref().abortable_system.weak_spawner(); let msg = format!("'utxo_history_loop' has been aborted for {}", coin.ticker()); - let fut = utxo_history_loop(coin, storage, metrics, current_balances); + let fut = utxo_history_loop(coin, storage, metrics, streaming_manager, current_balances); let settings = AbortSettings::info_on_abort(msg); spawner.spawn_with_settings(fut, settings); diff --git a/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs new file mode 100644 index 0000000000..8c27226959 --- /dev/null +++ b/mm2src/coins_activation/src/utxo_activation/init_bch_activation.rs @@ -0,0 +1,118 @@ +use crate::context::CoinsActivationContext; +use crate::prelude::TryFromCoinProtocol; +use crate::standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoinTaskHandleShared, + InitStandaloneCoinTaskManagerShared}; +use crate::utxo_activation::common_impl::{get_activation_result, priv_key_build_policy, + start_history_background_fetching}; +use crate::utxo_activation::init_utxo_standard_activation_error::InitUtxoStandardError; +use crate::utxo_activation::init_utxo_standard_statuses::{UtxoStandardAwaitingStatus, UtxoStandardInProgressStatus, + UtxoStandardUserAction}; +use crate::utxo_activation::utxo_standard_activation_result::UtxoStandardActivationResult; +use async_trait::async_trait; +use coins::my_tx_history_v2::TxHistoryStorage; +use coins::utxo::bch::CashAddrPrefix; +use coins::utxo::bch::{BchActivationRequest, BchCoin}; +use coins::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; +use coins::CoinProtocol; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; +use mm2_metrics::MetricsArc; +use mm2_number::BigDecimal; +use serde_json::Value as Json; +use std::collections::HashMap; +use std::str::FromStr; + +pub type BchTaskManagerShared = InitStandaloneCoinTaskManagerShared; +pub type BchRpcTaskHandleShared = InitStandaloneCoinTaskHandleShared; + +#[derive(Clone)] +pub struct BchProtocolInfo { + slp_prefix: String, +} + +impl TryFromCoinProtocol for BchProtocolInfo { + fn try_from_coin_protocol(proto: CoinProtocol) -> Result> + where + Self: Sized, + { + match proto { + CoinProtocol::BCH { slp_prefix } => Ok(BchProtocolInfo { slp_prefix }), + protocol => MmError::err(protocol), + } + } +} + +#[async_trait] +impl InitStandaloneCoinActivationOps for BchCoin { + type ActivationRequest = BchActivationRequest; + type StandaloneProtocol = BchProtocolInfo; + type ActivationResult = UtxoStandardActivationResult; + type ActivationError = InitUtxoStandardError; + type InProgressStatus = UtxoStandardInProgressStatus; + type AwaitingStatus = UtxoStandardAwaitingStatus; + type UserAction = UtxoStandardUserAction; + + fn rpc_task_manager(activation_ctx: &CoinsActivationContext) -> &BchTaskManagerShared { + &activation_ctx.init_bch_task_manager + } + + async fn init_standalone_coin( + ctx: MmArc, + ticker: String, + coin_conf: Json, + activation_request: &Self::ActivationRequest, + protocol_info: Self::StandaloneProtocol, + _task_handle: BchRpcTaskHandleShared, + ) -> Result> { + if activation_request.bchd_urls.is_empty() && !activation_request.allow_slp_unsafe_conf { + Err(InitUtxoStandardError::CoinCreationError { + ticker: ticker.clone(), + error: "Using empty bchd_urls is unsafe for SLP users!".into(), + })?; + } + let prefix = CashAddrPrefix::from_str(&protocol_info.slp_prefix).map_err(|e| { + InitUtxoStandardError::CoinCreationError { + ticker: ticker.clone(), + error: format!("Couldn't parse cash address prefix: {e:?}"), + } + })?; + let priv_key_policy = priv_key_build_policy(&ctx, activation_request.utxo_params.priv_key_policy)?; + + let bchd_urls = activation_request.bchd_urls.clone(); + let constructor = { move |utxo_arc| BchCoin::new(utxo_arc, prefix.clone(), bchd_urls.clone()) }; + + let coin = UtxoArcBuilder::new( + &ctx, + &ticker, + &coin_conf, + &activation_request.utxo_params, + priv_key_policy, + constructor, + ) + .build() + .await + .mm_err(|e| InitUtxoStandardError::from_build_err(e, ticker.clone()))?; + + Ok(coin) + } + + async fn get_activation_result( + &self, + ctx: MmArc, + task_handle: BchRpcTaskHandleShared, + activation_request: &Self::ActivationRequest, + ) -> MmResult { + get_activation_result(&ctx, self, task_handle, &activation_request.utxo_params).await + } + + fn start_history_background_fetching( + &self, + metrics: MetricsArc, + storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, + current_balances: HashMap, + ) { + start_history_background_fetching(self.clone(), metrics, storage, streaming_manager, current_balances) + } +} diff --git a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs index ae8cdec6ce..a6644f3275 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_qtum_activation.rs @@ -16,6 +16,7 @@ use coins::utxo::UtxoActivationParams; use coins::CoinProtocol; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use serde_json::Value as Json; @@ -83,8 +84,9 @@ impl InitStandaloneCoinActivationOps for QtumCoin { &self, metrics: MetricsArc, storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, current_balances: HashMap, ) { - start_history_background_fetching(self.clone(), metrics, storage, current_balances) + start_history_background_fetching(self.clone(), metrics, storage, streaming_manager, current_balances) } } diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs index 206c750f15..10715e2f0e 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation.rs @@ -17,6 +17,7 @@ use coins::CoinProtocol; use futures::StreamExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use serde_json::Value as Json; @@ -124,8 +125,9 @@ impl InitStandaloneCoinActivationOps for UtxoStandardCoin { &self, metrics: MetricsArc, storage: impl TxHistoryStorage, + streaming_manager: StreamingManager, current_balances: HashMap, ) { - start_history_background_fetching(self.clone(), metrics, storage, current_balances) + start_history_background_fetching(self.clone(), metrics, storage, streaming_manager, current_balances) } } diff --git a/mm2src/coins_activation/src/utxo_activation/mod.rs b/mm2src/coins_activation/src/utxo_activation/mod.rs index 5ef6021199..42764e5c93 100644 --- a/mm2src/coins_activation/src/utxo_activation/mod.rs +++ b/mm2src/coins_activation/src/utxo_activation/mod.rs @@ -1,10 +1,12 @@ mod common_impl; +mod init_bch_activation; mod init_qtum_activation; mod init_utxo_standard_activation; mod init_utxo_standard_activation_error; mod init_utxo_standard_statuses; mod utxo_standard_activation_result; +pub use init_bch_activation::BchTaskManagerShared; pub use init_qtum_activation::QtumTaskManagerShared; pub use init_utxo_standard_activation::UtxoStandardTaskManagerShared; @@ -14,7 +16,7 @@ pub mod for_tests { use common::{executor::Timer, now_ms, wait_until_ms}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::{MmResult, NotEqual}; - use rpc_task::RpcTaskStatus; + use rpc_task::{RpcInitReq, RpcTaskStatus}; use crate::{init_standalone_coin, init_standalone_coin_status, standalone_coin::{InitStandaloneCoinActivationOps, InitStandaloneCoinError, @@ -32,6 +34,10 @@ pub mod for_tests { InitStandaloneCoinError: From, (Standalone::ActivationError, InitStandaloneCoinError): NotEqual, { + let request = RpcInitReq { + client_id: 0, + inner: request, + }; let init_result = init_standalone_coin::(ctx.clone(), request).await.unwrap(); let timeout = wait_until_ms(150000); loop { diff --git a/mm2src/coins_activation/src/z_coin_activation.rs b/mm2src/coins_activation/src/z_coin_activation.rs index 70da5c4eae..2332710218 100644 --- a/mm2src/coins_activation/src/z_coin_activation.rs +++ b/mm2src/coins_activation/src/z_coin_activation.rs @@ -16,6 +16,7 @@ use derive_more::Display; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; use rpc_task::RpcTaskError; @@ -303,6 +304,7 @@ impl InitStandaloneCoinActivationOps for ZCoin { &self, _metrics: MetricsArc, _storage: impl TxHistoryStorage, + _streaming_manager: StreamingManager, _current_balances: HashMap, ) { } diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index b775741cf8..d515ee5f18 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -10,6 +10,7 @@ path = "common.rs" doctest = false [features] +for-tests = [] track-ctx-pointer = ["shared_ref_counter/enable", "shared_ref_counter/log"] [dependencies] @@ -18,6 +19,7 @@ async-trait = "0.1" backtrace = "0.3" bytes = "1.1" cfg-if = "1.0" +compatible-time = { version = "1.1.0", package = "web-time" } crossbeam = "0.8" env_logger = "0.9.3" derive_more = "0.99" @@ -37,7 +39,7 @@ parking_lot_core = { version = "0.6", features = ["nightly"] } paste = "1.0" primitive-types = "0.11.1" rand = { version = "0.7", features = ["std", "small_rng"] } -rustc-hash = "1.1.0" +rustc-hash = "2.0" regex = "1" serde = "1" serde_derive = "1" @@ -47,11 +49,9 @@ ser_error_derive = { path = "../derives/ser_error_derive" } sha2 = "0.10" shared_ref_counter = { path = "shared_ref_counter", optional = true } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } -instant = { version = "0.1.12" } [target.'cfg(target_arch = "wasm32")'.dependencies] chrono = { version = "0.4", features = ["wasmbind"] } -instant = { version = "0.1.12", features = ["wasm-bindgen"] } js-sys = "0.3.27" serde_repr = "0.1.6" serde-wasm-bindgen = "0.4.3" diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 288882d0ae..078f601f66 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -106,9 +106,15 @@ macro_rules! some_or_return_ok_none { #[macro_export] macro_rules! cross_test { ($test_name:ident, $test_code:block) => { - #[cfg(not(target_arch = "wasm32"))] - #[tokio::test(flavor = "multi_thread")] - async fn $test_name() { $test_code } + cross_test!($test_name, $test_code, not(target_arch = "wasm32")); + }; + + ($test_name:ident, $test_code:block, $($cfgs:meta),+) => { + $( + #[cfg($cfgs)] + #[tokio::test(flavor = "multi_thread")] + async fn $test_name() { $test_code } + )+ #[cfg(target_arch = "wasm32")] #[wasm_bindgen_test] @@ -128,12 +134,11 @@ pub mod crash_reports; pub mod custom_futures; pub mod custom_iter; #[path = "executor/mod.rs"] pub mod executor; -pub mod expirable_map; pub mod notifier; pub mod number_type_casting; +pub mod on_drop_callback; pub mod password_policy; pub mod seri; -pub mod time_cache; #[cfg(not(target_arch = "wasm32"))] #[path = "wio.rs"] @@ -184,6 +189,7 @@ cfg_native! { use findshlibs::{IterationControl, Segment, SharedLibrary, TargetSharedLibrary}; use std::env; use std::sync::Mutex; + use std::str::FromStr; } cfg_wasm32! { @@ -204,13 +210,18 @@ pub const APPLICATION_GRPC_WEB_TEXT_PROTO: &str = "application/grpc-web-text+pro pub const SATOSHIS: u64 = 100_000_000; +/// Dex fee public key for chains where SECP256K1 is supported pub const DEX_FEE_ADDR_PUBKEY: &str = "03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc06"; +/// Public key to collect the burn part of dex fee, for chains where SECP256K1 is supported +pub const DEX_BURN_ADDR_PUBKEY: &str = "0369aa10c061cd9e085f4adb7399375ba001b54136145cb748eb4c48657be13153"; pub const PROXY_REQUEST_EXPIRATION_SEC: i64 = 15; lazy_static! { pub static ref DEX_FEE_ADDR_RAW_PUBKEY: Vec = hex::decode(DEX_FEE_ADDR_PUBKEY).expect("DEX_FEE_ADDR_PUBKEY is expected to be a hexadecimal string"); + pub static ref DEX_BURN_ADDR_RAW_PUBKEY: Vec = + hex::decode(DEX_BURN_ADDR_PUBKEY).expect("DEX_BURN_ADDR_PUBKEY is expected to be a hexadecimal string"); } #[cfg(not(target_arch = "wasm32"))] @@ -619,6 +630,17 @@ pub fn var(name: &str) -> Result { } } +#[cfg(not(target_arch = "wasm32"))] +pub fn env_var_as_bool(name: &str) -> bool { + match env::var(name) { + Ok(v) => FromStr::from_str(&v).unwrap_or_default(), + Err(_err) => false, + } +} + +#[cfg(target_arch = "wasm32")] +pub fn env_var_as_bool(_name: &str) -> bool { false } + /// TODO make it wasm32 only #[cfg(target_arch = "wasm32")] pub fn var(_name: &str) -> Result { ERR!("Environment variable not supported in WASM") } @@ -1080,7 +1102,17 @@ impl Default for PagingOptionsEnum { } #[inline(always)] -pub fn get_utc_timestamp() -> i64 { Utc::now().timestamp() } +pub fn get_utc_timestamp() -> i64 { + // get_utc_timestamp for tests allowing to add some bias to 'now' + #[cfg(feature = "for-tests")] + return Utc::now().timestamp() + + std::env::var("TEST_TIMESTAMP_OFFSET") + .map(|s| s.as_str().parse::().unwrap_or_default()) + .unwrap_or_default(); + + #[cfg(not(feature = "for-tests"))] + return Utc::now().timestamp(); +} #[inline(always)] pub fn get_utc_timestamp_nanos() -> i64 { Utc::now().timestamp_nanos() } diff --git a/mm2src/common/custom_futures/repeatable.rs b/mm2src/common/custom_futures/repeatable.rs index 3aaba119c7..842c2f0a2b 100644 --- a/mm2src/common/custom_futures/repeatable.rs +++ b/mm2src/common/custom_futures/repeatable.rs @@ -504,6 +504,7 @@ mod tests { } #[test] + #[cfg(not(target_os = "macos"))] // https://github.com/KomodoPlatform/komodo-defi-framework/issues/1712#issuecomment-2669934159 fn test_until_success() { const ATTEMPTS_TO_FINISH: usize = 5; const LOWEST_TIMEOUT: Duration = Duration::from_millis(350); diff --git a/mm2src/common/expirable_map.rs b/mm2src/common/expirable_map.rs deleted file mode 100644 index 0b3110c066..0000000000 --- a/mm2src/common/expirable_map.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! This module provides a cross-compatible map that associates values with keys and supports expiring entries. -//! -//! Designed for performance-oriented use-cases utilizing `FxHashMap` under the hood, -//! and is not suitable for cryptographic purposes. - -use instant::{Duration, Instant}; -use rustc_hash::FxHashMap; -use std::{collections::BTreeMap, hash::Hash}; - -#[derive(Clone, Debug)] -pub struct ExpirableEntry { - pub(crate) value: V, - pub(crate) expires_at: Instant, -} - -impl ExpirableEntry { - #[inline(always)] - pub fn new(v: V, exp: Duration) -> Self { - Self { - expires_at: Instant::now() + exp, - value: v, - } - } - - #[inline(always)] - pub fn get_element(&self) -> &V { &self.value } - - #[inline(always)] - pub fn update_value(&mut self, v: V) { self.value = v } - - #[inline(always)] - pub fn update_expiration(&mut self, expires_at: Instant) { self.expires_at = expires_at } - - /// Checks whether entry has longer ttl than the given one. - #[inline(always)] - pub fn has_longer_life_than(&self, min_ttl: Duration) -> bool { self.expires_at > Instant::now() + min_ttl } -} - -impl Default for ExpirableMap { - fn default() -> Self { Self::new() } -} - -/// A map that allows associating values with keys and expiring entries. -/// It is important to note that this implementation does not have a background worker to -/// automatically clear expired entries. Outdated entries are only removed when the control flow -/// is handed back to the map mutably (i.e. some mutable method of the map is invoked). -/// -/// WARNING: This is designed for performance-oriented use-cases utilizing `FxHashMap` -/// under the hood and is not suitable for cryptographic purposes. -#[derive(Clone, Debug)] -pub struct ExpirableMap { - map: FxHashMap>, - /// A sorted inverse map from expiration times to keys to speed up expired entries clearing. - expiries: BTreeMap, -} - -impl ExpirableMap { - /// Creates a new empty `ExpirableMap` - #[inline] - pub fn new() -> Self { - Self { - map: FxHashMap::default(), - expiries: BTreeMap::new(), - } - } - - /// Returns the associated value if present and not expired. - #[inline] - pub fn get(&self, k: &K) -> Option<&V> { - self.map - .get(k) - .filter(|v| v.expires_at > Instant::now()) - .map(|v| &v.value) - } - - /// Removes a key-value pair from the map and returns the associated value if present and not expired. - #[inline] - pub fn remove(&mut self, k: &K) -> Option { - self.map.remove(k).filter(|v| v.expires_at > Instant::now()).map(|v| { - self.expiries.remove(&v.expires_at); - v.value - }) - } - - /// Inserts a key-value pair with an expiration duration. - /// - /// If a value already exists for the given key, it will be updated and then - /// the old one will be returned. - pub fn insert(&mut self, k: K, v: V, exp: Duration) -> Option { - self.clear_expired_entries(); - let entry = ExpirableEntry::new(v, exp); - self.expiries.insert(entry.expires_at, k); - self.map.insert(k, entry).map(|v| v.value) - } - - /// Clears the map. - pub fn clear(&mut self) { - self.map.clear(); - self.expiries.clear(); - } - - /// Removes expired entries from the map. - /// - /// Iterates through the `expiries` in order, removing entries that have expired. - /// Stops at the first non-expired entry, leveraging the sorted nature of `BTreeMap`. - fn clear_expired_entries(&mut self) { - let now = Instant::now(); - - // `pop_first()` is used here as it efficiently removes expired entries. - // `first_key_value()` was considered as it wouldn't need re-insertion for - // non-expired entries, but it would require an extra remove operation for - // each expired entry. `pop_first()` needs only one re-insertion per call, - // which is an acceptable trade-off compared to multiple remove operations. - while let Some((exp, key)) = self.expiries.pop_first() { - if exp > now { - self.expiries.insert(exp, key); - break; - } - self.map.remove(&key); - } - } -} - -#[cfg(any(test, target_arch = "wasm32"))] -mod tests { - use super::*; - use crate::cross_test; - use crate::executor::Timer; - - crate::cfg_wasm32! { - use wasm_bindgen_test::*; - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - } - - cross_test!(test_clear_expired_entries, { - let mut expirable_map = ExpirableMap::new(); - let value = "test_value"; - let exp = Duration::from_secs(1); - - // Insert 2 entries with 1 sec expiration time - expirable_map.insert("key1", value, exp); - expirable_map.insert("key2", value, exp); - - // Wait for entries to expire - Timer::sleep(2.).await; - - // Clear expired entries - expirable_map.clear_expired_entries(); - - // We waited for 2 seconds, so we shouldn't have any entry accessible - assert_eq!(expirable_map.map.len(), 0); - - // Insert 5 entries - expirable_map.insert("key1", value, Duration::from_secs(5)); - expirable_map.insert("key2", value, Duration::from_secs(4)); - expirable_map.insert("key3", value, Duration::from_secs(7)); - expirable_map.insert("key4", value, Duration::from_secs(2)); - expirable_map.insert("key5", value, Duration::from_millis(3750)); - - // Wait 2 seconds to expire some entries - Timer::sleep(2.).await; - - // Clear expired entries - expirable_map.clear_expired_entries(); - - // We waited for 2 seconds, only one entry should expire - assert_eq!(expirable_map.map.len(), 4); - }); -} diff --git a/mm2src/common/log.rs b/mm2src/common/log.rs index a30b0b83de..41116073ca 100644 --- a/mm2src/common/log.rs +++ b/mm2src/common/log.rs @@ -160,6 +160,22 @@ pub fn short_log_time(ms: u64) -> DelayedFormat> { time.format("%d %H:%M:%S") } +#[cfg(not(test))] +#[macro_export] +macro_rules! covered_warn { + ($($arg:tt)+) => { + common::log::warn!($($arg)+) + }; +} + +#[cfg(test)] +#[macro_export] +macro_rules! covered_warn { + ($($arg:tt)+) => { + panic!($($arg)+) + }; +} + /// Debug logging. /// /// This logging SHOULD be human-readable but it is not intended for the end users specifically. @@ -1176,4 +1192,10 @@ pub mod tests { assert!(tail[0].ends_with("/3:33) [tag] status 1%…")); }); } + + #[test] + #[should_panic(expected = "Fail with me...")] + fn test_covered_warn() { + covered_warn!("Fail with me..."); + } } diff --git a/mm2src/common/on_drop_callback.rs b/mm2src/common/on_drop_callback.rs new file mode 100644 index 0000000000..a454cc9554 --- /dev/null +++ b/mm2src/common/on_drop_callback.rs @@ -0,0 +1,19 @@ +/// Runs some function when this object is dropped. +/// +/// We wrap the callback function in an `Option` so that we can exercise the less strict `FnOnce` bound +/// (`FnOnce` is less strict than `Fn`). This way we can take out the function and execute it when dropping. +/// We also implement this with `Box` instead of generics so not to force users to use generics if +/// this callback handle is stored in some struct. +pub struct OnDropCallback(Option>); + +impl OnDropCallback { + pub fn new(f: impl FnOnce() + Send + 'static) -> Self { Self(Some(Box::new(f))) } +} + +impl Drop for OnDropCallback { + fn drop(&mut self) { + if let Some(func) = self.0.take() { + func() + } + } +} diff --git a/mm2src/common/time_cache.rs b/mm2src/common/time_cache.rs deleted file mode 100644 index a1c3987ec2..0000000000 --- a/mm2src/common/time_cache.rs +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! This implements a time-based LRU cache for checking gossipsub message duplicates. - -use fnv::FnvHashMap; -use instant::Instant; -use std::collections::hash_map::{self, - Entry::{Occupied, Vacant}, - Iter, Keys}; -use std::collections::VecDeque; -use std::time::Duration; - -use crate::expirable_map::ExpirableEntry; - -#[derive(Debug)] -pub struct TimeCache { - /// Mapping a key to its value together with its latest expire time (can be updated through - /// reinserts). - map: FnvHashMap>, - /// An ordered list of keys by expires time. - list: VecDeque>, - /// The time elements remain in the cache. - ttl: Duration, -} - -pub struct OccupiedEntry<'a, K, V> { - expiration: Instant, - entry: hash_map::OccupiedEntry<'a, K, ExpirableEntry>, - list: &'a mut VecDeque>, -} - -impl<'a, K, V> OccupiedEntry<'a, K, V> -where - K: Eq + std::hash::Hash + Clone, -{ - pub fn into_mut(self) -> &'a mut V { &mut self.entry.into_mut().value } - - #[allow(dead_code)] - pub fn insert_without_updating_expiration(&mut self, value: V) -> V { - //keep old expiration, only replace value of element - ::std::mem::replace(&mut self.entry.get_mut().value, value) - } - - #[allow(dead_code)] - pub fn insert_and_update_expiration(&mut self, value: V) -> V { - //We push back an additional element, the first reference in the list will be ignored - // since we also updated the expires in the map, see below. - self.list.push_back(ExpirableEntry { - value: self.entry.key().clone(), - expires_at: self.expiration, - }); - self.entry - .insert(ExpirableEntry { - value, - expires_at: self.expiration, - }) - .value - } - - pub fn into_mut_with_update_expiration(mut self) -> &'a mut V { - //We push back an additional element, the first reference in the list will be ignored - // since we also updated the expires in the map, see below. - self.list.push_back(ExpirableEntry { - value: self.entry.key().clone(), - expires_at: self.expiration, - }); - self.entry.get_mut().update_expiration(self.expiration); - &mut self.entry.into_mut().value - } -} - -pub struct VacantEntry<'a, K, V> { - expiration: Instant, - entry: hash_map::VacantEntry<'a, K, ExpirableEntry>, - list: &'a mut VecDeque>, -} - -impl<'a, K, V> VacantEntry<'a, K, V> -where - K: Eq + std::hash::Hash + Clone, -{ - pub fn insert(self, value: V) -> &'a mut V { - self.list.push_back(ExpirableEntry { - value: self.entry.key().clone(), - expires_at: self.expiration, - }); - &mut self - .entry - .insert(ExpirableEntry { - value, - expires_at: self.expiration, - }) - .value - } -} - -pub enum Entry<'a, K: 'a, V: 'a> { - Occupied(OccupiedEntry<'a, K, V>), - Vacant(VacantEntry<'a, K, V>), -} - -#[allow(dead_code)] -impl<'a, K: 'a, V: 'a> Entry<'a, K, V> -where - K: Eq + std::hash::Hash + Clone, -{ - pub fn or_insert_with V>(self, default: F) -> &'a mut V { - match self { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => entry.insert(default()), - } - } - - pub fn or_insert_with_update_expiration V>(self, default: F) -> &'a mut V { - match self { - Entry::Occupied(entry) => entry.into_mut_with_update_expiration(), - Entry::Vacant(entry) => entry.insert(default()), - } - } -} - -impl TimeCache -where - Key: Eq + std::hash::Hash + Clone, -{ - pub fn new(ttl: Duration) -> Self { - TimeCache { - map: FnvHashMap::default(), - list: VecDeque::new(), - ttl, - } - } - - fn remove_expired_keys(&mut self, now: Instant) { - while let Some(element) = self.list.pop_front() { - if element.expires_at > now { - self.list.push_front(element); - break; - } - if let Occupied(entry) = self.map.entry(element.value.clone()) { - if entry.get().expires_at <= now { - entry.remove(); - } - } - } - } - - pub fn entry(&mut self, key: Key) -> Entry { - let now = Instant::now(); - self.remove_expired_keys(now); - match self.map.entry(key) { - Occupied(entry) => Entry::Occupied(OccupiedEntry { - expiration: now + self.ttl, - entry, - list: &mut self.list, - }), - Vacant(entry) => Entry::Vacant(VacantEntry { - expiration: now + self.ttl, - entry, - list: &mut self.list, - }), - } - } - - // Inserts new element and removes any expired elements. - // - // If the key was not present this returns `true`. If the value was already present this - // returns `false`. - pub fn insert(&mut self, key: Key, value: Value) -> bool { - if let Entry::Vacant(entry) = self.entry(key) { - entry.insert(value); - true - } else { - false - } - } - - // Removes a certain key even if it didn't expire plus removing other expired keys - pub fn remove(&mut self, key: Key) -> Option { - let result = self.map.remove(&key).map(|el| el.value); - self.remove_expired_keys(Instant::now()); - result - } - - /// Empties the entire cache. - #[allow(dead_code)] - pub fn clear(&mut self) { - self.map.clear(); - self.list.clear(); - } - - pub fn contains_key(&self, key: &Key) -> bool { self.map.contains_key(key) } - - pub fn get(&self, key: &Key) -> Option<&Value> { self.map.get(key).map(|e| &e.value) } - - pub fn len(&self) -> usize { self.map.len() } - - pub fn is_empty(&self) -> bool { self.map.is_empty() } - - pub fn ttl(&self) -> Duration { self.ttl } - - pub fn iter(&self) -> Iter> { self.map.iter() } - - pub fn keys(&self) -> Keys> { self.map.keys() } -} - -impl TimeCache -where - Key: Eq + std::hash::Hash + Clone, - Value: Clone, -{ - pub fn as_hash_map(&self) -> std::collections::HashMap { - self.map - .iter() - .map(|(key, expiring_el)| (key.clone(), expiring_el.value.clone())) - .collect() - } -} - -pub struct DuplicateCache(TimeCache); - -impl DuplicateCache -where - Key: Eq + std::hash::Hash + Clone, -{ - pub fn new(ttl: Duration) -> Self { Self(TimeCache::new(ttl)) } - - // Inserts new elements and removes any expired elements. - // - // If the key was not present this returns `true`. If the value was already present this - // returns `false`. - pub fn insert(&mut self, key: Key) -> bool { - if let Entry::Vacant(entry) = self.0.entry(key) { - entry.insert(()); - true - } else { - false - } - } - - pub fn contains(&mut self, key: &Key) -> bool { self.0.contains_key(key) } - - // Removes a certain key even if it didn't expire plus removing other expired keys - #[inline] - pub fn remove(&mut self, key: Key) { self.0.remove(key); } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn time_cache_added_entries_exist() { - let mut cache = TimeCache::new(Duration::from_secs(10)); - - assert!(cache.insert("t", "tv".to_owned())); - assert!(cache.insert("e", "ev".to_owned())); - - // Should report that 't' and 't' already exists - assert!(!cache.insert("t", "td".to_owned())); - assert!(!cache.insert("e", "ed".to_owned())); - - assert_eq!(cache.get(&"t"), Some(&"tv".to_owned())); - assert_eq!(cache.get(&"e"), Some(&"ev".to_owned())); - assert_eq!(cache.get(&"f"), None); - } - - #[test] - fn time_cache_expired() { - let mut cache = TimeCache::new(Duration::from_secs(1)); - - assert!(cache.insert("t", "tv".to_owned())); - assert_eq!(cache.get(&"t"), Some(&"tv".to_owned())); - - std::thread::sleep(Duration::from_millis(500)); - assert!(cache.insert("e", "ev".to_owned())); - assert_eq!(cache.get(&"t"), Some(&"tv".to_owned())); - assert_eq!(cache.get(&"e"), Some(&"ev".to_owned())); - - std::thread::sleep(Duration::from_millis(700)); - // insert other value to initiate the expiration - assert!(cache.insert("f", "fv".to_owned())); - // must be expired already - assert_eq!(cache.get(&"t"), None); - assert_eq!(cache.get(&"e"), Some(&"ev".to_owned())); - - std::thread::sleep(Duration::from_millis(700)); - // insert other value to initiate the expiration - assert!(cache.insert("d", "dv".to_owned())); - // must be expired already - assert_eq!(cache.get(&"t"), None); - assert_eq!(cache.get(&"e"), None); - } - - #[test] - fn cache_added_entries_exist() { - let mut cache = DuplicateCache::new(Duration::from_secs(10)); - - cache.insert("t"); - cache.insert("e"); - - // Should report that 't' and 't' already exists - assert!(!cache.insert("t")); - assert!(!cache.insert("e")); - } - - #[test] - fn cache_entries_expire() { - let mut cache = DuplicateCache::new(Duration::from_millis(100)); - - cache.insert("t"); - assert!(!cache.insert("t")); - cache.insert("e"); - //assert!(!cache.insert("t")); - assert!(!cache.insert("e")); - // sleep until cache expiry - std::thread::sleep(Duration::from_millis(101)); - // add another element to clear previous cache - cache.insert("s"); - - // should be removed from the cache - assert!(cache.insert("t")); - } - - #[test] - fn test_remove() { - let mut cache = TimeCache::new(Duration::from_secs(10)); - - cache.insert("t", ""); - cache.insert("e", ""); - cache.remove("e"); - assert!(!cache.contains_key(&"e")); - } -} diff --git a/mm2src/crypto/src/encrypt.rs b/mm2src/crypto/src/encrypt.rs index 30c5246aa1..8ad537e7cf 100644 --- a/mm2src/crypto/src/encrypt.rs +++ b/mm2src/crypto/src/encrypt.rs @@ -9,6 +9,8 @@ use hmac::{Hmac, Mac}; use mm2_err_handle::prelude::*; use sha2::Sha256; +const ENCRYPTED_DATA_VERSION: u8 = 1; + type Aes256CbcEnc = cbc::Encryptor; #[derive(Debug, Display, PartialEq)] @@ -41,6 +43,10 @@ pub enum EncryptionAlgorithm { /// providing a robust and comprehensive approach to securing sensitive mnemonic data. #[derive(Serialize, Deserialize, Debug)] pub struct EncryptedData { + /// Version of the encrypted data format. + /// This version value allows future changes to this struct while maintaining backward compatibility. + pub version: u8, + /// The encryption algorithm used to encrypt the mnemonic. /// Example: "AES-256-CBC". pub encryption_algorithm: EncryptionAlgorithm, @@ -107,6 +113,7 @@ pub fn encrypt_data( let tag = mac.finalize().into_bytes(); let encrypted_data = EncryptedData { + version: ENCRYPTED_DATA_VERSION, encryption_algorithm: EncryptionAlgorithm::AES256CBC, key_derivation_details, iv: STANDARD.encode(iv), diff --git a/mm2src/crypto/src/hw_ctx.rs b/mm2src/crypto/src/hw_ctx.rs index 1ac7c9877f..8d2c84a8f9 100644 --- a/mm2src/crypto/src/hw_ctx.rs +++ b/mm2src/crypto/src/hw_ctx.rs @@ -185,4 +185,4 @@ impl HardwareWalletCtx { fn h160_from_h264(h264: &H264) -> H160 { dhash160(h264.as_slice()) } /// Converts `H264` into a serializable/deserializable Hardware wallet pubkey. -fn hw_pubkey_from_h264(h264: &H264) -> HwPubkey { HwPubkey::from(h160_from_h264(h264).as_slice()) } +fn hw_pubkey_from_h264(h264: &H264) -> HwPubkey { HwPubkey::from(h160_from_h264(h264).take()) } diff --git a/mm2src/crypto/src/key_derivation.rs b/mm2src/crypto/src/key_derivation.rs index 74500c3d52..69cd1a4d66 100644 --- a/mm2src/crypto/src/key_derivation.rs +++ b/mm2src/crypto/src/key_derivation.rs @@ -6,13 +6,14 @@ use hmac::{Hmac, Mac}; use mm2_err_handle::mm_error::MmResult; use mm2_err_handle::prelude::*; use sha2::Sha512; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; -const ARGON2_ALGORITHM: &str = "Argon2id"; -const ARGON2ID_VERSION: &str = "0x13"; +const ARGON2_ALGORITHM: &str = "argon2id"; +const ARGON2ID_VERSION: u32 = 19; const ARGON2ID_M_COST: u32 = 65536; const ARGON2ID_T_COST: u32 = 2; const ARGON2ID_P_COST: u32 = 1; +const ARGON2ID_OUTPUT_LEN: usize = 32; #[allow(dead_code)] type HmacSha512 = Hmac; @@ -25,6 +26,8 @@ pub enum KeyDerivationError { HmacInitialization, #[display(fmt = "Invalid key length")] InvalidKeyLength, + #[display(fmt = "Not supported: {}", _0)] + NotSupported(String), } impl From for KeyDerivationError { @@ -41,8 +44,8 @@ pub struct Argon2Params { /// The specific variant of the Argon2 algorithm used (e.g., Argon2id). algorithm: String, - /// The version of the Argon2 algorithm (e.g., 0x13 for the latest version). - version: String, + /// The version of the Argon2 algorithm (e.g., 0x13 19 in u32) for the latest version). + version: u32, /// The memory cost parameter defining the memory usage of the algorithm. /// Expressed in kibibytes (KiB). @@ -54,16 +57,20 @@ pub struct Argon2Params { /// The parallelism cost parameter defining the number of parallel threads. p_cost: u32, + + /// The size of the output hash in bytes. + output_len: usize, } impl Default for Argon2Params { fn default() -> Self { Argon2Params { algorithm: ARGON2_ALGORITHM.to_string(), - version: ARGON2ID_VERSION.to_string(), + version: ARGON2ID_VERSION, m_cost: ARGON2ID_M_COST, t_cost: ARGON2ID_T_COST, p_cost: ARGON2ID_P_COST, + output_len: ARGON2ID_OUTPUT_LEN, } } } @@ -95,45 +102,76 @@ pub enum KeyDerivationDetails { // Placeholder for future algorithms. } +fn build_argon2_instance(params: &Argon2Params) -> Result, KeyDerivationError> { + let argon2_params = argon2::Params::new(params.m_cost, params.t_cost, params.p_cost, Some(params.output_len)) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Invalid Argon2 parameters: {}", e)))?; + + let algorithm = argon2::Algorithm::new(¶ms.algorithm) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Unknown Argon2 algorithm: {}", e)))?; + + let version = argon2::Version::try_from(params.version) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Unknown Argon2 version: {}", e)))?; + + Ok(Argon2::new(algorithm, version, argon2_params)) +} + /// Derives AES and HMAC keys from a given password and salts for mnemonic encryption/decryption. /// /// # Returns /// A tuple containing the AES key and HMAC key as byte arrays, or a `MnemonicError` in case of failure. -#[allow(dead_code)] pub(crate) fn derive_keys_for_mnemonic( password: &str, - salt_aes: &SaltString, - salt_hmac: &SaltString, + key_details: &KeyDerivationDetails, ) -> MmResult<([u8; 32], [u8; 32]), KeyDerivationError> { - let argon2 = Argon2::default(); - - // Derive AES Key - let aes_password_hash = argon2.hash_password(password.as_bytes(), salt_aes)?; - let key_aes_output = aes_password_hash - .serialize() - .hash() - .ok_or_else(|| KeyDerivationError::PasswordHashingFailed("Error finding AES key hashing output".to_string()))?; - let key_aes = key_aes_output - .as_bytes() - .try_into() - .map_err(|_| KeyDerivationError::PasswordHashingFailed("Invalid AES key length".to_string()))?; - - // Derive HMAC Key - let hmac_password_hash = argon2.hash_password(password.as_bytes(), salt_hmac)?; - let key_hmac_output = hmac_password_hash.serialize().hash().ok_or_else(|| { - KeyDerivationError::PasswordHashingFailed("Error finding HMAC key hashing output".to_string()) - })?; - let key_hmac = key_hmac_output - .as_bytes() - .try_into() - .map_err(|_| KeyDerivationError::PasswordHashingFailed("Invalid HMAC key length".to_string()))?; + match key_details { + KeyDerivationDetails::Argon2 { + params, + salt_aes, + salt_hmac, + } => { + let argon2 = build_argon2_instance(params)?; + + let salt_aes = SaltString::from_b64(salt_aes) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Invalid AES salt: {}", e)))?; + let salt_hmac = SaltString::from_b64(salt_hmac) + .map_err(|e| KeyDerivationError::PasswordHashingFailed(format!("Invalid HMAC salt: {}", e)))?; + + // Derive AES Key + let aes_password_hash = argon2.hash_password(password.as_bytes(), &salt_aes)?; + let key_aes_output = aes_password_hash.serialize().hash().ok_or_else(|| { + KeyDerivationError::PasswordHashingFailed("Error finding AES key hashing output".to_string()) + })?; + let key_aes = key_aes_output + .as_bytes() + .try_into() + .map_err(|_| KeyDerivationError::PasswordHashingFailed("Invalid AES key length".to_string()))?; + + // Derive HMAC Key + let hmac_password_hash = argon2.hash_password(password.as_bytes(), &salt_hmac)?; + let key_hmac_output = hmac_password_hash.serialize().hash().ok_or_else(|| { + KeyDerivationError::PasswordHashingFailed("Error finding HMAC key hashing output".to_string()) + })?; + let key_hmac = key_hmac_output + .as_bytes() + .try_into() + .map_err(|_| KeyDerivationError::PasswordHashingFailed("Invalid HMAC key length".to_string()))?; - Ok((key_aes, key_hmac)) + Ok((key_aes, key_hmac)) + }, + KeyDerivationDetails::SLIP0021 { .. } => MmError::err(KeyDerivationError::NotSupported( + "SLIP-0021 key derivation is not supported for mnemonic keys".to_string(), + )), + } } /// Splits a path into its components and derives a key for each component. fn derive_key_from_path(master_node: &[u8], path: &str) -> MmResult<[u8; 32], KeyDerivationError> { + if master_node.len() < 64 { + return MmError::err(KeyDerivationError::InvalidKeyLength); + } + let mut current_key_material = master_node.to_vec(); + for segment in path.split('/').filter(|s| !s.is_empty()) { let mut mac = HmacSha512::new_from_slice(¤t_key_material[..32]) .map_err(|_| KeyDerivationError::HmacInitialization)?; diff --git a/mm2src/crypto/src/lib.rs b/mm2src/crypto/src/lib.rs index e2651a54ea..f735203232 100644 --- a/mm2src/crypto/src/lib.rs +++ b/mm2src/crypto/src/lib.rs @@ -12,6 +12,7 @@ pub mod hw_rpc_task; mod key_derivation; pub mod mnemonic; pub mod privkey; +pub mod secret_hash_algo; mod shared_db_id; mod slip21; mod standard_hd_path; diff --git a/mm2src/crypto/src/mnemonic.rs b/mm2src/crypto/src/mnemonic.rs index c92e23c05b..9d89b25fce 100644 --- a/mm2src/crypto/src/mnemonic.rs +++ b/mm2src/crypto/src/mnemonic.rs @@ -29,10 +29,6 @@ impl From for MnemonicError { fn from(e: bip39::Error) -> Self { MnemonicError::BIP39Error(e.to_string()) } } -impl From for MnemonicError { - fn from(e: argon2::password_hash::Error) -> Self { MnemonicError::KeyDerivationError(e.to_string()) } -} - impl From for MnemonicError { fn from(e: KeyDerivationError) -> Self { MnemonicError::KeyDerivationError(e.to_string()) } } @@ -84,7 +80,7 @@ pub fn encrypt_mnemonic(mnemonic: &str, password: &str) -> MmResult MmResult MmResult { - // Re-create the salts from Base64-encoded strings - let (salt_aes, salt_hmac) = match &encrypted_data.key_derivation_details { - KeyDerivationDetails::Argon2 { - salt_aes, salt_hmac, .. - } => (SaltString::from_b64(salt_aes)?, SaltString::from_b64(salt_hmac)?), - _ => { - return MmError::err(MnemonicError::KeyDerivationError( - "Key derivation details should be Argon2!".to_string(), - )) - }, - }; - +pub fn decrypt_mnemonic(encrypted_data: &EncryptedData, password: &str) -> MmResult { // Re-create the keys from the password and salts - let (key_aes, key_hmac) = derive_keys_for_mnemonic(password, &salt_aes, &salt_hmac)?; + let (key_aes, key_hmac) = derive_keys_for_mnemonic(password, &encrypted_data.key_derivation_details)?; // Decrypt the ciphertext let decrypted_data = @@ -126,8 +110,7 @@ pub fn decrypt_mnemonic(encrypted_data: &EncryptedData, password: &str) -> MmRes // Convert decrypted data back to a string let mnemonic_str = String::from_utf8(decrypted_data).map_to_mm(|e| MnemonicError::DecodeError(e.to_string()))?; - let mnemonic = Mnemonic::parse_normalized(&mnemonic_str)?; - Ok(mnemonic) + Ok(mnemonic_str) } #[cfg(any(test, target_arch = "wasm32"))] @@ -144,10 +127,23 @@ mod tests { let mnemonic = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; let password = "password"; - // Verify that the mnemonic is valid - let parsed_mnemonic = Mnemonic::parse_normalized(mnemonic); - assert!(parsed_mnemonic.is_ok()); - let parsed_mnemonic = parsed_mnemonic.unwrap(); + // Encrypt the mnemonic + let encrypted_data = encrypt_mnemonic(mnemonic, password); + assert!(encrypted_data.is_ok()); + let encrypted_data = encrypted_data.unwrap(); + + // Decrypt the mnemonic + let decrypted_mnemonic = decrypt_mnemonic(&encrypted_data, password); + assert!(decrypted_mnemonic.is_ok()); + let decrypted_mnemonic = decrypted_mnemonic.unwrap(); + + // Verify if decrypted mnemonic matches the original + assert_eq!(decrypted_mnemonic, mnemonic); + }); + + cross_test!(test_encrypt_decrypt_non_bip39_mnemonic, { + let mnemonic = "Helloworld"; + let password = "Helloworld"; // Encrypt the mnemonic let encrypted_data = encrypt_mnemonic(mnemonic, password); @@ -160,7 +156,7 @@ mod tests { let decrypted_mnemonic = decrypted_mnemonic.unwrap(); // Verify if decrypted mnemonic matches the original - assert_eq!(decrypted_mnemonic, parsed_mnemonic); + assert_eq!(decrypted_mnemonic, mnemonic); }); cross_test!(test_mnemonic_with_last_byte_zero, { @@ -173,7 +169,9 @@ mod tests { let encrypted_data = encrypted_data.unwrap(); // Decrypt the mnemonic - let decrypted_mnemonic = decrypt_mnemonic(&encrypted_data, password); + let decrypted_mnemonic_str = decrypt_mnemonic(&encrypted_data, password); + assert!(decrypted_mnemonic_str.is_ok()); + let decrypted_mnemonic = Mnemonic::parse_normalized(&decrypted_mnemonic_str.unwrap()); assert!(decrypted_mnemonic.is_err()); // Verify that the error is due to parsing and not padding diff --git a/mm2src/crypto/src/privkey.rs b/mm2src/crypto/src/privkey.rs index d86a0e930f..4d296d72a8 100644 --- a/mm2src/crypto/src/privkey.rs +++ b/mm2src/crypto/src/privkey.rs @@ -104,11 +104,7 @@ pub fn key_pair_from_seed(seed: &str) -> PrivKeyResult { Ok(pair) } -pub fn key_pair_from_secret(secret: &[u8]) -> PrivKeyResult { - if secret.len() != 32 { - return MmError::err(PrivKeyError::InvalidPrivKey(KeysError::InvalidPrivate.to_string())); - } - +pub fn key_pair_from_secret(secret: &[u8; 32]) -> PrivKeyResult { let private = Private { prefix: 0, secret: secret.into(), diff --git a/mm2src/crypto/src/secret_hash_algo.rs b/mm2src/crypto/src/secret_hash_algo.rs new file mode 100644 index 0000000000..c5add0492f --- /dev/null +++ b/mm2src/crypto/src/secret_hash_algo.rs @@ -0,0 +1,39 @@ +use bitcrypto::{dhash160, sha256}; +use derive_more::Display; +use std::convert::TryFrom; + +/// Algorithm used to hash swap secret. +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)] +pub enum SecretHashAlgo { + /// ripemd160(sha256(secret)) + #[default] + DHASH160 = 1, + /// sha256(secret) + SHA256 = 2, +} + +#[derive(Debug, Display)] +pub struct UnsupportedSecretHashAlgo(u8); + +impl std::error::Error for UnsupportedSecretHashAlgo {} + +impl TryFrom for SecretHashAlgo { + type Error = UnsupportedSecretHashAlgo; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(SecretHashAlgo::DHASH160), + 2 => Ok(SecretHashAlgo::SHA256), + unsupported => Err(UnsupportedSecretHashAlgo(unsupported)), + } + } +} + +impl SecretHashAlgo { + pub fn hash_secret(&self, secret: &[u8]) -> Vec { + match self { + SecretHashAlgo::DHASH160 => dhash160(secret).take().into(), + SecretHashAlgo::SHA256 => sha256(secret).take().into(), + } + } +} diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index bf95f316e4..2097703ed7 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -5,8 +5,8 @@ [package] name = "mm2_bin_lib" -version = "2.3.0-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 ", "Dimxy", "Omer Yacine"] +version = "2.4.0-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 ", "Dimxy", "Omer Yacine", "DeckerSU"] edition = "2018" default-run = "kdf" @@ -15,6 +15,7 @@ custom-swap-locktime = ["mm2_main/custom-swap-locktime"] # only for testing purp native = ["mm2_main/native"] # Deprecated track-ctx-pointer = ["mm2_main/track-ctx-pointer"] zhtlc-native-tests = ["mm2_main/zhtlc-native-tests"] +test-ext-api = ["mm2_main/test-ext-api"] [[bin]] name = "mm2" diff --git a/mm2src/mm2_bin_lib/build.rs b/mm2src/mm2_bin_lib/build.rs index c05c39a364..76cc3a9d4d 100644 --- a/mm2src/mm2_bin_lib/build.rs +++ b/mm2src/mm2_bin_lib/build.rs @@ -1,114 +1,48 @@ -use chrono::DateTime; -use gstuff::slurp; +use chrono::Utc; use regex::Regex; -use std::fs; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::str::from_utf8; +use std::{env, process::Command}; -fn path2s(path: PathBuf) -> String { - path.to_str() - .unwrap_or_else(|| panic!("Non-stringy path {:?}", path)) - .into() -} - -/// AtomicDEX's root. -fn root() -> PathBuf { - let super_net = Path::new(env!("CARGO_MANIFEST_DIR")); - let super_net = match super_net.canonicalize() { - Ok(p) => p, - Err(err) => panic!("Can't canonicalize {:?}: {}", super_net, err), - }; - // On Windows we're getting these "\\?\" paths from canonicalize but they aren't any good for CMake. - if cfg!(windows) { - let s = path2s(super_net); - let stripped = match s.strip_prefix(r"\\?\") { - Some(stripped) => stripped, - None => &s, - }; - Path::new(stripped).into() - } else { - super_net - } -} - -/// This function ensures that we have the “MM_VERSION” and “MM_DATETIME” variables during the build. -/// -/// The build script will usually help us by putting the MarketMaker version into the “MM_VERSION” file -/// and the corresponding ISO 8601 time into the “MM_DATETIME” file -/// -/// For the nightly builds the version contains the short commit hash. -/// -/// We're also trying to get the hash and the time from Git. -/// -/// Git information isn't always available during the build (for instance, when a build server is used, -/// we might skip synchronizing the Git repository there), -/// but if it is, then we're going to check if the “MM_DATETIME” and the Git data match. -fn mm_version() -> String { - // Reading version of `mm2_bin_lib` from cargo manifest - let mut version = env!("CARGO_PKG_VERSION").to_owned(); - - let mm_version_p = root().join("../../MM_VERSION"); - let v_file = String::from_utf8(slurp(&mm_version_p)).unwrap(); +fn crate_version() -> &'static str { env!("CARGO_PKG_VERSION") } - // if there is MM_VERSION file, that means CI wants to put a tag to version - if !v_file.is_empty() { - version = format!("{}_{}", version, v_file.trim()); +fn version_tag() -> Result { + if let Ok(tag) = env::var("KDF_BUILD_TAG") { + return Ok(tag); } - // put commit tag to the version - else { - let mut command = Command::new("git"); - command.arg("log").arg("--pretty=format:%h").arg("-n1"); - if let Ok(go) = command.output() { - if go.status.success() { - let commit_hash = from_utf8(&go.stdout).unwrap().trim().to_string(); - if !Regex::new(r"^\w+$").unwrap().is_match(&commit_hash) { - panic!("{}", commit_hash) - } - version = format!("{version}_{commit_hash}"); - } - } + let output = Command::new("git") + .args(["log", "--pretty=format:%h", "-n1"]) + .output() + .map_err(|e| format!("Failed to run git command: {e}\nSet `KDF_BUILD_TAG` manually instead.",))?; + + let commit_hash = String::from_utf8(output.stdout) + .map_err(|e| format!("Invalid UTF-8 sequence: {e}"))? + .trim() + .to_string(); + + if !Regex::new(r"^\w+$") + .expect("Failed to compile regex") + .is_match(&commit_hash) + { + return Err(format!("Invalid tag: {commit_hash}")); } - println!("cargo:rustc-env=MM_VERSION={}", version); - - let mut dt_git = None; - let mut command = Command::new("git"); - command.arg("log").arg("--pretty=format:%cI").arg("-n1"); // ISO 8601 - if let Ok(go) = command.output() { - if go.status.success() { - let got = from_utf8(&go.stdout).unwrap().trim(); - let _dt_check = DateTime::parse_from_rfc3339(got).unwrap(); - dt_git = Some(got.to_string()); - } - } + Ok(commit_hash) +} - let mm_datetime_p = root().join("../../MM_DATETIME"); - let dt_file = String::from_utf8(slurp(&mm_datetime_p)).unwrap(); - let mut dt_file = dt_file.trim().to_string(); - if let Some(ref dt_git) = dt_git { - if dt_git[..] != dt_file[..] { - // Create or update the “MM_DATETIME” file in order to appease the Cargo dependency management. - let mut mm_datetime_f = fs::File::create(&mm_datetime_p).unwrap(); - mm_datetime_f.write_all(dt_git.as_bytes()).unwrap(); - dt_file = dt_git.to_string(); - } - } +fn version() -> Result { version_tag().map(|tag| format!("{}_{}", crate_version(), tag)) } - println!("cargo:rustc-env=MM_DATETIME={}", dt_file); +fn build_datetime() -> String { Utc::now().to_rfc3339() } - version +fn set_build_variables() -> Result<(), String> { + println!("cargo:rustc-env=KDF_VERSION={}", version()?); + println!("cargo:rustc-env=KDF_DATETIME={}", build_datetime()); + Ok(()) } fn main() { - println!("cargo:rerun-if-env-changed=MANUAL_MM_VERSION"); - println!("cargo:rerun-if-changed=../../MM_VERSION"); - println!("cargo:rerun-if-changed=../../MM_DATETIME"); - if std::env::var("MANUAL_MM_VERSION").is_err() { - // This allows build script to run even if no source code files change as rerun-if-changed checks for a file that doesn't exist - println!("cargo:rerun-if-changed=NON_EXISTING_FILE"); - } - mm_version(); + println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION"); + println!("cargo:rerun-if-env-changed=KDF_BUILD_TAG"); + println!("cargo::rerun-if-changed=.git/HEAD"); + + set_build_variables().expect("Failed to set build variables"); } diff --git a/mm2src/mm2_bin_lib/src/lib.rs b/mm2src/mm2_bin_lib/src/lib.rs index 7ac292aa63..453f1b55f5 100644 --- a/mm2src/mm2_bin_lib/src/lib.rs +++ b/mm2src/mm2_bin_lib/src/lib.rs @@ -7,8 +7,8 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; #[cfg(not(target_arch = "wasm32"))] mod mm2_native_lib; #[cfg(target_arch = "wasm32")] mod mm2_wasm_lib; -const MM_VERSION: &str = env!("MM_VERSION"); -const MM_DATETIME: &str = env!("MM_DATETIME"); +const KDF_VERSION: &str = env!("KDF_VERSION"); +const KDF_DATETIME: &str = env!("KDF_DATETIME"); static LP_MAIN_RUNNING: AtomicBool = AtomicBool::new(false); static CTX: AtomicU32 = AtomicU32::new(0); @@ -41,7 +41,14 @@ fn mm2_status() -> MainStatus { Err(_) => return MainStatus::NoRpc, }; - if *ctx.rpc_started.get().unwrap_or(&false) { + #[cfg(not(target_arch = "wasm32"))] + match ctx.rpc_port.get() { + Some(_) => MainStatus::RpcIsUp, + None => MainStatus::NoRpc, + } + + #[cfg(target_arch = "wasm32")] + if ctx.wasm_rpc.get().is_some() { MainStatus::RpcIsUp } else { MainStatus::NoRpc @@ -101,3 +108,20 @@ async fn finalize_mm2_stop(ctx: MmArc) { dispatch_lp_event(ctx.clone(), StopCtxEvent.into()).await; let _ = ctx.stop().await; } + +#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize))] +#[derive(Clone, Copy, Debug, PartialEq, Primitive)] +pub enum StartupResultCode { + /// Operation completed successfully + Ok = 0, + /// Invalid parameters were provided to the function + InvalidParams = 1, + /// The configuration was invalid (missing required fields, etc.) + ConfigError = 2, + /// MM2 is already running + AlreadyRunning = 3, + /// MM2 initialization failed + InitError = 4, + /// Failed to spawn the MM2 process/thread + SpawnError = 5, +} diff --git a/mm2src/mm2_bin_lib/src/mm2_bin.rs b/mm2src/mm2_bin_lib/src/mm2_bin.rs index 1587103cc4..a944edaa8d 100644 --- a/mm2src/mm2_bin_lib/src/mm2_bin.rs +++ b/mm2src/mm2_bin_lib/src/mm2_bin.rs @@ -1,10 +1,10 @@ #[cfg(not(target_arch = "wasm32"))] use mm2_main::mm2_main; #[cfg(not(target_arch = "wasm32"))] -const MM_VERSION: &str = env!("MM_VERSION"); +const KDF_VERSION: &str = env!("KDF_VERSION"); #[cfg(not(target_arch = "wasm32"))] -const MM_DATETIME: &str = env!("MM_DATETIME"); +const KDF_DATETIME: &str = env!("KDF_DATETIME"); #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] #[global_allocator] @@ -13,6 +13,6 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; fn main() { #[cfg(not(target_arch = "wasm32"))] { - mm2_main(MM_VERSION.into(), MM_DATETIME.into()) + mm2_main(KDF_VERSION.into(), KDF_DATETIME.into()) } } diff --git a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs index 17d7a839bc..8ca51fd4f2 100644 --- a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs @@ -7,6 +7,7 @@ use enum_primitive_derive::Primitive; use gstuff::any_to_str; use libc::c_char; use mm2_core::mm_ctx::MmArc; +use mm2_main::LpMainParams; use num_traits::FromPrimitive; use serde_json::{self as json}; use std::ffi::{CStr, CString}; @@ -15,15 +16,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::Duration; -#[derive(Debug, PartialEq, Primitive)] -enum MainErr { - Ok = 0, - AlreadyRuns = 1, - ConfIsNull = 2, - ConfNotUtf8 = 3, - CantThread = 5, -} - /// Starts the MM2 in a detached singleton thread. #[no_mangle] #[allow(clippy::missing_safety_doc)] @@ -48,39 +40,101 @@ pub unsafe extern "C" fn mm2_main(conf: *const c_char, log_cb: extern "C" fn(lin }}; } - if LP_MAIN_RUNNING.load(Ordering::Relaxed) { - eret!(MainErr::AlreadyRuns) - } - CTX.store(0, Ordering::Relaxed); // Remove the old context ID during restarts. - if conf.is_null() { - eret!(MainErr::ConfIsNull) + eret!(StartupResultCode::InvalidParams, "Configuration is null") } - let conf = CStr::from_ptr(conf); - let conf = match conf.to_str() { + let conf_cstr = CStr::from_ptr(conf); + let conf_str = match conf_cstr.to_str() { Ok(s) => s, - Err(e) => eret!(MainErr::ConfNotUtf8, e), + Err(e) => eret!( + StartupResultCode::InvalidParams, + format!("Configuration is not valid UTF-8: {}", e) + ), + }; + + let conf: json::Value = match json::from_str(conf_str) { + Ok(v) => v, + Err(e) => eret!( + StartupResultCode::ConfigError, + format!("Failed to parse configuration: {}", e) + ), }; - let conf = conf.to_owned(); + + if let Err(true) = LP_MAIN_RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { + eret!(StartupResultCode::AlreadyRunning, "MM2 is already running"); + } + + // Remove the old context ID during restarts. + CTX.store(0, Ordering::Relaxed); register_callback(FfiCallback::with_ffi_function(log_cb)); - let rc = thread::Builder::new().name("lp_main".into()).spawn(move || { - if let Err(true) = LP_MAIN_RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { - log!("lp_main already started!"); - return; - } - let ctx_cb = &|ctx| CTX.store(ctx, Ordering::Relaxed); - match catch_unwind(move || mm2_main::run_lp_main(Some(&conf), ctx_cb, MM_VERSION.into(), MM_DATETIME.into())) { - Ok(Ok(_)) => log!("run_lp_main finished"), - Ok(Err(err)) => log!("run_lp_main error: {}", err), - Err(err) => log!("run_lp_main panic: {:?}", any_to_str(&*err)), + + let ctx_cb = &|ctx| CTX.store(ctx, Ordering::Relaxed); + let params = LpMainParams::with_conf(conf).log_filter(None); + + let ctx = match catch_unwind(|| { + block_on(mm2_main::lp_main( + params, + &ctx_cb, + KDF_VERSION.into(), + KDF_DATETIME.into(), + )) + }) { + Ok(Ok(ctx)) => ctx, + Ok(Err(e)) => { + log!("MM2 initialization failed: {}", e); + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + return StartupResultCode::InitError as i8; + }, + Err(err) => { + log!("MM2 initialization panicked: {:?}", any_to_str(&*err)); + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + return StartupResultCode::InitError as i8; + }, + }; + + // This allows us to use catch_unwind inside the lp_run thread despite MmCtx containing + // types with interior mutability (Mutex, RwLock, etc.) that aren't UnwindSafe. + // By passing just a numeric ID and recovering the actual context inside the + // catch_unwind block, we satisfy the compiler's safety requirements while properly + // handling potential panics. + let ctx_id = match ctx.ffi_handle() { + Ok(id) => id, + Err(e) => { + log!("MM2 thread setup failed: Failed to create FFI handle: {}", e); + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + return StartupResultCode::InitError as i8; + }, + }; + + let rc = thread::Builder::new().name("lp_run".into()).spawn(move || { + match catch_unwind(move || { + let ctx = match MmArc::from_ffi_handle(ctx_id) { + Ok(ctx) => ctx, + Err(err) => { + panic!("Failed to recover context in thread: {}", err); + }, + }; + block_on(mm2_main::lp_run(ctx)); + }) { + Ok(_) => log!("MM2 thread completed normally"), + Err(err) => { + log!("MM2 thread panicked: {:?}", any_to_str(&*err)); + }, }; + LP_MAIN_RUNNING.store(false, Ordering::Relaxed) }); + if let Err(e) = rc { - eret!(MainErr::CantThread, e) + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + eret!( + StartupResultCode::SpawnError, + format!("Failed to spawn MM2 thread: {:?}", e) + ); } - MainErr::Ok as i8 + + StartupResultCode::Ok as i8 } /// Checks if the MM2 singleton thread is currently running or not. @@ -108,7 +162,7 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char static RUNNING: AtomicBool = AtomicBool::new(false); if let Err(true) = RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { log!("mm2_test] Running already!"); - return -1; + return StartupResultCode::AlreadyRunning as i32; } // #402: Stop the MM in order to test the library restart. @@ -119,7 +173,7 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char Ok(ctx) => ctx, Err(err) => { log!("mm2_test] Invalid CTX? !from_ffi_handle: {}", err); - return -1; + return StartupResultCode::InvalidParams as i32; }, }; let conf = json::to_string(&ctx.conf).unwrap(); @@ -176,10 +230,10 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char log!("mm2_test] Restarting MM…"); let conf = CString::new(&conf[..]).unwrap(); let rc = unsafe { mm2_main(conf.as_ptr(), log_cb) }; - let rc = MainErr::from_i8(rc).unwrap(); - if rc != MainErr::Ok { + let rc = StartupResultCode::from_i8(rc).unwrap(); + if rc != StartupResultCode::Ok { log!("!mm2_main: {:?}", rc); - return -1; + return rc as i32; } // Wait for the new MM instance to allocate context. @@ -191,14 +245,14 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char } if now_float() - since > 60.0 { log!("mm2_test] Won't start"); - return -1; + return StartupResultCode::InitError as i32; } } let ctx_id = CTX.load(Ordering::Relaxed); if ctx_id == prev_ctx_id { log!("mm2_test] Context ID is the same"); - return -1; + return StartupResultCode::InvalidParams as i32; } log!("mm2_test] New MM instance {} started", ctx_id); } diff --git a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs index f878e1b914..9fcf795012 100644 --- a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs @@ -1,8 +1,5 @@ //! Some specifics of using the [`wasm_bindgen`] library: //! -//! # Currently only `Result` is allowed -//! [tracking issue]: https://github.com/rustwasm/wasm-bindgen/issues/1004 -//! //! # JavaScript enums do not support methods at all //! [tracking issue]: https://github.com/rustwasm/wasm-bindgen/issues/1715 //! @@ -21,18 +18,27 @@ use mm2_rpc::data::legacy::MmVersionResponse; use mm2_rpc::wasm_rpc::WasmRpcResponse; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; - -/// The errors can be thrown when using the `mm2_main` function incorrectly. #[wasm_bindgen] -#[derive(Primitive)] -pub enum Mm2MainErr { - AlreadyRuns = 1, - InvalidParams = 2, - NoCoinsInConf = 3, +#[derive(Debug, Clone, Serialize)] +struct StartupError { + code: StartupResultCode, + message: String, } -impl From for JsValue { - fn from(e: Mm2MainErr) -> Self { JsValue::from(e as i32) } +#[wasm_bindgen] +impl StartupError { + fn new(code: StartupResultCode, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } + + #[wasm_bindgen(getter)] + pub fn code(&self) -> i8 { self.code as i8 } + + #[wasm_bindgen(getter)] + pub fn message(&self) -> String { self.message.clone() } } #[derive(Deserialize)] @@ -58,7 +64,7 @@ impl From for LpMainParams { /// # Usage /// /// ```javascript -/// import init, {mm2_main, LogLevel, Mm2MainErr} from "./path/to/mm2.js"; +/// import init, {mm2_main, LogLevel, StartupResultCode} from "./path/to/mm2.js"; /// /// const params = { /// conf: { "gui":"WASMTEST", mm2:1, "passphrase":"YOUR_PASSPHRASE_HERE", "rpc_password":"test123", "coins":[{"coin":"ETH","protocol":{"type":"ETH"}}] }, @@ -68,8 +74,8 @@ impl From for LpMainParams { /// try { /// mm2_main(params, handle_log); /// } catch (e) { -/// switch (e) { -/// case Mm2MainErr.AlreadyRuns: +/// switch (e.code) { +/// case StartupResultCode.AlreadyRunning: /// alert("MarketMaker2 already runs..."); /// break; /// // handle other errors... @@ -80,51 +86,70 @@ impl From for LpMainParams { /// } /// ``` #[wasm_bindgen] -pub fn mm2_main(params: JsValue, log_cb: js_sys::Function) -> Result<(), JsValue> { +pub async fn mm2_main(params: JsValue, log_cb: js_sys::Function) -> Result { let params: MainParams = match deserialize_from_js(params.clone()) { Ok(p) => p, Err(e) => { - console_err!("Expected 'MainParams' as the first argument, found {:?}: {}", params, e); - return Err(Mm2MainErr::InvalidParams.into()); + let error = StartupError::new( + StartupResultCode::InvalidParams, + format!("Expected 'MainParams' as the first argument, found {:?}: {}", params, e), + ); + console_err!("{}", error.message()); + return Err(error.into()); }, }; if params.conf["coins"].is_null() { - console_err!("Config must contain 'coins' field: {:?}", params.conf); - return Err(Mm2MainErr::NoCoinsInConf.into()); + let error = StartupError::new( + StartupResultCode::ConfigError, + format!("Config must contain 'coins' field: {:?}", params.conf), + ); + console_err!("{}", error.message()); + return Err(error.into()); } let params = LpMainParams::from(params); if LP_MAIN_RUNNING.load(Ordering::Relaxed) { - return Err(Mm2MainErr::AlreadyRuns.into()); + let error = StartupError::new(StartupResultCode::AlreadyRunning, "MM2 is already running"); + console_err!("{}", error.message()); + return Err(error.into()); } - CTX.store(0, Ordering::Relaxed); // Remove the old context ID during restarts. + + // Remove the old context ID during restarts. + CTX.store(0, Ordering::Relaxed); register_callback(WasmCallback::with_js_function(log_cb)); + // Setting up a global panic hook to log panic information to the console + // This doesn't prevent termination of the WebAssembly instance, but ensures error details are visible + // We can't use catch_unwind directly with async/await in WebAssembly, so this is our best option for diagnostics + // If a panic occurs, the MM2 instance will terminate but the browser tab will remain responsive set_panic_hook(); - let fut = async move { - if let Err(true) = LP_MAIN_RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { - console_err!("lp_main already started!"); - return; - } - let ctx_cb = |ctx| CTX.store(ctx, Ordering::Relaxed); - // TODO figure out how to use catch_unwind here - // use futures::FutureExt; - // match mm2_main::lp_main(params, &ctx_cb).catch_unwind().await { - // Ok(Ok(_)) => console_info!("run_lp_main finished"), - // Ok(Err(err)) => console_err!("run_lp_main error: {}", err), - // Err(err) => console_err!("run_lp_main panic: {:?}", any_to_str(&*err)), - // }; - match mm2_main::lp_main(params, &ctx_cb, MM_VERSION.into(), MM_DATETIME.into()).await { - Ok(()) => console_info!("run_lp_main finished"), - Err(err) => console_err!("run_lp_main error: {}", err), - }; - LP_MAIN_RUNNING.store(false, Ordering::Relaxed) + if let Err(true) = LP_MAIN_RUNNING.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) { + let error = StartupError::new(StartupResultCode::AlreadyRunning, "lp_main already started!"); + console_err!("{}", error.message()); + return Err(error.into()); + } + + let ctx_cb = |ctx| CTX.store(ctx, Ordering::Relaxed); + let ctx = match mm2_main::lp_main(params, &ctx_cb, KDF_VERSION.into(), KDF_DATETIME.into()).await { + Ok(ctx) => { + console_info!("lp_main finished"); + ctx + }, + Err(err) => { + let error = StartupError::new(StartupResultCode::InitError, format!("lp_main error: {}", err)); + console_err!("{}", error.message()); + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + return Err(error.into()); + }, }; - // At this moment we still don't have `MmCtx` context to use its `MmCtx::abortable_system` spawner. - executor::spawn_local(fut); - Ok(()) + executor::spawn_local(async move { + mm2_main::lp_run(ctx).await; + LP_MAIN_RUNNING.store(false, Ordering::Relaxed); + }); + + Ok(StartupResultCode::Ok as i8) } /// Returns the MarketMaker2 instance status. @@ -242,8 +267,8 @@ pub async fn mm2_rpc(payload: JsValue) -> Result { #[wasm_bindgen] pub fn mm2_version() -> JsValue { serialize_to_js(&MmVersionResponse { - result: MM_VERSION.into(), - datetime: MM_DATETIME.into(), + result: KDF_VERSION.into(), + datetime: KDF_DATETIME.into(), }) .expect("expected serialization to succeed") } diff --git a/mm2src/mm2_bitcoin/chain/src/for_tests/firo_c50e5a3f16744ac86bacae28d9251a29bf754d250592bce16a953cd961b584d5 b/mm2src/mm2_bitcoin/chain/src/for_tests/firo_c50e5a3f16744ac86bacae28d9251a29bf754d250592bce16a953cd961b584d5 new file mode 100644 index 0000000000..0d09a41632 --- /dev/null +++ b/mm2src/mm2_bitcoin/chain/src/for_tests/firo_c50e5a3f16744ac86bacae28d9251a29bf754d250592bce16a953cd961b584d5 @@ -0,0 +1 @@ +03000900010000000000000000000000000000000000000000000000000000000000000000ffffffff01d3ffffffff02806297c5010000001976a914bd3dd0b83c5a1e7fe51034f605908a9f2cb2b33f88ac0000000000000000f5d201490af40c59cb005fd45dcf7792dc3c3d5724fcbb178bae1b854168a04da65929010015fd5d9648c64b23364773060b67ba517d34517fbe571e767d0be173b8c2187700005f299dabf2a9c38eec0ea51a1f522504dcf76639194914d4822a7dd4ad2cfca800005a0d6ba857b73eceb2d2085ecdccbad6e06ce156eb398242d62364563c3738f7daf128c5e1a494f334f566c5578501156bd41522c33ff3672a6365f0c1b5a8db6e2a563c6247ee8f33ad36e6ff1586998d33829325a4ded9ded4e310a2bb2ef0a34c471115e43c68b1e88e3e204d0dad7b805d41913248b5cf31c24a24b35238f3aa46af1509dd65de318760f6edd80f00fd470a0107000000000000000107000000000000006474978c121e8d0a7ab8f09bcccc7827c2f2fa481ca471064a93827f5a31aba5130e000000000000013e728e80a5e0032932ceaad63431caf9ba677e4d80ed00475d0c9f0da59309d1000001e8af7d7035ee6cf3b880b71c9e3cec51e9ede84bf291cd9e72b17cbf237bbdcc000001adaf89285f702bff5dcf46f13ee6847b08f8f9ea58f8c12274438be2964edee7010001d886847357e75dc388e6d067572c446dced4883a9ad2e66d25fd6f6b45d2d5ac0000cdec45699ca612920427f723df99233f43d39a62039f9832c0f5cbafcbb226890000058bd005c5f5eae7274328b2cfa681f79c6927b923a712e498040c905ad3a9d88d0000413950f2961d43c740ef019b68a508397163302e6692f3e7101b98440d1118220000eaab97b55c1b71a858192bb9c289c07db05b455b11d617bb43701d9bb8338b470000902b696251a0ca57f7d586672f302b9fec7d607d1d21f1c3facb6168e34a2d6d010071b53be475bec52ec3caaa6d46c8fa0076d3fedc9931fc68128158a62590146400000541fece7f41b553fc4d5ea741a5f4a0fece0c866fdcbd6559feaf19ef69de9e5e0100e60941da8114ac11b14dbd429a6e1a71303cae4c111859259031f2a2d688c2b600000f0b9cfbff6da35e42392ddbf3e19151fc9d48141eaafb2e36de7911755e2f7f000062c20003ff099fb105fe67f92745be3f52371fdeea3a90b608db38a087ec63db0000020fa71272340d563844c620d4ee9677e054363b3f888a1ca1ca9b6a03908b8200002362f2b3ec34fdc8a65022ff3430426fb4137ea80e82cbe4f9a91f2a2f67557000f32bd21536b15b4ebc1175dff6b42058aaced347a983527a65e6f0db18b3553f8af704be0a336843e7b4892f0bdd26664ae5421db4d4c1db89bb6c704af0b7888018981ab9005af52dbea8352a3096ffecd60d5570eea0173f3b70d1d9d4869cdb99cb01fb0ecefe7ab0c08b9ebd608930544a9e5f849b876565d18cf1a2b2721251e679e8d3adf759e2374048624fb095e5aa8ef88f816d47879b6b45042670792db2b9fc1941b76d23c8f3a95249f144ac0972ff035940e55afcfd17d54bd9eda6da5736f24c2102961185bd6b96077d8084282d1cca465529d90e918ce9e6c8d0c2558a93feddd77c0e5c40d36b3dc19037ad1a5c6c35d1fa1e8ac8105ed44073c624d0edfb5335e5089341eac21390deb98da1a1c7860a8399f6a85a68ebc19aef63c88b1addac03c9f384366c4317256f206cdd884a5094cd6494c663f7f1f330cf7ad1ebd1e8c5726b9b7157cc65e8f7e7eabaf36a84c4226f7b5b58fcfa407585a7b2a08641ab9383e503c4cc335c8f61d847a587980cf962b71bff90d3f21012f11b71a2a378ff85684c1b7ba91c200d2a071b9c2a8c6308d963786b94c6ce10693e32a30dc216ae8a4371fd794d5a300bc9654465e1651e0201b975d65ba742ded0596fed0bb13285297e8d0d4a836dafee0ecbdc1de574e56db4de153c218824800287e9756f9c4424303ed58de7b0113b1423be543adac7845a1f87d54174a552cdd2184b3ce0cdbc9c56991b908996ec165a9c09455e3cc1a59a21cb57fa56e8f4792a9195b830aa3ff46db51f462ec7a8ac0daa3f32d02bcec6b1f831d2ad521cd3d1f5e03ae606b0d78187ec03156cfcccc2be0a2c9d459bf239fbf5fb7fbd62675ee8b5af93db9731f9558d57941bdd2077288878d6a98b2647e40f74030a78bff8900575e0e67401228c326f270e18d38886116e2fdd98fe94cab6e9fb8bcd71f43816ce6a3ab1fff8982f837eedde7b90e52fe0b84c012c5b69e37d5d60ea21121bef8b9d55e9d348d8ce978c84d29790c3b71f13ed84d90b9e24afb7a6a2cc7c1894d39a0096586bcbae23930c89550748bcaebef880c4fd2721eaa1b65a81effbbbbacfcaff2b7a1ea90426d5440486836418ff5c0d70de633649ffd18d8c630bc7dff4b3c0f1a2ec1477c922f33f13fa8a791d775c4d058477a1d93ff450f2755755d1b8a6cd7a3b5dd331bb6f6bb82e67706d3116be61bf16b780f8dd7f440a7d1060c6703f86bbb805abf0b3c4d9eb9cec54f9695b77349e8af87c416b4debd2e71e441d2a4bb9bce36886d16a58f63f2a269eb974c0cbe0d65b0fc124bb2048c8abe7ffce5a56aa3d2b89d4330cad59d02d68f64d93e7df725aed8bfce8e605518de7b6ed8ca73b033d1fc7e0128bb441b3b966b4c13692c5af8e5b4c6b025d946a84061f48f4b8e14ad10401a35c601f2e3f14011225c16966078bdcbe8c6a119590bc1597e4268342dbb68e99e3045643378d8d877787e810683094a64176757fa582253605a5239239290191ac12656779260eb4c253f1ce3d34abaa6d6b20a39f64cd8a3fa1dd204c49f03eb75276d1fb525eec02f0141eb07642adec56827d693fe1228cdfe38906e35678947895d42ebd0660a7701dcfa3d369b0a83a5bd623bc6466fa330e23637ab5e58d8875687cc6dac1a3af6db5f5693c14db711bf04815eb71a98ae816644fa046179dcfc9f2dad8000001840b294363c52bc923f16ba81481d75384e4aac3b63ce083f6e5f05c6564b1ea010001063966547b14e002a9eec62228b773d96a313e2c55332dcf37d77c1c7e8e57c9c77cd6b5acdb406870365bdf417e25f09bafc24e6cde5cadc959e2073beb3b7eeafa1241af4a1604f5222d99f84a745f4b65c3764c181e570dcfee722b7c6e5f10e5a78ab27b425ea96b5456fa6810f3ab14e3f38b33d36a7c71bc35ce9947950000dff5172c7f4eb4b7ec49f671a96498e36ff9ccacff113c8807821673916e873bb10502dac6bbcd52a5886bcd8cefbee20a6ad4547d1385600cd0175d95dd16850100323c56c93e60ccd3c5a8d38c5cbd35708c404f0ce77b8cdc9d3ef25147567d2a000039020f1c1812fd55c3705d7a6724b93b02836180d25abe93a0272d0aca54d70d0100f59352298577b96d82b2191d57ab7f9cc482f31786a1a5b2b3b03ba2a8aba335bd90ca453691709cbeb89babc0ba499b283dfb8bdb359fa441f1790963214fd1954a999a5003ebdacca67a1b1ae5f02212cec5f32f2d387ef07df030ba966e4f06084665afe690311e8e4371b00684576c76a8f358b86336b6ead26c75a27eac1f0100947638974fcc163b8dbbcb5fc749693d9e2c1101720f96db55fb1c3616d3cfe10100b8008130a9103eeecfe7a790d4bc681e23a31f34a2bdf4a80c99612f8c9336380100f04c88c626a3d7f33303989195856d45790c6d152ed089ad1a2f6d6147568c8d00006e69a1ddc3f140293ae476e4ea53e4c17b684d00fb46b6e22a4c3b844d63df49010035c50f4847ee31bede3c18be5a973cba4786b86c4364e1db0936c7379b142400010006df3558338a7da778adce88b71c13751ae2e490857723442b0bb4276a400748a3000023dc38540a0038f3f90e0315d5bb7f00df82e23e56f38d3ed499902bf6be419d0000e71592f7b77d73ff6fec168e668d78e782f5e85d0ab8ac0d40500a8daa4ba93c01006cc488cb587093c1f0f6d9d4bbc64b64fa2e0af38b6382ce5bbc9379bfc3a89300003a3223e8bc8aaf9a65a699edc8fe3eb05602c5ecdeab83a5b05b8fa28a187a780000994b553d1af8eef2283d262e3e7280661392201dc0241ab380421f48b14e27a90000 \ No newline at end of file diff --git a/mm2src/mm2_bitcoin/chain/src/transaction.rs b/mm2src/mm2_bitcoin/chain/src/transaction.rs index 805b32caf7..16ccc1a680 100644 --- a/mm2src/mm2_bitcoin/chain/src/transaction.rs +++ b/mm2src/mm2_bitcoin/chain/src/transaction.rs @@ -229,6 +229,8 @@ pub struct Transaction { /// https://github.com/navcoin/navcoin-core/blob/556250920fef9dc3eddd28996329ba316de5f909/src/primitives/transaction.h#L497 pub str_d_zeel: Option, pub tx_hash_algo: TxHashAlgo, + /// https://github.com/firoorg/firo/blob/bf7c7fa555bd00db970c5c3e64e7452ef20c800e/src/primitives/transaction.h#L389 + pub v_extra_payload: Option>, // only available for special transaction types } impl From<&'static str> for Transaction { @@ -417,6 +419,13 @@ impl Serializable for Transaction { stream.append(&len); stream.append_slice(string.as_bytes()); } + if let Some(ref payload) = self.v_extra_payload { + if !payload.is_empty() { + let len: CompactInteger = payload.len().into(); + stream.append(&len); + stream.append_slice(payload); + } + } }, true => { stream @@ -454,6 +463,10 @@ where let overwintered: bool = (header >> 31) != 0; let version = if overwintered { header & 0x7FFFFFFF } else { header }; + // Check if we might have a version 3 (Spark) transaction using the 0xFFFF mask + let spark_version = header & 0xFFFF; + let maybe_spark = spark_version == 3; + let mut version_group_id = 0; if overwintered { version_group_id = reader.read()?; @@ -489,7 +502,7 @@ where let lock_time = reader.read()?; let mut posv = false; - n_time = if tx_type == TxType::PosvWithNTime { + n_time = if tx_type == TxType::PosvWithNTime && !maybe_spark { posv = true; Some(reader.read()?) } else { @@ -545,6 +558,16 @@ where None }; + // Check for extra payload if it might be a Spark transaction + let v_extra_payload = if maybe_spark && !reader.is_finished() { + let len: CompactInteger = reader.read()?; + let mut buf = vec![0; len.into()]; + reader.read_slice(&mut buf)?; + Some(buf) + } else { + None + }; + Ok(Transaction { version, n_time, @@ -565,6 +588,7 @@ where posv, str_d_zeel, tx_hash_algo: TxHashAlgo::DSHA256, + v_extra_payload, }) } @@ -580,6 +604,7 @@ impl Deserializable for Transaction { // specific use case let mut buffer = vec![]; reader.read_to_end(&mut buffer)?; + if let Ok(t) = deserialize_tx(&mut Reader::from_read(buffer.as_slice()), TxType::PosvWithNTime) { return Ok(t); } @@ -877,6 +902,7 @@ mod tests { posv: false, str_d_zeel: None, tx_hash_algo: TxHashAlgo::DSHA256, + v_extra_payload: None, }; assert_eq!(actual, expected); } @@ -981,6 +1007,15 @@ mod tests { assert_eq!(serialize(&t).to_hex::(), transaction); } + #[test] + fn firo_spark() { + let transaction = + include_str!("for_tests/firo_c50e5a3f16744ac86bacae28d9251a29bf754d250592bce16a953cd961b584d5"); + let t: Transaction = transaction.into(); + assert_eq!(2, t.outputs.len()); + assert_eq!(serialize(&t).to_hex::(), transaction); + } + #[test] // https://kmdexplorer.io/tx/687acd73ad23ce93e7ddabeece8eb228a0a0e15e4d265f7c717d7458ddce9bdd fn kmd_687acd73ad23ce93e7ddabeece8eb228a0a0e15e4d265f7c717d7458ddce9bdd() { @@ -1063,6 +1098,7 @@ mod tests { posv: false, str_d_zeel: None, tx_hash_algo: TxHashAlgo::DSHA256, + v_extra_payload: None, }; assert_eq!(actual, expected); } diff --git a/mm2src/mm2_bitcoin/crypto/src/lib.rs b/mm2src/mm2_bitcoin/crypto/src/lib.rs index 393712ccb1..fc034858f2 100644 --- a/mm2src/mm2_bitcoin/crypto/src/lib.rs +++ b/mm2src/mm2_bitcoin/crypto/src/lib.rs @@ -34,15 +34,17 @@ pub enum ChecksumType { pub fn ripemd160(input: &[u8]) -> H160 { let mut hasher = Ripemd160::new(); hasher.update(input); - (*hasher.finalize()).into() + let array: [u8; 20] = hasher.finalize().into(); + array.into() } /// SHA-1 #[inline] pub fn sha1(input: &[u8]) -> H160 { - let mut hasher = Sha1::default(); + let mut hasher = Sha1::new(); hasher.update(input); - (*hasher.finalize()).into() + let array: [u8; 20] = hasher.finalize().into(); + array.into() } /// SHA-256 @@ -50,7 +52,8 @@ pub fn sha1(input: &[u8]) -> H160 { pub fn sha256(input: &[u8]) -> H256 { let mut hasher = Sha256::new(); hasher.update(input); - (*hasher.finalize()).into() + let array: [u8; 32] = hasher.finalize().into(); + array.into() } /// Groestl-512 @@ -58,7 +61,8 @@ pub fn sha256(input: &[u8]) -> H256 { pub fn groestl512(input: &[u8]) -> H512 { let mut hasher = Groestl512::new(); hasher.update(input); - (*hasher.finalize()).into() + let array: [u8; 64] = hasher.finalize().into(); + array.into() } /// Keccak-256 @@ -66,7 +70,8 @@ pub fn groestl512(input: &[u8]) -> H512 { pub fn keccak256(input: &[u8]) -> H256 { let mut hasher = Keccak256::new(); hasher.update(input); - (*hasher.finalize()).into() + let array: [u8; 32] = hasher.finalize().into(); + array.into() } /// Double Keccak-256 diff --git a/mm2src/mm2_bitcoin/keys/src/signature.rs b/mm2src/mm2_bitcoin/keys/src/signature.rs index 46edce268f..90122ed630 100644 --- a/mm2src/mm2_bitcoin/keys/src/signature.rs +++ b/mm2src/mm2_bitcoin/keys/src/signature.rs @@ -4,7 +4,8 @@ use hash::H520; use hex::{FromHex, ToHex}; -use std::{fmt, ops, str}; +use std::convert::TryInto; +use std::{array::TryFromSliceError, convert::TryFrom, fmt, ops, str}; use Error; #[derive(PartialEq, Clone)] @@ -91,6 +92,10 @@ impl From for CompactSignature { fn from(h: H520) -> Self { CompactSignature(h) } } -impl From> for CompactSignature { - fn from(v: Vec) -> Self { CompactSignature(H520::from(&v[..])) } +impl TryFrom> for CompactSignature { + type Error = TryFromSliceError; + fn try_from(value: Vec) -> Result { + let bytes: &[u8; 65] = &value.as_slice().try_into()?; + Ok(CompactSignature(H520::from(bytes))) + } } diff --git a/mm2src/mm2_bitcoin/primitives/src/hash.rs b/mm2src/mm2_bitcoin/primitives/src/hash.rs index 46f86df4d4..7bf56f5700 100644 --- a/mm2src/mm2_bitcoin/primitives/src/hash.rs +++ b/mm2src/mm2_bitcoin/primitives/src/hash.rs @@ -2,6 +2,7 @@ use bitcoin_hashes::{sha256d, Hash as ExtHash}; use hex::{FromHex, FromHexError, ToHex}; +use std::convert::TryInto; use std::hash::{Hash, Hasher}; use std::{cmp, fmt, ops, str}; @@ -39,10 +40,10 @@ macro_rules! impl_hash { fn from(h: $name) -> Self { h.0 } } - impl<'a> From<&'a [u8]> for $name { - fn from(slc: &[u8]) -> Self { + impl<'a> From<&'a [u8; $size]> for $name { + fn from(slc: &[u8; $size]) -> Self { let mut inner = [0u8; $size]; - inner[..].clone_from_slice(&slc[0..$size]); + inner.copy_from_slice(slc); $name(inner) } } @@ -61,17 +62,9 @@ macro_rules! impl_hash { impl str::FromStr for $name { type Err = FromHexError; - fn from_str(s: &str) -> Result { let vec: Vec = s.from_hex()?; - match vec.len() { - $size => { - let mut result = [0u8; $size]; - result.copy_from_slice(&vec); - Ok($name(result)) - }, - _ => Err(FromHexError::InvalidHexLength), - } + Self::from_slice(&vec).map_err(|_| FromHexError::InvalidHexLength) } } @@ -143,6 +136,14 @@ macro_rules! impl_hash { pub fn size() -> usize { $size } pub fn is_zero(&self) -> bool { self.0.iter().all(|b| *b == 0) } + + /// Preferred method for constructing from a slice - checks length and returns Result + pub fn from_slice(slc: &[u8]) -> Result { + let bytes: [u8; $size] = slc + .try_into() + .map_err(|_| "Slice length must be exactly 40 bytes")?; + Ok(bytes.into()) + } } }; } diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs index 00ecf4f52f..08ae1d7afe 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs @@ -4,6 +4,7 @@ use primitives::hash::H256 as GlobalH256; use primitives::hash::H264 as GlobalH264; use serde; use serde::de::Unexpected; +use serde::ser::SerializeSeq; use std::cmp::Ordering; use std::fmt; use std::hash::{Hash, Hasher}; @@ -17,6 +18,60 @@ macro_rules! impl_hash { impl $name { pub const fn const_default() -> $name { $name([0; $size]) } + + pub fn serialize_to_byte_seq(value: &Self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(value.0.len()))?; + for byte in &value.0 { + seq.serialize_element(byte)?; + } + seq.end() + } + + pub fn deserialize_from_bytes<'de, D>(deserializer: D) -> Result<$name, D::Error> + where + D: serde::Deserializer<'de>, + { + struct BytesVisitor; + impl<'de> serde::de::Visitor<'de> for BytesVisitor { + type Value = $name; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a byte array or sequence of length {}", $size) + } + + fn visit_bytes(self, v: &[u8]) -> Result<$name, E> + where + E: serde::de::Error, + { + if v.len() != $size { + return Err(E::invalid_length(v.len(), &self)); + } + let mut arr = [0u8; $size]; + arr.copy_from_slice(v); + Ok($name(arr)) + } + + fn visit_seq(self, mut seq: A) -> Result<$name, A::Error> + where + A: serde::de::SeqAccess<'de>, + { + let mut vec = Vec::with_capacity($size); + while let Some(elem) = seq.next_element()? { + vec.push(elem); + } + if vec.len() != $size { + return Err(serde::de::Error::invalid_length(vec.len(), &self)); + } + let mut arr = [0u8; $size]; + arr.copy_from_slice(&vec); + Ok($name(arr)) + } + } + deserializer.deserialize_any(BytesVisitor) + } } impl Default for $name { diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/script.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/script.rs index 0be24b7214..c2a44d2211 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/script.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/script.rs @@ -13,12 +13,14 @@ pub enum ScriptType { NullData, WitnessScript, WitnessKey, + Taproot, // Qtum specific CallSender, CreateSender, Call, Create, LelantusMint, + SparkMint, ColdStaking, // Komodo smart chains specific CryptoCondition, @@ -35,6 +37,7 @@ impl From for ScriptType { GlobalScriptType::NullData => ScriptType::NullData, GlobalScriptType::WitnessScript => ScriptType::WitnessScript, GlobalScriptType::WitnessKey => ScriptType::WitnessKey, + GlobalScriptType::Taproot => ScriptType::Taproot, GlobalScriptType::CallSender => ScriptType::CallSender, GlobalScriptType::CreateSender => ScriptType::CreateSender, GlobalScriptType::Call => ScriptType::Call, @@ -59,11 +62,13 @@ impl Serialize for ScriptType { ScriptType::NullData => "nulldata".serialize(serializer), ScriptType::WitnessScript => "witness_v0_scripthash".serialize(serializer), ScriptType::WitnessKey => "witness_v0_keyhash".serialize(serializer), + ScriptType::Taproot => "witness_v1_taproot".serialize(serializer), ScriptType::CallSender => "call_sender".serialize(serializer), ScriptType::CreateSender => "create_sender".serialize(serializer), ScriptType::Call => "call".serialize(serializer), ScriptType::Create => "create".serialize(serializer), ScriptType::LelantusMint => "lelantusmint".serialize(serializer), + ScriptType::SparkMint => "sparksmint".serialize(serializer), ScriptType::ColdStaking => "cold_staking".serialize(serializer), ScriptType::CryptoCondition => "cryptocondition".serialize(serializer), } @@ -97,11 +102,13 @@ impl<'a> Deserialize<'a> for ScriptType { "nulldata" => Ok(ScriptType::NullData), "witness_v0_scripthash" => Ok(ScriptType::WitnessScript), "witness_v0_keyhash" => Ok(ScriptType::WitnessKey), + "witness_v1_taproot" => Ok(ScriptType::Taproot), "call_sender" => Ok(ScriptType::CallSender), "create_sender" => Ok(ScriptType::CreateSender), "call" => Ok(ScriptType::Call), "create" => Ok(ScriptType::Create), "lelantusmint" => Ok(ScriptType::LelantusMint), + "sparksmint" => Ok(ScriptType::SparkMint), "cold_staking" => Ok(ScriptType::ColdStaking), "cryptocondition" => Ok(ScriptType::CryptoCondition), _ => Err(E::invalid_value(Unexpected::Str(value), &self)), diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/transaction.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/transaction.rs index 29f04ef99c..0a43c4cbe4 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/transaction.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/transaction.rs @@ -75,6 +75,8 @@ pub enum TransactionInputEnum { Sigma(SigmaInput), /// FIRO specific Lelantus(LelantusInput), + /// FIRO specific + Spark(SparkInput), } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -99,6 +101,17 @@ pub struct LelantusInput { sequence: u32, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SparkInput { + #[serde(rename = "scriptSig")] + pub script_sig: TransactionInputScript, + #[serde(rename = "nFees")] + pub n_fees: f64, + #[serde(rename = "lTags")] + l_tags: Vec, + sequence: u32, +} + impl TransactionInputEnum { pub fn is_coinbase(&self) -> bool { matches!(self, TransactionInputEnum::Coinbase(_)) } } diff --git a/mm2src/mm2_bitcoin/script/src/script.rs b/mm2src/mm2_bitcoin/script/src/script.rs index 9244f043f7..c5e4b57d0f 100644 --- a/mm2src/mm2_bitcoin/script/src/script.rs +++ b/mm2src/mm2_bitcoin/script/src/script.rs @@ -2,6 +2,7 @@ use bytes::Bytes; use keys::{self, AddressHashEnum, Public}; +use std::convert::TryInto; use std::{fmt, ops}; use {Error, Opcode}; @@ -19,6 +20,7 @@ pub enum ScriptType { NullData, WitnessScript, WitnessKey, + Taproot, // Qtum specific CallSender, CreateSender, @@ -350,7 +352,7 @@ impl Script { ScriptType::WitnessKey } else if self.is_pay_to_witness_script_hash() { ScriptType::WitnessScript - // TODO add Call + // TODO add Call } else { ScriptType::NonStandard } @@ -425,12 +427,18 @@ impl Script { ))] }) }, - ScriptType::PubKeyHash => Ok(vec![ScriptAddress::new_p2pkh(AddressHashEnum::AddressHash( - self.data[3..23].into(), - ))]), - ScriptType::ScriptHash => Ok(vec![ScriptAddress::new_p2sh(AddressHashEnum::AddressHash( - self.data[2..22].into(), - ))]), + ScriptType::PubKeyHash => { + let bytes = self.data.get(3..23).ok_or(keys::Error::InvalidAddress)?; + let hash: [u8; 20] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; + let address_hash = AddressHashEnum::AddressHash(hash.into()); + Ok(vec![ScriptAddress::new_p2pkh(address_hash)]) + }, + ScriptType::ScriptHash => { + let bytes = self.data.get(2..22).ok_or(keys::Error::InvalidAddress)?; + let hash: [u8; 20] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; + let address_hash = AddressHashEnum::AddressHash(hash.into()); + Ok(vec![ScriptAddress::new_p2sh(address_hash)]) + }, ScriptType::Multisig => { let mut addresses: Vec = Vec::new(); let mut pc = 1; @@ -448,12 +456,21 @@ impl Script { Ok(addresses) }, ScriptType::NullData => Ok(vec![]), - ScriptType::WitnessScript => Ok(vec![ScriptAddress::new_p2wsh(AddressHashEnum::WitnessScriptHash( - self.data[2..34].into(), - ))]), - ScriptType::WitnessKey => Ok(vec![ScriptAddress::new_p2wpkh(AddressHashEnum::AddressHash( - self.data[2..22].into(), - ))]), + ScriptType::WitnessScript => { + let bytes = self.data.get(2..34).ok_or(keys::Error::InvalidAddress)?; + let hash: [u8; 32] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; + let address_hash = AddressHashEnum::WitnessScriptHash(hash.into()); + Ok(vec![ScriptAddress::new_p2wsh(address_hash)]) + }, + ScriptType::WitnessKey => { + let bytes = self.data.get(2..22).ok_or(keys::Error::InvalidAddress)?; + let hash: [u8; 20] = bytes.try_into().map_err(|_| keys::Error::InvalidAddress)?; + let address_hash = AddressHashEnum::AddressHash(hash.into()); + Ok(vec![ScriptAddress::new_p2wpkh(address_hash)]) + }, + ScriptType::Taproot => { + Ok(vec![]) // TODO + }, ScriptType::CallSender => { Ok(vec![]) // TODO }, diff --git a/mm2src/mm2_bitcoin/script/src/sign.rs b/mm2src/mm2_bitcoin/script/src/sign.rs index a70e423eea..089c25a614 100644 --- a/mm2src/mm2_bitcoin/script/src/sign.rs +++ b/mm2src/mm2_bitcoin/script/src/sign.rs @@ -9,6 +9,7 @@ use hash::{H256, H512}; use keys::KeyPair; use ser::Stream; use serde::Deserialize; +use std::convert::TryInto; use {Builder, Script}; const ZCASH_PREVOUTS_HASH_PERSONALIZATION: &[u8] = b"ZcashPrevoutHash"; @@ -162,6 +163,7 @@ pub struct TransactionInputSigner { pub posv: bool, pub str_d_zeel: Option, pub hash_algo: SignerHashAlgo, + pub v_extra_payload: Option>, } /// Used for resigning and loading test transactions @@ -185,6 +187,7 @@ impl From for TransactionInputSigner { posv: t.posv, str_d_zeel: t.str_d_zeel, hash_algo: t.tx_hash_algo.into(), + v_extra_payload: t.v_extra_payload, } } } @@ -222,6 +225,7 @@ impl From for Transaction { join_split_sig: H512::default(), str_d_zeel: t.str_d_zeel, tx_hash_algo: t.hash_algo.into(), + v_extra_payload: t.v_extra_payload, } } } @@ -370,6 +374,7 @@ impl TransactionInputSigner { posv: self.posv, str_d_zeel: self.str_d_zeel.clone(), tx_hash_algo: self.hash_algo.into(), + v_extra_payload: None, }; let mut stream = Stream::default(); @@ -466,7 +471,7 @@ impl TransactionInputSigner { sig_hash_stream.append(&blake_2b_256_personal( &prev_out_stream.out(), ZCASH_PREVOUTS_HASH_PERSONALIZATION, - )); + )?); } else { sig_hash_stream.append(&H256::default()); } @@ -480,7 +485,7 @@ impl TransactionInputSigner { sig_hash_stream.append(&blake_2b_256_personal( &sequence_stream.out(), ZCASH_SEQUENCE_HASH_PERSONALIZATION, - )); + )?); } else { sig_hash_stream.append(&H256::default()); } @@ -494,7 +499,7 @@ impl TransactionInputSigner { sig_hash_stream.append(&blake_2b_256_personal( &outputs_stream.out(), ZCASH_OUTPUTS_HASH_PERSONALIZATION, - )); + )?); } else if sighash.base == SighashBase::Single && input_index < self.outputs.len() { let mut outputs_stream = Stream::new(); outputs_stream.append(&self.outputs[input_index]); @@ -502,7 +507,7 @@ impl TransactionInputSigner { sig_hash_stream.append(&blake_2b_256_personal( &outputs_stream.out(), ZCASH_OUTPUTS_HASH_PERSONALIZATION, - )); + )?); } else { sig_hash_stream.append(&H256::default()); } @@ -515,7 +520,7 @@ impl TransactionInputSigner { sig_hash_stream.append(&blake_2b_256_personal( &join_splits_stream.out(), ZCASH_JOIN_SPLITS_HASH_PERSONALIZATION, - )); + )?); } else { sig_hash_stream.append(&H256::default()); } @@ -533,7 +538,7 @@ impl TransactionInputSigner { sig_hash_stream.append(&blake_2b_256_personal( &s_spends_stream.out(), ZCASH_SHIELDED_SPENDS_HASH_PERSONALIZATION, - )); + )?); } else { sig_hash_stream.append(&H256::default()); } @@ -544,7 +549,7 @@ impl TransactionInputSigner { s_outputs_stream.append(output); } let hash_shielded_outputs = - blake_2b_256_personal(&s_outputs_stream.out(), ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION); + blake_2b_256_personal(&s_outputs_stream.out(), ZCASH_SHIELDED_OUTPUTS_HASH_PERSONALIZATION)?; sig_hash_stream.append(&hash_shielded_outputs); } else { sig_hash_stream.append(&H256::default()); @@ -560,7 +565,7 @@ impl TransactionInputSigner { sig_hash_stream.append(&self.inputs[input_index].amount); sig_hash_stream.append(&self.inputs[input_index].sequence); - Ok(blake_2b_256_personal(&sig_hash_stream.out(), &personalization)) + blake_2b_256_personal(&sig_hash_stream.out(), &personalization) } } @@ -608,16 +613,17 @@ fn compute_hash_outputs(sighash: Sighash, input_index: usize, outputs: &[Transac } } -fn blake_2b_256_personal(input: &[u8], personal: &[u8]) -> H256 { - H256::from( - Blake2b::new() - .hash_length(32) - .personal(personal) - .to_state() - .update(input) - .finalize() - .as_bytes(), - ) +fn blake_2b_256_personal(input: &[u8], personal: &[u8]) -> Result { + let bytes: [u8; 32] = Blake2b::new() + .hash_length(32) + .personal(personal) + .to_state() + .update(input) + .finalize() + .as_bytes() + .try_into() + .map_err(|_| "Invalid length".to_string())?; + Ok(H256::from(bytes)) } #[cfg(test)] @@ -689,6 +695,7 @@ mod tests { posv: false, str_d_zeel: None, hash_algo: SignerHashAlgo::DSHA256, + v_extra_payload: None, }; let hash = input_signer.signature_hash(0, 0, &previous_output, SignatureVersion::Base, SighashBase::All.into()); @@ -741,6 +748,7 @@ mod tests { posv: true, str_d_zeel: None, hash_algo: SignerHashAlgo::DSHA256, + v_extra_payload: None, }; let hash = input_signer.signature_hash(0, 0, &previous_output, SignatureVersion::Base, SighashBase::All.into()); @@ -783,7 +791,7 @@ mod tests { #[test] fn test_blake_2b_personal() { - let hash = blake_2b_256_personal(b"", b"ZcashPrevoutHash"); + let hash = blake_2b_256_personal(b"", b"ZcashPrevoutHash").unwrap(); assert_eq!( H256::from("d53a633bbecf82fe9e9484d8a0e727c73bb9e68c96e72dec30144f6a84afa136"), hash diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index 365592bffe..b3756f9b94 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -6,19 +6,23 @@ edition = "2021" [lib] doctest = false +[features] +new-db-arch = [] + [dependencies] arrayref = "0.3" async-std = { version = "1.5", features = ["unstable"] } async-trait = "0.1" cfg-if = "1.0" common = { path = "../common" } +compatible-time = { version = "1.1.0", package = "web-time" } db_common = { path = "../db_common" } derive_more = "0.99" futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } gstuff = { version = "0.7", features = ["nightly"] } hex = "0.4.2" lazy_static = "1.4" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify"] } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_metrics = { path = "../mm2_metrics" } @@ -32,11 +36,11 @@ shared_ref_counter = { path = "../common/shared_ref_counter" } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -instant = { version = "0.1.12", features = ["wasm-bindgen"] } mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } +timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } wasm-bindgen-test = { version = "0.3.2" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rustls = { version = "0.21", default-features = false } -instant = "0.1.12" tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } +timed-map = { version = "1.3", features = ["rustc-hash"] } diff --git a/mm2src/mm2_core/src/data_asker.rs b/mm2src/mm2_core/src/data_asker.rs index 7f32f93365..2e9a125d56 100644 --- a/mm2src/mm2_core/src/data_asker.rs +++ b/mm2src/mm2_core/src/data_asker.rs @@ -1,9 +1,9 @@ -use common::expirable_map::ExpirableMap; +use common::custom_futures::timeout::FutureTimerExt; use common::{HttpStatusCode, StatusCode}; +use compatible_time::Duration; use derive_more::Display; use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; -use instant::Duration; use mm2_err_handle::prelude::*; use mm2_event_stream::Event; use ser_error_derive::SerializeErrorType; @@ -12,25 +12,38 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::sync::atomic::{self, AtomicUsize}; use std::sync::Arc; +use timed_map::{MapKind, TimedMap}; use crate::mm_ctx::{MmArc, MmCtx}; const EVENT_NAME: &str = "DATA_NEEDED"; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct DataAsker { data_id: Arc, - awaiting_asks: Arc>>>, + awaiting_asks: Arc>>>, +} + +impl Default for DataAsker { + fn default() -> Self { + Self { + data_id: Default::default(), + awaiting_asks: Arc::new(AsyncMutex::new( + TimedMap::new_with_map_kind(MapKind::FxHashMap).expiration_tick_cap(5), + )), + } + } } #[derive(Debug, Display)] pub enum AskForDataError { #[display( - fmt = "Expected JSON data, but given(from data provider) one was not deserializable: {:?}", + fmt = "Expected JSON data, but the received data (from data provider) was not deserializable: {:?}", _0 )] DeserializationError(serde_json::Error), Internal(String), + Timeout, } impl MmCtx { @@ -59,7 +72,7 @@ impl MmCtx { .awaiting_asks .lock() .await - .insert(data_id, sender, timeout); + .insert_expirable(data_id, sender, timeout); } let input = json!({ @@ -68,18 +81,18 @@ impl MmCtx { "data": data }); - self.stream_channel_controller - .broadcast(Event::new(format!("{EVENT_NAME}:{data_type}"), input.to_string())) - .await; + self.event_stream_manager + .broadcast_all(Event::new(format!("{EVENT_NAME}:{data_type}"), input)); - match receiver.await { - Ok(response) => match serde_json::from_value::(response) { + match receiver.timeout(timeout).await { + Ok(Ok(response)) => match serde_json::from_value::(response) { Ok(value) => Ok(value), Err(error) => MmError::err(AskForDataError::DeserializationError(error)), }, - Err(error) => MmError::err(AskForDataError::Internal(format!( - "Sender channel is not alive. {error}" + Ok(Err(error)) => MmError::err(AskForDataError::Internal(format!( + "Receiver channel is not alive. {error}" ))), + Err(_) => MmError::err(AskForDataError::Timeout), } } } @@ -129,7 +142,7 @@ mod tests { use crate::mm_ctx::MmCtxBuilder; use common::block_on; use common::executor::Timer; - use instant::Duration; + use compatible_time::Duration; use serde::Deserialize; use serde_json::json; use std::thread; diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 0a1afb2eea..292dc69ca6 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -1,16 +1,15 @@ #[cfg(feature = "track-ctx-pointer")] use common::executor::Timer; +use common::executor::{abortable_queue::{AbortableQueue, WeakSpawner}, + graceful_shutdown, AbortableSystem}; use common::log::{self, LogLevel, LogOnError, LogState}; use common::{cfg_native, cfg_wasm32, small_rng}; -use common::{executor::{abortable_queue::{AbortableQueue, WeakSpawner}, - graceful_shutdown, AbortSettings, AbortableSystem, SpawnAbortable, SpawnFuture}, - expirable_map::ExpirableMap}; use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; use gstuff::{try_s, ERR, ERRL}; use lazy_static::lazy_static; use libp2p::PeerId; -use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; +use mm2_event_stream::{EventStreamingConfiguration, StreamingManager}; use mm2_metrics::{MetricsArc, MetricsOps}; use primitives::hash::H160; use rand::Rng; @@ -20,9 +19,9 @@ use std::any::Any; use std::collections::hash_map::{Entry, HashMap}; use std::collections::HashSet; use std::fmt; -use std::future::Future; use std::ops::Deref; use std::sync::{Arc, Mutex, OnceLock}; +use timed_map::{MapKind, TimedMap}; use crate::data_asker::DataAsker; @@ -44,6 +43,8 @@ cfg_native! { /// Default interval to export and record metrics to log. const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; +/// File extension for files containing a wallet's encrypted mnemonic phrase. +pub const WALLET_FILE_EXTENSION: &str = "json"; /// MarketMaker state, shared between the various MarketMaker threads. /// @@ -77,14 +78,13 @@ pub struct MmCtx { /// If there are things that are loaded in background then they should be separately optional, /// without invalidating the entire state. pub initialized: OnceLock, - /// True if the RPC HTTP server was started. - pub rpc_started: OnceLock, - /// Controller for continuously streaming data using streaming channels of `mm2_event_stream`. - pub stream_channel_controller: Controller, + /// RPC port of the HTTP server if it was started. + #[cfg(not(target_arch = "wasm32"))] + pub rpc_port: OnceLock, /// Data transfer bridge between server and client where server (which is the mm2 runtime) initiates the request. pub(crate) data_asker: DataAsker, - /// Configuration of event streaming used for SSE. - pub event_stream_configuration: Option, + /// A manager for the event streaming system. To be used to start/stop/communicate with event streamers. + pub event_stream_manager: StreamingManager, /// True if the MarketMaker instance needs to stop. pub stop: OnceLock, /// Unique context identifier, allowing us to more easily pass the context through the FFI boundaries. @@ -128,6 +128,20 @@ pub struct MmCtx { /// Deprecated, please create `shared_async_sqlite_conn` for new implementations and call db `KOMODEFI-shared.db`. #[cfg(not(target_arch = "wasm32"))] pub shared_sqlite_conn: OnceLock>>, + /// The DB connection to the global DB hosting common data (e.g. stats) and other data needed for correctly bootstrapping on restarts. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub global_db_conn: OnceLock>>, + /// The DB connection to the wallet DB the KDF instance will use for current execution. + /// + /// The wallet DB path is based on the seed that KDF is initialized with. An initialization with different seed will use a different wallet DB. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub wallet_db_conn: OnceLock>>, + /// The DB connection to the wallet DB the KDF instance will use for current execution. + /// + /// This is the same DB as `self.wallet_db_conn` but made available via an asynchronous interface. + /// Use this if favor of `self.wallet_db_conn` for new implementations. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub async_wallet_db_conn: OnceLock>>, pub mm_version: String, pub datetime: String, pub mm_init_ctx: Mutex>>, @@ -146,7 +160,7 @@ pub struct MmCtx { #[cfg(not(target_arch = "wasm32"))] pub async_sqlite_connection: OnceLock>>, /// Links the RPC context to the P2P context to handle health check responses. - pub healthcheck_response_handler: AsyncMutex>>, + pub healthcheck_response_handler: AsyncMutex>>, } impl MmCtx { @@ -156,10 +170,10 @@ impl MmCtx { log: log::LogArc::new(log), metrics: MetricsArc::new(), initialized: OnceLock::default(), - rpc_started: OnceLock::default(), - stream_channel_controller: Controller::new(), + #[cfg(not(target_arch = "wasm32"))] + rpc_port: OnceLock::default(), data_asker: DataAsker::default(), - event_stream_configuration: None, + event_stream_manager: Default::default(), stop: OnceLock::default(), ffi_handle: OnceLock::default(), ordermatch_ctx: Mutex::new(None), @@ -186,6 +200,12 @@ impl MmCtx { sqlite_connection: OnceLock::default(), #[cfg(not(target_arch = "wasm32"))] shared_sqlite_conn: OnceLock::default(), + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + global_db_conn: OnceLock::default(), + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + wallet_db_conn: OnceLock::default(), + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + async_wallet_db_conn: OnceLock::default(), mm_version: "".into(), datetime: "".into(), mm_init_ctx: Mutex::new(None), @@ -196,7 +216,9 @@ impl MmCtx { nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] async_sqlite_connection: OnceLock::default(), - healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), + healthcheck_response_handler: AsyncMutex::new( + TimedMap::new_with_map_kind(MapKind::FxHashMap).expiration_tick_cap(3), + ), } } @@ -214,6 +236,8 @@ impl MmCtx { self.shared_db_id.get().unwrap_or(&*DEFAULT) } + pub fn is_seed_node(&self) -> bool { self.conf["i_am_seed"].as_bool().unwrap_or(false) } + #[cfg(not(target_arch = "wasm32"))] pub fn rpc_ip_port(&self) -> Result { let port = match self.conf.get("rpcport") { @@ -231,8 +255,9 @@ impl MmCtx { }, None => 7783, // Default port if `rpcport` does not exist in the config }; - if port < 1000 { - return ERR!("rpcport < 1000"); + // A 0 value indicates that the rpc interface should bind on any available port. + if port != 0 && port < 1024 { + return ERR!("rpcport < 1024"); } if port > u16::MAX as u64 { return ERR!("rpcport > u16"); @@ -296,10 +321,6 @@ impl MmCtx { /// Returns the path to the MM databases root. #[cfg(not(target_arch = "wasm32"))] pub fn db_root(&self) -> PathBuf { path_to_db_root(self.conf["dbdir"].as_str()) } - #[cfg(not(target_arch = "wasm32"))] - pub fn wallet_file_path(&self, wallet_name: &str) -> PathBuf { - self.db_root().join(wallet_name.to_string() + ".dat") - } /// MM database path. /// Defaults to a relative "DB". @@ -323,9 +344,69 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] pub fn shared_dbdir(&self) -> PathBuf { path_to_dbdir(self.conf["dbdir"].as_str(), self.shared_db_id()) } - pub fn is_watcher(&self) -> bool { self.conf["is_watcher"].as_bool().unwrap_or_default() } + /// Returns the path to the global common directory. + /// + /// Such directory isn't bound to a specific seed/wallet or address. + /// Data that should be stored there is public and shared between all seeds and addresses (e.g. stats, block headers, etc...). + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub fn global_dir(&self) -> PathBuf { self.db_root().join("global") } - pub fn use_watchers(&self) -> bool { self.conf["use_watchers"].as_bool().unwrap_or(true) } + /// Returns the path to wallet's data directory. + /// + /// This path depends on `self.rmd160()` of the wallet derived from the seed. + /// For HD wallets, this `rmd160` is derived from `mm2_internal_derivation_path`. + /// For Iguana, this `rmd160` is simply a hash of the seed. + /// Use this directory to store seed/wallet related data rather than address related data (e.g. HD wallet accounts, HD wallet tx history, etc...) + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub fn wallet_dir(&self) -> PathBuf { + self.db_root() + .join("wallets") + .join(hex::encode(self.rmd160().as_slice())) + } + + /// Returns the path to the provided address' data directory. + /// + /// Use this directory for data related to a specific address and only that specific address (e.g. swap data, order data, etc...). + /// This makes sure that when this address is activated using a different technique, this data is still accessible. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub fn address_dir(&self, address: &str) -> Result { + let path = self.db_root().join("addresses").join(address); + if !path.exists() { + std::fs::create_dir_all(&path).map_err(AddressDataError::CreateAddressDirFailure)?; + } + Ok(path) + } + + /// Returns a SQL connection to the global database. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub fn global_db(&self) -> MutexGuard { self.global_db_conn.get().unwrap().lock().unwrap() } + + /// Returns a SQL connection to the shared wallet database. + /// + /// For new implementations, use `self.async_wallet_db()` instead. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub fn wallet_db(&self) -> MutexGuard { self.wallet_db_conn.get().unwrap().lock().unwrap() } + + /// Returns an AsyncSQL connection to the shared wallet database. + /// + /// This replaces `self.wallet_db()` and should be used for new implementations. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub async fn async_wallet_db(&self) -> Arc> { + self.async_wallet_db_conn.get().unwrap().clone() + } + + /// Returns a SQL connection to the address database. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub fn address_db(&self, address: &str) -> Result { + let path = self.address_dir(address)?.join("MM2.db"); + log_sqlite_file_open_attempt(&path); + let connection = Connection::open(path).map_err(AddressDataError::SqliteConnectionFailure)?; + Ok(connection) + } + + pub fn is_watcher(&self) -> bool { self.conf["is_watcher"].as_bool().unwrap_or(false) } + + pub fn disable_watchers_globally(&self) -> bool { !self.conf["use_watchers"].as_bool().unwrap_or(true) } pub fn netid(&self) -> u16 { let netid = self.conf["netid"].as_u64().unwrap_or(0); @@ -342,8 +423,13 @@ impl MmCtx { /// Returns whether node is configured to use [Upgraded Trading Protocol](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) pub fn use_trading_proto_v2(&self) -> bool { self.conf["use_trading_proto_v2"].as_bool().unwrap_or_default() } - /// Returns the cloneable `MmFutSpawner`. - pub fn spawner(&self) -> MmFutSpawner { MmFutSpawner::new(&self.abortable_system) } + /// Returns the event streaming configuration in use. + pub fn event_streaming_configuration(&self) -> Option { + serde_json::from_value(self.conf["event_streaming_configuration"].clone()).ok() + } + + /// Returns the cloneable `WeakSpawner`. + pub fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } /// True if the MarketMaker instance needs to stop. pub fn is_stopping(&self) -> bool { *self.stop.get().unwrap_or(&false) } @@ -352,6 +438,26 @@ impl MmCtx { pub fn mm_version(&self) -> &str { &self.mm_version } + /// Initialize the global and wallet directories and databases which are constants over the lifetime of KDF. + #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + pub async fn init_global_and_wallet_db(&self) -> Result<(), String> { + let global_db = Connection::open(self.global_dir().join("global.db")).map_err(|e| e.to_string())?; + let wallet_db = Connection::open(self.wallet_dir().join("wallet.db")).map_err(|e| e.to_string())?; + let async_wallet_db = AsyncConnection::open(self.wallet_dir().join("wallet.db")) + .await + .map_err(|e| e.to_string())?; + self.global_db_conn + .set(Arc::new(Mutex::new(global_db))) + .map_err(|_| "Global DB already set".to_string())?; + self.wallet_db_conn + .set(Arc::new(Mutex::new(wallet_db))) + .map_err(|_| "Wallet DB already set".to_string())?; + self.async_wallet_db_conn + .set(Arc::new(AsyncMutex::new(async_wallet_db))) + .map_err(|_| "Async Wallet DB already set".to_string())?; + Ok(()) + } + #[cfg(not(target_arch = "wasm32"))] pub fn init_sqlite_connection(&self) -> Result<(), String> { let sqlite_file_path = self.dbdir().join("MM2.db"); @@ -427,6 +533,12 @@ impl Drop for MmCtx { } } +#[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] +pub enum AddressDataError { + CreateAddressDirFailure(std::io::Error), + SqliteConnectionFailure(db_common::sqlite::rusqlite::Error), +} + /// Returns the path to the MM database root. /// /// Path priority: @@ -667,44 +779,6 @@ impl MmArc { } } -/// The futures spawner pinned to the `MmCtx` context. -/// It's used to spawn futures that can be aborted immediately or after a timeout -/// on the [`MmArc::stop`] function call. -/// -/// # Note -/// -/// `MmFutSpawner` doesn't prevent the spawned futures from being aborted. -#[derive(Clone)] -pub struct MmFutSpawner { - inner: WeakSpawner, -} - -impl MmFutSpawner { - pub fn new(system: &AbortableQueue) -> MmFutSpawner { - MmFutSpawner { - inner: system.weak_spawner(), - } - } -} - -impl SpawnFuture for MmFutSpawner { - fn spawn(&self, f: F) - where - F: Future + Send + 'static, - { - self.inner.spawn(f) - } -} - -impl SpawnAbortable for MmFutSpawner { - fn spawn_with_settings(&self, fut: F, settings: AbortSettings) - where - F: Future + Send + 'static, - { - self.inner.spawn_with_settings(fut, settings) - } -} - /// Helps getting a crate context from a corresponding `MmCtx` field. /// /// * `ctx_field` - A dedicated crate context field in `MmCtx`, such as the `MmCtx::portfolio_ctx`. @@ -784,14 +858,6 @@ impl MmCtxBuilder { if let Some(conf) = self.conf { ctx.conf = conf; - - let event_stream_configuration = &ctx.conf["event_stream_configuration"]; - if !event_stream_configuration.is_null() { - let event_stream_configuration: EventStreamConfiguration = - json::from_value(event_stream_configuration.clone()) - .expect("Invalid json value in 'event_stream_configuration'."); - ctx.event_stream_configuration = Some(event_stream_configuration); - } } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/mm2_event_stream/Cargo.toml b/mm2src/mm2_event_stream/Cargo.toml index adf20e7ee2..5b1677fa0e 100644 --- a/mm2src/mm2_event_stream/Cargo.toml +++ b/mm2src/mm2_event_stream/Cargo.toml @@ -10,10 +10,11 @@ common = { path = "../common" } futures = { version = "0.3", default-features = false } parking_lot = "0.12" serde = { version = "1", features = ["derive", "rc"] } -tokio = { version = "1", features = ["sync"] } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +tokio = "1.20" [dev-dependencies] -tokio = { version = "1", features = ["sync", "macros", "time", "rt"] } +tokio = { version = "1.20", features = ["macros"] } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-test = { version = "0.3.2" } diff --git a/mm2src/mm2_event_stream/src/behaviour.rs b/mm2src/mm2_event_stream/src/behaviour.rs deleted file mode 100644 index ff2cfbefa9..0000000000 --- a/mm2src/mm2_event_stream/src/behaviour.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{ErrorEventName, EventName, EventStreamConfiguration}; -use async_trait::async_trait; -use futures::channel::oneshot; - -#[derive(Clone, Debug)] -pub enum EventInitStatus { - Inactive, - Success, - Failed(String), -} - -#[async_trait] -pub trait EventBehaviour { - /// Returns the unique name of the event as an EventName enum variant. - fn event_name() -> EventName; - - /// Returns the name of the error event as an ErrorEventName enum variant. - /// By default, it returns `ErrorEventName::GenericError,` which shows as "ERROR" in the event stream. - fn error_event_name() -> ErrorEventName { ErrorEventName::GenericError } - - /// Event handler that is responsible for broadcasting event data to the streaming channels. - async fn handle(self, interval: f64, tx: oneshot::Sender); - - /// Spawns the `Self::handle` in a separate thread if the event is active according to the mm2 configuration. - /// Does nothing if the event is not active. - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus; -} diff --git a/mm2src/mm2_event_stream/src/configuration.rs b/mm2src/mm2_event_stream/src/configuration.rs new file mode 100644 index 0000000000..590665d581 --- /dev/null +++ b/mm2src/mm2_event_stream/src/configuration.rs @@ -0,0 +1,19 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(default)] +/// The network-related configuration of the event streaming interface. +// TODO: This better fits in mm2_net but then we would have circular dependency error trying to import it in mm2_core. +pub struct EventStreamingConfiguration { + pub worker_path: String, + pub access_control_allow_origin: String, +} + +impl Default for EventStreamingConfiguration { + fn default() -> Self { + Self { + worker_path: "event_streaming_worker.js".to_string(), + access_control_allow_origin: "*".to_string(), + } + } +} diff --git a/mm2src/mm2_event_stream/src/controller.rs b/mm2src/mm2_event_stream/src/controller.rs deleted file mode 100644 index 72870308b4..0000000000 --- a/mm2src/mm2_event_stream/src/controller.rs +++ /dev/null @@ -1,199 +0,0 @@ -use parking_lot::Mutex; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::mpsc::{self, Receiver, Sender}; - -type ChannelId = u64; - -/// Root controller of streaming channels -pub struct Controller(Arc>>); - -impl Clone for Controller { - fn clone(&self) -> Self { Self(Arc::clone(&self.0)) } -} - -/// Inner part of the controller -pub struct ChannelsInner { - last_id: u64, - channels: HashMap>, -} - -struct Channel { - tx: Sender>, -} - -/// guard to trace channels disconnection -pub struct ChannelGuard { - channel_id: ChannelId, - controller: Controller, -} - -/// Receiver to cleanup resources on `Drop` -pub struct GuardedReceiver { - rx: Receiver>, - #[allow(dead_code)] - guard: ChannelGuard, -} - -impl Controller { - /// Creates a new channels controller - pub fn new() -> Self { Default::default() } - - /// Creates a new channel and returns it's events receiver - pub fn create_channel(&mut self, concurrency: usize) -> GuardedReceiver { - let (tx, rx) = mpsc::channel::>(concurrency); - let channel = Channel { tx }; - - let mut inner = self.0.lock(); - let channel_id = inner.last_id.overflowing_add(1).0; - inner.channels.insert(channel_id, channel); - inner.last_id = channel_id; - - let guard = ChannelGuard::new(channel_id, self.clone()); - GuardedReceiver { rx, guard } - } - - /// Returns number of active channels - pub fn num_connections(&self) -> usize { self.0.lock().channels.len() } - - /// Broadcast message to all channels - pub async fn broadcast(&self, message: M) { - let msg = Arc::new(message); - for rx in self.all_senders() { - rx.send(Arc::clone(&msg)).await.ok(); - } - } - - /// Removes the channel from the controller - fn remove_channel(&mut self, channel_id: &ChannelId) { - let mut inner = self.0.lock(); - inner.channels.remove(channel_id); - } - - /// Returns all the active channels - fn all_senders(&self) -> Vec>> { self.0.lock().channels.values().map(|c| c.tx.clone()).collect() } -} - -impl Default for Controller { - fn default() -> Self { - let inner = ChannelsInner { - last_id: 0, - channels: HashMap::new(), - }; - Self(Arc::new(Mutex::new(inner))) - } -} - -impl ChannelGuard { - fn new(channel_id: ChannelId, controller: Controller) -> Self { Self { channel_id, controller } } -} - -impl Drop for ChannelGuard { - fn drop(&mut self) { - common::log::debug!("Dropping event channel with id: {}", self.channel_id); - - self.controller.remove_channel(&self.channel_id); - } -} - -impl GuardedReceiver { - /// Receives the next event from the channel - pub async fn recv(&mut self) -> Option> { self.rx.recv().await } -} - -#[cfg(any(test, target_arch = "wasm32"))] -mod tests { - use super::*; - use common::cross_test; - - common::cfg_wasm32! { - use wasm_bindgen_test::*; - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - } - - cross_test!(test_create_channel_and_broadcast, { - let mut controller = Controller::new(); - let mut guard_receiver = controller.create_channel(1); - - controller.broadcast("Message".to_string()).await; - - let received_msg = guard_receiver.recv().await.unwrap(); - assert_eq!(*received_msg, "Message".to_string()); - }); - - cross_test!(test_multiple_channels_and_broadcast, { - let mut controller = Controller::new(); - - let mut receivers = Vec::new(); - for _ in 0..3 { - receivers.push(controller.create_channel(1)); - } - - controller.broadcast("Message".to_string()).await; - - for receiver in &mut receivers { - let received_msg = receiver.recv().await.unwrap(); - assert_eq!(*received_msg, "Message".to_string()); - } - }); - - cross_test!(test_channel_cleanup_on_drop, { - let mut controller: Controller<()> = Controller::new(); - let guard_receiver = controller.create_channel(1); - - assert_eq!(controller.num_connections(), 1); - - drop(guard_receiver); - - common::executor::Timer::sleep(0.1).await; // Give time for the drop to execute - - assert_eq!(controller.num_connections(), 0); - }); - - cross_test!(test_broadcast_across_channels, { - let mut controller = Controller::new(); - - let mut receivers = Vec::new(); - for _ in 0..3 { - receivers.push(controller.create_channel(1)); - } - - controller.broadcast("Message".to_string()).await; - - for receiver in &mut receivers { - let received_msg = receiver.recv().await.unwrap(); - assert_eq!(*received_msg, "Message".to_string()); - } - }); - - cross_test!(test_multiple_messages_and_drop, { - let mut controller = Controller::new(); - let mut guard_receiver = controller.create_channel(6); - - controller.broadcast("Message 1".to_string()).await; - controller.broadcast("Message 2".to_string()).await; - controller.broadcast("Message 3".to_string()).await; - controller.broadcast("Message 4".to_string()).await; - controller.broadcast("Message 5".to_string()).await; - controller.broadcast("Message 6".to_string()).await; - - let mut received_msgs = Vec::new(); - for _ in 0..6 { - let received_msg = guard_receiver.recv().await.unwrap(); - received_msgs.push(received_msg); - } - - assert_eq!(*received_msgs[0], "Message 1".to_string()); - assert_eq!(*received_msgs[1], "Message 2".to_string()); - assert_eq!(*received_msgs[2], "Message 3".to_string()); - assert_eq!(*received_msgs[3], "Message 4".to_string()); - assert_eq!(*received_msgs[4], "Message 5".to_string()); - assert_eq!(*received_msgs[5], "Message 6".to_string()); - - // Consume the GuardedReceiver to trigger drop and channel cleanup - drop(guard_receiver); - - common::executor::Timer::sleep(0.1).await; // Give time for the drop to execute - - assert_eq!(controller.num_connections(), 0); - }); -} diff --git a/mm2src/mm2_event_stream/src/event.rs b/mm2src/mm2_event_stream/src/event.rs new file mode 100644 index 0000000000..306bbc9e49 --- /dev/null +++ b/mm2src/mm2_event_stream/src/event.rs @@ -0,0 +1,47 @@ +use serde_json::Value as Json; + +// Note `Event` shouldn't be `Clone`able, but rather Arc/Rc wrapped and then shared. +// This is only for testing. +/// Multi-purpose/generic event type that can easily be used over the event streaming +#[cfg_attr(any(test, target_arch = "wasm32"), derive(Clone, Debug, PartialEq))] +#[derive(Default)] +pub struct Event { + /// The type of the event (balance, network, swap, etc...). + event_type: String, + /// The message to be sent to the client. + message: Json, + /// Indicating whether this event is an error event or a normal one. + error: bool, +} + +impl Event { + /// Creates a new `Event` instance with the specified event type and message. + #[inline(always)] + pub fn new(streamer_id: String, message: Json) -> Self { + Self { + event_type: streamer_id, + message, + error: false, + } + } + + /// Create a new error `Event` instance with the specified error event type and message. + #[inline(always)] + pub fn err(streamer_id: String, message: Json) -> Self { + Self { + event_type: streamer_id, + message, + error: true, + } + } + + /// Returns the `event_type` (the ID of the streamer firing this event). + #[inline(always)] + pub fn origin(&self) -> &str { &self.event_type } + + /// Returns the event type and message as a pair. + pub fn get(&self) -> (String, &Json) { + let prefix = if self.error { "ERROR:" } else { "" }; + (format!("{prefix}{}", self.event_type), &self.message) + } +} diff --git a/mm2src/mm2_event_stream/src/lib.rs b/mm2src/mm2_event_stream/src/lib.rs index 1dc15bcd53..db4587a77a 100644 --- a/mm2src/mm2_event_stream/src/lib.rs +++ b/mm2src/mm2_event_stream/src/lib.rs @@ -1,125 +1,10 @@ -use serde::Deserialize; -use std::collections::HashMap; -use std::fmt; -#[cfg(target_arch = "wasm32")] use std::path::PathBuf; - -#[cfg(target_arch = "wasm32")] -const DEFAULT_WORKER_PATH: &str = "event_streaming_worker.js"; - -/// Multi-purpose/generic event type that can easily be used over the event streaming -pub struct Event { - _type: String, - message: String, -} - -impl Event { - /// Creates a new `Event` instance with the specified event type and message. - #[inline] - pub fn new(event_type: String, message: String) -> Self { - Self { - _type: event_type, - message, - } - } - - /// Gets the event type. - #[inline] - pub fn event_type(&self) -> &str { &self._type } - - /// Gets the event message. - #[inline] - pub fn message(&self) -> &str { &self.message } -} - -/// Event types streamed to clients through channels like Server-Sent Events (SSE). -#[derive(Deserialize, Eq, Hash, PartialEq)] -pub enum EventName { - /// Indicates a change in the balance of a coin. - #[serde(rename = "COIN_BALANCE")] - CoinBalance, - /// Event triggered at regular intervals to indicate that the system is operational. - HEARTBEAT, - /// Returns p2p network information at a regular interval. - NETWORK, -} - -impl fmt::Display for EventName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::CoinBalance => write!(f, "COIN_BALANCE"), - Self::HEARTBEAT => write!(f, "HEARTBEAT"), - Self::NETWORK => write!(f, "NETWORK"), - } - } -} - -/// Error event types used to indicate various kinds of errors to clients through channels like Server-Sent Events (SSE). -pub enum ErrorEventName { - /// A generic error that doesn't fit any other specific categories. - GenericError, - /// Signifies an error related to fetching or calculating the balance of a coin. - CoinBalanceError, -} - -impl fmt::Display for ErrorEventName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::GenericError => write!(f, "ERROR"), - Self::CoinBalanceError => write!(f, "COIN_BALANCE_ERROR"), - } - } -} - -/// Configuration for event streaming -#[derive(Deserialize)] -pub struct EventStreamConfiguration { - /// The value to set for the `Access-Control-Allow-Origin` header. - #[serde(default)] - pub access_control_allow_origin: String, - #[serde(default)] - active_events: HashMap, - /// The path to the worker script for event streaming. - #[cfg(target_arch = "wasm32")] - #[serde(default = "default_worker_path")] - pub worker_path: PathBuf, -} - -#[cfg(target_arch = "wasm32")] -#[inline] -fn default_worker_path() -> PathBuf { PathBuf::from(DEFAULT_WORKER_PATH) } - -/// Represents the configuration for a specific event within the event stream. -#[derive(Clone, Default, Deserialize)] -pub struct EventConfig { - /// The interval in seconds at which the event should be streamed. - #[serde(default = "default_stream_interval")] - pub stream_interval_seconds: f64, -} - -const fn default_stream_interval() -> f64 { 5. } - -impl Default for EventStreamConfiguration { - fn default() -> Self { - Self { - access_control_allow_origin: String::from("*"), - active_events: Default::default(), - #[cfg(target_arch = "wasm32")] - worker_path: default_worker_path(), - } - } -} - -impl EventStreamConfiguration { - /// Retrieves the configuration for a specific event by its name. - #[inline] - pub fn get_event(&self, event_name: &EventName) -> Option { - self.active_events.get(event_name).cloned() - } - - /// Gets the total number of active events in the configuration. - #[inline] - pub fn total_active_events(&self) -> usize { self.active_events.len() } -} - -pub mod behaviour; -pub mod controller; +pub mod configuration; +pub mod event; +pub mod manager; +pub mod streamer; + +// Re-export important types. +pub use configuration::EventStreamingConfiguration; +pub use event::Event; +pub use manager::{StreamingManager, StreamingManagerError}; +pub use streamer::{Broadcaster, EventStreamer, NoDataIn, StreamHandlerInput}; diff --git a/mm2src/mm2_event_stream/src/manager.rs b/mm2src/mm2_event_stream/src/manager.rs new file mode 100644 index 0000000000..b480ddd070 --- /dev/null +++ b/mm2src/mm2_event_stream/src/manager.rs @@ -0,0 +1,553 @@ +use std::any::Any; +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +use crate::streamer::spawn; +use crate::{Event, EventStreamer}; +use common::executor::abortable_queue::WeakSpawner; +use common::log::{error, LogOnError}; + +use common::on_drop_callback::OnDropCallback; +use futures::channel::mpsc::UnboundedSender; +use futures::channel::oneshot; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tokio::sync::mpsc; + +/// The errors that could originate from the streaming manager. +#[derive(Debug)] +pub enum StreamingManagerError { + /// There is no streamer with the given ID. + StreamerNotFound, + /// Couldn't send the data to the streamer. + SendError(String), + /// The streamer doesn't accept an input. + NoDataIn, + /// Couldn't spawn the streamer. + SpawnError(String), + /// The client is not known/registered. + UnknownClient, + /// A client with the same ID already exists. + ClientExists, + /// The client is already listening to the streamer. + ClientAlreadyListening, +} + +#[derive(Debug)] +struct StreamerInfo { + /// The communication channel to the streamer. + data_in: Option>>, + /// Clients the streamer is serving for. + clients: HashSet, + /// The shutdown handle of the streamer. + shutdown: oneshot::Sender<()>, +} + +impl StreamerInfo { + fn new(data_in: Option>>, shutdown: oneshot::Sender<()>) -> Self { + Self { + data_in, + clients: HashSet::new(), + shutdown, + } + } + + fn add_client(&mut self, client_id: u64) { self.clients.insert(client_id); } + + fn remove_client(&mut self, client_id: &u64) { self.clients.remove(client_id); } + + fn is_down(&self) -> bool { self.shutdown.is_canceled() } +} + +#[derive(Debug)] +struct ClientInfo { + /// The streamers the client is listening to. + listening_to: HashSet, + /// The communication/stream-out channel to the client. + // NOTE: Here we are using `tokio`'s `mpsc` because the one in `futures` have some extra feature + // (ref: https://users.rust-lang.org/t/why-does-try-send-from-crate-futures-require-mut-self/100389). + // This feature is aimed towards the multi-producer case (which we don't use) and requires a mutable + // reference on `try_send` calls. This will require us to put the channel in a mutex and degrade the + // broadcasting performance. + channel: mpsc::Sender>, +} + +impl ClientInfo { + fn new(channel: mpsc::Sender>) -> Self { + Self { + listening_to: HashSet::new(), + channel, + } + } + + fn add_streamer(&mut self, streamer_id: String) { self.listening_to.insert(streamer_id); } + + fn remove_streamer(&mut self, streamer_id: &str) { self.listening_to.remove(streamer_id); } + + fn listens_to(&self, streamer_id: &str) -> bool { self.listening_to.contains(streamer_id) } + + fn send_event(&self, event: Arc) { + // Only `try_send` here. If the channel is full (client is slow), the message + // will be dropped and the client won't receive it. + // This avoids blocking the broadcast to other receivers. + self.channel.try_send(event).error_log(); + } +} + +#[derive(Default, Debug)] +struct StreamingManagerInner { + /// A map from streamer IDs to their communication channels (if present) and shutdown handles. + streamers: HashMap, + /// An inverse map from client IDs to the streamers they are listening to and the communication channel with the client. + clients: HashMap, +} + +#[derive(Clone, Default, Debug)] +pub struct StreamingManager(Arc>); + +impl StreamingManager { + /// Returns a read guard over the streaming manager. + fn read(&self) -> RwLockReadGuard { self.0.read() } + + /// Returns a write guard over the streaming manager. + fn write(&self) -> RwLockWriteGuard { self.0.write() } + + /// Spawns and adds a new streamer `streamer` to the manager. + pub async fn add( + &self, + client_id: u64, + streamer: impl EventStreamer, + spawner: WeakSpawner, + ) -> Result { + let streamer_id = streamer.streamer_id(); + // Remove the streamer if it died for some reason. + self.remove_streamer_if_down(&streamer_id); + + // Pre-checks before spawning the streamer. Inside another scope to drop the lock early. + { + let mut this = self.write(); + match this.clients.get(&client_id) { + // We don't know that client. We don't have a connection to it. + None => return Err(StreamingManagerError::UnknownClient), + // The client is already listening to that streamer. + Some(client_info) if client_info.listens_to(&streamer_id) => { + return Err(StreamingManagerError::ClientAlreadyListening); + }, + _ => (), + } + + // If a streamer is already up and running, we won't spawn another one. + if let Some(streamer_info) = this.streamers.get_mut(&streamer_id) { + // Register the client as a listener to the streamer. + streamer_info.add_client(client_id); + // Register the streamer as listened-to by the client. + if let Some(client_info) = this.clients.get_mut(&client_id) { + client_info.add_streamer(streamer_id.clone()); + } + return Ok(streamer_id); + } + } + + // Spawn a new streamer. + let (shutdown, data_in) = spawn(streamer, spawner, self.clone()) + .await + .map_err(StreamingManagerError::SpawnError)?; + let streamer_info = StreamerInfo::new(data_in, shutdown); + + // Note that we didn't hold the lock while spawning the streamer (potentially a long operation). + // This means we can't assume either that the client still exists at this point or + // that the streamer still doesn't exist. + let mut this = self.write(); + if let Some(client_info) = this.clients.get_mut(&client_id) { + client_info.add_streamer(streamer_id.clone()); + this.streamers + .entry(streamer_id.clone()) + .or_insert(streamer_info) + .add_client(client_id); + } else { + // The client was removed while we were spawning the streamer. + // We no longer have a connection for it. + return Err(StreamingManagerError::UnknownClient); + } + Ok(streamer_id) + } + + /// Sends data to a streamer with `streamer_id`. + pub fn send(&self, streamer_id: &str, data: T) -> Result<(), StreamingManagerError> { + let this = self.read(); + let streamer_info = this + .streamers + .get(streamer_id) + .ok_or(StreamingManagerError::StreamerNotFound)?; + let data_in = streamer_info.data_in.as_ref().ok_or(StreamingManagerError::NoDataIn)?; + data_in + .unbounded_send(Box::new(data)) + .map_err(|e| StreamingManagerError::SendError(e.to_string())) + } + + /// Same as `StreamingManager::send`, but computes that data to send to a streamer using a closure, + /// thus avoiding computations & cloning if the intended streamer isn't running (more like the + /// laziness of `*_or_else()` functions). + /// + /// `data_fn` will only be evaluated if the streamer is found and accepts an input. + pub fn send_fn( + &self, + streamer_id: &str, + data_fn: impl FnOnce() -> T, + ) -> Result<(), StreamingManagerError> { + let this = self.read(); + let streamer_info = this + .streamers + .get(streamer_id) + .ok_or(StreamingManagerError::StreamerNotFound)?; + let data_in = streamer_info.data_in.as_ref().ok_or(StreamingManagerError::NoDataIn)?; + data_in + .unbounded_send(Box::new(data_fn())) + .map_err(|e| StreamingManagerError::SendError(e.to_string())) + } + + /// Stops streaming from the streamer with `streamer_id` to the client with `client_id`. + pub fn stop(&self, client_id: u64, streamer_id: &str) -> Result<(), StreamingManagerError> { + let mut this = self.write(); + let client_info = this + .clients + .get_mut(&client_id) + .ok_or(StreamingManagerError::UnknownClient)?; + client_info.remove_streamer(streamer_id); + + this.streamers + .get_mut(streamer_id) + .ok_or(StreamingManagerError::StreamerNotFound)? + .remove_client(&client_id); + + // If there are no more listening clients, terminate the streamer. + if this.streamers.get(streamer_id).map(|info| info.clients.len()) == Some(0) { + this.streamers.remove(streamer_id); + } + Ok(()) + } + + /// Broadcasts some event to clients listening to it. + /// + /// In contrast to `StreamingManager::send`, which sends some data to a streamer, + /// this method broadcasts an event to the listening *clients* directly, independently + /// of any streamer (i.e. bypassing any streamer). + pub fn broadcast(&self, event: Event) { + let event = Arc::new(event); + let this = self.read(); + if let Some(client_ids) = this.streamers.get(event.origin()).map(|info| &info.clients) { + client_ids.iter().for_each(|client_id| { + if let Some(info) = this.clients.get(client_id) { + info.send_event(event.clone()); + } + }); + }; + } + + /// Broadcasts (actually just *sends* in this case) some event to a specific client. + /// + /// Could be used in case we have a single known client and don't want to spawn up a streamer just for that. + pub fn broadcast_to(&self, event: Event, client_id: u64) -> Result<(), StreamingManagerError> { + let event = Arc::new(event); + self.read() + .clients + .get(&client_id) + .map(|info| info.send_event(event)) + .ok_or(StreamingManagerError::UnknownClient) + } + + /// Forcefully broadcasts an event to all known clients even if they are not listening for such an event. + pub fn broadcast_all(&self, event: Event) { + let event = Arc::new(event); + self.read().clients.values().for_each(|info| { + info.send_event(event.clone()); + }); + } + + /// Creates a new client and returns the event receiver for this client. + pub fn new_client(&self, client_id: u64) -> Result { + let mut this = self.write(); + if this.clients.contains_key(&client_id) { + return Err(StreamingManagerError::ClientExists); + } + // Note that events queued in the channel are `Arc<` shared. + // So a 1024 long buffer isn't actually heavy on memory. + let (tx, rx) = mpsc::channel(1024); + let client_info = ClientInfo::new(tx); + this.clients.insert(client_id, client_info); + let manager = self.clone(); + Ok(ClientHandle { + rx, + _on_drop_callback: OnDropCallback::new(move || { + manager.remove_client(client_id).ok(); + }), + }) + } + + /// Removes a client from the manager. + pub fn remove_client(&self, client_id: u64) -> Result<(), StreamingManagerError> { + let mut this = self.write(); + // Remove the client from our known-clients map. + let client_info = this + .clients + .remove(&client_id) + .ok_or(StreamingManagerError::UnknownClient)?; + // Remove the client from all the streamers it was listening to. + for streamer_id in client_info.listening_to { + if let Some(streamer_info) = this.streamers.get_mut(&streamer_id) { + streamer_info.remove_client(&client_id); + } else { + error!("Client {client_id} was listening to a non-existent streamer {streamer_id}. This is a bug!"); + } + // If there are no more listening clients, terminate the streamer. + if this.streamers.get(&streamer_id).map(|info| info.clients.len()) == Some(0) { + this.streamers.remove(&streamer_id); + } + } + Ok(()) + } + + /// Removes a streamer if it is no longer running. + /// + /// Aside from us shutting down a streamer when all its clients are disconnected, + /// the streamer might die by itself (e.g. the spawner it was spawned with aborted). + /// In this case, we need to remove the streamer and de-list it from all clients. + fn remove_streamer_if_down(&self, streamer_id: &str) { + let mut this = self.write(); + let Some(streamer_info) = this.streamers.get(streamer_id) else { + return; + }; + if !streamer_info.is_down() { + return; + } + // Remove the streamer from our registry. + let Some(streamer_info) = this.streamers.remove(streamer_id) else { + return; + }; + // And remove the streamer from all clients listening to it. + for client_id in streamer_info.clients { + if let Some(info) = this.clients.get_mut(&client_id) { + info.remove_streamer(streamer_id); + } + } + } +} + +/// A handle that is returned on [`StreamingManager::new_client`] calls that will auto remove +/// the client when dropped. +/// So this handle must live as long as the client is connected. +pub struct ClientHandle { + rx: mpsc::Receiver>, + _on_drop_callback: OnDropCallback, +} + +/// Deref the handle to the receiver inside for ease of use. +impl Deref for ClientHandle { + type Target = mpsc::Receiver>; + fn deref(&self) -> &Self::Target { &self.rx } +} + +/// Also DerefMut since the receiver inside is mutated when consumed. +impl DerefMut for ClientHandle { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.rx } +} + +#[cfg(any(test, target_arch = "wasm32"))] +mod tests { + use super::*; + use crate::streamer::test_utils::{InitErrorStreamer, PeriodicStreamer, ReactiveStreamer}; + + use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, Timer}; + use common::{cfg_wasm32, cross_test}; + use serde_json::json; + cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + cross_test!(test_add_remove_client, { + let manager = StreamingManager::default(); + let client_id1 = 1; + let client_id2 = 2; + let client_id3 = 3; + + let c1_handle = manager.new_client(client_id1); + assert!(matches!(c1_handle, Ok(..))); + // Adding the same client again should fail. + assert!(matches!( + manager.new_client(client_id1), + Err(StreamingManagerError::ClientExists) + )); + // Adding a different new client should be OK. + let c2_handle = manager.new_client(client_id2); + assert!(matches!(c2_handle, Ok(..))); + + assert!(matches!(manager.remove_client(client_id1), Ok(()))); + // Removing a removed client should fail. + assert!(matches!( + manager.remove_client(client_id1), + Err(StreamingManagerError::UnknownClient) + )); + // Same as removing a non-existent client. + assert!(matches!( + manager.remove_client(client_id3), + Err(StreamingManagerError::UnknownClient) + )); + }); + + cross_test!(test_broadcast_all, { + // Create a manager and add register two clients with it. + let manager = StreamingManager::default(); + let mut client1 = manager.new_client(1).unwrap(); + let mut client2 = manager.new_client(2).unwrap(); + let event = Event::new("test".to_string(), json!("test")); + + // Broadcast the event to all clients. + manager.broadcast_all(event.clone()); + + // The clients should receive the events. + assert_eq!(*client1.try_recv().unwrap(), event); + assert_eq!(*client2.try_recv().unwrap(), event); + + // Remove the clients. + manager.remove_client(1).unwrap(); + manager.remove_client(2).unwrap(); + + // `recv` shouldn't work at this point since the client is removed. + assert!(client1.try_recv().is_err()); + assert!(client2.try_recv().is_err()); + }); + + // https://github.com/KomodoPlatform/komodo-defi-framework/issues/1712#issuecomment-2669924113 + cross_test!( + test_periodic_streamer, + { + let manager = StreamingManager::default(); + let system = AbortableQueue::default(); + let (client_id1, client_id2) = (1, 2); + // Register a new client with the manager. + let mut client1 = manager.new_client(client_id1).unwrap(); + // Another client whom we won't have it subscribe to the streamer. + let mut client2 = manager.new_client(client_id2).unwrap(); + // Subscribe the new client to PeriodicStreamer. + let streamer_id = manager + .add(client_id1, PeriodicStreamer, system.weak_spawner()) + .await + .unwrap(); + + // We should be hooked now. try to receive some events from the streamer. + for _ in 0..3 { + // The streamer should send an event every 0.1s. Wait for 0.15s for safety. + Timer::sleep(0.15).await; + let event = client1.try_recv().unwrap(); + assert_eq!(event.origin(), streamer_id); + } + + // The other client shouldn't have received any events. + assert!(client2.try_recv().is_err()); + }, + target_os = "linux", + target_os = "windows" + ); + + cross_test!(test_reactive_streamer, { + let manager = StreamingManager::default(); + let system = AbortableQueue::default(); + let (client_id1, client_id2) = (1, 2); + // Register a new client with the manager. + let mut client1 = manager.new_client(client_id1).unwrap(); + // Another client whom we won't have it subscribe to the streamer. + let mut client2 = manager.new_client(client_id2).unwrap(); + // Subscribe the new client to ReactiveStreamer. + let streamer_id = manager + .add(client_id1, ReactiveStreamer, system.weak_spawner()) + .await + .unwrap(); + + // We should be hooked now. try to receive some events from the streamer. + for i in 1..=3 { + let msg = format!("send{}", i); + manager.send(&streamer_id, msg.clone()).unwrap(); + // Wait for a little bit to make sure the streamer received the data we sent. + Timer::sleep(0.1).await; + // The streamer should broadcast some event to the subscribed clients. + let event = client1.try_recv().unwrap(); + assert_eq!(event.origin(), streamer_id); + // It's an echo streamer, so the message should be the same. + assert_eq!(event.get().1, &json!(msg)); + } + + // If we send the wrong datatype (void here instead of String), the streamer should ignore it. + manager.send(&streamer_id, ()).unwrap(); + Timer::sleep(0.1).await; + assert!(client1.try_recv().is_err()); + + // The other client shouldn't have received any events. + assert!(client2.try_recv().is_err()); + }); + + cross_test!(test_erroring_streamer, { + let manager = StreamingManager::default(); + let system = AbortableQueue::default(); + let client_id = 1; + // Register a new client with the manager. + let _client = manager.new_client(client_id).unwrap(); + // Subscribe the new client to InitErrorStreamer. + let error = manager + .add(client_id, InitErrorStreamer, system.weak_spawner()) + .await + .unwrap_err(); + + assert!(matches!(error, StreamingManagerError::SpawnError(..))); + }); + + cross_test!(test_remove_streamer_if_down, { + let manager = StreamingManager::default(); + let system = AbortableQueue::default(); + let client_id = 1; + // Register a new client with the manager. + let _client = manager.new_client(client_id).unwrap(); + // Subscribe the new client to PeriodicStreamer. + let streamer_id = manager + .add(client_id, PeriodicStreamer, system.weak_spawner()) + .await + .unwrap(); + + // The streamer is up and streaming to `client_id`. + assert!(manager + .0 + .read() + .streamers + .get(&streamer_id) + .unwrap() + .clients + .contains(&client_id)); + + // The client should be registered and listening to `streamer_id`. + assert!(manager + .0 + .read() + .clients + .get(&client_id) + .unwrap() + .listens_to(&streamer_id)); + + // Abort the system to kill the streamer. + system.abort_all().unwrap(); + // Wait a little bit since the abortion doesn't take effect immediately (the aborted task needs to yield first). + Timer::sleep(0.1).await; + + manager.remove_streamer_if_down(&streamer_id); + + // The streamer should be removed. + assert!(manager.read().streamers.get(&streamer_id).is_none()); + // And the client is no more listening to it. + assert!(!manager + .0 + .read() + .clients + .get(&client_id) + .unwrap() + .listens_to(&streamer_id)); + }); +} diff --git a/mm2src/mm2_event_stream/src/streamer.rs b/mm2src/mm2_event_stream/src/streamer.rs new file mode 100644 index 0000000000..6c319cb89c --- /dev/null +++ b/mm2src/mm2_event_stream/src/streamer.rs @@ -0,0 +1,233 @@ +use std::any::{self, Any}; + +use crate::{Event, StreamingManager}; +use common::executor::{abortable_queue::WeakSpawner, AbortSettings, SpawnAbortable}; +use common::log::{error, info}; + +use async_trait::async_trait; +use futures::channel::{mpsc, oneshot}; +use futures::{future, select, FutureExt, Stream, StreamExt}; + +/// A marker to indicate that the event streamer doesn't take any input data. +pub struct NoDataIn; + +/// A mixture trait combining `Stream`, `Send` & `Unpin` together (to avoid confusing annotation). +pub trait StreamHandlerInput: Stream + Send + Unpin {} +/// Implement the trait for all types `T` that implement `Stream + Send + Unpin` for any `D`. +impl StreamHandlerInput for T where T: Stream + Send + Unpin {} + +#[async_trait] +pub trait EventStreamer +where + Self: Sized + Send + 'static, +{ + type DataInType: Send; + + /// Returns a human readable unique identifier for the event streamer. + /// No other event streamer should have the same identifier. + fn streamer_id(&self) -> String; + + /// Event handler that is responsible for broadcasting event data to the streaming channels. + /// + /// `ready_tx` is a oneshot sender that is used to send the initialization status of the event. + /// `data_rx` is a receiver that the streamer *could* use to receive data from the outside world. + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + data_rx: impl StreamHandlerInput, + ); +} + +/// Spawns the [`EventStreamer::handle`] in a separate task using [`WeakSpawner`]. +/// +/// Returns a [`oneshot::Sender`] to shutdown the handler and an optional [`mpsc::UnboundedSender`] +/// to send data to the handler. +pub(crate) async fn spawn( + streamer: S, + spawner: WeakSpawner, + streaming_manager: StreamingManager, +) -> Result<(oneshot::Sender<()>, Option>>), String> +where + S: EventStreamer, +{ + let streamer_id = streamer.streamer_id(); + info!("Spawning event streamer: {streamer_id}"); + + // A oneshot channel to receive the initialization status of the handler through. + let (tx_ready, ready_rx) = oneshot::channel(); + // A oneshot channel to shutdown the handler. + let (tx_shutdown, rx_shutdown) = oneshot::channel::<()>(); + // An unbounded channel to send data to the handler. + let (any_data_sender, any_data_receiver) = mpsc::unbounded::>(); + // A middleware to cast the data of type `Box` to the actual input datatype of this streamer. + let data_receiver = any_data_receiver.filter_map({ + let streamer_id = streamer_id.clone(); + move |any_input_data| { + let streamer_id = streamer_id.clone(); + future::ready( + any_input_data + .downcast() + .map(|input_data| *input_data) + .map_err(|_| { + error!("Couldn't downcast a received message to {}. This message wasn't intended to be sent to this streamer ({streamer_id}).", any::type_name::()); + }) + .ok(), + ) + } + }); + + let handler_with_shutdown = { + let streamer_id = streamer_id.clone(); + async move { + select! { + _ = rx_shutdown.fuse() => { + info!("Manually shutting down event streamer: {streamer_id}.") + } + _ = streamer.handle(Broadcaster::new(streaming_manager), tx_ready, data_receiver).fuse() => {} + } + } + }; + let settings = AbortSettings::info_on_abort(format!("{streamer_id} streamer has stopped.")); + spawner.spawn_with_settings(handler_with_shutdown, settings); + + ready_rx.await.unwrap_or_else(|e| { + Err(format!( + "The handler was aborted before sending event initialization status: {e}" + )) + })?; + + // If the handler takes no input data, return `None` for the data sender. + if any::TypeId::of::() == any::TypeId::of::() { + Ok((tx_shutdown, None)) + } else { + Ok((tx_shutdown, Some(any_data_sender))) + } +} + +/// A wrapper around `StreamingManager` to only expose the `broadcast` method. +pub struct Broadcaster(StreamingManager); + +impl Broadcaster { + pub fn new(inner: StreamingManager) -> Self { Self(inner) } + + pub fn broadcast(&self, event: Event) { self.0.broadcast(event); } +} + +#[cfg(any(test, target_arch = "wasm32"))] +pub mod test_utils { + use super::*; + + use common::executor::Timer; + use serde_json::json; + + /// A test event streamer that broadcasts an event periodically. + /// Broadcasts `json!("hello")` every tenth of a second. + pub struct PeriodicStreamer; + + #[async_trait] + impl EventStreamer for PeriodicStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { "periodic_streamer".to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + ready_tx.send(Ok(())).unwrap(); + loop { + broadcaster.broadcast(Event::new(self.streamer_id(), json!("hello"))); + Timer::sleep(0.1).await; + } + } + } + + /// A test event streamer that broadcasts an event whenever it receives a new message through `data_rx`. + pub struct ReactiveStreamer; + + #[async_trait] + impl EventStreamer for ReactiveStreamer { + type DataInType = String; + + fn streamer_id(&self) -> String { "reactive_streamer".to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx.send(Ok(())).unwrap(); + while let Some(msg) = data_rx.next().await { + // Just echo back whatever we receive. + broadcaster.broadcast(Event::new(self.streamer_id(), json!(msg))); + } + } + } + + /// A test event streamer that fails upon initialization. + pub struct InitErrorStreamer; + + #[async_trait] + impl EventStreamer for InitErrorStreamer { + type DataInType = NoDataIn; + + fn streamer_id(&self) -> String { "init_error_streamer".to_string() } + + async fn handle( + self, + _: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + // Fail the initialization and stop. + ready_tx.send(Err("error".to_string())).unwrap(); + } + } +} + +#[cfg(any(test, target_arch = "wasm32"))] +mod tests { + use super::test_utils::{InitErrorStreamer, PeriodicStreamer, ReactiveStreamer}; + use super::*; + + use common::executor::abortable_queue::AbortableQueue; + use common::{cfg_wasm32, cross_test}; + cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + cross_test!(test_spawn_periodic_streamer, { + let system = AbortableQueue::default(); + // Spawn the periodic streamer. + let (_, data_in) = spawn(PeriodicStreamer, system.weak_spawner(), StreamingManager::default()) + .await + .unwrap(); + // Periodic streamer shouldn't be ingesting any input. + assert!(data_in.is_none()); + }); + + cross_test!(test_spawn_reactive_streamer, { + let system = AbortableQueue::default(); + // Spawn the reactive streamer. + let (_, data_in) = spawn(ReactiveStreamer, system.weak_spawner(), StreamingManager::default()) + .await + .unwrap(); + // Reactive streamer should be ingesting some input. + assert!(data_in.is_some()); + }); + + cross_test!(test_spawn_erroring_streamer, { + let system = AbortableQueue::default(); + // Try to spawn the erroring streamer. + let err = spawn(InitErrorStreamer, system.weak_spawner(), StreamingManager::default()) + .await + .unwrap_err(); + // The streamer should return an error. + assert_eq!(err, "error"); + }); +} diff --git a/mm2src/mm2_io/src/fs.rs b/mm2src/mm2_io/src/fs.rs index 489b3bad8b..960886a2b2 100644 --- a/mm2src/mm2_io/src/fs.rs +++ b/mm2src/mm2_io/src/fs.rs @@ -197,7 +197,7 @@ async fn filter_files_by_extension(dir_path: &Path, extension: &str) -> IoResult let entries = read_dir_async(dir_path) .await? .into_iter() - .filter(|path| path.extension().map(|ext| ext.to_ascii_lowercase()) == ext) + .filter(|path| path.extension().map(|ext| ext.to_ascii_lowercase()) == ext && !path.is_dir()) .collect(); Ok(entries) } diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 13d53b8eea..28291cb210 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -24,6 +24,8 @@ run-device-tests = [] enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] sepolia-maker-swap-v2-tests = [] sepolia-taker-swap-v2-tests = [] +test-ext-api = ["trading_api/test-ext-api"] +new-db-arch = [] # A temporary feature to integrate the new db architecture incrementally [dependencies] async-std = { version = "1.5", features = ["unstable"] } @@ -37,6 +39,7 @@ cfg-if = "1.0" coins = { path = "../coins" } coins_activation = { path = "../coins_activation" } common = { path = "../common" } +compatible-time = { version = "1.1.0", package = "web-time" } crc32fast = { version = "1.3.2", features = ["std", "nightly"] } crossbeam = "0.8" crypto = { path = "../crypto" } @@ -54,7 +57,6 @@ 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" @@ -65,9 +67,9 @@ mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_gui_storage = { path = "../mm2_gui_storage" } mm2_io = { path = "../mm2_io" } -mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p", features = ["application"] } +mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p" } mm2_metrics = { path = "../mm2_metrics" } -mm2_net = { path = "../mm2_net" } +mm2_net = { path = "../mm2_net"} mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} mm2_state_machine = { path = "../mm2_state_machine" } @@ -100,10 +102,13 @@ trie-root = "0.16.0" uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] +# TODO: Removing this causes `wasm-pack` to fail when starting a web session (even though we don't use this crate). +# Investigate why. instant = { version = "0.1.12", features = ["wasm-bindgen"] } js-sys = { version = "0.3.27" } mm2_db = { path = "../mm2_db" } mm2_test_helpers = { path = "../mm2_test_helpers" } +timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } wasm-bindgen = "0.2.86" wasm-bindgen-futures = { version = "0.4.1" } wasm-bindgen-test = { version = "0.3.1" } @@ -116,6 +121,7 @@ hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } rcgen = "0.10" rustls = { version = "0.21", default-features = false } rustls-pemfile = "1.0.2" +timed-map = { version = "1.3", features = ["rustc-hash"] } tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net", "signal"] } [target.'cfg(windows)'.dependencies] @@ -124,8 +130,9 @@ winapi = "0.3" [dev-dependencies] coins = { path = "../coins", features = ["for-tests"] } coins_activation = { path = "../coins_activation", features = ["for-tests"] } +common = { path = "../common", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } -trading_api = { path = "../trading_api", features = ["mocktopus"] } +trading_api = { path = "../trading_api", features = ["for-tests"] } mocktopus = "0.8.0" testcontainers = "0.15.0" web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["http-rustls-tls"] } @@ -141,3 +148,4 @@ chrono = "0.4" gstuff = { version = "0.7", features = ["nightly"] } prost-build = { version = "0.12", default-features = false } regex = "1" + diff --git a/mm2src/mm2_main/src/database.rs b/mm2src/mm2_main/src/database.rs index 8f278d3adc..6945204643 100644 --- a/mm2src/mm2_main/src/database.rs +++ b/mm2src/mm2_main/src/database.rs @@ -119,6 +119,13 @@ fn migration_12() -> Vec<(&'static str, Vec)> { ] } +fn migration_13() -> Vec<(&'static str, Vec)> { + vec![ + (my_swaps::ADD_SWAP_VERSION_FIELD, vec![]), // Step 1: Add new column + (my_swaps::SET_LEGACY_SWAP_VERSION, vec![]), // Step 2: Update old rows + ] +} + async fn statements_for_migration(ctx: &MmArc, current_migration: i64) -> Option)>> { match current_migration { 1 => Some(migration_1(ctx).await), @@ -133,6 +140,7 @@ async fn statements_for_migration(ctx: &MmArc, current_migration: i64) -> Option 10 => Some(migration_10(ctx).await), 11 => Some(migration_11()), 12 => Some(migration_12()), + 13 => Some(migration_13()), _ => None, } } diff --git a/mm2src/mm2_main/src/database/my_swaps.rs b/mm2src/mm2_main/src/database/my_swaps.rs index 2fe1a85890..91c2200bba 100644 --- a/mm2src/mm2_main/src/database/my_swaps.rs +++ b/mm2src/mm2_main/src/database/my_swaps.rs @@ -52,8 +52,12 @@ pub const TRADING_PROTO_UPGRADE_MIGRATION: &[&str] = &[ "ALTER TABLE my_swaps ADD COLUMN taker_coin_nota BOOLEAN;", ]; +/// Adds Swap Protocol version column to `my_swaps` table +pub const ADD_SWAP_VERSION_FIELD: &str = "ALTER TABLE my_swaps ADD COLUMN swap_version INTEGER;"; +/// Sets default value for `swap_version` to `1` for existing rows +pub const SET_LEGACY_SWAP_VERSION: &str = "UPDATE my_swaps SET swap_version = 1 WHERE swap_version IS NULL;"; pub const ADD_OTHER_P2P_PUBKEY_FIELD: &str = "ALTER TABLE my_swaps ADD COLUMN other_p2p_pub BLOB;"; -// Storing rational numbers as text to maintain precision +/// Storing rational numbers as text to maintain precision pub const ADD_DEX_FEE_BURN_FIELD: &str = "ALTER TABLE my_swaps ADD COLUMN dex_fee_burn TEXT;"; /// The query to insert swap on migration 1, during this migration swap_type column doesn't exist @@ -97,7 +101,8 @@ const INSERT_MY_SWAP_V2: &str = r#"INSERT INTO my_swaps ( maker_coin_nota, taker_coin_confs, taker_coin_nota, - other_p2p_pub + other_p2p_pub, + swap_version ) VALUES ( :my_coin, :other_coin, @@ -118,7 +123,8 @@ const INSERT_MY_SWAP_V2: &str = r#"INSERT INTO my_swaps ( :maker_coin_nota, :taker_coin_confs, :taker_coin_nota, - :other_p2p_pub + :other_p2p_pub, + :swap_version );"#; pub fn insert_new_swap_v2(ctx: &MmArc, params: &[(&str, &dyn ToSql)]) -> SqlResult<()> { @@ -296,6 +302,7 @@ pub fn select_unfinished_swaps_uuids(conn: &Connection, swap_type: u8) -> SqlRes /// The SQL query selecting upgraded swap data and send it to user through RPC API /// It omits sensitive data (swap secret, p2p privkey, etc) for security reasons +/// TODO: should we add burn amount for rpc? pub const SELECT_MY_SWAP_V2_FOR_RPC_BY_UUID: &str = r#"SELECT my_coin, other_coin, @@ -311,12 +318,14 @@ pub const SELECT_MY_SWAP_V2_FOR_RPC_BY_UUID: &str = r#"SELECT maker_coin_confs, maker_coin_nota, taker_coin_confs, - taker_coin_nota + taker_coin_nota, + swap_version FROM my_swaps WHERE uuid = :uuid; "#; /// The SQL query selecting upgraded swap data required to re-initialize the swap e.g., on restart. +/// NOTE: for maker v2 swap the dex_fee is stored as default (the real one could be no fee if taker is the dex pubkey) pub const SELECT_MY_SWAP_V2_BY_UUID: &str = r#"SELECT my_coin, other_coin, @@ -337,7 +346,8 @@ pub const SELECT_MY_SWAP_V2_BY_UUID: &str = r#"SELECT taker_coin_confs, taker_coin_nota, p2p_privkey, - other_p2p_pub + other_p2p_pub, + swap_version FROM my_swaps WHERE uuid = :uuid; "#; diff --git a/mm2src/mm2_main/src/database/stats_nodes.rs b/mm2src/mm2_main/src/database/stats_nodes.rs index 458f4159bc..c62c84aa3e 100644 --- a/mm2src/mm2_main/src/database/stats_nodes.rs +++ b/mm2src/mm2_main/src/database/stats_nodes.rs @@ -37,19 +37,28 @@ pub fn insert_node_info(ctx: &MmArc, node_info: &NodeInfo) -> SqlResult<()> { node_info.address.clone(), node_info.peer_id.clone(), ]; + #[cfg(not(feature = "new-db-arch"))] let conn = ctx.sqlite_connection(); + #[cfg(feature = "new-db-arch")] + let conn = ctx.global_db(); 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]; + #[cfg(not(feature = "new-db-arch"))] let conn = ctx.sqlite_connection(); + #[cfg(feature = "new-db-arch")] + let conn = ctx.global_db(); conn.execute(DELETE_NODE, params_from_iter(params.iter())).map(|_| ()) } pub fn select_peers_addresses(ctx: &MmArc) -> SqlResult, SqlError> { + #[cfg(not(feature = "new-db-arch"))] let conn = ctx.sqlite_connection(); + #[cfg(feature = "new-db-arch")] + let conn = ctx.global_db(); let mut stmt = conn.prepare(SELECT_PEERS_ADDRESSES)?; let peers_addresses = stmt .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? @@ -59,10 +68,17 @@ pub fn select_peers_addresses(ctx: &MmArc) -> SqlResult, S } pub fn select_peers_names(ctx: &MmArc) -> SqlResult, SqlError> { - ctx.sqlite_connection() + #[cfg(not(feature = "new-db-arch"))] + let conn = ctx.sqlite_connection(); + #[cfg(feature = "new-db-arch")] + let conn = ctx.global_db(); + // TODO: Can't use `conn` in the return statement because it's a mutex borrow, and also clippy complains when assigning the result into a temporary `result`. + #[allow(clippy::let_and_return)] + let result = conn .prepare(SELECT_PEERS_NAMES)? .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? - .collect::>>() + .collect::>>(); + result } pub fn insert_node_version_stat(ctx: &MmArc, node_version_stat: NodeVersionStat) -> SqlResult<()> { @@ -76,6 +92,9 @@ pub fn insert_node_version_stat(ctx: &MmArc, node_version_stat: NodeVersionStat) node_version_stat.timestamp.to_string(), node_version_stat.error.unwrap_or_default(), ]; + #[cfg(not(feature = "new-db-arch"))] let conn = ctx.sqlite_connection(); + #[cfg(feature = "new-db-arch")] + let conn = ctx.global_db(); conn.execute(INSERT_STAT, params_from_iter(params.iter())).map(|_| ()) } diff --git a/mm2src/mm2_main/src/ext_api.rs b/mm2src/mm2_main/src/ext_api.rs deleted file mode 100644 index f1b92c145f..0000000000 --- a/mm2src/mm2_main/src/ext_api.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! RPCs for integration with external third party trading APIs. - -pub mod one_inch; diff --git a/mm2src/mm2_main/src/heartbeat_event.rs b/mm2src/mm2_main/src/heartbeat_event.rs index 6c4d19d77b..a2c46f2fb6 100644 --- a/mm2src/mm2_main/src/heartbeat_event.rs +++ b/mm2src/mm2_main/src/heartbeat_event.rs @@ -1,52 +1,50 @@ use async_trait::async_trait; -use common::{executor::{SpawnFuture, Timer}, - log::info}; -use futures::channel::oneshot::{self, Receiver, Sender}; -use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - Event, EventName, EventStreamConfiguration}; +use common::executor::Timer; +use futures::channel::oneshot; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields, default)] +pub struct HeartbeatEventConfig { + /// The time in seconds to wait before sending another ping event. + pub stream_interval_seconds: f64, +} + +impl Default for HeartbeatEventConfig { + fn default() -> Self { + Self { + stream_interval_seconds: 5.0, + } + } +} pub struct HeartbeatEvent { - ctx: MmArc, + config: HeartbeatEventConfig, } impl HeartbeatEvent { - pub fn new(ctx: MmArc) -> Self { Self { ctx } } + pub fn new(config: HeartbeatEventConfig) -> Self { Self { config } } } #[async_trait] -impl EventBehaviour for HeartbeatEvent { - fn event_name() -> EventName { EventName::HEARTBEAT } +impl EventStreamer for HeartbeatEvent { + type DataInType = NoDataIn; - async fn handle(self, interval: f64, tx: oneshot::Sender) { - tx.send(EventInitStatus::Success).unwrap(); + fn streamer_id(&self) -> String { "HEARTBEAT".to_string() } - loop { - self.ctx - .stream_channel_controller - .broadcast(Event::new(Self::event_name().to_string(), json!({}).to_string())) - .await; + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { + ready_tx.send(Ok(())).unwrap(); - Timer::sleep(interval).await; - } - } + loop { + broadcaster.broadcast(Event::new(self.streamer_id(), json!({}))); - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - info!( - "{} event is activated with {} seconds interval.", - Self::event_name(), - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - self.ctx.spawner().spawn(self.handle(event.stream_interval_seconds, tx)); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive + Timer::sleep(self.config.stream_interval_seconds).await; } } } diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 20a6004c95..fb09652f09 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,11 +1,10 @@ use async_std::prelude::FutureExt; use chrono::Utc; use common::executor::SpawnFuture; -use common::expirable_map::ExpirableEntry; use common::{log, HttpStatusCode, StatusCode}; +use compatible_time::{Duration, Instant}; use derive_more::Display; use futures::channel::oneshot::{self, Receiver, Sender}; -use instant::{Duration, Instant}; use lazy_static::lazy_static; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; @@ -80,35 +79,27 @@ impl HealthcheckMessage { const MIN_DURATION_FOR_REUSABLE_MSG: Duration = Duration::from_secs(5); lazy_static! { - static ref RECENTLY_GENERATED_MESSAGE: Mutex> = - Mutex::new(ExpirableEntry::new( - // Using dummy values in order to initialize `HealthcheckMessage` context. - HealthcheckMessage { - signature: vec![], - data: HealthcheckData { - sender_public_key: vec![], - expires_at_secs: 0, - is_a_reply: false, - }, - }, - Duration::from_secs(0) - )); + static ref RECENTLY_GENERATED_MESSAGE: Mutex> = Mutex::new(None); } // If recently generated message has longer life than `MIN_DURATION_FOR_REUSABLE_MSG`, we can reuse it to // reduce the message generation overhead under high pressure. let mut mutexed_msg = RECENTLY_GENERATED_MESSAGE.lock().unwrap(); - if mutexed_msg.has_longer_life_than(MIN_DURATION_FOR_REUSABLE_MSG) { - Ok(mutexed_msg.get_element().clone()) - } else { - let new_msg = HealthcheckMessage::generate_message(ctx, true)?; + if let Some((ref msg, expiration)) = *mutexed_msg { + if expiration > Instant::now() + MIN_DURATION_FOR_REUSABLE_MSG { + return Ok(msg.clone()); + } + } - mutexed_msg.update_value(new_msg.clone()); - mutexed_msg.update_expiration(Instant::now() + Duration::from_secs(healthcheck_message_exp_secs())); + let new_msg = HealthcheckMessage::generate_message(ctx, true)?; - Ok(new_msg) - } + *mutexed_msg = Some(( + new_msg.clone(), + Instant::now() + Duration::from_secs(healthcheck_message_exp_secs()), + )); + + Ok(new_msg) } fn is_received_message_valid(&self) -> Result { @@ -266,7 +257,7 @@ pub async fn peer_connection_healthcheck_rpc( { let mut book = ctx.healthcheck_response_handler.lock().await; - book.insert(target_peer_address.into(), tx, address_record_exp); + book.insert_expirable(target_peer_address.into(), tx, address_record_exp); } broadcast_p2p_msg( diff --git a/mm2src/mm2_main/src/lp_init/init_context.rs b/mm2src/mm2_main/src/lp_init/init_context.rs index 8b03751b69..a260b4ab67 100644 --- a/mm2src/mm2_main/src/lp_init/init_context.rs +++ b/mm2src/mm2_main/src/lp_init/init_context.rs @@ -16,9 +16,9 @@ impl MmInitContext { pub fn from_ctx(ctx: &MmArc) -> Result, String> { from_ctx(&ctx.mm_init_ctx, move || { Ok(MmInitContext { - init_hw_task_manager: RpcTaskManager::new_shared(), + init_hw_task_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), #[cfg(target_arch = "wasm32")] - init_metamask_manager: RpcTaskManager::new_shared(), + init_metamask_manager: RpcTaskManager::new_shared(ctx.event_stream_manager.clone()), }) }) } diff --git a/mm2src/mm2_main/src/lp_init/init_hw.rs b/mm2src/mm2_main/src/lp_init/init_hw.rs index b9d0c67664..6148a44f53 100644 --- a/mm2src/mm2_main/src/lp_init/init_hw.rs +++ b/mm2src/mm2_main/src/lp_init/init_hw.rs @@ -12,8 +12,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, - RpcTaskTypes}; +use rpc_task::{RpcInitReq, RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, + RpcTaskStatus, RpcTaskTypes}; use std::sync::Arc; use std::time::Duration; @@ -165,7 +165,8 @@ impl RpcTask for InitHwTask { } } -pub async fn init_trezor(ctx: MmArc, req: InitHwRequest) -> MmResult { +pub async fn init_trezor(ctx: MmArc, req: RpcInitReq) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let init_ctx = MmInitContext::from_ctx(&ctx).map_to_mm(InitHwError::Internal)?; let spawner = ctx.spawner(); let task = InitHwTask { @@ -173,7 +174,7 @@ pub async fn init_trezor(ctx: MmArc, req: InitHwRequest) -> MmResult; pub type InitMetamaskStatus = @@ -132,12 +133,13 @@ impl RpcTask for InitMetamaskTask { pub async fn connect_metamask( ctx: MmArc, - req: InitMetamaskRequest, + req: RpcInitReq, ) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); let init_ctx = MmInitContext::from_ctx(&ctx).map_to_mm(InitMetamaskError::Internal)?; let spawner = ctx.spawner(); let task = InitMetamaskTask { ctx, req }; - let task_id = RpcTaskManager::spawn_rpc_task(&init_ctx.init_metamask_manager, &spawner, task)?; + let task_id = RpcTaskManager::spawn_rpc_task(&init_ctx.init_metamask_manager, &spawner, task, client_id)?; Ok(InitRpcTaskResponse { task_id }) } diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 1e5f7feff0..fc37bc8624 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -18,9 +18,19 @@ // marketmaker // +#[cfg(not(target_arch = "wasm32"))] +use crate::database::init_and_migrate_sql_db; +use crate::lp_healthcheck::peer_healthcheck_topic; +use crate::lp_message_service::{init_message_service, InitMessageServiceError}; +use crate::lp_network::{lp_network_ports, p2p_event_process_loop, subscribe_to_topic, NetIdError}; +use crate::lp_ordermatch::{broadcast_maker_orders_keep_alive_loop, clean_memory_loop, init_ordermatch_context, + lp_ordermatch_loop, orders_kick_start, BalanceUpdateOrdermatchHandler, OrdermatchInitError}; +use crate::lp_swap::swap_kick_starts; +use crate::lp_wallet::{initialize_wallet_passphrase, WalletInitError}; +use crate::rpc::spawn_rpc; use bitcrypto::sha256; use coins::register_balance_update_handler; -use common::executor::{SpawnFuture, Timer}; +use common::executor::SpawnFuture; use common::log::{info, warn}; use crypto::{from_hw_error, CryptoCtx, HwError, HwProcessingError, HwRpcError, WithHwRpcError}; use derive_more::Display; @@ -28,8 +38,6 @@ use enum_derives::EnumFromTrait; use mm2_core::mm_ctx::{MmArc, MmCtx}; use mm2_err_handle::common_errors::InternalError; use mm2_err_handle::prelude::*; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; -use mm2_libp2p::application::network_event::NetworkEvent; use mm2_libp2p::behaviours::atomicdex::{generate_ed25519_keypair, GossipsubConfig, DEPRECATED_NETID_LIST}; use mm2_libp2p::p2p_ctx::P2PContext; use mm2_libp2p::{spawn_gossipsub, AdexBehaviourError, NodeType, RelayAddress, RelayAddressError, SeedNodeInfo, @@ -44,18 +52,6 @@ use std::str; use std::time::Duration; use std::{fs, usize}; -#[cfg(not(target_arch = "wasm32"))] -use crate::database::init_and_migrate_sql_db; -use crate::heartbeat_event::HeartbeatEvent; -use crate::lp_healthcheck::peer_healthcheck_topic; -use crate::lp_message_service::{init_message_service, InitMessageServiceError}; -use crate::lp_network::{lp_network_ports, p2p_event_process_loop, subscribe_to_topic, NetIdError}; -use crate::lp_ordermatch::{broadcast_maker_orders_keep_alive_loop, clean_memory_loop, init_ordermatch_context, - lp_ordermatch_loop, orders_kick_start, BalanceUpdateOrdermatchHandler, OrdermatchInitError}; -use crate::lp_swap::{running_swaps_num, swap_kick_starts}; -use crate::lp_wallet::{initialize_wallet_passphrase, WalletInitError}; -use crate::rpc::spawn_rpc; - cfg_native! { use db_common::sqlite::rusqlite::Error as SqlError; use mm2_io::fs::{ensure_dir_is_writable, ensure_file_is_writable}; @@ -67,7 +63,7 @@ cfg_native! { #[path = "lp_init/init_hw.rs"] pub mod init_hw; cfg_wasm32! { - use mm2_net::wasm_event_stream::handle_worker_stream; + use mm2_net::event_streaming::wasm_event_stream::handle_worker_stream; #[path = "lp_init/init_metamask.rs"] pub mod init_metamask; @@ -76,43 +72,39 @@ cfg_wasm32! { const DEFAULT_NETID_SEEDNODES: &[SeedNodeInfo] = &[ SeedNodeInfo::new( "12D3KooWHKkHiNhZtKceQehHhPqwU5W1jXpoVBgS1qst899GjvTm", - "168.119.236.251", "viserion.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWAToxtunEBWCoAHjefSv74Nsmxranw8juy3eKEdrQyGRF", - "168.119.236.240", "rhaegal.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWSmEi8ypaVzFA1AGde2RjxNW5Pvxw3qa2fVe48PjNs63R", - "168.119.236.239", "drogon.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWMrjLmrv8hNgAoVf1RfumfjyPStzd4nv5XL47zN4ZKisb", - "168.119.237.8", "falkor.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWEWzbYcosK2JK9XpFXzumfgsWJW1F7BZS15yLTrhfjX2Z", - "65.21.51.47", "smaug.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWJWBnkVsVNjiqUEPjLyHpiSmQVAJ5t6qt1Txv5ctJi9Xd", - "135.181.34.220", "balerion.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWPR2RoPi19vQtLugjCdvVmCcGLP2iXAzbDfP3tp81ZL4d", - "168.119.237.13", "kalessin.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWEaZpH61H4yuQkaNG5AsyGdpBhKRppaLdAY52a774ab5u", - "46.4.78.11", - "fr1.cipig.net", + "seed01.kmdefi.net", + ), + SeedNodeInfo::new( + "12D3KooWAd5gPXwX7eDvKWwkr2FZGfoJceKDCA53SHmTFFVkrN7Q", + "seed02.kmdefi.net", ), ]; @@ -205,10 +197,8 @@ pub enum MmInitError { OrdersKickStartError(String), #[display(fmt = "Error initializing wallet: {}", _0)] WalletInitError(String), - #[display(fmt = "NETWORK event initialization failed: {}", _0)] - NetworkEventInitFailed(String), - #[display(fmt = "HEARTBEAT event initialization failed: {}", _0)] - HeartbeatEventInitFailed(String), + #[display(fmt = "Event streamer initialization failed: {}", _0)] + EventStreamerInitFailed(String), #[from_trait(WithHwRpcError::hw_rpc_error)] #[display(fmt = "{}", _0)] HwError(HwRpcError), @@ -319,11 +309,10 @@ fn default_seednodes(netid: u16) -> Vec { #[cfg(not(target_arch = "wasm32"))] fn default_seednodes(netid: u16) -> Vec { - use crate::lp_network::addr_to_ipv4_string; if netid == 8762 { DEFAULT_NETID_SEEDNODES .iter() - .filter_map(|SeedNodeInfo { domain, .. }| addr_to_ipv4_string(domain).ok()) + .filter_map(|SeedNodeInfo { domain, .. }| mm2_net::ip_addr::addr_to_ipv4_string(domain).ok()) .map(RelayAddress::IPv4) .collect() } else { @@ -427,25 +416,11 @@ fn migrate_db(ctx: &MmArc) -> MmInitResult<()> { #[cfg(not(target_arch = "wasm32"))] fn migration_1(_ctx: &MmArc) {} -async fn init_event_streaming(ctx: &MmArc) -> MmInitResult<()> { - // This condition only executed if events were enabled in mm2 configuration. - if let Some(config) = &ctx.event_stream_configuration { - if let EventInitStatus::Failed(err) = NetworkEvent::new(ctx.clone()).spawn_if_active(config).await { - return MmError::err(MmInitError::NetworkEventInitFailed(err)); - } - - if let EventInitStatus::Failed(err) = HeartbeatEvent::new(ctx.clone()).spawn_if_active(config).await { - return MmError::err(MmInitError::HeartbeatEventInitFailed(err)); - } - } - - Ok(()) -} - #[cfg(target_arch = "wasm32")] fn init_wasm_event_streaming(ctx: &MmArc) { - if ctx.event_stream_configuration.is_some() { - ctx.spawner().spawn(handle_worker_stream(ctx.clone())); + if let Some(event_streaming_config) = ctx.event_streaming_configuration() { + ctx.spawner() + .spawn(handle_worker_stream(ctx.clone(), event_streaming_config.worker_path)); } } @@ -469,6 +444,20 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { .map_to_mm(MmInitError::ErrorSqliteInitializing)?; init_and_migrate_sql_db(&ctx).await?; migrate_db(&ctx)?; + #[cfg(feature = "new-db-arch")] + { + let global_dir = ctx.global_dir(); + let wallet_dir = ctx.wallet_dir(); + if !ensure_dir_is_writable(&global_dir) { + return MmError::err(MmInitError::db_directory_is_not_writable("global")); + }; + if !ensure_dir_is_writable(&wallet_dir) { + return MmError::err(MmInitError::db_directory_is_not_writable("wallets")); + } + ctx.init_global_and_wallet_db() + .await + .map_to_mm(MmInitError::ErrorSqliteInitializing)?; + } } init_message_service(&ctx).await?; @@ -484,8 +473,6 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { // an order and start new swap that might get started 2 times because of kick-start kick_start(ctx.clone()).await?; - init_event_streaming(&ctx).await?; - ctx.spawner().spawn(lp_ordermatch_loop(ctx.clone())); ctx.spawner().spawn(broadcast_maker_orders_keep_alive_loop(ctx.clone())); @@ -528,21 +515,6 @@ pub async fn lp_init(ctx: MmArc, version: String, datetime: String) -> MmInitRes } }); - // In the mobile version we might depend on `lp_init` staying around until the context stops. - loop { - if ctx.is_stopping() { - break; - }; - Timer::sleep(0.2).await - } - - // wait for swaps to stop - loop { - if running_swaps_num(&ctx) == 0 { - break; - }; - Timer::sleep(0.2).await - } Ok(()) } @@ -578,7 +550,7 @@ fn get_p2p_key(ctx: &MmArc, i_am_seed: bool) -> P2PResult<[u8; 32]> { } pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { - let i_am_seed = ctx.conf["i_am_seed"].as_bool().unwrap_or(false); + let i_am_seed = ctx.is_seed_node(); let netid = ctx.netid(); if DEPRECATED_NETID_LIST.contains(&netid) { diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index b2ef53f3fb..3047510088 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -23,9 +23,9 @@ use coins::lp_coinfind; use common::executor::SpawnFuture; use common::{log, Future01CompatExt}; +use compatible_time::Instant; use derive_more::Display; use futures::{channel::oneshot, StreamExt}; -use instant::Instant; use keys::KeyPair; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; @@ -37,7 +37,6 @@ use mm2_libp2p::{AdexBehaviourCmd, AdexBehaviourEvent, AdexEventRx, AdexResponse use mm2_libp2p::{PeerAddresses, RequestResponseBehaviourEvent}; use mm2_metrics::{mm_label, mm_timing}; use serde::de; -use std::net::ToSocketAddrs; use crate::{lp_healthcheck, lp_ordermatch, lp_stats, lp_swap}; @@ -238,9 +237,11 @@ fn process_p2p_request( response_channel: mm2_libp2p::AdexResponseChannel, ) -> P2PRequestResult<()> { let request = decode_message::(&request)?; + log::debug!("Got P2PRequest {:?}", request); + let result = match request { P2PRequest::Ordermatch(req) => lp_ordermatch::process_peer_request(ctx.clone(), req), - P2PRequest::NetworkInfo(req) => lp_stats::process_info_request(ctx.clone(), req), + P2PRequest::NetworkInfo(req) => lp_stats::process_info_request(ctx.clone(), req).map(Some), }; let res = match result { @@ -437,51 +438,6 @@ pub fn add_reserved_peer_addresses(ctx: &MmArc, peer: PeerId, addresses: PeerAdd }; } -#[derive(Debug, Display)] -pub enum ParseAddressError { - #[display(fmt = "Address/Seed {} resolved to IPv6 which is not supported", _0)] - UnsupportedIPv6Address(String), - #[display(fmt = "Address/Seed {} to_socket_addrs empty iter", _0)] - EmptyIterator(String), - #[display(fmt = "Couldn't resolve '{}' Address/Seed: {}", _0, _1)] - UnresolvedAddress(String, String), -} - -#[cfg(not(target_arch = "wasm32"))] -pub fn addr_to_ipv4_string(address: &str) -> Result> { - // Remove "https:// or http://" etc.. from address str - let formated_address = address.split("://").last().unwrap_or(address); - let address_with_port = if formated_address.contains(':') { - formated_address.to_string() - } else { - format!("{}:0", formated_address) - }; - match address_with_port.as_str().to_socket_addrs() { - Ok(mut iter) => match iter.next() { - Some(addr) => { - if addr.is_ipv4() { - Ok(addr.ip().to_string()) - } else { - log::warn!( - "Address/Seed {} resolved to IPv6 {} which is not supported", - address, - addr - ); - MmError::err(ParseAddressError::UnsupportedIPv6Address(address.into())) - } - }, - None => { - log::warn!("Address/Seed {} to_socket_addrs empty iter", address); - MmError::err(ParseAddressError::EmptyIterator(address.into())) - }, - }, - Err(e) => { - log::error!("Couldn't resolve '{}' seed: {}", address, e); - MmError::err(ParseAddressError::UnresolvedAddress(address.into(), e.to_string())) - }, - } -} - #[derive(Clone, Debug, Display, Serialize)] pub enum NetIdError { #[display(fmt = "Netid {} is larger than max {}", netid, max_netid)] diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 620cb79bfb..18475ce191 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -25,11 +25,10 @@ use blake2::digest::{Update, VariableOutput}; use blake2::Blake2bVar; use coins::utxo::{compressed_pub_key_from_priv_raw, ChecksumType, UtxoAddressFormat}; use coins::{coin_conf, find_pair, lp_coinfind, BalanceTradeFeeUpdatedHandler, CoinProtocol, CoinsContext, - FeeApproxStage, MarketCoinOps, MmCoinEnum}; + FeeApproxStage, MakerCoinSwapOpsV2, MmCoin, MmCoinEnum, TakerCoinSwapOpsV2}; use common::executor::{simple_map::AbortableSimpleMap, AbortSettings, AbortableSystem, AbortedError, SpawnAbortable, SpawnFuture, Timer}; use common::log::{error, warn, LogOnError}; -use common::time_cache::TimeCache; use common::{bits256, log, new_uuid, now_ms, now_sec}; use crypto::privkey::SerializableSecp256k1Keypair; use crypto::{CryptoCtx, CryptoCtxError}; @@ -41,6 +40,7 @@ use http::Response; use keys::{AddressFormat, KeyPair}; use mm2_core::mm_ctx::{from_ctx, MmArc, MmWeak}; use mm2_err_handle::prelude::*; +use mm2_event_stream::StreamingManager; use mm2_libp2p::application::request_response::ordermatch::OrdermatchRequest; use mm2_libp2p::application::request_response::P2PRequest; use mm2_libp2p::{decode_signed, encode_and_sign, encode_message, pub_sub_topic, PublicKey, TopicHash, TopicPrefix, @@ -55,8 +55,11 @@ use my_orders_storage::{delete_my_maker_order, delete_my_taker_order, save_maker save_my_new_maker_order, save_my_new_taker_order, MyActiveOrders, MyOrdersFilteringHistory, MyOrdersHistory, MyOrdersStorage}; use num_traits::identities::Zero; +use order_events::{OrderStatusEvent, OrderStatusStreamer}; +use orderbook_events::{OrderbookItemChangeEvent, OrderbookStreamer}; use parking_lot::Mutex as PaMutex; use rpc::v1::types::H256 as H256Json; +use secp256k1::PublicKey as Secp256k1Pubkey; use serde_json::{self as json, Value as Json}; use sp_trie::{delta_trie_root, MemoryDB, Trie, TrieConfiguration, TrieDB, TrieDBMut, TrieHash, TrieMut}; use std::collections::hash_map::{Entry, HashMap, RawEntryMut}; @@ -67,6 +70,7 @@ use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use timed_map::{MapKind, TimedMap}; use trie_db::NodeCodec as NodeCodecT; use uuid::Uuid; @@ -74,17 +78,19 @@ use crate::lp_network::{broadcast_p2p_msg, request_any_relay, request_one_peer, use crate::lp_swap::maker_swap_v2::{self, MakerSwapStateMachine, MakerSwapStorage}; use crate::lp_swap::taker_swap_v2::{self, TakerSwapStateMachine, TakerSwapStorage}; use crate::lp_swap::{calc_max_maker_vol, check_balance_for_maker_swap, check_balance_for_taker_swap, - check_other_coin_balance_for_swap, detect_secret_hash_algo, dex_fee_amount_from_taker_coin, - generate_secret, get_max_maker_vol, insert_new_swap_to_db, is_pubkey_banned, lp_atomic_locktime, + check_other_coin_balance_for_swap, detect_secret_hash_algo_v2, generate_secret, + get_max_maker_vol, insert_new_swap_to_db, is_pubkey_banned, lp_atomic_locktime, p2p_keypair_and_peer_id_to_broadcast, p2p_private_and_peer_id_to_broadcast, run_maker_swap, run_taker_swap, swap_v2_topic, AtomicLocktimeVersion, CheckBalanceError, CheckBalanceResult, CoinVolumeInfo, MakerSwap, RunMakerSwapInput, RunTakerSwapInput, SwapConfirmationsSettings, TakerSwap, LEGACY_SWAP_TYPE}; +use crate::swap_versioning::{legacy_swap_version, SwapVersion}; #[cfg(any(test, feature = "run-docker-tests"))] use crate::lp_swap::taker_swap::FailAt; pub use best_orders::{best_orders_rpc, best_orders_rpc_v2}; +use crypto::secret_hash_algo::SecretHashAlgo; pub use orderbook_depth::orderbook_depth_rpc; pub use orderbook_rpc::{orderbook_rpc, orderbook_rpc_v2}; @@ -99,11 +105,14 @@ mod best_orders; mod lp_bot; pub use lp_bot::{start_simple_market_maker_bot, stop_simple_market_maker_bot, StartSimpleMakerBotRequest, TradingBotEvent}; +use primitives::hash::{H256, H264}; mod my_orders_storage; mod new_protocol; +pub(crate) mod order_events; mod order_requests_tracker; mod orderbook_depth; +pub(crate) mod orderbook_events; mod orderbook_rpc; #[cfg(all(test, not(target_arch = "wasm32")))] #[path = "ordermatch_tests.rs"] @@ -131,6 +140,8 @@ const TRIE_STATE_HISTORY_TIMEOUT: u64 = 3; const TRIE_ORDER_HISTORY_TIMEOUT: u64 = 300; #[cfg(test)] const TRIE_ORDER_HISTORY_TIMEOUT: u64 = 3; +/// Current swap protocol version +const SWAP_VERSION_DEFAULT: u8 = 2; pub type OrderbookP2PHandlerResult = Result<(), MmError>; @@ -365,7 +376,9 @@ fn process_maker_order_cancelled(ctx: &MmArc, from_pubkey: String, cancelled_msg // is received within the `RECENTLY_CANCELLED_TIMEOUT` timeframe. // We do this even if the order is in the order_set, because it could have been added through // means other than the order creation message. - orderbook.recently_cancelled.insert(uuid, from_pubkey.clone()); + orderbook + .recently_cancelled + .insert_expirable(uuid, from_pubkey.clone(), RECENTLY_CANCELLED_TIMEOUT); if let Some(order) = orderbook.order_set.get(&uuid) { if order.pubkey == from_pubkey { orderbook.remove_order_trie_update(uuid); @@ -408,6 +421,7 @@ async fn request_and_fill_orderbook(ctx: &MmArc, base: &str, rel: &str) -> Resul rel: rel.to_string(), }; + let i_am_seed = ctx.is_seed_node(); let response = try_s!(request_any_relay::(ctx.clone(), P2PRequest::Ordermatch(request)).await); let (pubkey_orders, protocol_infos, conf_infos) = match response { Some(( @@ -418,7 +432,14 @@ async fn request_and_fill_orderbook(ctx: &MmArc, base: &str, rel: &str) -> Resul }, _peer_id, )) => (pubkey_orders, protocol_infos, conf_infos), - None => return Ok(()), + None => { + if i_am_seed { + warn!("No response received from any peer for GetOrderbook request"); + return Ok(()); + } else { + return Err("No response received from any peer for GetOrderbook request".to_string()); + } + }, }; let ordermatch_ctx = OrdermatchContext::from_ctx(ctx).unwrap(); @@ -436,11 +457,19 @@ async fn request_and_fill_orderbook(ctx: &MmArc, base: &str, rel: &str) -> Resul }, }; + let pubkey_without_prefix: [u8; 32] = match pubkey_bytes.get(1..).map(|slice| slice.try_into()) { + Some(Ok(arr)) => arr, + _ => { + warn!("Invalid pubkey length (not 32 bytes) for {}", pubkey); + continue; + }, + }; + if is_my_order(&pubkey, &my_pubsecp, &orderbook.my_p2p_pubkeys) { continue; } - if is_pubkey_banned(ctx, &pubkey_bytes[1..].into()) { + if is_pubkey_banned(ctx, &pubkey_without_prefix.into()) { warn!("Pubkey {} is banned", pubkey); continue; } @@ -510,7 +539,7 @@ fn remove_pubkey_pair_orders(orderbook: &mut Orderbook, pubkey: &str, alb_pair: return; } - pubkey_state.order_pairs_trie_state_history.remove(alb_pair.into()); + pubkey_state.order_pairs_trie_state_history.remove(&alb_pair.to_owned()); let mut orders_to_remove = Vec::with_capacity(pubkey_state.orders_uuids.len()); pubkey_state.orders_uuids.retain(|(uuid, alb)| { @@ -638,7 +667,6 @@ impl TryFromBytes for Uuid { } pub fn process_peer_request(ctx: MmArc, request: OrdermatchRequest) -> Result>, String> { - log::debug!("Got ordermatch request {:?}", request); match request { OrdermatchRequest::GetOrderbook { base, rel } => process_get_orderbook_request(ctx, base, rel), OrdermatchRequest::SyncPubkeyOrderbookState { pubkey, trie_roots } => { @@ -1161,12 +1189,12 @@ pub struct TakerRequest { #[serde(default)] match_by: MatchBy, conf_settings: Option, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub base_protocol_info: Option>, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub rel_protocol_info: Option>, + #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] + pub swap_version: SwapVersion, } impl TakerRequest { @@ -1187,6 +1215,7 @@ impl TakerRequest { conf_settings: Some(message.conf_settings), base_protocol_info: message.base_protocol_info, rel_protocol_info: message.rel_protocol_info, + swap_version: message.swap_version, } } @@ -1232,6 +1261,7 @@ impl From for new_protocol::OrdermatchMessage { conf_settings: taker_order.request.conf_settings.unwrap(), base_protocol_info: taker_order.request.base_protocol_info, rel_protocol_info: taker_order.request.rel_protocol_info, + swap_version: taker_order.request.swap_version, }) } } @@ -1257,6 +1287,7 @@ pub struct TakerOrderBuilder<'a> { min_volume: Option, timeout: u64, save_in_history: bool, + swap_version: u8, } pub enum TakerOrderBuildError { @@ -1336,6 +1367,7 @@ impl<'a> TakerOrderBuilder<'a> { order_type: OrderType::GoodTillCancelled, timeout: TAKER_ORDER_TIMEOUT, save_in_history: true, + swap_version: SWAP_VERSION_DEFAULT, } } @@ -1399,6 +1431,12 @@ impl<'a> TakerOrderBuilder<'a> { self } + /// When a new [TakerOrderBuilder::new] is created, it sets [SWAP_VERSION_DEFAULT]. + /// However, if user has not specified in the config to use TPU V2, + /// the TakerOrderBuilder's swap_version is changed to legacy. + /// In the future alls users will be using TPU V2 by default without "use_trading_proto_v2" configuration. + pub fn set_legacy_swap_v(&mut self) { self.swap_version = legacy_swap_version() } + /// Validate fields and build #[allow(clippy::result_large_err)] pub fn build(self) -> Result { @@ -1487,6 +1525,7 @@ impl<'a> TakerOrderBuilder<'a> { conf_settings: self.conf_settings, base_protocol_info: Some(base_protocol_info), rel_protocol_info: Some(rel_protocol_info), + swap_version: SwapVersion::from(self.swap_version), }, matches: Default::default(), min_volume, @@ -1527,6 +1566,7 @@ impl<'a> TakerOrderBuilder<'a> { conf_settings: self.conf_settings, base_protocol_info: Some(base_protocol_info), rel_protocol_info: Some(rel_protocol_info), + swap_version: SwapVersion::from(self.swap_version), }, matches: HashMap::new(), min_volume: Default::default(), @@ -1688,6 +1728,8 @@ pub struct MakerOrder { /// A custom priv key for more privacy to prevent linking orders of the same node between each other /// Commonly used with privacy coins (ARRR, ZCash, etc.) p2p_privkey: Option, + #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] + pub swap_version: SwapVersion, } pub struct MakerOrderBuilder<'a> { @@ -1700,6 +1742,7 @@ pub struct MakerOrderBuilder<'a> { rel_orderbook_ticker: Option, conf_settings: Option, save_in_history: bool, + swap_version: u8, } pub enum MakerOrderBuildError { @@ -1849,6 +1892,7 @@ impl<'a> MakerOrderBuilder<'a> { price: 0.into(), conf_settings: None, save_in_history: true, + swap_version: SWAP_VERSION_DEFAULT, } } @@ -1887,6 +1931,12 @@ impl<'a> MakerOrderBuilder<'a> { self } + /// When a new [MakerOrderBuilder::new] is created, it sets [SWAP_VERSION_DEFAULT]. + /// However, if user has not specified in the config to use TPU V2, + /// the MakerOrderBuilder's swap_version is changed to legacy. + /// In the future alls users will be using TPU V2 by default without "use_trading_proto_v2" configuration. + pub fn set_legacy_swap_v(&mut self) { self.swap_version = legacy_swap_version() } + /// Build MakerOrder #[allow(clippy::result_large_err)] pub fn build(self) -> Result { @@ -1943,6 +1993,7 @@ impl<'a> MakerOrderBuilder<'a> { base_orderbook_ticker: self.base_orderbook_ticker, rel_orderbook_ticker: self.rel_orderbook_ticker, p2p_privkey, + swap_version: SwapVersion::from(self.swap_version), }) } @@ -1967,6 +2018,7 @@ impl<'a> MakerOrderBuilder<'a> { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::from(self.swap_version), } } } @@ -2097,6 +2149,7 @@ impl From for MakerOrder { base_orderbook_ticker: taker_order.base_orderbook_ticker, rel_orderbook_ticker: taker_order.rel_orderbook_ticker, p2p_privkey: taker_order.p2p_privkey, + swap_version: taker_order.request.swap_version, }, // The "buy" taker order is recreated with reversed pair as Maker order is always considered as "sell" TakerAction::Buy => { @@ -2119,6 +2172,7 @@ impl From for MakerOrder { base_orderbook_ticker: taker_order.rel_orderbook_ticker, rel_orderbook_ticker: taker_order.base_orderbook_ticker, p2p_privkey: taker_order.p2p_privkey, + swap_version: taker_order.request.swap_version, } }, } @@ -2165,12 +2219,12 @@ pub struct MakerReserved { sender_pubkey: H256Json, dest_pub_key: H256Json, conf_settings: Option, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub base_protocol_info: Option>, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub rel_protocol_info: Option>, + #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] + pub swap_version: SwapVersion, } impl MakerReserved { @@ -2198,6 +2252,7 @@ impl MakerReserved { conf_settings: Some(message.conf_settings), base_protocol_info: message.base_protocol_info, rel_protocol_info: message.rel_protocol_info, + swap_version: message.swap_version, } } } @@ -2214,6 +2269,7 @@ impl From for new_protocol::OrdermatchMessage { conf_settings: maker_reserved.conf_settings.unwrap(), base_protocol_info: maker_reserved.base_protocol_info, rel_protocol_info: maker_reserved.rel_protocol_info, + swap_version: maker_reserved.swap_version, }) } } @@ -2345,7 +2401,7 @@ struct TrieDiff { #[derive(Debug)] struct TrieDiffHistory { - inner: TimeCache>, + inner: TimedMap>, } impl TrieDiffHistory { @@ -2355,30 +2411,31 @@ impl TrieDiffHistory { return; } - match self.inner.remove(diff.next_root) { + match self.inner.remove(&diff.next_root) { Some(mut diff) => { // we reached a state that was already reached previously // history can be cleaned up to this state hash - while let Some(next_diff) = self.inner.remove(diff.next_root) { + while let Some(next_diff) = self.inner.remove(&diff.next_root) { diff = next_diff; } }, None => { - self.inner.insert(insert_at, diff); + self.inner + .insert_expirable(insert_at, diff, Duration::from_secs(TRIE_ORDER_HISTORY_TIMEOUT)); }, }; } #[allow(dead_code)] - fn remove_key(&mut self, key: H64) { self.inner.remove(key); } + fn remove_key(&mut self, key: H64) { self.inner.remove(&key); } #[allow(dead_code)] - fn contains_key(&self, key: &H64) -> bool { self.inner.contains_key(key) } + fn contains_key(&self, key: &H64) -> bool { self.inner.get(key).is_some() } fn get(&self, key: &H64) -> Option<&TrieDiff> { self.inner.get(key) } #[allow(dead_code)] - fn len(&self) -> usize { self.inner.len() } + fn len(&self) -> usize { self.inner.len_unchecked() } } type TrieOrderHistory = TrieDiffHistory; @@ -2388,7 +2445,7 @@ struct OrderbookPubkeyState { last_keep_alive: u64, /// The map storing historical data about specific pair subtrie changes /// Used to get diffs of orders of pair between specific root hashes - order_pairs_trie_state_history: TimeCache, + order_pairs_trie_state_history: TimedMap, /// The known UUIDs owned by pubkey with alphabetically ordered pair to ease the lookup during pubkey orderbook requests orders_uuids: HashSet<(Uuid, AlbOrderedOrderbookPair)>, /// The map storing alphabetically ordered pair with trie root hash of orders owned by pubkey. @@ -2396,10 +2453,10 @@ struct OrderbookPubkeyState { } impl OrderbookPubkeyState { - pub fn with_history_timeout(ttl: Duration) -> OrderbookPubkeyState { + pub fn new() -> OrderbookPubkeyState { OrderbookPubkeyState { last_keep_alive: now_sec(), - order_pairs_trie_state_history: TimeCache::new(ttl), + order_pairs_trie_state_history: TimedMap::new_with_map_kind(MapKind::FxHashMap), orders_uuids: HashSet::default(), trie_roots: HashMap::default(), } @@ -2424,7 +2481,7 @@ fn pubkey_state_mut<'a>( match state.raw_entry_mut().from_key(from_pubkey) { RawEntryMut::Occupied(e) => e.into_mut(), RawEntryMut::Vacant(e) => { - let state = OrderbookPubkeyState::with_history_timeout(Duration::new(TRIE_STATE_HISTORY_TIMEOUT, 0)); + let state = OrderbookPubkeyState::new(); e.insert(from_pubkey.to_string(), state).1 }, } @@ -2437,17 +2494,6 @@ fn order_pair_root_mut<'a>(state: &'a mut HashMap, } } -fn pair_history_mut<'a>( - state: &'a mut TimeCache, - pair: &str, -) -> &'a mut TrieOrderHistory { - state - .entry(pair.into()) - .or_insert_with_update_expiration(|| TrieOrderHistory { - inner: TimeCache::new(Duration::from_secs(TRIE_ORDER_HISTORY_TIMEOUT)), - }) -} - /// `parity_util_mem::malloc_size` crushes for some reason on wasm32 #[cfg(target_arch = "wasm32")] fn collect_orderbook_metrics(_ctx: &MmArc, _orderbook: &Orderbook) {} @@ -2473,15 +2519,17 @@ struct Orderbook { order_set: HashMap, /// a map of orderbook states of known maker pubkeys pubkeys_state: HashMap, - /// The `TimeCache` of recently canceled orders, mapping `Uuid` to the maker pubkey as `String`, + /// `TimedMap` of recently canceled orders, mapping `Uuid` to the maker pubkey as `String`, /// used to avoid order recreation in case of out-of-order p2p messages, /// e.g., when receiving the order cancellation message before the order is created. /// Entries are kept for `RECENTLY_CANCELLED_TIMEOUT` seconds. - recently_cancelled: TimeCache, + recently_cancelled: TimedMap, topics_subscribed_to: HashMap, /// MemoryDB instance to store Patricia Tries data memory_db: MemoryDB, my_p2p_pubkeys: HashSet, + /// A copy of the streaming manager to stream orderbook events out. + streaming_manager: StreamingManager, } impl Default for Orderbook { @@ -2493,10 +2541,11 @@ impl Default for Orderbook { unordered: HashMap::default(), order_set: HashMap::default(), pubkeys_state: HashMap::default(), - recently_cancelled: TimeCache::new(RECENTLY_CANCELLED_TIMEOUT), + recently_cancelled: TimedMap::new_with_map_kind(MapKind::FxHashMap), topics_subscribed_to: HashMap::default(), memory_db: MemoryDB::default(), my_p2p_pubkeys: HashSet::default(), + streaming_manager: Default::default(), } } } @@ -2504,6 +2553,13 @@ impl Default for Orderbook { fn hashed_null_node() -> TrieHash { ::hashed_null_node() } impl Orderbook { + fn new(streaming_manager: StreamingManager) -> Orderbook { + Orderbook { + streaming_manager, + ..Default::default() + } + } + fn find_order_by_uuid_and_pubkey(&self, uuid: &Uuid, from_pubkey: &str) -> Option { self.order_set.get(uuid).and_then(|order| { if order.pubkey == from_pubkey { @@ -2558,7 +2614,31 @@ impl Orderbook { } if prev_root != H64::default() { - let history = pair_history_mut(&mut pubkey_state.order_pairs_trie_state_history, &alb_ordered); + let _ = pubkey_state + .order_pairs_trie_state_history + .update_expiration_status(alb_ordered.clone(), Duration::from_secs(TRIE_STATE_HISTORY_TIMEOUT)); + + let history = match pubkey_state + .order_pairs_trie_state_history + .get_mut_unchecked(&alb_ordered) + { + Some(t) => t, + None => { + pubkey_state.order_pairs_trie_state_history.insert_expirable( + alb_ordered.clone(), + TrieOrderHistory { + inner: TimedMap::new_with_map_kind(MapKind::FxHashMap), + }, + Duration::from_secs(TRIE_STATE_HISTORY_TIMEOUT), + ); + + pubkey_state + .order_pairs_trie_state_history + .get_mut_unchecked(&alb_ordered) + .expect("must exist") + }, + }; + history.insert_new_diff(prev_root, TrieDiff { delta: vec![(order.uuid, Some(order.clone()))], next_root: *pair_root, @@ -2607,6 +2687,11 @@ impl Orderbook { .or_insert_with(HashSet::new) .insert(order.uuid); + self.streaming_manager + .send_fn(&OrderbookStreamer::derive_streamer_id(&order.base, &order.rel), || { + OrderbookItemChangeEvent::NewOrUpdatedItem(Box::new(order.clone().into())) + }) + .ok(); self.order_set.insert(order.uuid, order); } @@ -2657,13 +2742,25 @@ impl Orderbook { }, }; - if pubkey_state.order_pairs_trie_state_history.get(&alb_ordered).is_some() { - let history = pair_history_mut(&mut pubkey_state.order_pairs_trie_state_history, &alb_ordered); + let _ = pubkey_state + .order_pairs_trie_state_history + .update_expiration_status(alb_ordered.clone(), Duration::from_secs(TRIE_STATE_HISTORY_TIMEOUT)); + + if let Some(history) = pubkey_state + .order_pairs_trie_state_history + .get_mut_unchecked(&alb_ordered) + { history.insert_new_diff(old_state, TrieDiff { delta: vec![(uuid, None)], next_root: *pair_state, }); } + + self.streaming_manager + .send_fn(&OrderbookStreamer::derive_streamer_id(&order.base, &order.rel), || { + OrderbookItemChangeEvent::RemovedItem(order.uuid) + }) + .ok(); Some(order) } @@ -2765,7 +2862,7 @@ pub fn init_ordermatch_context(ctx: &MmArc) -> OrdermatchInitResult<()> { let ordermatch_context = OrdermatchContext { maker_orders_ctx: PaMutex::new(MakerOrdersContext::new(ctx)?), my_taker_orders: Default::default(), - orderbook: Default::default(), + orderbook: PaMutex::new(Orderbook::new(ctx.event_stream_manager.clone())), pending_maker_reserved: Default::default(), orderbook_tickers, original_tickers, @@ -2795,7 +2892,7 @@ impl OrdermatchContext { Ok(OrdermatchContext { maker_orders_ctx: PaMutex::new(try_s!(MakerOrdersContext::new(ctx))), my_taker_orders: Default::default(), - orderbook: Default::default(), + orderbook: PaMutex::new(Orderbook::new(ctx.event_stream_manager.clone())), pending_maker_reserved: Default::default(), orderbook_tickers: Default::default(), original_tickers: Default::default(), @@ -2892,6 +2989,25 @@ impl MakerOrdersContext { fn balance_loop_exists(&mut self, ticker: &str) -> bool { self.balance_loops.lock().contains(ticker).unwrap() } } +struct LegacySwapParams<'a> { + maker_coin: &'a MmCoinEnum, + taker_coin: &'a MmCoinEnum, + uuid: &'a Uuid, + my_conf_settings: &'a SwapConfirmationsSettings, + my_persistent_pub: &'a H264, + maker_amount: &'a MmNumber, + taker_amount: &'a MmNumber, + locktime: &'a u64, +} +struct StateMachineParams<'a> { + secret_hash_algo: &'a SecretHashAlgo, + uuid: &'a Uuid, + my_conf_settings: &'a SwapConfirmationsSettings, + locktime: &'a u64, + maker_amount: &'a MmNumber, + taker_amount: &'a MmNumber, +} + #[cfg_attr(test, mockable)] fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerOrder, taker_p2p_pubkey: PublicKey) { let spawner = ctx.spawner(); @@ -2922,14 +3038,14 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO return; }, }; - let alice = bits256::from(maker_match.request.sender_pubkey.0); + let taker_pubkey = bits256::from(maker_match.request.sender_pubkey.0); let maker_amount = maker_match.reserved.get_base_amount().clone(); let taker_amount = maker_match.reserved.get_rel_amount().clone(); // lp_connect_start_bob is called only from process_taker_connect, which returns if CryptoCtx is not initialized let crypto_ctx = CryptoCtx::from_ctx(&ctx).expect("'CryptoCtx' must be initialized already"); let raw_priv = crypto_ctx.mm2_internal_privkey_secret(); - let my_persistent_pub = compressed_pub_key_from_priv_raw(raw_priv.as_slice(), ChecksumType::DSHA256).unwrap(); + let my_persistent_pub = compressed_pub_key_from_priv_raw(&raw_priv.take(), ChecksumType::DSHA256).unwrap(); let my_conf_settings = choose_maker_confs_and_notas( maker_order.conf_settings.clone(), @@ -2967,8 +3083,6 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO uuid ); - let now = now_sec(); - let secret = match generate_secret() { Ok(s) => s.into(), Err(e) => { @@ -2977,72 +3091,65 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO }, }; - if ctx.use_trading_proto_v2() { - let secret_hash_algo = detect_secret_hash_algo(&maker_coin, &taker_coin); - match (maker_coin, taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { - let mut maker_swap_state_machine = MakerSwapStateMachine { - storage: MakerSwapStorage::new(ctx.clone()), - abortable_system: ctx - .abortable_system - .create_subsystem() - .expect("create_subsystem should not fail"), - ctx, - started_at: now_sec(), - maker_coin: m.clone(), - maker_volume: maker_amount, - secret, - taker_coin: t.clone(), - dex_fee: dex_fee_amount_from_taker_coin(&t, m.ticker(), &taker_amount), - taker_volume: taker_amount, - taker_premium: Default::default(), - conf_settings: my_conf_settings, - p2p_topic: swap_v2_topic(&uuid), - uuid, - p2p_keypair: maker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), - secret_hash_algo, - lock_duration: lock_time, - taker_p2p_pubkey: match taker_p2p_pubkey { - PublicKey::Secp256k1(pubkey) => pubkey.into(), - }, - }; - #[allow(clippy::box_default)] - maker_swap_state_machine - .run(Box::new(maker_swap_v2::Initialize::default())) - .await - .error_log(); - }, - _ => todo!("implement fallback to the old protocol here"), - } - } else { - if let Err(e) = insert_new_swap_to_db( - ctx.clone(), - maker_coin.ticker(), - taker_coin.ticker(), - uuid, - now, - LEGACY_SWAP_TYPE, - ) - .await - { - error!("Error {} on new swap insertion", e); - } - let maker_swap = MakerSwap::new( - ctx.clone(), - alice, - maker_amount.to_decimal(), - taker_amount.to_decimal(), - my_persistent_pub, - uuid, - Some(maker_order.uuid), - my_conf_settings, - maker_coin, - taker_coin, - lock_time, - maker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), - secret, - ); - run_maker_swap(RunMakerSwapInput::StartNew(maker_swap), ctx).await; + let alice_swap_v = maker_match.request.swap_version; + let bob_swap_v = maker_order.swap_version; + + // Start a legacy swap if either the taker or maker uses the legacy swap protocol (version 1) + if alice_swap_v.is_legacy() || bob_swap_v.is_legacy() { + let params = LegacySwapParams { + maker_coin: &maker_coin, + taker_coin: &taker_coin, + uuid: &uuid, + my_conf_settings: &my_conf_settings, + my_persistent_pub: &my_persistent_pub, + maker_amount: &maker_amount, + taker_amount: &taker_amount, + locktime: &lock_time, + }; + start_maker_legacy_swap(&ctx, maker_order, taker_pubkey, secret, params).await; + return; + } + + // Ensure detect_secret_hash_algo_v2 returns the correct secret hash algorithm when adding new coin support in TPU. + let params = StateMachineParams { + secret_hash_algo: &detect_secret_hash_algo_v2(&maker_coin, &taker_coin), + uuid: &uuid, + my_conf_settings: &my_conf_settings, + locktime: &lock_time, + maker_amount: &maker_amount, + taker_amount: &taker_amount, + }; + let taker_p2p_pubkey = match taker_p2p_pubkey { + PublicKey::Secp256k1(pubkey) => pubkey.into(), + }; + + // TODO try to handle it more gracefully during project redesign + match (&maker_coin, &taker_coin) { + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + start_maker_swap_state_machine(&ctx, &maker_order, &taker_p2p_pubkey, &secret, m, t, ¶ms).await; + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + start_maker_swap_state_machine(&ctx, &maker_order, &taker_p2p_pubkey, &secret, m, t, ¶ms).await; + }, + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + start_maker_swap_state_machine(&ctx, &maker_order, &taker_p2p_pubkey, &secret, m, t, ¶ms).await; + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + start_maker_swap_state_machine(&ctx, &maker_order, &taker_p2p_pubkey, &secret, m, t, ¶ms).await; + }, + _ => { + let params = LegacySwapParams { + maker_coin: &maker_coin, + taker_coin: &taker_coin, + uuid: &uuid, + my_conf_settings: &my_conf_settings, + my_persistent_pub: &my_persistent_pub, + maker_amount: &maker_amount, + taker_amount: &taker_amount, + locktime: &lock_time, + }; + start_maker_legacy_swap(&ctx, maker_order, taker_pubkey, secret, params).await + }, } }; @@ -3050,13 +3157,94 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO spawner.spawn_with_settings(fut, settings); } +async fn start_maker_legacy_swap( + ctx: &MmArc, + maker_order: MakerOrder, + taker_pubkey: bits256, + secret: H256, + params: LegacySwapParams<'_>, +) { + if let Err(e) = insert_new_swap_to_db( + ctx.clone(), + params.maker_coin.ticker(), + params.taker_coin.ticker(), + *params.uuid, + now_sec(), + LEGACY_SWAP_TYPE, + ) + .await + { + error!("Error {} on new swap insertion", e); + } + + let maker_swap = MakerSwap::new( + ctx.clone(), + taker_pubkey, + params.maker_amount.to_decimal(), + params.taker_amount.to_decimal(), + *params.my_persistent_pub, + *params.uuid, + Some(maker_order.uuid), + *params.my_conf_settings, + params.maker_coin.clone(), + params.taker_coin.clone(), + *params.locktime, + maker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), + secret, + ); + run_maker_swap(RunMakerSwapInput::StartNew(maker_swap), ctx.clone()).await; +} + +async fn start_maker_swap_state_machine< + MakerCoin: MmCoin + MakerCoinSwapOpsV2 + Clone, + TakerCoin: MmCoin + TakerCoinSwapOpsV2 + Clone, +>( + ctx: &MmArc, + maker_order: &MakerOrder, + taker_p2p_pubkey: &Secp256k1Pubkey, + secret: &H256, + maker_coin: &MakerCoin, + taker_coin: &TakerCoin, + params: &StateMachineParams<'_>, +) { + let mut maker_swap_state_machine = MakerSwapStateMachine { + storage: MakerSwapStorage::new(ctx.clone()), + abortable_system: ctx + .abortable_system + .create_subsystem() + .expect("create_subsystem should not fail"), + ctx: ctx.clone(), + started_at: now_sec(), + maker_coin: maker_coin.clone(), + maker_volume: params.maker_amount.clone(), + secret: *secret, + taker_coin: taker_coin.clone(), + taker_volume: params.taker_amount.clone(), + taker_premium: Default::default(), + conf_settings: *params.my_conf_settings, + p2p_topic: swap_v2_topic(params.uuid), + uuid: *params.uuid, + p2p_keypair: maker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), + secret_hash_algo: *params.secret_hash_algo, + lock_duration: *params.locktime, + taker_p2p_pubkey: *taker_p2p_pubkey, + require_taker_payment_spend_confirm: true, + swap_version: maker_order.swap_version.version, + }; + #[allow(clippy::box_default)] + maker_swap_state_machine + .run(Box::new(maker_swap_v2::Initialize::default())) + .await + .error_log(); +} + fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMatch, maker_p2p_pubkey: PublicKey) { let spawner = ctx.spawner(); let uuid = taker_match.reserved.taker_order_uuid; let fut = async move { // aka "taker_loop" - let maker = bits256::from(taker_match.reserved.sender_pubkey.0); + let maker_pubkey = bits256::from(taker_match.reserved.sender_pubkey.0); let taker_coin_ticker = taker_order.taker_coin_ticker(); let taker_coin = match lp_coinfind(&ctx, taker_coin_ticker).await { Ok(Some(c)) => c, @@ -3086,7 +3274,7 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat // lp_connected_alice is called only from process_maker_connected, which returns if CryptoCtx is not initialized let crypto_ctx = CryptoCtx::from_ctx(&ctx).expect("'CryptoCtx' must be initialized already"); let raw_priv = crypto_ctx.mm2_internal_privkey_secret(); - let my_persistent_pub = compressed_pub_key_from_priv_raw(raw_priv.as_slice(), ChecksumType::DSHA256).unwrap(); + let my_persistent_pub = compressed_pub_key_from_priv_raw(&raw_priv.take(), ChecksumType::DSHA256).unwrap(); let maker_amount = taker_match.reserved.get_base_amount().clone(); let taker_amount = taker_match.reserved.get_rel_amount().clone(); @@ -3127,86 +3315,77 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat uuid ); - let now = now_sec(); - if ctx.use_trading_proto_v2() { - let taker_secret = match generate_secret() { - Ok(s) => s.into(), - Err(e) => { - error!("Error {} on secret generation", e); - return; - }, + let bob_swap_v = taker_match.reserved.swap_version; + let alice_swap_v = taker_order.request.swap_version; + + // Start a legacy swap if either the maker or taker uses the legacy swap protocol (version 1) + if bob_swap_v.is_legacy() || alice_swap_v.is_legacy() { + let params = LegacySwapParams { + maker_coin: &maker_coin, + taker_coin: &taker_coin, + uuid: &uuid, + my_conf_settings: &my_conf_settings, + my_persistent_pub: &my_persistent_pub, + maker_amount: &maker_amount, + taker_amount: &taker_amount, + locktime: &locktime, }; - let secret_hash_algo = detect_secret_hash_algo(&maker_coin, &taker_coin); - match (maker_coin, taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { - let mut taker_swap_state_machine = TakerSwapStateMachine { - storage: TakerSwapStorage::new(ctx.clone()), - abortable_system: ctx - .abortable_system - .create_subsystem() - .expect("create_subsystem should not fail"), - ctx, - started_at: now, - lock_duration: locktime, - maker_coin: m.clone(), - maker_volume: maker_amount, - taker_coin: t.clone(), - dex_fee: dex_fee_amount_from_taker_coin(&t, maker_coin_ticker, &taker_amount), - taker_volume: taker_amount, - taker_premium: Default::default(), - secret_hash_algo, - conf_settings: my_conf_settings, - p2p_topic: swap_v2_topic(&uuid), - uuid, - p2p_keypair: taker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), - taker_secret, - maker_p2p_pubkey: match maker_p2p_pubkey { - PublicKey::Secp256k1(pubkey) => pubkey.into(), - }, - require_maker_payment_confirm_before_funding_spend: true, - }; - #[allow(clippy::box_default)] - taker_swap_state_machine - .run(Box::new(taker_swap_v2::Initialize::default())) - .await - .error_log(); - }, - _ => todo!("implement fallback to the old protocol here"), - } - } else { - #[cfg(any(test, feature = "run-docker-tests"))] - let fail_at = std::env::var("TAKER_FAIL_AT").map(FailAt::from).ok(); + start_taker_legacy_swap(&ctx, taker_order, maker_pubkey, params).await; + return; + } - if let Err(e) = insert_new_swap_to_db( - ctx.clone(), - taker_coin.ticker(), - maker_coin.ticker(), - uuid, - now, - LEGACY_SWAP_TYPE, - ) - .await - { - error!("Error {} on new swap insertion", e); - } + let taker_secret = match generate_secret() { + Ok(s) => s.into(), + Err(e) => { + error!("Error {} on secret generation", e); + return; + }, + }; - let taker_swap = TakerSwap::new( - ctx.clone(), - maker, - maker_amount, - taker_amount, - my_persistent_pub, - uuid, - Some(uuid), - my_conf_settings, - maker_coin, - taker_coin, - locktime, - taker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), - #[cfg(any(test, feature = "run-docker-tests"))] - fail_at, - ); - run_taker_swap(RunTakerSwapInput::StartNew(taker_swap), ctx).await + // Ensure detect_secret_hash_algo_v2 returns the correct secret hash algorithm when adding new coin support in TPU. + let params = StateMachineParams { + secret_hash_algo: &detect_secret_hash_algo_v2(&maker_coin, &taker_coin), + uuid: &uuid, + my_conf_settings: &my_conf_settings, + locktime: &locktime, + maker_amount: &maker_amount, + taker_amount: &taker_amount, + }; + let maker_p2p_pubkey = match maker_p2p_pubkey { + PublicKey::Secp256k1(pubkey) => pubkey.into(), + }; + + // TODO try to handle it more gracefully during project redesign + match (&maker_coin, &taker_coin) { + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + start_taker_swap_state_machine(&ctx, &taker_order, &maker_p2p_pubkey, &taker_secret, m, t, ¶ms) + .await; + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + start_taker_swap_state_machine(&ctx, &taker_order, &maker_p2p_pubkey, &taker_secret, m, t, ¶ms) + .await; + }, + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + start_taker_swap_state_machine(&ctx, &taker_order, &maker_p2p_pubkey, &taker_secret, m, t, ¶ms) + .await; + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + start_taker_swap_state_machine(&ctx, &taker_order, &maker_p2p_pubkey, &taker_secret, m, t, ¶ms) + .await; + }, + _ => { + let params = LegacySwapParams { + maker_coin: &maker_coin, + taker_coin: &taker_coin, + uuid: &uuid, + my_conf_settings: &my_conf_settings, + my_persistent_pub: &my_persistent_pub, + maker_amount: &maker_amount, + taker_amount: &taker_amount, + locktime: &locktime, + }; + start_taker_legacy_swap(&ctx, taker_order, maker_pubkey, params).await; + }, } }; @@ -3214,6 +3393,91 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat spawner.spawn_with_settings(fut, settings) } +async fn start_taker_legacy_swap( + ctx: &MmArc, + taker_order: TakerOrder, + maker_pubkey: bits256, + params: LegacySwapParams<'_>, +) { + #[cfg(any(test, feature = "run-docker-tests"))] + let fail_at = std::env::var("TAKER_FAIL_AT").map(FailAt::from).ok(); + + if let Err(e) = insert_new_swap_to_db( + ctx.clone(), + params.taker_coin.ticker(), + params.maker_coin.ticker(), + *params.uuid, + now_sec(), + LEGACY_SWAP_TYPE, + ) + .await + { + error!("Error {} on new swap insertion", e); + } + + let taker_swap = TakerSwap::new( + ctx.clone(), + maker_pubkey, + params.maker_amount.clone(), + params.taker_amount.clone(), + *params.my_persistent_pub, + *params.uuid, + Some(*params.uuid), + *params.my_conf_settings, + params.maker_coin.clone(), + params.taker_coin.clone(), + *params.locktime, + taker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), + #[cfg(any(test, feature = "run-docker-tests"))] + fail_at, + ); + run_taker_swap(RunTakerSwapInput::StartNew(taker_swap), ctx.clone()).await +} + +async fn start_taker_swap_state_machine< + MakerCoin: MmCoin + MakerCoinSwapOpsV2 + Clone, + TakerCoin: MmCoin + TakerCoinSwapOpsV2 + Clone, +>( + ctx: &MmArc, + taker_order: &TakerOrder, + maker_p2p_pubkey: &Secp256k1Pubkey, + taker_secret: &H256, + maker_coin: &MakerCoin, + taker_coin: &TakerCoin, + params: &StateMachineParams<'_>, +) { + let mut taker_swap_state_machine = TakerSwapStateMachine { + storage: TakerSwapStorage::new(ctx.clone()), + abortable_system: ctx + .abortable_system + .create_subsystem() + .expect("create_subsystem should not fail"), + ctx: ctx.clone(), + started_at: now_sec(), + lock_duration: *params.locktime, + maker_coin: maker_coin.clone(), + maker_volume: params.maker_amount.clone(), + taker_coin: taker_coin.clone(), + taker_volume: params.taker_amount.clone(), + taker_premium: Default::default(), + secret_hash_algo: *params.secret_hash_algo, + conf_settings: *params.my_conf_settings, + p2p_topic: swap_v2_topic(params.uuid), + uuid: *params.uuid, + p2p_keypair: taker_order.p2p_privkey.map(SerializableSecp256k1Keypair::into_inner), + taker_secret: *taker_secret, + maker_p2p_pubkey: *maker_p2p_pubkey, + require_maker_payment_confirm_before_funding_spend: true, + require_maker_payment_spend_confirm: true, + swap_version: taker_order.request.swap_version.version, + }; + #[allow(clippy::box_default)] + taker_swap_state_machine + .run(Box::new(taker_swap_v2::Initialize::default())) + .await + .error_log(); +} + pub async fn lp_ordermatch_loop(ctx: MmArc) { // lp_ordermatch_loop is spawned only if CryptoCtx is initialized let my_pubsecp = CryptoCtx::from_ctx(&ctx) @@ -3569,6 +3833,13 @@ async fn process_maker_reserved(ctx: MmArc, from_pubkey: H256Json, reserved_msg: connected: None, last_updated: now_ms(), }; + + ctx.event_stream_manager + .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + OrderStatusEvent::TakerMatch(taker_match.clone()) + }) + .ok(); + my_order .matches .insert(taker_match.reserved.maker_order_uuid, taker_match); @@ -3617,6 +3888,13 @@ async fn process_maker_connected(ctx: MmArc, from_pubkey: PublicKey, connected: error!("Connected message sender pubkey != reserved message sender pubkey"); return; } + + ctx.event_stream_manager + .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + OrderStatusEvent::TakerConnected(order_match.clone()) + }) + .ok(); + // alice lp_connected_alice( ctx.clone(), @@ -3712,6 +3990,7 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: }), base_protocol_info: Some(base_coin.coin_protocol_info(None)), rel_protocol_info: Some(rel_coin.coin_protocol_info(Some(rel_amount.clone()))), + swap_version: order.swap_version, }; let topic = order.orderbook_topic(); log::debug!("Request matched sending reserved {:?}", reserved); @@ -3723,6 +4002,13 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: connected: None, last_updated: now_ms(), }; + + ctx.event_stream_manager + .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + OrderStatusEvent::MakerMatch(maker_match.clone()) + }) + .ok(); + order.matches.insert(maker_match.request.uuid, maker_match); storage .update_active_maker_order(&order) @@ -3787,6 +4073,13 @@ async fn process_taker_connect(ctx: MmArc, sender_pubkey: PublicKey, connect_msg order_match.connect = Some(connect_msg); order_match.connected = Some(connected.clone()); let order_match = order_match.clone(); + + ctx.event_stream_manager + .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + OrderStatusEvent::MakerConnected(order_match.clone()) + }) + .ok(); + my_order.started_swaps.push(order_match.request.uuid); lp_connect_start_bob(ctx.clone(), order_match, my_order.clone(), sender_pubkey); let topic = my_order.orderbook_topic(); @@ -3870,7 +4163,7 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { /// Created when maker order is matched with taker request #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -struct MakerMatch { +pub struct MakerMatch { request: TakerRequest, reserved: MakerReserved, connect: Option, @@ -3880,7 +4173,7 @@ struct MakerMatch { /// Created upon taker request broadcast #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -struct TakerMatch { +pub struct TakerMatch { reserved: MakerReserved, connect: TakerConnect, connected: Option, @@ -3945,6 +4238,10 @@ pub async fn lp_auto_buy( .with_save_in_history(input.save_in_history) .with_base_orderbook_ticker(ordermatch_ctx.orderbook_ticker(base_coin.ticker())) .with_rel_orderbook_ticker(ordermatch_ctx.orderbook_ticker(rel_coin.ticker())); + if !ctx.use_trading_proto_v2() { + order_builder.set_legacy_swap_v(); + } + if let Some(timeout) = input.timeout { order_builder = order_builder.with_timeout(timeout); } @@ -3981,7 +4278,7 @@ pub async fn lp_auto_buy( /// Orderbook Item P2P message /// DO NOT CHANGE - it will break backwards compatibility #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -struct OrderbookP2PItem { +pub struct OrderbookP2PItem { pubkey: String, base: String, rel: String, @@ -4682,7 +4979,7 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result Result Result>, response.entry(coin.clone()).or_insert_with(Vec::new).push(entry); } } + } else { + return Err("No response from any peer".to_string()); } let res = json!({ "result": response, "original_tickers": &ordermatch_ctx.original_tickers }); @@ -388,6 +390,8 @@ pub async fn best_orders_rpc_v2( orders.entry(coin.clone()).or_insert_with(Vec::new).push(entry); } } + } else { + return MmError::err(BestOrdersRpcError::P2PError("No response from any peer".to_string())); } Ok(BestOrdersV2Response { 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 05322325a5..1847720416 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs @@ -695,6 +695,7 @@ mod tests { use super::*; use crate::lp_ordermatch::ordermatch_wasm_db::{ItemId, MyFilteringHistoryOrdersTable}; use crate::lp_ordermatch::{OrdermatchContext, TakerRequest}; + use crate::swap_versioning::SwapVersion; use common::{new_uuid, now_ms}; use futures::compat::Future01CompatExt; use itertools::Itertools; @@ -724,6 +725,7 @@ mod tests { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), } } @@ -742,6 +744,7 @@ mod tests { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }, matches: HashMap::new(), created_at: now_ms(), diff --git a/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs b/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs index 72aa53597d..745b3933f6 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs @@ -1,3 +1,5 @@ +use crate::lp_ordermatch::{AlbOrderedOrderbookPair, H64}; +use crate::swap_versioning::SwapVersion; use common::now_sec; use compact_uuid::CompactUuid; use mm2_number::{BigRational, MmNumber}; @@ -5,8 +7,6 @@ use mm2_rpc::data::legacy::{MatchBy as SuperMatchBy, OrderConfirmationsSettings, use std::collections::{HashMap, HashSet}; use uuid::Uuid; -use crate::lp_ordermatch::{AlbOrderedOrderbookPair, H64}; - #[derive(Debug, Deserialize, Serialize)] #[allow(clippy::large_enum_variant)] pub enum OrdermatchMessage { @@ -31,6 +31,7 @@ impl From for OrdermatchMessage { /// MsgPack compact representation does not work with tagged enums (encoding works, but decoding fails) /// This is untagged representation also using compact Uuid representation #[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(Eq, PartialEq))] pub enum MatchBy { Any, Orders(HashSet), @@ -250,6 +251,7 @@ impl MakerOrderUpdated { } #[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(Eq, PartialEq))] pub struct TakerRequest { pub base: String, pub rel: String, @@ -259,15 +261,16 @@ pub struct TakerRequest { pub uuid: CompactUuid, pub match_by: MatchBy, pub conf_settings: OrderConfirmationsSettings, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub base_protocol_info: Option>, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub rel_protocol_info: Option>, + #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] + pub swap_version: SwapVersion, } #[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(test, derive(Eq, PartialEq))] pub struct MakerReserved { pub base: String, pub rel: String, @@ -276,12 +279,12 @@ pub struct MakerReserved { pub taker_order_uuid: CompactUuid, pub maker_order_uuid: CompactUuid, pub conf_settings: OrderConfirmationsSettings, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub base_protocol_info: Option>, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub rel_protocol_info: Option>, + #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] + pub swap_version: SwapVersion, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -422,4 +425,156 @@ mod new_protocol_tests { let new_serialized = rmp_serde::to_vec_named(&new).unwrap(); let _old_from_new: MakerOrderCreatedV1 = rmp_serde::from_slice(&new_serialized).unwrap(); } + + #[test] + fn test_old_new_taker_request_rmp() { + // Old TakerRequest didn't have swap_version field + #[derive(Debug, Eq, Serialize, Deserialize, PartialEq)] + struct OldTakerRequest { + base: String, + rel: String, + base_amount: BigRational, + rel_amount: BigRational, + action: TakerAction, + uuid: CompactUuid, + match_by: MatchBy, + conf_settings: OrderConfirmationsSettings, + #[serde(default, skip_serializing_if = "Option::is_none")] + base_protocol_info: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + rel_protocol_info: Option>, + } + + let old_instance = OldTakerRequest { + base: "BTC".to_string(), + rel: "ETH".to_string(), + base_amount: BigRational::from_integer(1.into()), + rel_amount: BigRational::from_integer(50.into()), + action: TakerAction::Buy, + uuid: CompactUuid::from(Uuid::new_v4()), + match_by: MatchBy::Any, + conf_settings: OrderConfirmationsSettings::default(), + base_protocol_info: Some(vec![1u8; 10]), + rel_protocol_info: Some(vec![2u8; 10]), + }; + + // ------------------------------------------ + // Step 1: Test Deserialization from Old Format + // ------------------------------------------ + let old_serialized = rmp_serde::to_vec_named(&old_instance).expect("Old MessagePack serialization failed"); + let new_instance: TakerRequest = + rmp_serde::from_slice(&old_serialized).expect("Deserialization into new TakerRequest failed"); + + assert_eq!(new_instance.base, old_instance.base); + assert_eq!(new_instance.rel, old_instance.rel); + assert_eq!(new_instance.base_amount, old_instance.base_amount); + assert_eq!(new_instance.rel_amount, old_instance.rel_amount); + assert_eq!(new_instance.action, old_instance.action); + assert_eq!(new_instance.uuid, old_instance.uuid); + assert_eq!(new_instance.match_by, old_instance.match_by); + assert_eq!(new_instance.conf_settings, old_instance.conf_settings); + assert_eq!(new_instance.base_protocol_info, old_instance.base_protocol_info); + assert_eq!(new_instance.rel_protocol_info, old_instance.rel_protocol_info); + assert_eq!(new_instance.swap_version, SwapVersion::default()); // Default swap_version + + // ------------------------------------------ + // Step 2: Test Serialization from New Format to Old Format + // ------------------------------------------ + let new_serialized = rmp_serde::to_vec_named(&new_instance).expect("Serialization of new type failed"); + let old_from_new: OldTakerRequest = + rmp_serde::from_slice(&new_serialized).expect("Old deserialization from new serialization failed"); + + assert_eq!(old_from_new.base, new_instance.base); + assert_eq!(old_from_new.rel, new_instance.rel); + assert_eq!(old_from_new.base_amount, new_instance.base_amount); + assert_eq!(old_from_new.rel_amount, new_instance.rel_amount); + assert_eq!(old_from_new.action, new_instance.action); + assert_eq!(old_from_new.uuid, new_instance.uuid); + assert_eq!(old_from_new.match_by, new_instance.match_by); + assert_eq!(old_from_new.conf_settings, new_instance.conf_settings); + assert_eq!(old_from_new.base_protocol_info, new_instance.base_protocol_info); + assert_eq!(old_from_new.rel_protocol_info, new_instance.rel_protocol_info); + + // ------------------------------------------ + // Step 3: Round-Trip Test of the New Format + // ------------------------------------------ + let rt_serialized = rmp_serde::to_vec_named(&new_instance).expect("Round-trip serialization failed"); + let round_trip: TakerRequest = + rmp_serde::from_slice(&rt_serialized).expect("Round-trip deserialization failed"); + assert_eq!(round_trip, new_instance); + } + + #[test] + fn test_old_new_maker_reserved_rmp() { + // Old MakerReserved didnt have swap_version field + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + struct OldMakerReserved { + base: String, + rel: String, + base_amount: BigRational, + rel_amount: BigRational, + taker_order_uuid: CompactUuid, + maker_order_uuid: CompactUuid, + conf_settings: OrderConfirmationsSettings, + #[serde(default, skip_serializing_if = "Option::is_none")] + base_protocol_info: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + rel_protocol_info: Option>, + } + + let old_instance = OldMakerReserved { + base: "BTC".to_string(), + rel: "ETH".to_string(), + base_amount: BigRational::from_integer(1.into()), + rel_amount: BigRational::from_integer(50.into()), + taker_order_uuid: CompactUuid::from(Uuid::new_v4()), + maker_order_uuid: CompactUuid::from(Uuid::new_v4()), + conf_settings: OrderConfirmationsSettings::default(), + base_protocol_info: Some(vec![1u8; 10]), + rel_protocol_info: Some(vec![2u8; 10]), + }; + + // ------------------------------------------ + // Step 1: Test Deserialization from Old Format + // ------------------------------------------ + let old_serialized = rmp_serde::to_vec_named(&old_instance).expect("Old MessagePack serialization failed"); + let new_instance: MakerReserved = + rmp_serde::from_slice(&old_serialized).expect("Deserialization into new MakerReserved failed"); + + assert_eq!(new_instance.base, old_instance.base); + assert_eq!(new_instance.rel, old_instance.rel); + assert_eq!(new_instance.base_amount, old_instance.base_amount); + assert_eq!(new_instance.rel_amount, old_instance.rel_amount); + assert_eq!(new_instance.taker_order_uuid, old_instance.taker_order_uuid); + assert_eq!(new_instance.maker_order_uuid, old_instance.maker_order_uuid); + assert_eq!(new_instance.conf_settings, old_instance.conf_settings); + assert_eq!(new_instance.base_protocol_info, old_instance.base_protocol_info); + assert_eq!(new_instance.rel_protocol_info, old_instance.rel_protocol_info); + assert_eq!(new_instance.swap_version, SwapVersion::default()); // Default swap_version + + // ------------------------------------------ + // Step 2: Test Serialization from New Format to Old Format + // ------------------------------------------ + let new_serialized = rmp_serde::to_vec_named(&new_instance).expect("Serialization of new type failed"); + let old_from_new: OldMakerReserved = + rmp_serde::from_slice(&new_serialized).expect("Old deserialization from new serialization failed"); + + assert_eq!(old_from_new.base, new_instance.base); + assert_eq!(old_from_new.rel, new_instance.rel); + assert_eq!(old_from_new.base_amount, new_instance.base_amount); + assert_eq!(old_from_new.rel_amount, new_instance.rel_amount); + assert_eq!(old_from_new.taker_order_uuid, new_instance.taker_order_uuid); + assert_eq!(old_from_new.maker_order_uuid, new_instance.maker_order_uuid); + assert_eq!(old_from_new.conf_settings, new_instance.conf_settings); + assert_eq!(old_from_new.base_protocol_info, new_instance.base_protocol_info); + assert_eq!(old_from_new.rel_protocol_info, new_instance.rel_protocol_info); + + // ------------------------------------------ + // Step 3: Round-Trip Test of the New Format + // ------------------------------------------ + let rt_serialized = rmp_serde::to_vec_named(&new_instance).expect("Round-trip serialization failed"); + let round_trip: MakerReserved = + rmp_serde::from_slice(&rt_serialized).expect("Round-trip deserialization failed"); + assert_eq!(round_trip, new_instance); + } } diff --git a/mm2src/mm2_main/src/lp_ordermatch/order_events.rs b/mm2src/mm2_main/src/lp_ordermatch/order_events.rs new file mode 100644 index 0000000000..547ee7df4e --- /dev/null +++ b/mm2src/mm2_main/src/lp_ordermatch/order_events.rs @@ -0,0 +1,49 @@ +use super::{MakerMatch, TakerMatch}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::StreamExt; + +pub struct OrderStatusStreamer; + +impl OrderStatusStreamer { + #[inline(always)] + pub fn new() -> Self { Self } + + #[inline(always)] + pub const fn derive_streamer_id() -> &'static str { "ORDER_STATUS" } +} + +#[derive(Serialize)] +#[serde(tag = "order_type", content = "order_data")] +pub enum OrderStatusEvent { + MakerMatch(MakerMatch), + TakerMatch(TakerMatch), + MakerConnected(MakerMatch), + TakerConnected(TakerMatch), +} + +#[async_trait] +impl EventStreamer for OrderStatusStreamer { + type DataInType = OrderStatusEvent; + + fn streamer_id(&self) -> String { Self::derive_streamer_id().to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + while let Some(order_data) = data_rx.next().await { + let event_data = serde_json::to_value(order_data).expect("Serialization shouldn't fail."); + let event = Event::new(self.streamer_id(), event_data); + broadcaster.broadcast(event); + } + } +} diff --git a/mm2src/mm2_main/src/lp_ordermatch/order_requests_tracker.rs b/mm2src/mm2_main/src/lp_ordermatch/order_requests_tracker.rs index 0c9a8d3bdd..66a0ba6fef 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/order_requests_tracker.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/order_requests_tracker.rs @@ -1,5 +1,5 @@ #![allow(dead_code)] -use instant::Instant; +use compatible_time::Instant; use std::{collections::hash_map::{HashMap, RawEntryMut}, num::NonZeroUsize, time::Duration}; diff --git a/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs b/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs new file mode 100644 index 0000000000..f7149bd05e --- /dev/null +++ b/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs @@ -0,0 +1,90 @@ +use super::{orderbook_topic_from_base_rel, subscribe_to_orderbook_topic, OrderbookP2PItem}; +use coins::{is_wallet_only_ticker, lp_coinfind}; +use mm2_core::mm_ctx::MmArc; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::StreamExt; +use uuid::Uuid; + +pub struct OrderbookStreamer { + ctx: MmArc, + base: String, + rel: String, +} + +impl OrderbookStreamer { + pub fn new(ctx: MmArc, base: String, rel: String) -> Self { Self { ctx, base, rel } } + + pub fn derive_streamer_id(base: &str, rel: &str) -> String { + format!("ORDERBOOK_UPDATE/{}", orderbook_topic_from_base_rel(base, rel)) + } +} + +#[derive(Serialize)] +#[serde(tag = "order_type", content = "order_data")] +pub enum OrderbookItemChangeEvent { + // NOTE(clippy): This is box-ed due to in-balance of the size of enum variants. + /// New or updated orderbook item. + NewOrUpdatedItem(Box), + /// Removed orderbook item (only UUID is relevant in this case). + RemovedItem(Uuid), +} + +#[async_trait] +impl EventStreamer for OrderbookStreamer { + type DataInType = OrderbookItemChangeEvent; + + fn streamer_id(&self) -> String { Self::derive_streamer_id(&self.base, &self.rel) } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; + if let Err(err) = sanity_checks(&self.ctx, &self.base, &self.rel).await { + ready_tx.send(Err(err.clone())).expect(RECEIVER_DROPPED_MSG); + panic!("{}", err); + } + // We need to subscribe to the orderbook, otherwise we won't get any updates from the P2P network. + if let Err(err) = subscribe_to_orderbook_topic(&self.ctx, &self.base, &self.rel, false).await { + let err = format!("Subscribing to orderbook topic failed: {err:?}"); + ready_tx.send(Err(err.clone())).expect(RECEIVER_DROPPED_MSG); + panic!("{}", err); + } + ready_tx.send(Ok(())).expect(RECEIVER_DROPPED_MSG); + + while let Some(orderbook_update) = data_rx.next().await { + let event_data = serde_json::to_value(orderbook_update).expect("Serialization shouldn't fail."); + let event = Event::new(self.streamer_id(), event_data); + broadcaster.broadcast(event); + } + } +} + +async fn sanity_checks(ctx: &MmArc, base: &str, rel: &str) -> Result<(), String> { + // TODO: This won't work with no-login mode. + lp_coinfind(ctx, base) + .await + .map_err(|e| format!("Coin {base} not found: {e}"))?; + if is_wallet_only_ticker(ctx, base) { + return Err(format!("Coin {base} is wallet-only.")); + } + lp_coinfind(ctx, rel) + .await + .map_err(|e| format!("Coin {base} not found: {e}"))?; + if is_wallet_only_ticker(ctx, rel) { + return Err(format!("Coin {rel} is wallet-only.")); + } + Ok(()) +} + +impl Drop for OrderbookStreamer { + fn drop(&mut self) { + // TODO: Do we want to unsubscribe from the orderbook topic when streaming is dropped? + // Also, we seem to never unsubscribe from an orderbook topic after doing an orderbook RPC! + } +} diff --git a/mm2src/mm2_main/src/lp_stats.rs b/mm2src/mm2_main/src/lp_stats.rs index 185996ecd1..b4d908e65a 100644 --- a/mm2src/mm2_main/src/lp_stats.rs +++ b/mm2src/mm2_main/src/lp_stats.rs @@ -9,12 +9,13 @@ use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; use mm2_libp2p::application::request_response::network_info::NetworkInfoRequest; use mm2_libp2p::{encode_message, NetworkInfo, PeerId, RelayAddress, RelayAddressError}; +use mm2_net::ip_addr::ParseAddressError; use serde_json::{self as json, Value as Json}; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::sync::Arc; -use crate::lp_network::{add_reserved_peer_addresses, lp_network_ports, request_peers, NetIdError, ParseAddressError, - PeerDecodedResponse}; +use crate::lp_network::{add_reserved_peer_addresses, lp_network_ports, request_peers, NetIdError, PeerDecodedResponse}; use std::str::FromStr; pub type NodeVersionResult = Result>; @@ -126,8 +127,6 @@ pub async fn add_node_to_version_stat(_ctx: MmArc, _req: Json) -> NodeVersionRes /// Adds node info. to db to be used later for stats collection #[cfg(not(target_arch = "wasm32"))] pub async fn add_node_to_version_stat(ctx: MmArc, req: Json) -> NodeVersionResult { - use crate::lp_network::addr_to_ipv4_string; - let node_info: NodeInfo = json::from_value(req)?; // Check that the entered peer_id is valid @@ -136,7 +135,7 @@ pub async fn add_node_to_version_stat(ctx: MmArc, req: Json) -> NodeVersionResul .parse::() .map_to_mm(|e| NodeVersionError::PeerIdParseError(node_info.peer_id.clone(), e.to_string()))?; - let ipv4_addr = addr_to_ipv4_string(&node_info.address)?; + let ipv4_addr = mm2_net::ip_addr::addr_to_ipv4_string(&node_info.address)?; let node_info_with_ipv4_addr = NodeInfo { name: node_info.name, address: ipv4_addr, @@ -170,16 +169,24 @@ struct Mm2VersionRes { nodes: HashMap, } -fn process_get_version_request(ctx: MmArc) -> Result>, String> { +fn process_get_version_request(ctx: MmArc) -> Result, String> { let response = ctx.mm_version().to_string(); - let encoded = try_s!(encode_message(&response)); - Ok(Some(encoded)) + encode_message(&response).map_err(|e| e.to_string()) +} + +fn process_get_peer_utc_timestamp_request() -> Result, String> { + let timestamp = common::get_utc_timestamp(); + let timestamp: u64 = timestamp + .try_into() + .unwrap_or_else(|_| panic!("`common::get_utc_timestamp` returned invalid data: {}", timestamp)); + + encode_message(×tamp).map_err(|e| e.to_string()) } -pub fn process_info_request(ctx: MmArc, request: NetworkInfoRequest) -> Result>, String> { - log::debug!("Got stats request {:?}", request); +pub fn process_info_request(ctx: MmArc, request: NetworkInfoRequest) -> Result, String> { match request { NetworkInfoRequest::GetMm2Version => process_get_version_request(ctx), + NetworkInfoRequest::GetPeerUtcTimestamp => process_get_peer_utc_timestamp_request(), } } diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 0acb7fc443..b15806c059 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -59,39 +59,38 @@ use super::lp_network::P2PRequestResult; use crate::lp_network::{broadcast_p2p_msg, Libp2pPeerId, P2PProcessError, P2PProcessResult, P2PRequestError}; -use crate::lp_swap::maker_swap_v2::{MakerSwapStateMachine, MakerSwapStorage}; -use crate::lp_swap::taker_swap_v2::{TakerSwapStateMachine, TakerSwapStorage}; -use bitcrypto::{dhash160, sha256}; -use coins::{lp_coinfind, lp_coinfind_or_err, CoinFindError, DexFee, MmCoin, MmCoinEnum, TradeFee, TransactionEnum}; +use crate::lp_swap::maker_swap_v2::MakerSwapStorage; +use crate::lp_swap::taker_swap_v2::TakerSwapStorage; +use bitcrypto::sha256; +use coins::{lp_coinfind, lp_coinfind_or_err, CoinFindError, MmCoinEnum, TradeFee, TransactionEnum}; use common::log::{debug, warn}; use common::now_sec; -use common::time_cache::DuplicateCache; use common::{bits256, calc_total_pages, executor::{spawn_abortable, AbortOnDropHandle, SpawnFuture, Timer}, log::{error, info}, - var, HttpStatusCode, PagingOptions, StatusCode}; + HttpStatusCode, PagingOptions, StatusCode}; use derive_more::Display; use http::Response; use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; +use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; use mm2_libp2p::{decode_signed, encode_and_sign, pub_sub_topic, PeerId, TopicPrefix}; -use mm2_number::{BigDecimal, BigRational, MmNumber, MmNumberMultiRepr}; +use mm2_number::{BigDecimal, MmNumber, MmNumberMultiRepr}; use mm2_state_machine::storable_state_machine::StateMachineStorage; use parking_lot::Mutex as PaMutex; -use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264}; use secp256k1::{PublicKey, SecretKey, Signature}; use serde::Serialize; use serde_json::{self as json, Value as Json}; use std::collections::{HashMap, HashSet}; -use std::convert::TryFrom; use std::num::NonZeroUsize; use std::path::PathBuf; use std::str::FromStr; -use std::sync::{Arc, Mutex, Weak}; -use std::time::Duration; +use std::sync::{Arc, Mutex}; +use timed_map::{MapKind, TimedMap}; use uuid::Uuid; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use std::sync::atomic::{AtomicU64, Ordering}; mod check_balance; @@ -106,6 +105,7 @@ mod swap_lock; #[path = "lp_swap/komodefi.swap_v2.pb.rs"] #[rustfmt::skip] mod swap_v2_pb; +pub(crate) mod swap_events; mod swap_v2_common; pub(crate) mod swap_v2_rpcs; pub(crate) mod swap_watcher; @@ -117,7 +117,7 @@ mod trade_preimage; #[cfg(target_arch = "wasm32")] mod swap_wasm_db; pub use check_balance::{check_other_coin_balance_for_swap, CheckBalanceError, CheckBalanceResult}; -use coins::utxo::utxo_standard::UtxoStandardCoin; +use crypto::secret_hash_algo::SecretHashAlgo; use crypto::CryptoCtx; use keys::{KeyPair, SECP_SIGN, SECP_VERIFY}; use maker_swap::MakerSwapEvent; @@ -130,7 +130,8 @@ use pubkey_banning::BanReason; pub use pubkey_banning::{ban_pubkey_rpc, is_pubkey_banned, list_banned_pubkeys_rpc, unban_pubkeys_rpc}; pub use recreate_swap_data::recreate_swap_data; pub use saved_swap::{SavedSwap, SavedSwapError, SavedSwapIo, SavedSwapResult}; -use swap_v2_common::{get_unfinished_swaps_uuids, swap_kickstart_handler, ActiveSwapV2Info}; +use swap_v2_common::{get_unfinished_swaps_uuids, swap_kickstart_handler_for_maker, swap_kickstart_handler_for_taker, + ActiveSwapV2Info}; use swap_v2_pb::*; use swap_v2_rpcs::{get_maker_swap_data_for_rpc, get_swap_type, get_taker_swap_data_for_rpc}; pub use swap_watcher::{process_watcher_msg, watcher_topic, TakerSwapWatcherData, MAKER_PAYMENT_SPEND_FOUND_LOG, @@ -155,12 +156,19 @@ pub(crate) const TAKER_SWAP_V2_TYPE: u8 = 2; pub(crate) const TAKER_FEE_VALIDATION_ATTEMPTS: usize = 6; pub(crate) const TAKER_FEE_VALIDATION_RETRY_DELAY_SECS: f64 = 10.; -const MAX_STARTED_AT_DIFF: u64 = 60; const NEGOTIATE_SEND_INTERVAL: f64 = 30.; /// If a certain P2P message is not received, swap will be aborted after this time expires. const NEGOTIATION_TIMEOUT_SEC: u64 = 90; +/// Add refund fee to calculate maximum available balance for a swap (including possible refund) +pub(crate) const INCLUDE_REFUND_FEE: bool = true; + +/// Do not add refund fee to calculate fee needed only to make a successful swap +pub(crate) const NO_REFUND_FEE: bool = false; + +const MAX_STARTED_AT_DIFF: u64 = MAX_TIME_GAP_FOR_CONNECTED_PEER * 3; + cfg_wasm32! { use mm2_db::indexed_db::{ConstructibleDb, DbLocked}; use saved_swap::migrate_swaps_data; @@ -326,6 +334,19 @@ pub fn broadcast_p2p_tx_msg(ctx: &MmArc, topic: String, msg: &TransactionEnum, p broadcast_p2p_msg(ctx, topic, encoded_msg, from); } +impl SwapMsg { + fn swap_msg_to_store(self, msg_store: &mut SwapMsgStore) { + match self { + SwapMsg::Negotiation(data) => msg_store.negotiation = Some(data), + SwapMsg::NegotiationReply(data) => msg_store.negotiation_reply = Some(data), + SwapMsg::Negotiated(negotiated) => msg_store.negotiated = Some(negotiated), + SwapMsg::TakerFee(data) => msg_store.taker_fee = Some(data), + SwapMsg::MakerPayment(data) => msg_store.maker_payment = Some(data), + SwapMsg::TakerPayment(taker_payment) => msg_store.taker_payment = Some(taker_payment), + } + } +} + pub async fn process_swap_msg(ctx: MmArc, topic: &str, msg: &[u8]) -> P2PRequestResult<()> { let uuid = Uuid::from_str(topic).map_to_mm(|e| P2PRequestError::DecodeError(e.to_string()))?; @@ -363,14 +384,7 @@ pub async fn process_swap_msg(ctx: MmArc, topic: &str, msg: &[u8]) -> P2PRequest let mut msgs = swap_ctx.swap_msgs.lock().unwrap(); if let Some(msg_store) = msgs.get_mut(&uuid) { if msg_store.accept_only_from.bytes == msg.2.unprefixed() { - match msg.0 { - SwapMsg::Negotiation(data) => msg_store.negotiation = Some(data), - SwapMsg::NegotiationReply(data) => msg_store.negotiation_reply = Some(data), - SwapMsg::Negotiated(negotiated) => msg_store.negotiated = Some(negotiated), - SwapMsg::TakerFee(data) => msg_store.taker_fee = Some(data), - SwapMsg::MakerPayment(data) => msg_store.maker_payment = Some(data), - SwapMsg::TakerPayment(taker_payment) => msg_store.taker_payment = Some(taker_payment), - } + msg.0.swap_msg_to_store(msg_store); } else { warn!("Received message from unexpected sender for swap {}", uuid); } @@ -420,13 +434,13 @@ async fn recv_swap_msg( /// in order to give different and/or heavy communication channels a chance. const BASIC_COMM_TIMEOUT: u64 = 90; -#[cfg(not(feature = "custom-swap-locktime"))] +#[cfg(not(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests")))] /// Default atomic swap payment locktime, in seconds. /// Maker sends payment with LOCKTIME * 2 /// Taker sends payment with LOCKTIME const PAYMENT_LOCKTIME: u64 = 3600 * 2 + 300 * 2; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] /// Default atomic swap payment locktime, in seconds. /// Maker sends payment with LOCKTIME * 2 /// Taker sends payment with LOCKTIME @@ -435,9 +449,9 @@ pub(crate) static PAYMENT_LOCKTIME: AtomicU64 = AtomicU64::new(super::CUSTOM_PAY #[inline] /// Returns `PAYMENT_LOCKTIME` pub fn get_payment_locktime() -> u64 { - #[cfg(not(feature = "custom-swap-locktime"))] + #[cfg(not(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests")))] return PAYMENT_LOCKTIME; - #[cfg(feature = "custom-swap-locktime")] + #[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] PAYMENT_LOCKTIME.load(Ordering::Relaxed) } @@ -516,12 +530,12 @@ struct LockedAmountInfo { } struct SwapsContext { - running_swaps: Mutex>>, + running_swaps: Mutex>>, active_swaps_v2_infos: Mutex>, banned_pubkeys: Mutex>, swap_msgs: Mutex>, swap_v2_msgs: Mutex>, - taker_swap_watchers: PaMutex>>, + taker_swap_watchers: PaMutex, ()>>, locked_amounts: Mutex>>, #[cfg(target_arch = "wasm32")] swap_db: ConstructibleDb, @@ -532,14 +546,12 @@ impl SwapsContext { fn from_ctx(ctx: &MmArc) -> Result, String> { Ok(try_s!(from_ctx(&ctx.swaps_ctx, move || { Ok(SwapsContext { - running_swaps: Mutex::new(vec![]), + running_swaps: Mutex::new(HashMap::new()), active_swaps_v2_infos: Mutex::new(HashMap::new()), banned_pubkeys: Mutex::new(HashMap::new()), swap_msgs: Mutex::new(HashMap::new()), swap_v2_msgs: Mutex::new(HashMap::new()), - taker_swap_watchers: PaMutex::new(DuplicateCache::new(Duration::from_secs( - TAKER_SWAP_ENTRY_TIMEOUT_SEC, - ))), + taker_swap_watchers: PaMutex::new(TimedMap::new_with_map_kind(MapKind::FxHashMap)), locked_amounts: Mutex::new(HashMap::new()), #[cfg(target_arch = "wasm32")] swap_db: ConstructibleDb::new(ctx), @@ -617,21 +629,21 @@ pub fn get_locked_amount(ctx: &MmArc, coin: &str) -> MmNumber { let swap_ctx = SwapsContext::from_ctx(ctx).unwrap(); let swap_lock = swap_ctx.running_swaps.lock().unwrap(); - let mut locked = swap_lock - .iter() - .filter_map(|swap| swap.upgrade()) - .flat_map(|swap| swap.locked_amount()) - .fold(MmNumber::from(0), |mut total_amount, locked| { - if locked.coin == coin { - total_amount += locked.amount; - } - if let Some(trade_fee) = locked.trade_fee { - if trade_fee.coin == coin && !trade_fee.paid_from_trading_vol { - total_amount += trade_fee.amount; + let mut locked = + swap_lock + .values() + .flat_map(|swap| swap.locked_amount()) + .fold(MmNumber::from(0), |mut total_amount, locked| { + if locked.coin == coin { + total_amount += locked.amount; } - } - total_amount - }); + if let Some(trade_fee) = locked.trade_fee { + if trade_fee.coin == coin && !trade_fee.paid_from_trading_vol { + total_amount += trade_fee.amount; + } + } + total_amount + }); drop(swap_lock); let locked_amounts = swap_ctx.locked_amounts.lock().unwrap(); @@ -652,14 +664,12 @@ pub fn get_locked_amount(ctx: &MmArc, coin: &str) -> MmNumber { locked } -/// Get number of currently running swaps -pub fn running_swaps_num(ctx: &MmArc) -> u64 { +/// Clear up all the running swaps. +/// +/// This doesn't mean that these swaps will be stopped. They can only be stopped from the abortable systems they are running on top of. +pub fn clear_running_swaps(ctx: &MmArc) { let swap_ctx = SwapsContext::from_ctx(ctx).unwrap(); - let swaps = swap_ctx.running_swaps.lock().unwrap(); - swaps.iter().fold(0, |total, swap| match swap.upgrade() { - Some(_) => total + 1, - None => total, - }) + swap_ctx.running_swaps.lock().unwrap().clear(); } /// Get total amount of selected coin locked by all currently ongoing swaps except the one with selected uuid @@ -668,8 +678,7 @@ fn get_locked_amount_by_other_swaps(ctx: &MmArc, except_uuid: &Uuid, coin: &str) let swap_lock = swap_ctx.running_swaps.lock().unwrap(); swap_lock - .iter() - .filter_map(|swap| swap.upgrade()) + .values() .filter(|swap| swap.uuid() != except_uuid) .flat_map(|swap| swap.locked_amount()) .fold(MmNumber::from(0), |mut total_amount, locked| { @@ -689,11 +698,9 @@ pub fn active_swaps_using_coins(ctx: &MmArc, coins: &HashSet) -> Result< let swap_ctx = try_s!(SwapsContext::from_ctx(ctx)); let swaps = try_s!(swap_ctx.running_swaps.lock()); let mut uuids = vec![]; - for swap in swaps.iter() { - if let Some(swap) = swap.upgrade() { - if coins.contains(&swap.maker_coin().to_string()) || coins.contains(&swap.taker_coin().to_string()) { - uuids.push(*swap.uuid()) - } + for swap in swaps.values() { + if coins.contains(&swap.maker_coin().to_string()) || coins.contains(&swap.taker_coin().to_string()) { + uuids.push(*swap.uuid()) } } drop(swaps); @@ -709,15 +716,13 @@ pub fn active_swaps_using_coins(ctx: &MmArc, coins: &HashSet) -> Result< pub fn active_swaps(ctx: &MmArc) -> Result, String> { let swap_ctx = try_s!(SwapsContext::from_ctx(ctx)); - let swaps = swap_ctx.running_swaps.lock().unwrap(); - let mut uuids = vec![]; - for swap in swaps.iter() { - if let Some(swap) = swap.upgrade() { - uuids.push((*swap.uuid(), LEGACY_SWAP_TYPE)) - } - } - - drop(swaps); + let mut uuids: Vec<_> = swap_ctx + .running_swaps + .lock() + .unwrap() + .keys() + .map(|uuid| (*uuid, LEGACY_SWAP_TYPE)) + .collect(); let swaps_v2 = swap_ctx.active_swaps_v2_infos.lock().unwrap(); uuids.extend(swaps_v2.iter().map(|(uuid, info)| (*uuid, info.swap_type))); @@ -792,68 +797,16 @@ pub fn lp_atomic_locktime(maker_coin: &str, taker_coin: &str, version: AtomicLoc } } -fn dex_fee_rate(base: &str, rel: &str) -> MmNumber { - let fee_discount_tickers: &[&str] = if var("MYCOIN_FEE_DISCOUNT").is_ok() { - &["KMD", "MYCOIN"] - } else { - &["KMD"] - }; - if fee_discount_tickers.contains(&base) || fee_discount_tickers.contains(&rel) { - // 1/777 - 10% - BigRational::new(9.into(), 7770.into()).into() - } else { - BigRational::new(1.into(), 777.into()).into() - } -} - -pub fn dex_fee_amount(base: &str, rel: &str, trade_amount: &MmNumber, min_tx_amount: &MmNumber) -> DexFee { - let rate = dex_fee_rate(base, rel); - let fee = trade_amount * &rate; - - if &fee <= min_tx_amount { - return DexFee::Standard(min_tx_amount.clone()); - } - - if base == "KMD" { - // Drop the fee by 25%, which will be burned during the taker fee payment. - // - // This cut will be dropped before return if the final amount is less than - // the minimum transaction amount. - - // Fee with 25% cut - let new_fee = &fee * &MmNumber::from("0.75"); - - let (fee, burn) = if &new_fee >= min_tx_amount { - // Use the max burn value, which is 25%. - let burn_amount = &fee - &new_fee; - - (new_fee, burn_amount) - } else { - // Burn only the exceed amount because fee after 25% cut is less - // than `min_tx_amount`. - let burn_amount = &fee - min_tx_amount; - - (min_tx_amount.clone(), burn_amount) - }; - - return DexFee::with_burn(fee, burn); - } - - DexFee::Standard(fee) -} - -/// Calculates DEX fee with a threshold based on min tx amount of the taker coin. -pub fn dex_fee_amount_from_taker_coin(taker_coin: &dyn MmCoin, maker_coin: &str, trade_amount: &MmNumber) -> DexFee { - let min_tx_amount = MmNumber::from(taker_coin.min_tx_amount()); - dex_fee_amount(taker_coin.ticker(), maker_coin, trade_amount, &min_tx_amount) -} - #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] pub struct NegotiationDataV1 { started_at: u64, payment_locktime: u64, secret_hash: [u8; 20], - persistent_pubkey: Vec, + #[serde( + deserialize_with = "H264::deserialize_from_bytes", + serialize_with = "H264::serialize_to_byte_seq" + )] + persistent_pubkey: H264, } #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] @@ -861,7 +814,11 @@ pub struct NegotiationDataV2 { started_at: u64, payment_locktime: u64, secret_hash: Vec, - persistent_pubkey: Vec, + #[serde( + deserialize_with = "H264::deserialize_from_bytes", + serialize_with = "H264::serialize_to_byte_seq" + )] + persistent_pubkey: H264, maker_coin_swap_contract: Vec, taker_coin_swap_contract: Vec, } @@ -873,8 +830,16 @@ pub struct NegotiationDataV3 { secret_hash: Vec, maker_coin_swap_contract: Vec, taker_coin_swap_contract: Vec, - maker_coin_htlc_pub: Vec, - taker_coin_htlc_pub: Vec, + #[serde( + deserialize_with = "H264::deserialize_from_bytes", + serialize_with = "H264::serialize_to_byte_seq" + )] + maker_coin_htlc_pub: H264, + #[serde( + deserialize_with = "H264::deserialize_from_bytes", + serialize_with = "H264::serialize_to_byte_seq" + )] + taker_coin_htlc_pub: H264, } #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] @@ -910,7 +875,7 @@ impl NegotiationDataMsg { } } - pub fn maker_coin_htlc_pub(&self) -> &[u8] { + pub fn maker_coin_htlc_pub(&self) -> &H264 { match self { NegotiationDataMsg::V1(v1) => &v1.persistent_pubkey, NegotiationDataMsg::V2(v2) => &v2.persistent_pubkey, @@ -918,7 +883,7 @@ impl NegotiationDataMsg { } } - pub fn taker_coin_htlc_pub(&self) -> &[u8] { + pub fn taker_coin_htlc_pub(&self) -> &H264 { match self { NegotiationDataMsg::V1(v1) => &v1.persistent_pubkey, NegotiationDataMsg::V2(v2) => &v2.persistent_pubkey, @@ -1090,6 +1055,8 @@ struct MySwapStatusResponse { my_info: Option, recoverable: bool, is_finished: bool, + #[serde(skip_serializing_if = "Option::is_none")] + is_success: Option, } impl From for MySwapStatusResponse { @@ -1099,6 +1066,8 @@ impl From for MySwapStatusResponse { my_info: swap.get_my_info(), recoverable: swap.is_recoverable(), is_finished: swap.is_finished(), + // only serialize is_success field if swap is successful + is_success: swap.is_success().ok(), swap, } } @@ -1409,12 +1378,8 @@ pub async fn swap_kick_starts(ctx: MmArc) -> Result, String> { coins.insert(maker_swap_repr.maker_coin.clone()); coins.insert(maker_swap_repr.taker_coin.clone()); - let fut = swap_kickstart_handler::>( - ctx.clone(), - maker_swap_repr, - maker_swap_storage.clone(), - maker_uuid, - ); + let fut = + swap_kickstart_handler_for_maker(ctx.clone(), maker_swap_repr, maker_swap_storage.clone(), maker_uuid); ctx.spawner().spawn(fut); } @@ -1434,12 +1399,8 @@ pub async fn swap_kick_starts(ctx: MmArc) -> Result, String> { coins.insert(taker_swap_repr.maker_coin.clone()); coins.insert(taker_swap_repr.taker_coin.clone()); - let fut = swap_kickstart_handler::>( - ctx.clone(), - taker_swap_repr, - taker_swap_storage.clone(), - taker_uuid, - ); + let fut = + swap_kickstart_handler_for_taker(ctx.clone(), taker_swap_repr, taker_swap_storage.clone(), taker_uuid); ctx.spawner().spawn(fut); } Ok(coins) @@ -1624,42 +1585,6 @@ pub async fn active_swaps_rpc(ctx: MmArc, req: Json) -> Result> Ok(try_s!(Response::builder().body(res))) } -/// Algorithm used to hash swap secret. -#[derive(Clone, Copy, Debug, Deserialize, Serialize, Default)] -pub enum SecretHashAlgo { - /// ripemd160(sha256(secret)) - #[default] - DHASH160 = 1, - /// sha256(secret) - SHA256 = 2, -} - -#[derive(Debug, Display)] -pub struct UnsupportedSecretHashAlgo(u8); - -impl std::error::Error for UnsupportedSecretHashAlgo {} - -impl TryFrom for SecretHashAlgo { - type Error = UnsupportedSecretHashAlgo; - - fn try_from(value: u8) -> Result { - match value { - 1 => Ok(SecretHashAlgo::DHASH160), - 2 => Ok(SecretHashAlgo::SHA256), - unsupported => Err(UnsupportedSecretHashAlgo(unsupported)), - } - } -} - -impl SecretHashAlgo { - fn hash_secret(&self, secret: &[u8]) -> Vec { - match self { - SecretHashAlgo::DHASH160 => dhash160(secret).take().into(), - SecretHashAlgo::SHA256 => sha256(secret).take().into(), - } - } -} - // Todo: Maybe add a secret_hash_algo method to the SwapOps trait instead /// Selects secret hash algorithm depending on types of coins being swapped #[cfg(not(target_arch = "wasm32"))] @@ -1684,6 +1609,19 @@ pub fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) } } +/// Determines the secret hash algorithm for TPU, prioritizing SHA256 if either coin supports it. +/// # Attention +/// When adding new coins support, ensure their `secret_hash_algo_v2` implementation returns correct secret hash algorithm. +pub fn detect_secret_hash_algo_v2(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> SecretHashAlgo { + let maker_algo = maker_coin.secret_hash_algo_v2(); + let taker_algo = taker_coin.secret_hash_algo_v2(); + if maker_algo == SecretHashAlgo::SHA256 || taker_algo == SecretHashAlgo::SHA256 { + SecretHashAlgo::SHA256 + } else { + SecretHashAlgo::DHASH160 + } +} + pub struct SwapPubkeys { pub maker: String, pub taker: String, @@ -1828,11 +1766,6 @@ pub fn generate_secret() -> Result<[u8; 32], rand::Error> { Ok(sec) } -/// Add refund fee to calculate maximum available balance for a swap (including possible refund) -pub(crate) const INCLUDE_REFUND_FEE: bool = true; -/// Do not add refund fee to calculate fee needed only to make a successful swap -pub(crate) const NO_REFUND_FEE: bool = false; - #[cfg(all(test, not(target_arch = "wasm32")))] mod lp_swap_tests { use super::*; @@ -1841,58 +1774,13 @@ mod lp_swap_tests { use coins::utxo::rpc_clients::ElectrumConnectionSettings; use coins::utxo::utxo_standard::utxo_standard_coin_with_priv_key; use coins::utxo::{UtxoActivationParams, UtxoRpcMode}; - use coins::MarketCoinOps; use coins::PrivKeyActivationPolicy; + use coins::{DexFee, MarketCoinOps, TestCoin}; use common::{block_on, new_uuid}; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::{morty_conf, rick_conf, MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS}; - - #[test] - fn test_dex_fee_amount() { - let min_tx_amount = MmNumber::from("0.0001"); - - let base = "BTC"; - let rel = "ETH"; - let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - let expected_fee = DexFee::Standard(amount / 777u64.into()); - assert_eq!(expected_fee, actual_fee); - - let base = "KMD"; - let rel = "ETH"; - let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - let expected_fee = amount.clone() * (9, 7770).into() * MmNumber::from("0.75"); - let expected_burn_amount = amount * (9, 7770).into() * MmNumber::from("0.25"); - assert_eq!(DexFee::with_burn(expected_fee, expected_burn_amount), actual_fee); - - // check the case when KMD taker fee is close to dust - let base = "KMD"; - let rel = "BTC"; - let amount = (1001 * 777, 90000000).into(); - let min_tx_amount = "0.00001".into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - assert_eq!( - DexFee::WithBurn { - fee_amount: "0.00001".into(), - burn_amount: "0.00000001".into() - }, - actual_fee - ); - - let base = "BTC"; - let rel = "KMD"; - let amount = 1.into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - let expected_fee = DexFee::Standard(amount * (9, 7770).into()); - assert_eq!(expected_fee, actual_fee); - - let base = "BTC"; - let rel = "KMD"; - let amount: MmNumber = "0.001".parse::().unwrap().into(); - let actual_fee = dex_fee_amount(base, rel, &amount, &min_tx_amount); - assert_eq!(DexFee::Standard(min_tx_amount), actual_fee); - } + use mocktopus::mocking::*; + use std::convert::TryFrom; #[test] fn test_lp_atomic_locktime() { @@ -2072,14 +1960,14 @@ mod lp_swap_tests { started_at: 0, payment_locktime: 0, secret_hash: [0; 20], - persistent_pubkey: vec![1; 33], + persistent_pubkey: [1; 33].into(), }; let expected = NegotiationDataMsg::V1(NegotiationDataV1 { started_at: 0, payment_locktime: 0, secret_hash: [0; 20], - persistent_pubkey: vec![1; 33], + persistent_pubkey: [1; 33].into(), }); let serialized = rmp_serde::to_vec_named(&v1).unwrap(); @@ -2093,7 +1981,7 @@ mod lp_swap_tests { started_at: 0, payment_locktime: 0, secret_hash: vec![0; 20], - persistent_pubkey: vec![1; 33], + persistent_pubkey: [1; 33].into(), maker_coin_swap_contract: vec![1; 20], taker_coin_swap_contract: vec![1; 20], }); @@ -2102,7 +1990,7 @@ mod lp_swap_tests { started_at: 0, payment_locktime: 0, secret_hash: [0; 20], - persistent_pubkey: vec![1; 33], + persistent_pubkey: [1; 33].into(), }; let serialized = rmp_serde::to_vec_named(&v2).unwrap(); @@ -2116,7 +2004,7 @@ mod lp_swap_tests { started_at: 0, payment_locktime: 0, secret_hash: vec![0; 20], - persistent_pubkey: vec![1; 33], + persistent_pubkey: [1; 33].into(), maker_coin_swap_contract: vec![1; 20], taker_coin_swap_contract: vec![1; 20], }); @@ -2133,8 +2021,8 @@ mod lp_swap_tests { secret_hash: vec![0; 20], maker_coin_swap_contract: vec![1; 20], taker_coin_swap_contract: vec![1; 20], - maker_coin_htlc_pub: vec![1; 33], - taker_coin_htlc_pub: vec![1; 33], + maker_coin_htlc_pub: [1; 33].into(), + taker_coin_htlc_pub: [1; 33].into(), }); // v3 must be deserialized to v3, backward compatibility is not required @@ -2348,7 +2236,7 @@ mod lp_swap_tests { taker_key_pair.public().compressed_unprefixed().unwrap().into(), maker_amount.clone(), taker_amount.clone(), - maker_key_pair.public_slice().into(), + <[u8; 33]>::try_from(maker_key_pair.public_slice()).unwrap().into(), uuid, None, conf_settings, @@ -2369,7 +2257,7 @@ mod lp_swap_tests { maker_key_pair.public().compressed_unprefixed().unwrap().into(), maker_amount.into(), taker_amount.into(), - taker_key_pair.public_slice().into(), + <[u8; 33]>::try_from(taker_key_pair.public_slice()).unwrap().into(), uuid, None, conf_settings, @@ -2411,49 +2299,155 @@ mod lp_swap_tests { std::env::set_var("MYCOIN_FEE_DISCOUNT", ""); let kmd = coins::TestCoin::new("KMD"); - let (kmd_taker_fee, kmd_burn_amount) = match dex_fee_amount_from_taker_coin(&kmd, "", &MmNumber::from(6150)) { - DexFee::Standard(_) => panic!("Wrong variant returned for KMD from `dex_fee_amount_from_taker_coin`."), + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (kmd_fee_amount, kmd_burn_amount) = match DexFee::new_from_taker_coin(&kmd, "ETH", &MmNumber::from(6150)) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for KMD from `DexFee::new_from_taker_coin`.") + }, DexFee::WithBurn { fee_amount, burn_amount, + .. } => (fee_amount, burn_amount), }; + TestCoin::should_burn_dex_fee.clear_mock(); let mycoin = coins::TestCoin::new("MYCOIN"); - let mycoin_taker_fee = match dex_fee_amount_from_taker_coin(&mycoin, "", &MmNumber::from(6150)) { - DexFee::Standard(t) => t, - DexFee::WithBurn { .. } => { - panic!("Wrong variant returned for MYCOIN from `dex_fee_amount_from_taker_coin`.") - }, - }; + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (mycoin_fee_amount, mycoin_burn_amount) = + match DexFee::new_from_taker_coin(&mycoin, "ETH", &MmNumber::from(6150)) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for MYCOIN from `DexFee::new_from_taker_coin`.") + }, + DexFee::WithBurn { + fee_amount, + burn_amount, + .. + } => (fee_amount, burn_amount), + }; + TestCoin::should_burn_dex_fee.clear_mock(); - let expected_mycoin_taker_fee = &kmd_taker_fee / &MmNumber::from("0.75"); - let expected_kmd_burn_amount = &mycoin_taker_fee - &kmd_taker_fee; + let expected_mycoin_total_fee = &kmd_fee_amount / &MmNumber::from("0.75"); + let expected_kmd_burn_amount = &expected_mycoin_total_fee - &kmd_fee_amount; - assert_eq!(expected_mycoin_taker_fee, mycoin_taker_fee); + assert_eq!(kmd_fee_amount, mycoin_fee_amount); assert_eq!(expected_kmd_burn_amount, kmd_burn_amount); + // assuming for TestCoin dust is zero + assert_eq!(mycoin_burn_amount, kmd_burn_amount); } #[test] - fn test_dex_fee_amount_from_taker_coin_discount() { + fn test_dex_fee_from_taker_coin_discount() { std::env::set_var("MYCOIN_FEE_DISCOUNT", ""); let mycoin = coins::TestCoin::new("MYCOIN"); - let mycoin_taker_fee = match dex_fee_amount_from_taker_coin(&mycoin, "", &MmNumber::from(6150)) { - DexFee::Standard(t) => t, - DexFee::WithBurn { .. } => { - panic!("Wrong variant returned for MYCOIN from `dex_fee_amount_from_taker_coin`.") - }, - }; + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (mycoin_taker_fee, mycoin_burn_amount) = + match DexFee::new_from_taker_coin(&mycoin, "", &MmNumber::from(6150)) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for MYCOIN from `DexFee::new_from_taker_coin`.") + }, + DexFee::WithBurn { + fee_amount, + burn_amount, + .. + } => (fee_amount, burn_amount), + }; + TestCoin::should_burn_dex_fee.clear_mock(); let testcoin = coins::TestCoin::default(); - let testcoin_taker_fee = match dex_fee_amount_from_taker_coin(&testcoin, "", &MmNumber::from(6150)) { - DexFee::Standard(t) => t, - DexFee::WithBurn { .. } => { - panic!("Wrong variant returned for TEST coin from `dex_fee_amount_from_taker_coin`.") - }, + TestCoin::should_burn_dex_fee.mock_safe(|_| MockResult::Return(true)); + let (testcoin_taker_fee, testcoin_burn_amount) = + match DexFee::new_from_taker_coin(&testcoin, "", &MmNumber::from(6150)) { + DexFee::Standard(_) | DexFee::NoFee => { + panic!("Wrong variant returned for TEST coin from `DexFee::new_from_taker_coin`.") + }, + DexFee::WithBurn { + fee_amount, + burn_amount, + .. + } => (fee_amount, burn_amount), + }; + TestCoin::should_burn_dex_fee.clear_mock(); + assert_eq!(testcoin_taker_fee * MmNumber::from("0.90"), mycoin_taker_fee); + assert_eq!(testcoin_burn_amount * MmNumber::from("0.90"), mycoin_burn_amount); + } + + #[test] + fn test_legacy_new_negotiation_rmp() { + // In legacy messages, persistent_pubkey was represented as Vec instead of H264. + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + struct LegacyNegotiationDataV2 { + started_at: u64, + payment_locktime: u64, + secret_hash: Vec, + persistent_pubkey: Vec, + maker_coin_swap_contract: Vec, + taker_coin_swap_contract: Vec, + } + + let legacy_instance = LegacyNegotiationDataV2 { + started_at: 1620000000, + payment_locktime: 1620003600, + secret_hash: vec![0u8; 20], + persistent_pubkey: vec![1u8; 33], + maker_coin_swap_contract: vec![1u8; 20], + taker_coin_swap_contract: vec![1u8; 20], }; - assert_eq!(testcoin_taker_fee * MmNumber::from("0.90"), mycoin_taker_fee); + // ------------------------------------------ + // Step 1: Test Deserialization from Legacy Format + // ------------------------------------------ + let legacy_serialized = + rmp_serde::to_vec_named(&legacy_instance).expect("Legacy MessagePack serialization failed"); + let new_instance: NegotiationDataV2 = + rmp_serde::from_slice(&legacy_serialized).expect("Deserialization into new NegotiationDataV2 failed"); + + assert_eq!(new_instance.started_at, legacy_instance.started_at); + assert_eq!(new_instance.payment_locktime, legacy_instance.payment_locktime); + assert_eq!(new_instance.secret_hash, legacy_instance.secret_hash); + assert_eq!( + new_instance.persistent_pubkey.0.to_vec(), + legacy_instance.persistent_pubkey + ); + assert_eq!( + new_instance.maker_coin_swap_contract, + legacy_instance.maker_coin_swap_contract + ); + assert_eq!( + new_instance.taker_coin_swap_contract, + legacy_instance.taker_coin_swap_contract + ); + + // ------------------------------------------ + // Step 2: Test Serialization from New Format to Legacy Format + // ------------------------------------------ + let new_serialized = rmp_serde::to_vec_named(&new_instance).expect("Serialization of new type failed"); + let legacy_from_new: LegacyNegotiationDataV2 = + rmp_serde::from_slice(&new_serialized).expect("Legacy deserialization from new serialization failed"); + + assert_eq!(legacy_from_new.started_at, new_instance.started_at); + assert_eq!(legacy_from_new.payment_locktime, new_instance.payment_locktime); + assert_eq!(legacy_from_new.secret_hash, new_instance.secret_hash); + assert_eq!( + legacy_from_new.persistent_pubkey, + new_instance.persistent_pubkey.0.to_vec() + ); + assert_eq!( + legacy_from_new.maker_coin_swap_contract, + new_instance.maker_coin_swap_contract + ); + assert_eq!( + legacy_from_new.taker_coin_swap_contract, + new_instance.taker_coin_swap_contract + ); + + // ------------------------------------------ + // Step 3: Round-Trip Test of the New Format + // ------------------------------------------ + let rt_serialized = rmp_serde::to_vec_named(&new_instance).expect("Round-trip serialization failed"); + let round_trip: NegotiationDataV2 = + rmp_serde::from_slice(&rt_serialized).expect("Round-trip deserialization failed"); + assert_eq!(round_trip, new_instance); } } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 0eb72b8a71..c7e5a43329 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -4,27 +4,28 @@ 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_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, INCLUDE_REFUND_FEE, NO_REFUND_FEE, TAKER_FEE_VALIDATION_ATTEMPTS, - TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, WAIT_CONFIRM_INTERVAL_SEC}; + check_other_coin_balance_for_swap, detect_secret_hash_algo, 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, SwapConfirmationsSettings, SwapError, SwapMsg, + SwapPubkeys, SwapTxDataMsg, SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, NO_REFUND_FEE, + TAKER_FEE_VALIDATION_ATTEMPTS, TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::MakerOrderBuilder; +use crate::lp_swap::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use crate::lp_swap::swap_v2_common::mark_swap_as_finished; use crate::lp_swap::{broadcast_swap_message, taker_payment_spend_duration, MAX_STARTED_AT_DIFF}; use coins::lp_price::fetch_swap_coins_price; -use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MmCoin, - MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, - SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, TradeFee, - TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput}; +use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, + MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, + TradeFee, TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput, WatcherReward}; use common::log::{debug, error, info, warn}; -use common::{bits256, executor::Timer, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{bits256, executor::Timer, now_ms}; use common::{now_sec, wait_until_sec}; use crypto::privkey::SerializableSecp256k1Keypair; +use crypto::secret_hash_algo::SecretHashAlgo; use crypto::CryptoCtx; use futures::{compat::Future01CompatExt, select, FutureExt}; use keys::KeyPair; @@ -36,6 +37,7 @@ use parking_lot::Mutex as PaMutex; use primitives::hash::{H256, H264}; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; use std::any::TypeId; +use std::convert::TryInto; use std::ops::Deref; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; @@ -141,7 +143,8 @@ impl TakerNegotiationData { pub struct MakerSwapData { pub taker_coin: String, pub maker_coin: String, - pub taker: H256Json, + #[serde(rename = "taker")] + pub taker_pubkey: H256Json, pub secret: H256Json, pub secret_hash: Option, pub my_persistent_pub: H264Json, @@ -169,8 +172,10 @@ pub struct MakerSwapData { #[serde(skip_serializing_if = "Option::is_none")] pub taker_coin_swap_contract_address: Option, /// Temporary pubkey used in HTLC redeem script when applicable for maker coin + /// Note: it's temporary for zcoin. For other coins it's currently obtained from iguana key or HD wallet activated key pub maker_coin_htlc_pubkey: Option, /// Temporary pubkey used in HTLC redeem script when applicable for taker coin + /// Note: it's temporary for zcoin. For other coins it's currently obtained from iguana key or HD wallet activated key pub taker_coin_htlc_pubkey: Option, /// Temporary privkey used to sign P2P messages when applicable pub p2p_privkey: Option, @@ -216,7 +221,7 @@ pub struct MakerSwap { maker_amount: BigDecimal, taker_amount: BigDecimal, my_persistent_pub: H264, - taker: bits256, + taker_pubkey: bits256, uuid: Uuid, my_order_uuid: Option, taker_payment_lock: AtomicU64, @@ -356,7 +361,7 @@ impl MakerSwap { #[allow(clippy::too_many_arguments)] pub fn new( ctx: MmArc, - taker: bits256, + taker_pubkey: bits256, maker_amount: BigDecimal, taker_amount: BigDecimal, my_persistent_pub: H264, @@ -376,7 +381,7 @@ impl MakerSwap { maker_amount, taker_amount, my_persistent_pub, - taker, + taker_pubkey, uuid, my_order_uuid, taker_payment_lock: AtomicU64::new(0), @@ -426,7 +431,7 @@ impl MakerSwap { NegotiationDataMsg::V2(NegotiationDataV2 { started_at: r.data.started_at, payment_locktime: r.data.maker_payment_lock, - persistent_pubkey: r.data.my_persistent_pub.0.to_vec(), + persistent_pubkey: r.data.my_persistent_pub, secret_hash, maker_coin_swap_contract, taker_coin_swap_contract, @@ -438,8 +443,8 @@ impl MakerSwap { secret_hash, maker_coin_swap_contract, taker_coin_swap_contract, - maker_coin_htlc_pub: self.my_maker_coin_htlc_pub().into(), - taker_coin_htlc_pub: self.my_taker_coin_htlc_pub().into(), + maker_coin_htlc_pub: self.my_maker_coin_htlc_pub(), + taker_coin_htlc_pub: self.my_taker_coin_htlc_pub(), }) } } @@ -548,7 +553,7 @@ impl MakerSwap { let data = MakerSwapData { taker_coin: self.taker_coin.ticker().to_owned(), maker_coin: self.maker_coin.ticker().to_owned(), - taker: self.taker.bytes.into(), + taker_pubkey: self.taker_pubkey.bytes.into(), secret: self.secret.into(), secret_hash: Some(self.secret_hash().into()), started_at, @@ -569,8 +574,8 @@ impl MakerSwap { taker_payment_spend_trade_fee: Some(SavedTradeFee::from(taker_payment_spend_trade_fee)), maker_coin_swap_contract_address, taker_coin_swap_contract_address, - maker_coin_htlc_pubkey: Some(maker_coin_htlc_pubkey.as_slice().into()), - taker_coin_htlc_pubkey: Some(taker_coin_htlc_pubkey.as_slice().into()), + maker_coin_htlc_pubkey: Some(maker_coin_htlc_pubkey.into()), + taker_coin_htlc_pubkey: Some(taker_coin_htlc_pubkey.into()), p2p_privkey: self.p2p_privkey.map(SerializableSecp256k1Keypair::from), }; @@ -583,14 +588,15 @@ impl MakerSwap { async fn negotiate(&self) -> Result<(Option, Vec), String> { let negotiation_data = self.get_my_negotiation_data(); - let maker_negotiation_data = SwapMsg::Negotiation(negotiation_data); + let maker_negotiation_msg = SwapMsg::Negotiation(negotiation_data); + const NEGOTIATION_TIMEOUT_SEC: u64 = 90; - debug!("Sending maker negotiation data {:?}", maker_negotiation_data); + debug!("Sending maker negotiation data: {:?}", maker_negotiation_msg); let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), swap_topic(&self.uuid), - maker_negotiation_data, + maker_negotiation_msg, NEGOTIATION_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); @@ -610,6 +616,7 @@ impl MakerSwap { }, }; drop(send_abort_handle); + let time_dif = self.r().data.started_at.abs_diff(taker_data.started_at()); if time_dif > MAX_STARTED_AT_DIFF { self.broadcast_negotiated_false(); @@ -658,7 +665,10 @@ impl MakerSwap { }; // Validate maker_coin_htlc_pubkey realness - if let Err(err) = self.maker_coin.validate_other_pubkey(taker_data.maker_coin_htlc_pub()) { + if let Err(err) = self + .maker_coin + .validate_other_pubkey(&taker_data.maker_coin_htlc_pub().0) + { self.broadcast_negotiated_false(); return Ok((Some(MakerSwapCommand::Finish), vec![MakerSwapEvent::NegotiateFailed( ERRL!("!taker_data.maker_coin_htlc_pub {}", err).into(), @@ -666,7 +676,10 @@ impl MakerSwap { }; // Validate taker_coin_htlc_pubkey realness - if let Err(err) = self.taker_coin.validate_other_pubkey(taker_data.taker_coin_htlc_pub()) { + if let Err(err) = self + .taker_coin + .validate_other_pubkey(&taker_data.taker_coin_htlc_pub().0) + { self.broadcast_negotiated_false(); return Ok((Some(MakerSwapCommand::Finish), vec![MakerSwapEvent::NegotiateFailed( ERRL!("!taker_data.taker_coin_htlc_pub {}", err).into(), @@ -681,8 +694,8 @@ impl MakerSwap { taker_pubkey: H264Json::default(), maker_coin_swap_contract_addr, taker_coin_swap_contract_addr, - maker_coin_htlc_pubkey: Some(taker_data.maker_coin_htlc_pub().into()), - taker_coin_htlc_pubkey: Some(taker_data.taker_coin_htlc_pub().into()), + maker_coin_htlc_pubkey: Some(*taker_data.maker_coin_htlc_pub()), + taker_coin_htlc_pubkey: Some(*taker_data.taker_coin_htlc_pub()), }), ])) } @@ -739,6 +752,29 @@ impl MakerSwap { }; swap_events.push(MakerSwapEvent::MakerPaymentInstructionsReceived(instructions)); + let taker_amount = MmNumber::from(self.taker_amount.clone()); + let dex_fee = DexFee::new_with_taker_pubkey( + self.taker_coin.deref(), + &self.r().data.maker_coin, + &taker_amount, + self.r().other_taker_coin_htlc_pub.to_vec().as_ref(), + ); + debug!( + "MakerSwap::wait_taker_fee dex_fee={:?} my_taker_coin_htlc_pub={}", + dex_fee, + hex::encode(self.my_taker_coin_htlc_pub().0) + ); + + if matches!(dex_fee, DexFee::NoFee) { + info!("Taker fee is not expected for dex taker"); + let fee_ident = TransactionIdentifier { + tx_hex: BytesJson::from(vec![]), + tx_hash: BytesJson::from(vec![]), + }; + swap_events.push(MakerSwapEvent::TakerFeeValidated(fee_ident)); + return Ok((Some(MakerSwapCommand::SendPayment), swap_events)); + } + let taker_fee = match self.taker_coin.tx_enum_from_bytes(payload.data()) { Ok(tx) => tx, Err(e) => { @@ -751,8 +787,6 @@ impl MakerSwap { let hash = taker_fee.tx_hash_as_bytes(); info!("Taker fee tx {:02x}", hash); - let taker_amount = MmNumber::from(self.taker_amount.clone()); - let dex_fee = dex_fee_amount_from_taker_coin(self.taker_coin.deref(), &self.r().data.maker_coin, &taker_amount); let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; let taker_coin_start_block = self.r().data.taker_coin_start_block; @@ -763,7 +797,6 @@ impl MakerSwap { .validate_fee(ValidateFeeArgs { fee_tx: &taker_fee, expected_sender: &*other_taker_coin_htlc_pub, - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &dex_fee, min_block_number: taker_coin_start_block, uuid: self.uuid.as_bytes(), @@ -793,83 +826,54 @@ impl MakerSwap { Ok((Some(MakerSwapCommand::SendPayment), swap_events)) } - async fn maker_payment(&self) -> Result<(Option, Vec), String> { - let lock_duration = self.r().data.lock_duration; - let timeout = self.r().data.started_at + lock_duration / 3; - let now = now_sec(); - if now > timeout { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), - ])); + /// Sets up the watcher reward for the maker's payment in the swap. + /// + /// The reward mainly serves as compensation to watchers for the mining fees + /// paid to execute the transactions. + /// + /// The reward configuration depends on the specific requirements of the coins + /// involved in the swap. + /// Some coins may not support watcher rewards at all. + async fn setup_watcher_reward(&self, wait_maker_payment_until: u64) -> Result, String> { + if !self.r().watcher_reward { + return Ok(None); } + self.maker_coin + .get_maker_watcher_reward(&self.taker_coin, self.watcher_reward_amount(), wait_maker_payment_until) + .await + .map_err(|err| err.into_inner().to_string()) + } + + async fn maker_payment(&self) -> Result<(Option, Vec), String> { + // Extract values from lock before async operations + let lock_duration = self.r().data.lock_duration; let maker_payment_lock = self.r().data.maker_payment_lock; let other_maker_coin_htlc_pub = self.r().other_maker_coin_htlc_pub; let secret_hash = self.secret_hash(); let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); let unique_data = self.unique_swap_data(); let payment_instructions = self.r().payment_instructions.clone(); - let transaction_f = self.maker_coin.check_if_my_payment_sent(CheckIfMyPaymentSentArgs { - time_lock: maker_payment_lock, - other_pub: &*other_maker_coin_htlc_pub, - secret_hash: secret_hash.as_slice(), - search_from_block: self.r().data.maker_coin_start_block, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - amount: &self.maker_amount, - payment_instructions: &payment_instructions, - }); - + let maker_coin_start_block = self.r().data.maker_coin_start_block; let wait_maker_payment_until = wait_for_maker_payment_conf_until(self.r().data.started_at, lock_duration); - let watcher_reward = if self.r().watcher_reward { - match self - .maker_coin - .get_maker_watcher_reward(&self.taker_coin, self.watcher_reward_amount(), wait_maker_payment_until) - .await - { - Ok(reward) => reward, - Err(err) => { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed(err.into_inner().to_string().into()), - ])) - }, - } - } else { - None - }; - - let transaction = match transaction_f.await { - Ok(res) => match res { - Some(tx) => tx, - None => { - let payment = self - .maker_coin - .send_maker_payment(SendPaymentArgs { - time_lock_duration: lock_duration, - time_lock: maker_payment_lock, - other_pubkey: &*other_maker_coin_htlc_pub, - secret_hash: secret_hash.as_slice(), - amount: self.maker_amount.clone(), - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - payment_instructions: &payment_instructions, - watcher_reward, - wait_for_confirmation_until: wait_maker_payment_until, - }) - .await; - match payment { - Ok(t) => t, - Err(err) => { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed( - ERRL!("{}", err.get_plain_text_format()).into(), - ), - ])); - }, - } - }, - }, + // Look for previously sent maker payment in case of restart + let maybe_existing_payment = match self + .maker_coin + .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: maker_payment_lock, + other_pub: &*other_maker_coin_htlc_pub, + secret_hash: secret_hash.as_slice(), + search_from_block: maker_coin_start_block, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + amount: &self.maker_amount, + payment_instructions: &payment_instructions, + }) + .await + { + Ok(Some(tx)) => Some(tx), + Ok(None) => None, Err(e) => { return Ok((Some(MakerSwapCommand::Finish), vec![ MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("{}", e).into()), @@ -877,6 +881,60 @@ impl MakerSwap { }, }; + // If the payment is not yet sent, make sure we didn't miss the deadline for sending it. + if maybe_existing_payment.is_none() { + let timeout = self.r().data.started_at + lock_duration / 3; + let now = now_sec(); + if now > timeout { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), + ])); + } + } + + // Set up watcher reward if enabled + let watcher_reward = match self.setup_watcher_reward(wait_maker_payment_until).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed(err.into()), + ])) + }, + }; + + // Use existing payment or create new one + let transaction = match maybe_existing_payment { + Some(tx) => tx, + None => { + match self + .maker_coin + .send_maker_payment(SendPaymentArgs { + time_lock_duration: lock_duration, + time_lock: maker_payment_lock, + other_pubkey: &*other_maker_coin_htlc_pub, + secret_hash: secret_hash.as_slice(), + amount: self.maker_amount.clone(), + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + payment_instructions: &payment_instructions, + watcher_reward, + wait_for_confirmation_until: wait_maker_payment_until, + }) + .await + { + Ok(t) => t, + Err(err) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed( + ERRL!("{}", err.get_plain_text_format()).into(), + ), + ])); + }, + } + }, + }; + + // Build transaction identifier and prepare events let tx_hash = transaction.tx_hash_as_bytes(); info!("{}: Maker payment tx {:02x}", MAKER_PAYMENT_SENT_LOG, tx_hash); @@ -1338,10 +1396,12 @@ impl MakerSwap { } let mut taker = bits256::from([0; 32]); - taker.bytes = data.taker.0; + taker.bytes = data.taker_pubkey.0; let crypto_ctx = try_s!(CryptoCtx::from_ctx(&ctx)); - let my_persistent_pub = H264::from(&**crypto_ctx.mm2_internal_key_pair().public()); + let my_persistent_pub = H264::from(try_s!(TryInto::<[u8; 33]>::try_into( + crypto_ctx.mm2_internal_key_pair().public_slice() + ))); let conf_settings = SwapConfirmationsSettings { maker_coin_confs: data.maker_payment_confirmations, @@ -1818,7 +1878,7 @@ impl MakerSavedSwap { event: MakerSwapEvent::Started(MakerSwapData { taker_coin: "".to_string(), maker_coin: "".to_string(), - taker: Default::default(), + taker_pubkey: Default::default(), secret: Default::default(), secret_hash: None, my_persistent_pub: Default::default(), @@ -2095,17 +2155,19 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { }; } let running_swap = Arc::new(swap); - let weak_ref = Arc::downgrade(&running_swap); let swap_ctx = SwapsContext::from_ctx(&ctx).unwrap(); - swap_ctx.init_msg_store(running_swap.uuid, running_swap.taker); - swap_ctx.running_swaps.lock().unwrap().push(weak_ref); + swap_ctx.init_msg_store(running_swap.uuid, running_swap.taker_pubkey); + // Register the swap in the running swaps map. + swap_ctx + .running_swaps + .lock() + .unwrap() + .insert(uuid, running_swap.clone()); let mut swap_fut = Box::pin( async move { - let mut events; loop { let res = running_swap.handle_command(command).await.expect("!handle_command"); - events = res.1; - for event in events { + for event in res.1 { let to_save = MakerSavedEvent { timestamp: now_ms(), event: event.clone(), @@ -2118,13 +2180,20 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { .dispatch_async(ctx.clone(), LpEvents::MakerSwapStatusChanged(event_to_send)) .await; drop(dispatcher); + // Send a notification to the swap status streamer about a new event. + ctx.event_stream_manager + .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::MakerV1 { + uuid: running_swap.uuid, + event: to_save.clone(), + }) + .ok(); save_my_maker_swap_event(&ctx, &running_swap, to_save) .await .expect("!save_my_maker_swap_event"); if event.should_ban_taker() { ban_pubkey_on_failed_swap( &ctx, - running_swap.taker.bytes.into(), + running_swap.taker_pubkey.bytes.into(), &running_swap.uuid, event.clone().into(), ) @@ -2162,6 +2231,8 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { _swap = swap_fut => (), // swap finished normally _touch = touch_loop => unreachable!("Touch loop can not stop!"), }; + // Remove the swap from the running swaps map. + swap_ctx.running_swaps.lock().unwrap().remove(&uuid); } pub struct MakerSwapPreparedParams { @@ -2317,7 +2388,7 @@ pub async fn calc_max_maker_vol( ) -> CheckBalanceResult { let ticker = coin.ticker(); let locked_by_swaps = get_locked_amount(ctx, ticker); - let available = &MmNumber::from(balance.clone()) - &locked_by_swaps; + let available = &MmNumber::from(balance) - &locked_by_swaps; let mut volume = available.clone(); let preimage_value = TradePreimageValue::UpperBound(volume.to_decimal()); @@ -2347,7 +2418,7 @@ pub async fn calc_max_maker_vol( } Ok(CoinVolumeInfo { volume, - balance: MmNumber::from(balance.clone()), + balance: MmNumber::from(balance), locked_by_swaps, }) } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index d0e667a752..c4592fd494 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -1,13 +1,15 @@ +use super::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use super::swap_v2_common::*; use super::{swap_v2_topic, LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC}; use crate::lp_swap::maker_swap::MakerSwapPreparedParams; use crate::lp_swap::swap_lock::SwapLock; -use crate::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_maker_swap, recv_swap_v2_msg, SecretHashAlgo, +use crate::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_maker_swap, recv_swap_v2_msg, SwapConfirmationsSettings, TransactionIdentifier, MAKER_SWAP_V2_TYPE, MAX_STARTED_AT_DIFF}; use crate::lp_swap::{swap_v2_pb::*, NO_REFUND_FEE}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; +use coins::hd_wallet::AddrToString; use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SearchForFundingSpendErr, SendMakerPaymentArgs, SwapTxTypeWithSecretHash, @@ -15,8 +17,9 @@ use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, FundingT use common::executor::abortable_queue::AbortableQueue; use common::executor::{AbortableSystem, Timer}; use common::log::{debug, error, info, warn}; -use common::{now_sec, Future01CompatExt, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{now_sec, Future01CompatExt}; use crypto::privkey::SerializableSecp256k1Keypair; +use crypto::secret_hash_algo::SecretHashAlgo; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -40,13 +43,14 @@ cfg_native!( cfg_wasm32!( use crate::lp_swap::swap_wasm_db::{MySwapsFiltersTable, SavedSwapTable}; + use crate::swap_versioning::legacy_swap_version; ); // This is needed to have Debug on messages #[allow(unused_imports)] use prost::Message; /// Negotiation data representation to be stored in DB. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct StoredNegotiationData { taker_payment_locktime: u64, taker_funding_locktime: u64, @@ -58,7 +62,7 @@ pub struct StoredNegotiationData { } /// Represents events produced by maker swap states. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "event_type", content = "event_data")] pub enum MakerSwapEvent { /// Swap has been successfully initialized. @@ -114,6 +118,14 @@ pub enum MakerSwapEvent { maker_payment: TransactionIdentifier, taker_payment: TransactionIdentifier, }, + /// 'Taker payment' has been received and preimage of 'taker payment spend' has been skipped. + TakerPaymentReceivedAndPreimageValidationSkipped { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + negotiation_data: StoredNegotiationData, + maker_payment: TransactionIdentifier, + taker_payment: TransactionIdentifier, + }, /// Maker successfully spent taker's payment. TakerPaymentSpent { maker_coin_start_block: u64, @@ -121,6 +133,7 @@ pub enum MakerSwapEvent { maker_payment: TransactionIdentifier, taker_payment: TransactionIdentifier, taker_payment_spend: TransactionIdentifier, + negotiation_data: StoredNegotiationData, }, /// Swap has been aborted before maker payment was sent. Aborted { reason: AbortReason }, @@ -170,6 +183,7 @@ impl StateMachineStorage for MakerSwapStorage { ":taker_coin_confs": repr.conf_settings.taker_coin_confs, ":taker_coin_nota": repr.conf_settings.taker_coin_nota, ":other_p2p_pub": repr.taker_p2p_pub.to_bytes(), + ":swap_version": repr.swap_version, }; insert_new_swap_v2(&ctx, sql_params)?; Ok(()) @@ -277,6 +291,9 @@ pub struct MakerSwapDbRepr { pub events: Vec, /// Taker's P2P pubkey pub taker_p2p_pub: Secp256k1PubkeySerialize, + /// Swap protocol version + #[cfg_attr(target_arch = "wasm32", serde(default = "legacy_swap_version"))] + pub swap_version: u8, } impl StateMachineDbRepr for MakerSwapDbRepr { @@ -343,6 +360,7 @@ impl MakerSwapDbRepr { .map_err(|e| SqlError::FromSqlConversionFailure(19, SqlType::Blob, Box::new(e))) })? .into(), + swap_version: row.get(20)?, }) } } @@ -373,8 +391,6 @@ pub struct MakerSwapStateMachine @@ -411,6 +431,31 @@ impl Vec { self.secret_hash() } + + /// Calculate dex fee while taker pub is not known yet + fn dex_fee(&self) -> DexFee { + // Set DexFee::NoFee for swaps with KMD coin. + if self.maker_coin.ticker() == "KMD" || self.taker_coin.ticker() == "KMD" { + DexFee::NoFee + } else { + DexFee::new_from_taker_coin(&self.taker_coin, self.maker_coin.ticker(), &self.taker_volume) + } + } + + /// Calculate updated dex fee when taker pub is already received + fn dex_fee_updated(&self, taker_pub: &[u8]) -> DexFee { + // Set DexFee::NoFee for swaps with KMD coin. + if self.maker_coin.ticker() == "KMD" || self.taker_coin.ticker() == "KMD" { + DexFee::NoFee + } else { + DexFee::new_with_taker_pubkey( + &self.taker_coin, + self.maker_coin.ticker(), + &self.taker_volume, + taker_pub, + ) + } + } } #[async_trait] @@ -436,13 +481,14 @@ impl Box::new(TakerPaymentReceivedAndPreimageValidationSkipped { + maker_coin_start_block, + taker_coin_start_block, + maker_payment: recreate_ctx + .maker_coin + .parse_tx(&maker_payment.tx_hex.0) + .map_err(|e| SwapRecreateError::FailedToParseData(e.to_string()))?, + taker_payment: recreate_ctx + .taker_coin + .parse_tx(&taker_payment.tx_hex.0) + .map_err(|e| SwapRecreateError::FailedToParseData(e.to_string()))?, + negotiation_data: NegotiationData::from_stored_data( + negotiation_data, + &recreate_ctx.maker_coin, + &recreate_ctx.taker_coin, + )?, + }), MakerSwapEvent::TakerPaymentSpent { maker_coin_start_block, taker_coin_start_block, maker_payment, taker_payment, taker_payment_spend, + negotiation_data, } => Box::new(TakerPaymentSpent { maker_coin_start_block, taker_coin_start_block, @@ -608,6 +678,11 @@ impl return MmError::err(SwapRecreateError::SwapAborted), MakerSwapEvent::Completed => return MmError::err(SwapRecreateError::SwapCompleted), @@ -616,12 +691,6 @@ impl MmNumber::default() { - DexFee::with_burn(repr.dex_fee_amount, repr.dex_fee_burn) - } else { - DexFee::Standard(repr.dex_fee_amount) - }; - let machine = MakerSwapStateMachine { ctx: storage.ctx.clone(), abortable_system: storage @@ -639,12 +708,13 @@ impl (), } + // Send a notification to the swap status streamer about a new event. + self.ctx + .event_stream_manager + .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::MakerV2 { + uuid: self.uuid, + event: event.clone(), + }) + .ok(); } - fn on_kickstart_event( - &mut self, - event: <::DbRepr as StateMachineDbRepr>::Event, - ) { + fn on_kickstart_event(&mut self, event: MakerSwapEvent) { match event { MakerSwapEvent::Initialized { maker_payment_trade_fee, @@ -759,6 +835,7 @@ impl (), @@ -900,6 +977,7 @@ impl, state_machine: &mut Self::StateMachine) -> StateResult { let unique_data = state_machine.unique_data(); + let taker_coin_address = state_machine.taker_coin.my_addr().await; let maker_negotiation_msg = MakerNegotiation { started_at: state_machine.started_at, @@ -909,7 +987,7 @@ impl, state_machine: &mut Self::StateMachine) -> StateResult { let unique_data = state_machine.unique_data(); - let validation_args = ValidateTakerFundingArgs { funding_tx: &self.taker_funding, payment_time_lock: self.negotiation_data.taker_payment_locktime, @@ -1164,7 +1241,7 @@ impl { - let next_state = TakerPaymentReceived { - maker_coin_start_block: self.maker_coin_start_block, - taker_coin_start_block: self.taker_coin_start_block, - maker_payment: self.maker_payment, - taker_payment, - negotiation_data: self.negotiation_data, - }; - break Self::change_state(next_state, state_machine).await; + if state_machine.taker_coin.skip_taker_payment_spend_preimage() { + let next_state = TakerPaymentReceivedAndPreimageValidationSkipped { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + maker_payment: self.maker_payment, + taker_payment, + negotiation_data: self.negotiation_data, + }; + break Self::change_state(next_state, state_machine).await; + } else { + let next_state = TakerPaymentReceived { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + maker_payment: self.maker_payment, + taker_payment, + negotiation_data: self.negotiation_data, + }; + break Self::change_state(next_state, state_machine).await; + } }, // it's not really possible as taker's funding time lock is 3 * lock_duration, though we have to // handle this case anyway @@ -1411,6 +1499,7 @@ pub enum MakerPaymentRefundReason { TakerPaymentNotConfirmedInTime(String), DidNotGetTakerPaymentSpendPreimage(String), TakerPaymentSpendPreimageIsNotValid(String), + TakerPaymentSpendNotConfirmedInTime(String), FailedToParseTakerPreimage(String), FailedToParseTakerSignature(String), TakerPaymentSpendBroadcastFailed(String), @@ -1437,6 +1526,15 @@ impl TransitionFrom> for MakerPaymentRefundRequired { } +impl + TransitionFrom> + for MakerPaymentRefundRequired +{ +} +impl + TransitionFrom> for MakerPaymentRefundRequired +{ +} #[async_trait] impl State @@ -1616,7 +1714,6 @@ impl +{ + maker_coin_start_block: u64, + taker_coin_start_block: u64, + maker_payment: MakerCoin::Tx, + taker_payment: TakerCoin::Tx, + negotiation_data: NegotiationData, +} + +impl + TransitionFrom> + for TakerPaymentReceivedAndPreimageValidationSkipped +{ +} + +#[async_trait] +impl State + for TakerPaymentReceivedAndPreimageValidationSkipped +{ + type StateMachine = MakerSwapStateMachine; + + async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + info!("Skipped validation of the taker payment spend preimage because the taker's coin does not require it, and the taker did not generate and send it."); + + let input = ConfirmPaymentInput { + payment_tx: self.taker_payment.tx_hex(), + confirmations: state_machine.conf_settings.taker_coin_confs, + requires_nota: state_machine.conf_settings.taker_coin_nota, + wait_until: state_machine.taker_payment_conf_timeout(), + check_every: 10, + }; + + if let Err(e) = state_machine.taker_coin.wait_for_confirmations(input).compat().await { + let next_state = MakerPaymentRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, + maker_payment: self.maker_payment, + reason: MakerPaymentRefundReason::TakerPaymentNotConfirmedInTime(e), + }; + return Self::change_state(next_state, state_machine).await; + } + + let unique_data = state_machine.unique_data(); + + let gen_args = GenTakerPaymentSpendArgs { + taker_tx: &self.taker_payment, + time_lock: self.negotiation_data.taker_payment_locktime, + maker_secret_hash: &state_machine.secret_hash(), + maker_pub: &state_machine.taker_coin.derive_htlc_pubkey_v2(&unique_data), + maker_address: &state_machine.taker_coin.my_addr().await, + taker_pub: &self.negotiation_data.taker_coin_htlc_pub_from_taker, + dex_fee: &state_machine.dex_fee_updated(&self.negotiation_data.taker_coin_htlc_pub_from_taker.to_bytes()), + premium_amount: Default::default(), + trading_amount: state_machine.taker_volume.to_decimal(), + }; + + let taker_payment_spend = match state_machine + .taker_coin + .sign_and_broadcast_taker_payment_spend(None, &gen_args, state_machine.secret.as_slice(), &[]) + .await + { + Ok(tx) => tx, + Err(e) => { + let next_state = MakerPaymentRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, + maker_payment: self.maker_payment, + reason: MakerPaymentRefundReason::TakerPaymentSpendBroadcastFailed(format!("{:?}", e)), + }; + return Self::change_state(next_state, state_machine).await; + }, + }; + info!( + "Spent taker payment {} tx {:02x} during swap {}", + state_machine.taker_coin.ticker(), + taker_payment_spend.tx_hash_as_bytes(), + state_machine.uuid + ); + let next_state = TakerPaymentSpent { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + maker_payment: self.maker_payment, + taker_payment: self.taker_payment, + taker_payment_spend, + negotiation_data: self.negotiation_data, + }; + Self::change_state(next_state, state_machine).await + } +} + +impl StorableState + for TakerPaymentReceivedAndPreimageValidationSkipped +{ + type StateMachine = MakerSwapStateMachine; + + fn get_event(&self) -> MakerSwapEvent { + MakerSwapEvent::TakerPaymentReceivedAndPreimageValidationSkipped { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data.to_stored_data(), + maker_payment: TransactionIdentifier { + tx_hex: self.maker_payment.tx_hex().into(), + tx_hash: self.maker_payment.tx_hash_as_bytes(), + }, + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash_as_bytes(), + }, + } + } +} + struct TakerPaymentSpent { maker_coin_start_block: u64, taker_coin_start_block: u64, maker_payment: MakerCoin::Tx, taker_payment: TakerCoin::Tx, taker_payment_spend: TakerCoin::Tx, + negotiation_data: NegotiationData, } impl @@ -1747,6 +1959,12 @@ impl { } +impl + TransitionFrom> + for TakerPaymentSpent +{ +} + #[async_trait] impl State for TakerPaymentSpent @@ -1754,6 +1972,27 @@ impl; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + if state_machine.require_taker_payment_spend_confirm { + let input = ConfirmPaymentInput { + payment_tx: self.taker_payment_spend.tx_hex(), + confirmations: state_machine.conf_settings.taker_coin_confs, + requires_nota: state_machine.conf_settings.taker_coin_nota, + wait_until: state_machine.maker_payment_locktime(), + check_every: 10, + }; + + if let Err(e) = state_machine.taker_coin.wait_for_confirmations(input).compat().await { + let next_state = MakerPaymentRefundRequired { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + negotiation_data: self.negotiation_data, + maker_payment: self.maker_payment, + reason: MakerPaymentRefundReason::TakerPaymentSpendNotConfirmedInTime(e.to_string()), + }; + return Self::change_state(next_state, state_machine).await; + } + } + Self::change_state(Completed::new(), state_machine).await } } @@ -1779,6 +2018,7 @@ impl RecreateSwapRe let maker_started_event = MakerSwapEvent::Started(MakerSwapData { taker_coin: started_event.taker_coin, maker_coin: started_event.maker_coin, - taker: H256Json::from(taker_p2p_pubkey), + taker_pubkey: H256Json::from(taker_p2p_pubkey), // We could parse the `TakerSwapEvent::TakerPaymentSpent` event. // As for now, don't try to find the secret in the events since we can refund without it. secret: H256Json::default(), @@ -323,7 +323,7 @@ async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> Recreate let taker_started_event = TakerSwapEvent::Started(TakerSwapData { taker_coin: started_event.taker_coin, maker_coin: started_event.maker_coin.clone(), - maker: H256Json::from(maker_p2p_pubkey), + maker_pubkey: H256Json::from(maker_p2p_pubkey), my_persistent_pub: negotiated_event.taker_pubkey, lock_duration: started_event.lock_duration, maker_amount: started_event.maker_amount, @@ -454,7 +454,7 @@ async fn convert_maker_to_taker_events( MakerSwapEvent::TakerPaymentSpent(tx_ident) => { //Is the watcher_reward argument important here? let secret = match maker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex, false).await { - Ok(secret) => H256Json::from(secret.as_slice()), + Ok(secret) => H256Json::from(secret), Err(e) => { push_event!(TakerSwapEvent::TakerPaymentWaitForSpendFailed(ERRL!("{}", e).into())); push_event!(TakerSwapEvent::TakerPaymentWaitRefundStarted { wait_until: wait_refund_until }); @@ -495,6 +495,7 @@ mod tests { use super::*; use coins::{CoinsContext, MarketCoinOps, SwapOps, TestCoin}; use common::block_on; + use hex::FromHex; use mm2_core::mm_ctx::MmCtxBuilder; use mocktopus::mocking::{MockResult, Mockable}; use serde_json as json; @@ -534,7 +535,8 @@ mod tests { #[test] fn test_recreate_taker_swap() { TestCoin::extract_secret.mock_safe(|_coin, _secret_hash, _spend_tx, _watcher_reward| { - let secret = hex::decode("23a6bb64bc0ab2cc14cb84277d8d25134b814e5f999c66e578c9bba3c5e2d3a4").unwrap(); + let secret = + <[u8; 32]>::from_hex("23a6bb64bc0ab2cc14cb84277d8d25134b814e5f999c66e578c9bba3c5e2d3a4").unwrap(); MockResult::Return(Box::pin(async move { Ok(secret) })) }); TestCoin::platform_ticker.mock_safe(|_| MockResult::Return("TestCoin")); diff --git a/mm2src/mm2_main/src/lp_swap/saved_swap.rs b/mm2src/mm2_main/src/lp_swap/saved_swap.rs index c471f7eacb..185edd1584 100644 --- a/mm2src/mm2_main/src/lp_swap/saved_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/saved_swap.rs @@ -45,13 +45,15 @@ impl From for SavedSwap { } impl SavedSwap { - pub fn is_finished_and_success(&self) -> bool { + pub fn is_success(&self) -> Result { match self { - SavedSwap::Maker(swap) => swap.is_success().unwrap_or(false), - SavedSwap::Taker(swap) => swap.is_success().unwrap_or(false), + SavedSwap::Maker(swap) => swap.is_success(), + SavedSwap::Taker(swap) => swap.is_success(), } } + pub fn is_finished_and_success(&self) -> bool { self.is_success().unwrap_or(false) } + pub fn is_finished(&self) -> bool { match self { SavedSwap::Maker(swap) => swap.is_finished(), diff --git a/mm2src/mm2_main/src/lp_swap/swap_events.rs b/mm2src/mm2_main/src/lp_swap/swap_events.rs new file mode 100644 index 0000000000..7f4aaa90eb --- /dev/null +++ b/mm2src/mm2_main/src/lp_swap/swap_events.rs @@ -0,0 +1,53 @@ +use super::maker_swap::MakerSavedEvent; +use super::maker_swap_v2::MakerSwapEvent; +use super::taker_swap::TakerSavedEvent; +use super::taker_swap_v2::TakerSwapEvent; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use futures::StreamExt; +use uuid::Uuid; + +pub struct SwapStatusStreamer; + +impl SwapStatusStreamer { + #[inline(always)] + pub fn new() -> Self { Self } + + #[inline(always)] + pub const fn derive_streamer_id() -> &'static str { "SWAP_STATUS" } +} + +#[derive(Serialize)] +#[serde(tag = "swap_type", content = "swap_data")] +pub enum SwapStatusEvent { + MakerV1 { uuid: Uuid, event: MakerSavedEvent }, + TakerV1 { uuid: Uuid, event: TakerSavedEvent }, + MakerV2 { uuid: Uuid, event: MakerSwapEvent }, + TakerV2 { uuid: Uuid, event: TakerSwapEvent }, +} + +#[async_trait] +impl EventStreamer for SwapStatusStreamer { + type DataInType = SwapStatusEvent; + + fn streamer_id(&self) -> String { Self::derive_streamer_id().to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + mut data_rx: impl StreamHandlerInput, + ) { + ready_tx + .send(Ok(())) + .expect("Receiver is dropped, which should never happen."); + + while let Some(swap_data) = data_rx.next().await { + let event_data = serde_json::to_value(swap_data).expect("Serialization shouldn't fail."); + let event = Event::new(self.streamer_id(), event_data); + broadcaster.broadcast(event); + } + } +} diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs index 686d263d5a..3624d42b8d 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs @@ -1,8 +1,9 @@ use crate::lp_network::{subscribe_to_topic, unsubscribe_from_topic}; +use crate::lp_swap::maker_swap_v2::{MakerSwapDbRepr, MakerSwapStateMachine, MakerSwapStorage}; use crate::lp_swap::swap_lock::{SwapLock, SwapLockError, SwapLockOps}; +use crate::lp_swap::taker_swap_v2::{TakerSwapDbRepr, TakerSwapStateMachine, TakerSwapStorage}; use crate::lp_swap::{swap_v2_topic, SwapsContext}; -use coins::utxo::utxo_standard::UtxoStandardCoin; -use coins::{lp_coinfind, MmCoinEnum}; +use coins::{lp_coinfind, MakerCoinSwapOpsV2, MmCoin, MmCoinEnum, TakerCoinSwapOpsV2}; use common::executor::abortable_queue::AbortableQueue; use common::executor::{SpawnFuture, Timer}; use common::log::{error, info, warn}; @@ -37,7 +38,7 @@ pub struct ActiveSwapV2Info { } /// DB representation of tx preimage with signature -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct StoredTxPreimage { pub preimage: BytesJson, pub signature: BytesJson, @@ -292,25 +293,18 @@ pub(super) trait GetSwapCoins { fn taker_coin(&self) -> &str; } -/// Generic function for upgraded swaps kickstart handling. -/// It is implemented only for UtxoStandardCoin/UtxoStandardCoin case temporary. -pub(super) async fn swap_kickstart_handler< - T: StorableStateMachine>, ->( - ctx: MmArc, - swap_repr: ::DbRepr, - storage: T::Storage, - uuid: ::MachineId, -) where - ::MachineId: Copy + std::fmt::Display, - ::DbRepr: GetSwapCoins, - T::Error: std::fmt::Display, - T::RecreateError: std::fmt::Display, -{ +/// Attempts to find and return the maker and taker coins required for the swap to proceed. +/// If a coin is not activated, it logs the information and retries until the coin is found. +/// If an unexpected issue occurs, function logs the error and returns `None`. +pub(super) async fn swap_kickstart_coins( + ctx: &MmArc, + swap_repr: &T, + uuid: &Uuid, +) -> Option<(MmCoinEnum, MmCoinEnum)> { let taker_coin_ticker = swap_repr.taker_coin(); let taker_coin = loop { - match lp_coinfind(&ctx, taker_coin_ticker).await { + match lp_coinfind(ctx, taker_coin_ticker).await { Ok(Some(c)) => break c, Ok(None) => { info!( @@ -321,7 +315,7 @@ pub(super) async fn swap_kickstart_handler< }, Err(e) => { error!("Error {} on {} find attempt", e, taker_coin_ticker); - return; + return None; }, }; }; @@ -329,7 +323,7 @@ pub(super) async fn swap_kickstart_handler< let maker_coin_ticker = swap_repr.maker_coin(); let maker_coin = loop { - match lp_coinfind(&ctx, maker_coin_ticker).await { + match lp_coinfind(ctx, maker_coin_ticker).await { Ok(Some(c)) => break c, Ok(None) => { info!( @@ -340,22 +334,30 @@ pub(super) async fn swap_kickstart_handler< }, Err(e) => { error!("Error {} on {} find attempt", e, maker_coin_ticker); - return; + return None; }, }; }; - let (maker_coin, taker_coin) = match (maker_coin, taker_coin) { - (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => (m, t), - _ => { - error!( - "V2 swaps are not currently supported for {}/{} pair", - maker_coin_ticker, taker_coin_ticker - ); - return; - }, - }; + Some((maker_coin, taker_coin)) +} +/// Handles the recreation and kickstart of a swap state machine. +pub(super) async fn swap_kickstart_handler< + T: StorableStateMachine>, + MakerCoin: MmCoin + MakerCoinSwapOpsV2, + TakerCoin: MmCoin + TakerCoinSwapOpsV2, +>( + swap_repr: ::DbRepr, + storage: T::Storage, + uuid: ::MachineId, + maker_coin: MakerCoin, + taker_coin: TakerCoin, +) where + ::MachineId: Copy + std::fmt::Display, + T::Error: std::fmt::Display, + T::RecreateError: std::fmt::Display, +{ let recreate_context = SwapRecreateCtx { maker_coin, taker_coin }; let (mut state_machine, state) = match T::recreate_machine(uuid, storage, swap_repr, recreate_context).await { @@ -370,3 +372,65 @@ pub(super) async fn swap_kickstart_handler< error!("Error {} on trying to run the swap {}", e, uuid); } } + +pub(super) async fn swap_kickstart_handler_for_maker( + ctx: MmArc, + swap_repr: MakerSwapDbRepr, + storage: MakerSwapStorage, + uuid: Uuid, +) { + if let Some((maker_coin, taker_coin)) = swap_kickstart_coins(&ctx, &swap_repr, &uuid).await { + match (maker_coin, taker_coin) { + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + _ => { + error!( + "V2 swaps are not currently supported for {}/{} pair", + swap_repr.maker_coin(), + swap_repr.taker_coin() + ); + }, + } + } +} + +pub(super) async fn swap_kickstart_handler_for_taker( + ctx: MmArc, + swap_repr: TakerSwapDbRepr, + storage: TakerSwapStorage, + uuid: Uuid, +) { + if let Some((maker_coin, taker_coin)) = swap_kickstart_coins(&ctx, &swap_repr, &uuid).await { + match (maker_coin, taker_coin) { + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::UtxoCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::EthCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::UtxoCoin(m), MmCoinEnum::EthCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + (MmCoinEnum::EthCoin(m), MmCoinEnum::UtxoCoin(t)) => { + swap_kickstart_handler::, _, _>(swap_repr, storage, uuid, m, t).await + }, + _ => { + error!( + "V2 swaps are not currently supported for {}/{} pair", + swap_repr.maker_coin(), + swap_repr.taker_coin() + ); + }, + } + } +} diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs b/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs index e3f57e8400..669d17a492 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs @@ -108,6 +108,7 @@ pub(crate) struct MySwapForRpc { maker_coin_nota: bool, taker_coin_confs: i64, taker_coin_nota: bool, + swap_version: u8, } impl MySwapForRpc { @@ -141,6 +142,7 @@ impl MySwapForRpc { maker_coin_nota: row.get(12)?, taker_coin_confs: row.get(13)?, taker_coin_nota: row.get(14)?, + swap_version: row.get(15)?, }) } } @@ -218,6 +220,7 @@ pub(super) async fn get_maker_swap_data_for_rpc( maker_coin_nota: json_repr.conf_settings.maker_coin_nota, taker_coin_confs: json_repr.conf_settings.taker_coin_confs as i64, taker_coin_nota: json_repr.conf_settings.taker_coin_nota, + swap_version: json_repr.swap_version, })) } @@ -258,6 +261,7 @@ pub(super) async fn get_taker_swap_data_for_rpc( maker_coin_nota: json_repr.conf_settings.maker_coin_nota, taker_coin_confs: json_repr.conf_settings.taker_coin_confs as i64, taker_coin_nota: json_repr.conf_settings.taker_coin_nota, + swap_version: json_repr.swap_version, })) } diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 312f62e5c5..73fb31c67d 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -10,7 +10,8 @@ use coins::{CanRefundHtlc, ConfirmPaymentInput, FoundSwapTxSpend, MmCoinEnum, Re WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput}; use common::executor::{AbortSettings, SpawnAbortable, Timer}; use common::log::{debug, error, info}; -use common::{now_sec, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::now_sec; +use compatible_time::Duration; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MapToMmResult; @@ -20,7 +21,7 @@ use mm2_state_machine::state_machine::StateMachineTrait; use serde::{Deserialize, Serialize}; use serde_json as json; use std::cmp::min; -use std::convert::Infallible; +use std::convert::{Infallible, TryInto}; use std::sync::Arc; use uuid::Uuid; @@ -190,7 +191,6 @@ impl State for ValidateTakerFee { taker_fee_hash: watcher_ctx.data.taker_fee_hash.clone(), sender_pubkey: watcher_ctx.verified_pub.clone(), min_block_number: watcher_ctx.data.taker_coin_start_block, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.clone(), lock_duration: watcher_ctx.data.lock_duration, }) .compat() @@ -258,10 +258,7 @@ impl State for ValidateTakerPayment { let validate_input = WatcherValidatePaymentInput { payment_tx: taker_payment_hex.clone(), taker_payment_refund_preimage: watcher_ctx.data.taker_payment_refund_preimage.clone(), - time_lock: match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => watcher_ctx.data.swap_started_at, - Err(_) => watcher_ctx.taker_locktime(), - }, + time_lock: watcher_ctx.taker_locktime(), taker_pub: watcher_ctx.verified_pub.clone(), maker_pub: watcher_ctx.data.maker_pub.clone(), secret_hash: watcher_ctx.data.secret_hash.clone(), @@ -381,7 +378,7 @@ impl State for WaitForTakerPaymentSpend { .extract_secret(&watcher_ctx.data.secret_hash, &tx_hex, true) .await { - Ok(bytes) => H256Json::from(bytes.as_slice()), + Ok(secret) => H256Json::from(secret), Err(err) => { return Self::change_state(Stopped::from_reason(StopReason::Error( WatcherError::UnableToExtractSecret(err).into(), @@ -451,20 +448,18 @@ impl State for RefundTakerPayment { async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { debug!("Watcher refund taker payment"); - if std::env::var("USE_TEST_LOCKTIME").is_err() { - loop { - match watcher_ctx - .taker_coin - .can_refund_htlc(watcher_ctx.taker_locktime()) - .await - { - Ok(CanRefundHtlc::CanRefundNow) => break, - Ok(CanRefundHtlc::HaveToWait(to_sleep)) => Timer::sleep(to_sleep as f64).await, - Err(e) => { - error!("Error {} on can_refund_htlc, retrying in 30 seconds", e); - Timer::sleep(30.).await; - }, - } + loop { + match watcher_ctx + .taker_coin + .can_refund_htlc(watcher_ctx.taker_locktime()) + .await + { + Ok(CanRefundHtlc::CanRefundNow) => break, + Ok(CanRefundHtlc::HaveToWait(to_sleep)) => Timer::sleep(to_sleep as f64).await, + Err(e) => { + error!("Error {} on can_refund_htlc, retrying in 30 seconds", e); + Timer::sleep(30.).await; + }, } } @@ -565,7 +560,10 @@ impl SwapWatcherLock { fn lock_taker(swap_ctx: Arc, fee_hash: Vec) -> Option { { let mut guard = swap_ctx.taker_swap_watchers.lock(); - if !guard.insert(fee_hash.clone()) { + if guard + .insert_expirable(fee_hash.clone(), (), Duration::from_secs(TAKER_SWAP_ENTRY_TIMEOUT_SEC)) + .is_some() + { // There is the same hash already. return None; } @@ -582,7 +580,7 @@ impl SwapWatcherLock { impl Drop for SwapWatcherLock { fn drop(&mut self) { match self.watcher_type { - WatcherType::Taker => self.swap_ctx.taker_swap_watchers.lock().remove(self.fee_hash.clone()), + WatcherType::Taker => self.swap_ctx.taker_swap_watchers.lock().remove(&self.fee_hash.clone()), }; } } @@ -608,7 +606,17 @@ fn spawn_taker_swap_watcher(ctx: MmArc, watcher_data: TakerSwapWatcherData, veri }; let spawner = ctx.spawner(); - let fee_hash = H256Json::from(watcher_data.taker_fee_hash.as_slice()); + let taker_fee_bytes: [u8; 32] = match watcher_data.taker_fee_hash.as_slice().try_into() { + Ok(bytes) => bytes, + Err(_) => { + error!( + "Invalid taker fee hash length for {}", + hex::encode(&watcher_data.taker_fee_hash) + ); + return; + }, + }; + let fee_hash = H256Json::from(taker_fee_bytes); let fut = async move { let taker_coin = match lp_coinfind(&ctx, &watcher_data.taker_coin).await { diff --git a/mm2src/mm2_main/src/lp_swap/taker_restart.rs b/mm2src/mm2_main/src/lp_swap/taker_restart.rs index d934b6b11e..59ecd58b0a 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_restart.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_restart.rs @@ -154,11 +154,7 @@ pub async fn check_taker_payment_spend(swap: &TakerSwap) -> Result swap.r().data.started_at, - Err(_) => swap.r().data.taker_payment_lock, - }; + let taker_payment_lock = swap.r().data.taker_payment_lock; let secret_hash = swap.r().secret_hash.0.clone(); let unique_data = swap.unique_swap_data(); let watcher_reward = swap.r().watcher_reward; @@ -196,7 +192,7 @@ pub async fn add_taker_payment_spent_event( .extract_secret(&secret_hash, &tx_ident.tx_hex, watcher_reward) .await { - Ok(bytes) => H256::from(bytes.as_slice()), + Ok(secret) => H256::from(secret), Err(_) => { return ERR!("Could not extract secret from taker payment spend transaction"); }, @@ -223,10 +219,7 @@ pub async fn add_taker_payment_refunded_by_watcher_event( ) -> Result { let other_maker_coin_htlc_pub = swap.r().other_maker_coin_htlc_pub; let taker_coin_swap_contract_address = swap.r().data.taker_coin_swap_contract_address.clone(); - let taker_payment_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => swap.r().data.started_at, - Err(_) => swap.r().data.taker_payment_lock, - }; + let taker_payment_lock = swap.r().data.taker_payment_lock; let secret_hash = swap.r().secret_hash.0.clone(); let validate_input = ValidateWatcherSpendInput { diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index c7b1cf59a9..54f5ab3bfe 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -5,25 +5,26 @@ 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_msg_every, - check_other_coin_balance_for_swap, dex_fee_amount_from_taker_coin, dex_fee_rate, 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, INCLUDE_REFUND_FEE, NO_REFUND_FEE, WAIT_CONFIRM_INTERVAL_SEC}; + check_other_coin_balance_for_swap, 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, INCLUDE_REFUND_FEE, NO_REFUND_FEE, WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::TakerOrderBuilder; +use crate::lp_swap::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use crate::lp_swap::swap_v2_common::mark_swap_as_finished; use crate::lp_swap::taker_restart::get_command_based_on_maker_or_watcher_activity; use crate::lp_swap::{broadcast_p2p_tx_msg, broadcast_swap_msg_every_delayed, tx_helper_topic, wait_for_maker_payment_conf_duration, TakerSwapWatcherData, MAX_STARTED_AT_DIFF}; use coins::lp_price::fetch_swap_coins_price; -use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, +use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, - TradeFee, TradePreimageValue, ValidatePaymentInput, WaitForHTLCTxSpendArgs}; + TradeFee, TradePreimageValue, TransactionEnum, ValidatePaymentInput, WaitForHTLCTxSpendArgs, WatcherReward}; use common::executor::Timer; use common::log::{debug, error, info, warn}; -use common::{bits256, now_ms, now_sec, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{bits256, now_ms, now_sec, wait_until_sec}; use crypto::{privkey::SerializableSecp256k1Keypair, CryptoCtx}; use futures::{compat::Future01CompatExt, future::try_join, select, FutureExt}; use http::Response; @@ -36,6 +37,7 @@ use parking_lot::Mutex as PaMutex; use primitives::hash::H264; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; use serde_json::{self as json, Value as Json}; +use std::convert::TryInto; use std::ops::Deref; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; @@ -123,7 +125,7 @@ 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: if ctx.use_watchers() + success_events: if !ctx.disable_watchers_globally() && swap.taker_coin.is_supported_by_watchers() && swap.maker_coin.is_supported_by_watchers() { @@ -152,7 +154,7 @@ async fn save_my_taker_swap_event(ctx: &MmArc, swap: &TakerSwap, event: TakerSav } } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TakerSavedEvent { pub timestamp: u64, pub event: TakerSwapEvent, @@ -458,14 +460,17 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { let ctx = swap.ctx.clone(); subscribe_to_topic(&ctx, swap_topic(&swap.uuid)); let mut status = ctx.log.status_handle(); - let uuid = swap.uuid.to_string(); + let uuid_str = uuid.to_string(); let to_broadcast = !(swap.maker_coin.is_privacy() || swap.taker_coin.is_privacy()); let running_swap = Arc::new(swap); - let weak_ref = Arc::downgrade(&running_swap); let swap_ctx = SwapsContext::from_ctx(&ctx).unwrap(); - swap_ctx.init_msg_store(running_swap.uuid, running_swap.maker); - swap_ctx.running_swaps.lock().unwrap().push(weak_ref); - + swap_ctx.init_msg_store(running_swap.uuid, running_swap.maker_pubkey); + // Register the swap in the running swaps map. + swap_ctx + .running_swaps + .lock() + .unwrap() + .insert(uuid, running_swap.clone()); let mut swap_fut = Box::pin( async move { let mut events; @@ -478,23 +483,30 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { event: event.clone(), }; + // Send a notification to the swap status streamer about a new event. + ctx.event_stream_manager + .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::TakerV1 { + uuid: running_swap.uuid, + event: to_save.clone(), + }) + .ok(); save_my_taker_swap_event(&ctx, &running_swap, to_save) .await .expect("!save_my_taker_swap_event"); if event.should_ban_maker() { ban_pubkey_on_failed_swap( &ctx, - running_swap.maker.bytes.into(), + running_swap.maker_pubkey.bytes.into(), &running_swap.uuid, event.clone().into(), ) } if event.is_error() { - error!("[swap uuid={uuid}] {event:?}"); + error!("[swap uuid={uuid_str}] {event:?}"); } - status.status(&[&"swap", &("uuid", uuid.as_str())], &event.status_str()); + status.status(&[&"swap", &("uuid", uuid_str.as_str())], &event.status_str()); running_swap.apply_event(event); } match res.0 { @@ -503,12 +515,12 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { }, None => { if let Err(e) = mark_swap_as_finished(ctx.clone(), running_swap.uuid).await { - error!("!mark_swap_finished({}): {}", uuid, e); + error!("!mark_swap_finished({}): {}", uuid_str, e); } if to_broadcast { if let Err(e) = broadcast_my_swap_status(&ctx, running_swap.uuid).await { - error!("!broadcast_my_swap_status({}): {}", uuid, e); + error!("!broadcast_my_swap_status({}): {}", uuid_str, e); } } break; @@ -522,13 +534,16 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { _swap = swap_fut => (), // swap finished normally _touch = touch_loop => unreachable!("Touch loop can not stop!"), }; + // Remove the swap from the running swaps map. + swap_ctx.running_swaps.lock().unwrap().remove(&uuid); } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct TakerSwapData { pub taker_coin: String, pub maker_coin: String, - pub maker: H256Json, + #[serde(rename = "maker")] + pub maker_pubkey: H256Json, pub my_persistent_pub: H264Json, pub lock_duration: u64, pub maker_amount: BigDecimal, @@ -558,8 +573,10 @@ pub struct TakerSwapData { #[serde(skip_serializing_if = "Option::is_none")] pub taker_coin_swap_contract_address: Option, /// Temporary pubkey used in HTLC redeem script when applicable for maker coin + /// Note: it's temporary for zcoin. For other coins it's currently obtained from iguana key or HD wallet activated key pub maker_coin_htlc_pubkey: Option, /// Temporary pubkey used in HTLC redeem script when applicable for taker coin + /// Note: it's temporary for zcoin. For other coins it's currently obtained from iguana key or HD wallet activated key pub taker_coin_htlc_pubkey: Option, /// Temporary privkey used to sign P2P messages when applicable pub p2p_privkey: Option, @@ -618,7 +635,7 @@ pub struct TakerSwap { pub maker_amount: MmNumber, pub taker_amount: MmNumber, my_persistent_pub: H264, - maker: bits256, + maker_pubkey: bits256, uuid: Uuid, my_order_uuid: Option, pub maker_payment_lock: AtomicU64, @@ -901,7 +918,7 @@ impl TakerSwap { #[allow(clippy::too_many_arguments)] pub fn new( ctx: MmArc, - maker: bits256, + maker_pubkey: bits256, maker_amount: MmNumber, taker_amount: MmNumber, my_persistent_pub: H264, @@ -920,7 +937,7 @@ impl TakerSwap { maker_amount, taker_amount, my_persistent_pub, - maker, + maker_pubkey, uuid, my_order_uuid, maker_payment_confirmed: AtomicBool::new(false), @@ -965,13 +982,12 @@ impl TakerSwap { let equal = r.data.maker_coin_htlc_pubkey == r.data.taker_coin_htlc_pubkey; let same_as_persistent = r.data.maker_coin_htlc_pubkey == Some(r.data.my_persistent_pub); - if equal && same_as_persistent { NegotiationDataMsg::V2(NegotiationDataV2 { started_at: r.data.started_at, secret_hash, payment_locktime: r.data.taker_payment_lock, - persistent_pubkey: self.my_persistent_pub.to_vec(), + persistent_pubkey: self.my_persistent_pub.into(), maker_coin_swap_contract, taker_coin_swap_contract, }) @@ -982,8 +998,8 @@ impl TakerSwap { secret_hash, maker_coin_swap_contract, taker_coin_swap_contract, - maker_coin_htlc_pub: self.my_maker_coin_htlc_pub().into(), - taker_coin_htlc_pub: self.my_taker_coin_htlc_pub().into(), + maker_coin_htlc_pub: self.my_maker_coin_htlc_pub(), + taker_coin_htlc_pub: self.my_taker_coin_htlc_pub(), }) } } @@ -1026,18 +1042,30 @@ impl TakerSwap { async fn start(&self) -> Result<(Option, Vec), String> { // do not use self.r().data here as it is not initialized at this step yet let stage = FeeApproxStage::StartSwap; - let dex_fee = - dex_fee_amount_from_taker_coin(self.taker_coin.deref(), self.maker_coin.ticker(), &self.taker_amount); + let dex_fee = DexFee::new_with_taker_pubkey( + self.taker_coin.deref(), + self.maker_coin.ticker(), + &self.taker_amount, + &self.my_taker_coin_htlc_pub().0, + ); let preimage_value = TradePreimageValue::Exact(self.taker_amount.to_decimal()); - let fee_to_send_dex_fee_fut = self.taker_coin.get_fee_to_send_taker_fee(dex_fee.clone(), stage); - let fee_to_send_dex_fee = match fee_to_send_dex_fee_fut.await { - Ok(fee) => fee, - Err(e) => { - return Ok((Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::StartFailed( - ERRL!("!taker_coin.get_fee_to_send_taker_fee {}", e).into(), - )])) - }, + let fee_to_send_dex_fee = if matches!(dex_fee, DexFee::NoFee) { + TradeFee { + coin: self.taker_coin.ticker().to_owned(), + amount: MmNumber::from(0), + paid_from_trading_vol: false, + } + } else { + let fee_to_send_dex_fee_fut = self.taker_coin.get_fee_to_send_taker_fee(dex_fee.clone(), stage); + match fee_to_send_dex_fee_fut.await { + Ok(fee) => fee, + Err(e) => { + return Ok((Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::StartFailed( + ERRL!("!taker_coin.get_fee_to_send_taker_fee {}", e).into(), + )])) + }, + } }; let get_sender_trade_fee_fut = self .taker_coin @@ -1111,7 +1139,7 @@ impl TakerSwap { let data = TakerSwapData { taker_coin: self.taker_coin.ticker().to_owned(), maker_coin: self.maker_coin.ticker().to_owned(), - maker: self.maker.bytes.into(), + maker_pubkey: self.maker_pubkey.bytes.into(), started_at, lock_duration: self.payment_locktime, maker_amount: self.maker_amount.to_decimal(), @@ -1131,8 +1159,8 @@ impl TakerSwap { maker_payment_spend_trade_fee: Some(SavedTradeFee::from(maker_payment_spend_trade_fee)), maker_coin_swap_contract_address, taker_coin_swap_contract_address, - maker_coin_htlc_pubkey: Some(maker_coin_htlc_pubkey.as_slice().into()), - taker_coin_htlc_pubkey: Some(taker_coin_htlc_pubkey.as_slice().into()), + maker_coin_htlc_pubkey: Some(maker_coin_htlc_pubkey.into()), + taker_coin_htlc_pubkey: Some(taker_coin_htlc_pubkey.into()), p2p_privkey: self.p2p_privkey.map(SerializableSecp256k1Keypair::from), }; @@ -1215,14 +1243,20 @@ impl TakerSwap { }; // Validate maker_coin_htlc_pubkey realness - if let Err(err) = self.maker_coin.validate_other_pubkey(maker_data.maker_coin_htlc_pub()) { + if let Err(err) = self + .maker_coin + .validate_other_pubkey(&maker_data.maker_coin_htlc_pub().0) + { return Ok((Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::NegotiateFailed( ERRL!("!maker_data.maker_coin_htlc_pub {}", err).into(), )])); }; // Validate taker_coin_htlc_pubkey realness - if let Err(err) = self.taker_coin.validate_other_pubkey(maker_data.taker_coin_htlc_pub()) { + if let Err(err) = self + .taker_coin + .validate_other_pubkey(&maker_data.taker_coin_htlc_pub().0) + { return Ok((Some(TakerSwapCommand::Finish), vec![TakerSwapEvent::NegotiateFailed( ERRL!("!maker_data.taker_coin_htlc_pub {}", err).into(), )])); @@ -1247,15 +1281,17 @@ impl TakerSwap { taker_coin_swap_contract_bytes, ); - let taker_data = SwapMsg::NegotiationReply(my_negotiation_data); + let (topic, taker_data) = (swap_topic(&self.uuid), SwapMsg::NegotiationReply(my_negotiation_data)); + debug!("Sending taker negotiation data {:?}", taker_data); let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), - swap_topic(&self.uuid), + topic, taker_data, NEGOTIATE_TIMEOUT_SEC as f64 / 6., self.p2p_privkey, ); + let recv_fut = recv_swap_msg( self.ctx.clone(), |store| store.negotiated.take(), @@ -1287,8 +1323,8 @@ impl TakerSwap { secret_hash: maker_data.secret_hash().into(), maker_coin_swap_contract_addr, taker_coin_swap_contract_addr, - maker_coin_htlc_pubkey: Some(maker_data.maker_coin_htlc_pub().into()), - taker_coin_htlc_pubkey: Some(maker_data.taker_coin_htlc_pub().into()), + maker_coin_htlc_pubkey: Some(*maker_data.maker_coin_htlc_pub()), + taker_coin_htlc_pubkey: Some(*maker_data.taker_coin_htlc_pub()), }, )])) } @@ -1301,12 +1337,25 @@ impl TakerSwap { TakerSwapEvent::TakerFeeSendFailed(ERRL!("Timeout {} > {}", now, expire_at).into()), ])); } - - let fee_amount = - dex_fee_amount_from_taker_coin(self.taker_coin.deref(), &self.r().data.maker_coin, &self.taker_amount); + let dex_fee = DexFee::new_with_taker_pubkey( + self.taker_coin.deref(), + &self.r().data.maker_coin, + &self.taker_amount, + &self.my_taker_coin_htlc_pub().0, + ); + if matches!(dex_fee, DexFee::NoFee) { + info!("Taker fee tx not sent for dex taker"); + let empty_tx_ident = TransactionIdentifier { + tx_hex: BytesJson::from(vec![]), + tx_hash: BytesJson::from(vec![]), + }; + return Ok((Some(TakerSwapCommand::WaitForMakerPayment), vec![ + TakerSwapEvent::TakerFeeSent(empty_tx_ident), + ])); + } let fee_tx = self .taker_coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, fee_amount, self.uuid.as_bytes(), expire_at) + .send_taker_fee(dex_fee, self.uuid.as_bytes(), expire_at) .await; let transaction = match fee_tx { Ok(t) => t, @@ -1506,6 +1555,104 @@ impl TakerSwap { } } + /// Sets up the watcher reward for the taker's payment in the swap. + /// + /// The reward mainly serves as compensation to watchers for the mining fees + /// paid to execute the transactions. + /// + /// The reward configuration depends on the specific requirements of the coins + /// involved in the swap. + /// Some coins may not support watcher rewards at all. + async fn setup_watcher_reward(&self, taker_payment_lock: u64) -> Result, String> { + if !self.r().watcher_reward { + return Ok(None); + } + + let reward_amount = self.r().reward_amount.clone(); + self.taker_coin + .get_taker_watcher_reward( + &self.maker_coin, + Some(self.taker_amount.clone().into()), + Some(self.maker_amount.clone().into()), + reward_amount, + taker_payment_lock, + ) + .await + .map(Some) + .map_err(|err| ERRL!("Watcher reward error: {}", err.to_string())) + } + + /// Processes watcher-related logic for the swap by preparing and broadcasting necessary data. + /// + /// This function creates spend/refund preimages and broadcasts them to watchers if both coins + /// support watcher functionality and watchers are enabled. + /// + /// The preimages allow watchers to either complete the swap by spending the maker payment + /// or refund the taker payment if needed. + async fn process_watcher_logic(&self, transaction: &TransactionEnum) -> Option { + let watchers_enabled_and_supported = !self.ctx.disable_watchers_globally() + && self.taker_coin.is_supported_by_watchers() + && self.maker_coin.is_supported_by_watchers(); + + if !watchers_enabled_and_supported { + return None; + } + + let maker_payment_spend_preimage_fut = self.maker_coin.create_maker_payment_spend_preimage( + &self.r().maker_payment.as_ref().unwrap().tx_hex, + self.maker_payment_lock.load(Ordering::Relaxed), + self.r().other_maker_coin_htlc_pub.as_slice(), + &self.r().secret_hash.0, + &self.unique_swap_data()[..], + ); + + let taker_payment_refund_preimage_fut = self.taker_coin.create_taker_payment_refund_preimage( + &transaction.tx_hex(), + self.r().data.taker_payment_lock, + &*self.r().other_taker_coin_htlc_pub, + &self.r().secret_hash.0, + &self.r().data.taker_coin_swap_contract_address, + &self.unique_swap_data(), + ); + + match try_join( + maker_payment_spend_preimage_fut.compat(), + taker_payment_refund_preimage_fut.compat(), + ) + .await + { + Ok((maker_payment_spend, taker_payment_refund)) => { + let watcher_data = self.create_watcher_data( + transaction.tx_hash_as_bytes().into_vec(), + maker_payment_spend.tx_hex(), + taker_payment_refund.tx_hex(), + ); + let swpmsg_watcher = SwapWatcherMsg::TakerSwapWatcherMsg(watcher_data); + + let htlc_keypair = self.taker_coin.derive_htlc_key_pair(&self.unique_swap_data()); + broadcast_swap_message( + &self.ctx, + watcher_topic(&self.r().data.taker_coin), + swpmsg_watcher, + &Some(htlc_keypair), + ); + + info!("{}", WATCHER_MESSAGE_SENT_LOG); + Some(TakerSwapEvent::WatcherMessageSent( + Some(maker_payment_spend.tx_hex()), + Some(taker_payment_refund.tx_hex()), + )) + }, + Err(e) => { + error!( + "The watcher message could not be sent, error creating at least one of the preimages: {}", + e.get_plain_text_format() + ); + None + }, + } + } + async fn send_taker_payment(&self) -> Result<(Option, Vec), String> { #[cfg(test)] if self.fail_at == Some(FailAt::TakerPayment) { @@ -1514,96 +1661,33 @@ impl TakerSwap { ])); } - let timeout = self.r().data.maker_payment_wait; - let now = now_sec(); - if now > timeout { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), - ])); - } - + // Extract values from the lock before async operations let taker_payment_lock = self.r().data.taker_payment_lock; let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; let secret_hash = self.r().secret_hash.clone(); + let taker_coin_start_block = self.r().data.taker_coin_start_block; let taker_coin_swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); let unique_data = self.unique_swap_data(); let taker_amount_decimal = self.taker_amount.to_decimal(); let payment_instructions = self.r().payment_instructions.clone(); - let f = self.taker_coin.check_if_my_payment_sent(CheckIfMyPaymentSentArgs { - time_lock: taker_payment_lock, - other_pub: other_taker_coin_htlc_pub.as_slice(), - secret_hash: &secret_hash.0, - search_from_block: self.r().data.taker_coin_start_block, - swap_contract_address: &taker_coin_swap_contract_address, - swap_unique_data: &unique_data, - amount: &taker_amount_decimal, - payment_instructions: &payment_instructions, - }); - - let reward_amount = self.r().reward_amount.clone(); - let wait_until = taker_payment_lock; - let watcher_reward = if self.r().watcher_reward { - match self - .taker_coin - .get_taker_watcher_reward( - &self.maker_coin, - Some(self.taker_amount.clone().into()), - Some(self.maker_amount.clone().into()), - reward_amount, - wait_until, - ) - .await - { - Ok(reward) => Some(reward), - Err(err) => { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed( - ERRL!("Watcher reward error: {}", err.to_string()).into(), - ), - ])) - }, - } - } else { - None - }; - - let transaction = match f.await { - Ok(res) => match res { - Some(tx) => tx, - None => { - let time_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => taker_payment_lock, - }; - let lock_duration = self.r().data.lock_duration; - let payment = self - .taker_coin - .send_taker_payment(SendPaymentArgs { - time_lock_duration: lock_duration, - time_lock, - other_pubkey: &*other_taker_coin_htlc_pub, - secret_hash: &secret_hash.0, - amount: taker_amount_decimal, - swap_contract_address: &taker_coin_swap_contract_address, - swap_unique_data: &unique_data, - payment_instructions: &payment_instructions, - watcher_reward, - wait_for_confirmation_until: taker_payment_lock, - }) - .await; - match payment { - Ok(t) => t, - Err(err) => { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed( - ERRL!("{}", err.get_plain_text_format()).into(), - ), - ])); - }, - } - }, - }, + // Look for previously sent taker payment in case of restart + let maybe_existing_payment = match self + .taker_coin + .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: taker_payment_lock, + other_pub: other_taker_coin_htlc_pub.as_slice(), + secret_hash: &secret_hash.0, + search_from_block: taker_coin_start_block, + swap_contract_address: &taker_coin_swap_contract_address, + swap_unique_data: &unique_data, + amount: &taker_amount_decimal, + payment_instructions: &payment_instructions, + }) + .await + { + Ok(Some(tx)) => Some(tx), + Ok(None) => None, Err(e) => { return Ok((Some(TakerSwapCommand::Finish), vec![ TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("{}", e).into()), @@ -1611,6 +1695,61 @@ impl TakerSwap { }, }; + // If the payment is not yet sent, make sure we didn't miss the deadline for sending it. + if maybe_existing_payment.is_none() { + let timeout = self.r().data.maker_payment_wait; + let now = now_sec(); + if now > timeout { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), + ])); + } + } + + // Set up watcher reward if enable + let watcher_reward = match self.setup_watcher_reward(taker_payment_lock).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed(err.into()), + ])); + }, + }; + + // Use existing payment or create new one + let transaction = match maybe_existing_payment { + Some(tx) => tx, + None => { + let lock_duration = self.r().data.lock_duration; + match self + .taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration: lock_duration, + time_lock: taker_payment_lock, + other_pubkey: &*other_taker_coin_htlc_pub, + secret_hash: &secret_hash.0, + amount: taker_amount_decimal, + swap_contract_address: &taker_coin_swap_contract_address, + swap_unique_data: &unique_data, + payment_instructions: &payment_instructions, + watcher_reward, + wait_for_confirmation_until: taker_payment_lock, + }) + .await + { + Ok(t) => t, + Err(err) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed( + ERRL!("{}", err.get_plain_text_format()).into(), + ), + ])) + }, + } + }, + }; + + // Create transaction identifier and prepare `TakerPaymentSent` success event let tx_hash = transaction.tx_hash_as_bytes(); let tx_hex = BytesJson::from(transaction.tx_hex()); info!("Taker payment tx hash {:02x}", tx_hash); @@ -1618,65 +1757,11 @@ impl TakerSwap { tx_hex: tx_hex.clone(), tx_hash, }; - let mut swap_events = vec![TakerSwapEvent::TakerPaymentSent(tx_ident)]; - if self.ctx.use_watchers() - && self.taker_coin.is_supported_by_watchers() - && self.maker_coin.is_supported_by_watchers() - { - let maker_payment_spend_preimage_fut = self.maker_coin.create_maker_payment_spend_preimage( - &self.r().maker_payment.as_ref().unwrap().tx_hex, - self.maker_payment_lock.load(Ordering::Relaxed), - self.r().other_maker_coin_htlc_pub.as_slice(), - &self.r().secret_hash.0, - &self.unique_swap_data()[..], - ); - - let time_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => self.r().data.taker_payment_lock, - }; - let taker_payment_refund_preimage_fut = self.taker_coin.create_taker_payment_refund_preimage( - &transaction.tx_hex(), - time_lock, - &*self.r().other_taker_coin_htlc_pub, - &self.r().secret_hash.0, - &self.r().data.taker_coin_swap_contract_address, - &self.unique_swap_data(), - ); - let payment_fut_pair = try_join( - maker_payment_spend_preimage_fut.compat(), - taker_payment_refund_preimage_fut.compat(), - ); - match payment_fut_pair.await { - Ok((maker_payment_spend, taker_payment_refund)) => { - let watcher_data = self.create_watcher_data( - transaction.tx_hash_as_bytes().into_vec(), - maker_payment_spend.tx_hex(), - taker_payment_refund.tx_hex(), - ); - let swpmsg_watcher = SwapWatcherMsg::TakerSwapWatcherMsg(watcher_data); - - let htlc_keypair = self.taker_coin.derive_htlc_key_pair(&self.unique_swap_data()); - broadcast_swap_message( - &self.ctx, - watcher_topic(&self.r().data.taker_coin), - swpmsg_watcher, - &Some(htlc_keypair), - ); - - swap_events.push(TakerSwapEvent::WatcherMessageSent( - Some(maker_payment_spend.tx_hex()), - Some(taker_payment_refund.tx_hex()), - )); - info!("{}", WATCHER_MESSAGE_SENT_LOG); - }, - Err(e) => error!( - "The watcher message could not be sent, error creating at least one of the preimages: {}", - e.get_plain_text_format() - ), - } + // Process watcher logic if enabled and supported by both coins + if let Some(watcher_event) = self.process_watcher_logic(&transaction).await { + swap_events.push(watcher_event); } Ok((Some(TakerSwapCommand::WaitForTakerPaymentSpend), swap_events)) @@ -1689,7 +1774,7 @@ impl TakerSwap { let mut watcher_broadcast_abort_handle = None; // Watchers cannot be used for lightning swaps for now // Todo: Check if watchers can work in some cases with lightning and implement it if it's possible, this part will probably work if only the taker is lightning since the preimage is available - if self.ctx.use_watchers() + if !self.ctx.disable_watchers_globally() && self.taker_coin.is_supported_by_watchers() && self.maker_coin.is_supported_by_watchers() { @@ -1733,11 +1818,7 @@ impl TakerSwap { info!("Waiting for maker to spend taker payment!"); - let wait_until = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => self.r().data.taker_payment_lock, - }; - + let wait_until = self.r().data.taker_payment_lock; let secret_hash = self.r().secret_hash.clone(); let taker_coin_start_block = self.r().data.taker_coin_start_block; let taker_coin_swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); @@ -1776,7 +1857,7 @@ impl TakerSwap { .extract_secret(&secret_hash.0, &tx_ident.tx_hex, watcher_reward) .await { - Ok(bytes) => H256Json::from(bytes.as_slice()), + Ok(secret) => H256Json::from(secret), Err(e) => { return Ok((Some(TakerSwapCommand::Finish), vec![ TakerSwapEvent::TakerPaymentWaitForSpendFailed(ERRL!("{}", e).into()), @@ -2040,10 +2121,13 @@ impl TakerSwap { } let crypto_ctx = try_s!(CryptoCtx::from_ctx(&ctx)); - let my_persistent_pub = H264::from(&**crypto_ctx.mm2_internal_key_pair().public()); + let my_persistent_pub = { + let my_persistent_pub: [u8; 33] = try_s!(crypto_ctx.mm2_internal_key_pair().public_slice().try_into()); + my_persistent_pub.into() + }; let mut maker = bits256::from([0; 32]); - maker.bytes = data.maker.0; + maker.bytes = data.maker_pubkey.0; let conf_settings = SwapConfirmationsSettings { maker_coin_confs: data.maker_payment_confirmations, maker_coin_nota: data @@ -2355,13 +2439,17 @@ impl AtomicSwap for TakerSwap { let mut result = Vec::new(); // if taker fee is not sent yet it must be virtually locked - let taker_fee_amount = - dex_fee_amount_from_taker_coin(self.taker_coin.deref(), &self.r().data.maker_coin, &self.taker_amount); + let taker_fee = DexFee::new_with_taker_pubkey( + self.taker_coin.deref(), + &self.r().data.maker_coin, + &self.taker_amount, + &self.my_taker_coin_htlc_pub().0, + ); let trade_fee = self.r().data.fee_to_send_taker_fee.clone().map(TradeFee::from); if self.r().taker_fee.is_none() { result.push(LockedAmount { coin: self.taker_coin.ticker().to_owned(), - amount: taker_fee_amount.total_spend_amount(), + amount: taker_fee.total_spend_amount(), trade_fee, }); } @@ -2424,7 +2512,7 @@ pub async fn check_balance_for_taker_swap( let params = match prepared_params { Some(params) => params, None => { - let dex_fee = dex_fee_amount_from_taker_coin(my_coin, other_coin.ticker(), &volume); + let dex_fee = DexFee::new_from_taker_coin(my_coin, other_coin.ticker(), &volume); // taker_pubkey is not known yet so we get max dexfee to estimate max swap amount let fee_to_send_dex_fee = my_coin .get_fee_to_send_taker_fee(dex_fee.clone(), stage) .await @@ -2512,15 +2600,20 @@ pub async fn taker_swap_trade_preimage( TakerAction::Buy => rel_amount.clone(), }; - let dex_amount = dex_fee_amount_from_taker_coin(my_coin.deref(), other_coin_ticker, &my_coin_volume); + let dex_fee = DexFee::new_with_taker_pubkey( + my_coin.deref(), + other_coin_ticker, + &my_coin_volume, + &my_coin.derive_htlc_pubkey(&[]), // passing empty unique_data because we need only the permanent pubkey here (not derived from the unique data) + ); let taker_fee = TradeFee { coin: my_coin_ticker.to_owned(), - amount: dex_amount.total_spend_amount(), + amount: dex_fee.total_spend_amount(), paid_from_trading_vol: false, }; let fee_to_send_taker_fee = my_coin - .get_fee_to_send_taker_fee(dex_amount.clone(), stage) + .get_fee_to_send_taker_fee(dex_fee.clone(), stage) .await .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, my_coin_ticker))?; @@ -2536,7 +2629,7 @@ pub async fn taker_swap_trade_preimage( .mm_err(|e| TradePreimageRpcError::from_trade_preimage_error(e, other_coin_ticker))?; let prepared_params = TakerSwapPreparedParams { - dex_fee: dex_amount.total_spend_amount(), + dex_fee: dex_fee.total_spend_amount(), fee_to_send_dex_fee: fee_to_send_taker_fee.clone(), taker_payment_trade_fee: my_coin_trade_fee.clone(), maker_payment_spend_trade_fee: other_coin_trade_fee.clone(), @@ -2658,7 +2751,7 @@ pub async fn calc_max_taker_vol( let max_vol = if my_coin == max_trade_fee.coin { // second case let max_possible_2 = &max_possible - &max_trade_fee.amount; - let max_dex_fee = dex_fee_amount_from_taker_coin(coin.deref(), other_coin, &max_possible_2); + let max_dex_fee = DexFee::new_from_taker_coin(coin.deref(), other_coin, &max_possible_2); // taker_pubkey is not known yet so we get max dex fee to calc max volume let max_fee_to_send_taker_fee = coin .get_fee_to_send_taker_fee(max_dex_fee.clone(), stage) .await @@ -2703,7 +2796,7 @@ pub fn max_taker_vol_from_available( rel: &str, min_tx_amount: &MmNumber, ) -> Result> { - let dex_fee_rate = dex_fee_rate(base, rel); + let dex_fee_rate = DexFee::dex_fee_rate(base, rel); let threshold_coef = &(&MmNumber::from(1) + &dex_fee_rate) / &dex_fee_rate; let max_vol = if available > min_tx_amount * &threshold_coef { available / (MmNumber::from(1) + dex_fee_rate) @@ -2723,7 +2816,7 @@ pub fn max_taker_vol_from_available( #[cfg(all(test, not(target_arch = "wasm32")))] mod taker_swap_tests { use super::*; - use crate::lp_swap::{dex_fee_amount, get_locked_amount_by_other_swaps}; + use crate::lp_swap::get_locked_amount_by_other_swaps; use coins::eth::{addr_from_str, signed_eth_tx_from_bytes, SignedEthTx}; use coins::utxo::UtxoTx; use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TestCoin}; @@ -2846,7 +2939,7 @@ mod taker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - TestCoin::extract_secret.mock_safe(|_, _, _, _| MockResult::Return(Box::pin(async move { Ok(vec![]) }))); + TestCoin::extract_secret.mock_safe(|_, _, _, _| MockResult::Return(Box::pin(async move { Ok([0; 32]) }))); static mut MY_PAYMENT_SENT_CALLED: bool = false; TestCoin::check_if_my_payment_sent.mock_safe(|_, _| { @@ -2973,7 +3066,7 @@ mod taker_swap_tests { TestCoin::ticker.mock_safe(|_| MockResult::Return("ticker")); TestCoin::swap_contract_address.mock_safe(|_| MockResult::Return(None)); - TestCoin::extract_secret.mock_safe(|_, _, _, _| MockResult::Return(Box::pin(async move { Ok(vec![]) }))); + TestCoin::extract_secret.mock_safe(|_, _, _, _| MockResult::Return(Box::pin(async move { Ok([0; 32]) }))); static mut SEARCH_TX_SPEND_CALLED: bool = false; TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _| { @@ -3148,7 +3241,10 @@ mod taker_swap_tests { let max_taker_vol = max_taker_vol_from_available(available.clone(), "RICK", "MORTY", &min_tx_amount) .expect("!max_taker_vol_from_available"); - let dex_fee = dex_fee_amount(base, "MORTY", &max_taker_vol, &min_tx_amount).fee_amount(); + let coin = TestCoin::new(base); + let mock_min_tx_amount = min_tx_amount.clone(); + TestCoin::min_tx_amount.mock_safe(move |_| MockResult::Return(mock_min_tx_amount.clone().into())); + let dex_fee = DexFee::new_from_taker_coin(&coin, "MORTY", &max_taker_vol).total_spend_amount(); assert!(min_tx_amount < dex_fee); assert!(min_tx_amount <= max_taker_vol); assert_eq!(max_taker_vol + dex_fee, available); @@ -3168,7 +3264,11 @@ mod taker_swap_tests { let base = if is_kmd { "KMD" } else { "RICK" }; let max_taker_vol = max_taker_vol_from_available(available.clone(), base, "MORTY", &min_tx_amount) .expect("!max_taker_vol_from_available"); - let dex_fee = dex_fee_amount(base, "MORTY", &max_taker_vol, &min_tx_amount).fee_amount(); + + let coin = TestCoin::new(base); + let mock_min_tx_amount = min_tx_amount.clone(); + TestCoin::min_tx_amount.mock_safe(move |_| MockResult::Return(mock_min_tx_amount.clone().into())); + let dex_fee = DexFee::new_from_taker_coin(&coin, "MORTY", &max_taker_vol).fee_amount(); // returns Standard dex_fee (default for TestCoin) println!( "available={:?} max_taker_vol={:?} dex_fee={:?}", available.to_decimal(), @@ -3276,8 +3376,7 @@ mod taker_swap_tests { .unwrap(); let swaps_ctx = SwapsContext::from_ctx(&ctx).unwrap(); let arc = Arc::new(swap); - let weak_ref = Arc::downgrade(&arc); - swaps_ctx.running_swaps.lock().unwrap().push(weak_ref); + swaps_ctx.running_swaps.lock().unwrap().insert(arc.uuid, arc); let actual = get_locked_amount(&ctx, "RICK"); assert_eq!(actual, MmNumber::from(0)); diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs index 29f3d07277..eea040316d 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -1,13 +1,14 @@ +use super::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use super::swap_v2_common::*; use super::{LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, TakerSwapPreparedParams, NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC}; use crate::lp_swap::swap_lock::SwapLock; use crate::lp_swap::{broadcast_swap_v2_msg_every, check_balance_for_taker_swap, recv_swap_v2_msg, swap_v2_topic, - SecretHashAlgo, SwapConfirmationsSettings, TransactionIdentifier, MAX_STARTED_AT_DIFF, - TAKER_SWAP_V2_TYPE}; + SwapConfirmationsSettings, TransactionIdentifier, MAX_STARTED_AT_DIFF, TAKER_SWAP_V2_TYPE}; use crate::lp_swap::{swap_v2_pb::*, NO_REFUND_FEE}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; +use coins::hd_wallet::AddrToString; use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoin, ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, @@ -16,8 +17,9 @@ use coins::{CanRefundHtlc, ConfirmPaymentInput, DexFee, FeeApproxStage, GenTaker use common::executor::abortable_queue::AbortableQueue; use common::executor::{AbortableSystem, Timer}; use common::log::{debug, error, info, warn}; -use common::{Future01CompatExt, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::Future01CompatExt; use crypto::privkey::SerializableSecp256k1Keypair; +use crypto::secret_hash_algo::SecretHashAlgo; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -41,13 +43,14 @@ cfg_native!( cfg_wasm32!( use crate::lp_swap::swap_wasm_db::{MySwapsFiltersTable, SavedSwapTable}; + use crate::swap_versioning::legacy_swap_version; ); // This is needed to have Debug on messages #[allow(unused_imports)] use prost::Message; /// Negotiation data representation to be stored in DB. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct StoredNegotiationData { maker_payment_locktime: u64, maker_secret_hash: BytesJson, @@ -59,7 +62,7 @@ pub struct StoredNegotiationData { } /// Represents events produced by taker swap states. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "event_type", content = "event_data")] pub enum TakerSwapEvent { /// Swap has been successfully initialized. @@ -109,6 +112,14 @@ pub enum TakerSwapEvent { maker_payment: TransactionIdentifier, negotiation_data: StoredNegotiationData, }, + /// 'Taker payment`' was sent and preimage of 'taker payment spend' was skipped. + TakerPaymentSentAndPreimageSendingSkipped { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + taker_payment: TransactionIdentifier, + maker_payment: TransactionIdentifier, + negotiation_data: StoredNegotiationData, + }, /// Something went wrong, so taker payment refund is required. TakerPaymentRefundRequired { taker_payment: TransactionIdentifier, @@ -141,6 +152,7 @@ pub enum TakerSwapEvent { taker_payment: TransactionIdentifier, taker_payment_spend: TransactionIdentifier, maker_payment_spend: TransactionIdentifier, + negotiation_data: StoredNegotiationData, }, /// Swap has been finished with taker funding tx refund TakerFundingRefunded { @@ -202,6 +214,7 @@ impl StateMachineStorage for TakerSwapStorage { ":taker_coin_confs": repr.conf_settings.taker_coin_confs, ":taker_coin_nota": repr.conf_settings.taker_coin_nota, ":other_p2p_pub": repr.maker_p2p_pub.to_bytes(), + ":swap_version": repr.swap_version, }; insert_new_swap_v2(&ctx, sql_params)?; Ok(()) @@ -309,6 +322,9 @@ pub struct TakerSwapDbRepr { pub events: Vec, /// Maker's P2P pubkey pub maker_p2p_pub: Secp256k1PubkeySerialize, + /// Swap protocol version + #[cfg_attr(target_arch = "wasm32", serde(default = "legacy_swap_version"))] + pub swap_version: u8, } #[cfg(not(target_arch = "wasm32"))] @@ -363,6 +379,7 @@ impl TakerSwapDbRepr { .map_err(|e| SqlError::FromSqlConversionFailure(19, SqlType::Blob, Box::new(e))) })? .into(), + swap_version: row.get(20)?, }) } } @@ -399,8 +416,6 @@ pub struct TakerSwapStateMachine TakerSwapStateMachine { + #[inline] fn maker_payment_conf_timeout(&self) -> u64 { self.started_at + self.lock_duration / 3 } + #[inline] fn taker_funding_locktime(&self) -> u64 { self.started_at + self.lock_duration * 3 } + #[inline] fn taker_payment_locktime(&self) -> u64 { self.started_at + self.lock_duration } fn unique_data(&self) -> Vec { self.uuid.as_bytes().to_vec() } @@ -441,6 +463,26 @@ impl sha256(self.taker_secret.as_slice()).take().into(), } } + + fn dex_fee(&self) -> DexFee { + if self.taker_coin.ticker() == "KMD" || self.maker_coin.ticker() == "KMD" { + // Set DexFee::NoFee for swaps with KMD coin. + return DexFee::NoFee; + } + + if let Some(taker_pub) = self.taker_coin.taker_pubkey_bytes() { + // for dex fee calculation we need only permanent (non-derived for HTLC) taker pubkey here + DexFee::new_with_taker_pubkey( + &self.taker_coin, + self.maker_coin.ticker(), + &self.taker_volume, + taker_pub.as_slice(), + ) + } else { + // Return max dex fee (if taker_pub is not known yet) + DexFee::new_from_taker_coin(&self.taker_coin, self.maker_coin.ticker(), &self.taker_volume) + } + } } #[async_trait] @@ -466,13 +508,14 @@ impl Box::new(TakerPaymentSentAndPreimageSendingSkipped { + maker_coin_start_block, + taker_coin_start_block, + taker_payment: recreate_ctx + .taker_coin + .parse_tx(&taker_payment.tx_hex.0) + .map_err(|e| SwapRecreateError::FailedToParseData(e.to_string()))?, + maker_payment: recreate_ctx + .maker_coin + .parse_tx(&maker_payment.tx_hex.0) + .map_err(|e| SwapRecreateError::FailedToParseData(e.to_string()))?, + negotiation_data: NegotiationData::from_stored_data( + negotiation_data, + &recreate_ctx.maker_coin, + &recreate_ctx.taker_coin, + )?, + }), TakerSwapEvent::TakerPaymentRefundRequired { taker_payment, negotiation_data, @@ -702,6 +768,7 @@ impl Box::new(MakerPaymentSpent { maker_coin_start_block, taker_coin_start_block, @@ -721,6 +788,11 @@ impl return MmError::err(SwapRecreateError::SwapAborted), TakerSwapEvent::Completed => return MmError::err(SwapRecreateError::SwapCompleted), @@ -732,12 +804,6 @@ impl MmNumber::default() { - DexFee::with_burn(repr.dex_fee_amount, repr.dex_fee_burn) - } else { - DexFee::Standard(repr.dex_fee_amount) - }; - let machine = TakerSwapStateMachine { ctx: storage.ctx.clone(), abortable_system: storage @@ -752,7 +818,6 @@ impl (), } + // Send a notification to the swap status streamer about a new event. + self.ctx + .event_stream_manager + .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::TakerV2 { + uuid: self.uuid, + event: event.clone(), + }) + .ok(); } - fn on_kickstart_event( - &mut self, - event: <::DbRepr as StateMachineDbRepr>::Event, - ) { + fn on_kickstart_event(&mut self, event: TakerSwapEvent) { match event { TakerSwapEvent::Initialized { taker_payment_fee, .. } | TakerSwapEvent::Negotiated { taker_payment_fee, .. } => { @@ -853,7 +926,7 @@ impl Negotiation StoredNegotiationData { maker_payment_locktime: self.maker_payment_locktime, maker_secret_hash: self.maker_secret_hash.clone().into(), - taker_coin_maker_address: self.taker_coin_maker_address.to_string(), + taker_coin_maker_address: self.taker_coin_maker_address.addr_to_string(), maker_coin_htlc_pub_from_maker: self.maker_coin_htlc_pub_from_maker.to_bytes().into(), taker_coin_htlc_pub_from_maker: self.taker_coin_htlc_pub_from_maker.to_bytes().into(), maker_coin_swap_contract: self.maker_coin_swap_contract.clone().map(|b| b.into()), @@ -1228,7 +1302,7 @@ impl { + maker_coin_start_block: u64, + taker_coin_start_block: u64, + taker_payment: TakerCoin::Tx, + maker_payment: MakerCoin::Tx, + negotiation_data: NegotiationData, +} + +impl + TransitionFrom> + for TakerPaymentSentAndPreimageSendingSkipped +{ +} +impl + TransitionFrom> + for TakerPaymentSentAndPreimageSendingSkipped +{ +} + +#[async_trait] +impl State + for TakerPaymentSentAndPreimageSendingSkipped +{ + type StateMachine = TakerSwapStateMachine; + + async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + info!("Skipped the generation of the taker payment spend preimage and its p2p message broadcast because the taker's coin does not require this preimage for the process."); + + if !state_machine.require_maker_payment_confirm_before_funding_spend { + let input = ConfirmPaymentInput { + payment_tx: self.maker_payment.tx_hex(), + confirmations: state_machine.conf_settings.maker_coin_confs, + requires_nota: state_machine.conf_settings.maker_coin_nota, + wait_until: state_machine.maker_payment_conf_timeout(), + check_every: 10, + }; + + if let Err(e) = state_machine.maker_coin.wait_for_confirmations(input).compat().await { + let next_state = TakerPaymentRefundRequired { + taker_payment: self.taker_payment, + negotiation_data: self.negotiation_data, + reason: TakerPaymentRefundReason::MakerPaymentNotConfirmedInTime(e), + }; + return Self::change_state(next_state, state_machine).await; + } + } + + let taker_payment_spend = match state_machine + .taker_coin + .find_taker_payment_spend_tx( + &self.taker_payment, + self.taker_coin_start_block, + state_machine.taker_payment_locktime(), + ) + .await + { + Ok(tx) => tx, + Err(e) => { + let next_state = TakerPaymentRefundRequired { + taker_payment: self.taker_payment, + negotiation_data: self.negotiation_data, + reason: TakerPaymentRefundReason::MakerDidNotSpendInTime(format!("{}", e)), + }; + return Self::change_state(next_state, state_machine).await; + }, + }; + info!( + "Found taker payment spend {} tx {:02x} during swap {}", + state_machine.taker_coin.ticker(), + taker_payment_spend.tx_hash_as_bytes(), + state_machine.uuid + ); + + let next_state = TakerPaymentSpent { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + maker_payment: self.maker_payment, + taker_payment: self.taker_payment, + taker_payment_spend, + negotiation_data: self.negotiation_data, + }; + Self::change_state(next_state, state_machine).await + } +} + +impl StorableState + for TakerPaymentSentAndPreimageSendingSkipped +{ + type StateMachine = TakerSwapStateMachine; + + fn get_event(&self) -> TakerSwapEvent { + TakerSwapEvent::TakerPaymentSentAndPreimageSendingSkipped { + maker_coin_start_block: self.maker_coin_start_block, + taker_coin_start_block: self.taker_coin_start_block, + taker_payment: TransactionIdentifier { + tx_hex: self.taker_payment.tx_hex().into(), + tx_hash: self.taker_payment.tx_hash_as_bytes(), + }, + maker_payment: TransactionIdentifier { + tx_hex: self.maker_payment.tx_hex().into(), + tx_hash: self.maker_payment.tx_hash_as_bytes(), + }, + negotiation_data: self.negotiation_data.to_stored_data(), + } + } +} + /// Represents the reason taker funding refund #[derive(Clone, Debug, Deserialize, Serialize)] pub enum TakerFundingRefundReason { @@ -1798,10 +1989,10 @@ impl TransitionFrom> for TakerPaymentRefundRequired { } +impl + TransitionFrom> + for TakerPaymentRefundRequired +{ +} impl TransitionFrom> for TakerPaymentRefundRequired { } +impl + TransitionFrom> for TakerPaymentRefundRequired +{ +} #[async_trait] impl State @@ -1908,7 +2109,7 @@ impl TransitionFrom> for TakerPaymentSpent { } +impl + TransitionFrom> + for TakerPaymentSpent +{ +} #[async_trait] impl State @@ -2072,11 +2289,7 @@ impl s, @@ -2091,7 +2304,7 @@ impl, } impl @@ -2186,6 +2401,7 @@ impl; async fn on_changed(self: Box, state_machine: &mut Self::StateMachine) -> StateResult { + if state_machine.require_maker_payment_spend_confirm { + let input = ConfirmPaymentInput { + payment_tx: self.maker_payment_spend.tx_hex(), + confirmations: state_machine.conf_settings.maker_coin_confs, + requires_nota: state_machine.conf_settings.maker_coin_nota, + wait_until: state_machine.taker_payment_locktime(), + check_every: 10, + }; + + if let Err(e) = state_machine.maker_coin.wait_for_confirmations(input).compat().await { + let next_state = TakerPaymentRefundRequired { + taker_payment: self.taker_payment, + negotiation_data: self.negotiation_data, + reason: TakerPaymentRefundReason::MakerPaymentSpendNotConfirmedInTime(e.to_string()), + }; + return Self::change_state(next_state, state_machine).await; + } + } + Self::change_state(Completed::new(), state_machine).await } } diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index 20a154b28c..b06318e56f 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -1,6 +1,8 @@ +use common::password_policy::{password_policy, PasswordPolicyError}; use common::HttpStatusCode; use crypto::{decrypt_mnemonic, encrypt_mnemonic, generate_mnemonic, CryptoCtx, CryptoInitError, EncryptedData, MnemonicError}; +use enum_derives::EnumFromStringify; use http::StatusCode; use itertools::Itertools; use mm2_core::mm_ctx::MmArc; @@ -21,13 +23,12 @@ cfg_wasm32! { cfg_native! { use mnemonics_storage::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError}; } - #[cfg(not(target_arch = "wasm32"))] mod mnemonics_storage; #[cfg(target_arch = "wasm32")] mod mnemonics_wasm_db; type WalletInitResult = Result>; -#[derive(Debug, Deserialize, Display, Serialize)] +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize)] pub enum WalletInitError { #[display(fmt = "Error deserializing '{}' config field: {}", field, error)] ErrorDeserializingConfig { @@ -48,6 +49,9 @@ pub enum WalletInitError { MnemonicError(String), #[display(fmt = "Error initializing crypto context: {}", _0)] CryptoInitError(String), + #[display(fmt = "Password does not meet policy requirements: {}", _0)] + #[from_stringify("PasswordPolicyError")] + PasswordPolicyViolation(String), InternalError(String), } @@ -139,7 +143,7 @@ async fn read_and_decrypt_passphrase_if_available( Some(encrypted_passphrase) => { let mnemonic = decrypt_mnemonic(&encrypted_passphrase, wallet_password) .mm_err(|e| ReadPassphraseError::DecryptionError(e.to_string()))?; - Ok(Some(mnemonic.to_string())) + Ok(Some(mnemonic)) }, None => Ok(None), } @@ -173,6 +177,15 @@ async fn retrieve_or_create_passphrase( Ok(Some(passphrase_from_file)) }, None => { + if wallet_password.is_empty() { + return MmError::err(WalletInitError::PasswordPolicyViolation( + "`wallet_password` cannot be empty".to_string(), + )); + } + let is_weak_password_accepted = ctx.conf["allow_weak_password"].as_bool().unwrap_or(false); + if !is_weak_password_accepted { + password_policy(wallet_password)?; + } // If no passphrase is found, generate a new one let new_passphrase = generate_mnemonic(ctx)?.to_string(); // Encrypt and save the new passphrase @@ -195,6 +208,15 @@ async fn confirm_or_encrypt_and_store_passphrase( Ok(Some(passphrase_from_file)) }, None => { + if wallet_password.is_empty() { + return MmError::err(WalletInitError::PasswordPolicyViolation( + "`wallet_password` cannot be empty".to_string(), + )); + } + let is_weak_password_accepted = ctx.conf["allow_weak_password"].as_bool().unwrap_or(false); + if !is_weak_password_accepted { + password_policy(wallet_password)?; + } // If no passphrase is found in the file, encrypt and save the provided passphrase encrypt_and_save_passphrase(ctx, wallet_name, passphrase, wallet_password).await?; Ok(Some(passphrase.to_string())) @@ -214,7 +236,7 @@ async fn decrypt_validate_or_save_passphrase( wallet_password: &str, ) -> WalletInitResult> { // Decrypt the provided encrypted passphrase - let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?.to_string(); + let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?; match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { Some(passphrase_from_file) if decrypted_passphrase == passphrase_from_file => { @@ -253,7 +275,7 @@ async fn process_wallet_with_name( async fn process_passphrase_logic( ctx: &MmArc, - wallet_name: Option, + wallet_name: Option<&str>, passphrase: Option, ) -> WalletInitResult> { match (wallet_name, passphrase) { @@ -268,7 +290,7 @@ async fn process_passphrase_logic( (Some(wallet_name), passphrase_option) => { let wallet_password = deserialize_config_field::(ctx, "wallet_password")?; - process_wallet_with_name(ctx, &wallet_name, passphrase_option, &wallet_password).await + process_wallet_with_name(ctx, wallet_name, passphrase_option, &wallet_password).await }, } } @@ -307,8 +329,8 @@ pub(crate) async fn initialize_wallet_passphrase(ctx: &MmArc) -> WalletInitResul ctx.wallet_name .set(wallet_name.clone()) .map_to_mm(|_| WalletInitError::InternalError("Already Initialized".to_string()))?; - let passphrase = process_passphrase_logic(ctx, wallet_name, passphrase).await?; + let passphrase = process_passphrase_logic(ctx, wallet_name.as_deref(), passphrase).await?; if let Some(passphrase) = passphrase { initialize_crypto_context(ctx, &passphrase)?; } @@ -413,22 +435,30 @@ pub struct GetMnemonicResponse { pub mnemonic: MnemonicForRpc, } -#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType, EnumFromStringify)] #[serde(tag = "error_type", content = "error_data")] -pub enum GetMnemonicError { +pub enum MnemonicRpcError { #[display(fmt = "Invalid request error: {}", _0)] InvalidRequest(String), #[display(fmt = "Wallets storage error: {}", _0)] WalletsStorageError(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), + #[display(fmt = "Invalid password error: {}", _0)] + #[from_stringify("MnemonicError")] + InvalidPassword(String), + #[display(fmt = "Password does not meet policy requirements: {}", _0)] + #[from_stringify("PasswordPolicyError")] + PasswordPolicyViolation(String), } -impl HttpStatusCode for GetMnemonicError { +impl HttpStatusCode for MnemonicRpcError { fn status_code(&self) -> StatusCode { match self { - GetMnemonicError::InvalidRequest(_) => StatusCode::BAD_REQUEST, - GetMnemonicError::WalletsStorageError(_) | GetMnemonicError::Internal(_) => { + MnemonicRpcError::InvalidRequest(_) + | MnemonicRpcError::InvalidPassword(_) + | MnemonicRpcError::PasswordPolicyViolation(_) => StatusCode::BAD_REQUEST, + MnemonicRpcError::WalletsStorageError(_) | MnemonicRpcError::Internal(_) => { StatusCode::INTERNAL_SERVER_ERROR }, } @@ -436,17 +466,17 @@ impl HttpStatusCode for GetMnemonicError { } #[cfg(not(target_arch = "wasm32"))] -impl From for GetMnemonicError { - fn from(e: WalletsStorageError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) } +impl From for MnemonicRpcError { + fn from(e: WalletsStorageError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) } } #[cfg(target_arch = "wasm32")] -impl From for GetMnemonicError { - fn from(e: WalletsDBError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) } +impl From for MnemonicRpcError { + fn from(e: WalletsDBError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) } } -impl From for GetMnemonicError { - fn from(e: ReadPassphraseError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) } +impl From for MnemonicRpcError { + fn from(e: ReadPassphraseError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) } } /// Retrieves the wallet mnemonic in the requested format. @@ -456,7 +486,7 @@ impl From for GetMnemonicError { /// A `Result` type containing: /// /// * [`Ok`]([`GetMnemonicResponse`]) - The wallet mnemonic in the requested format. -/// * [`MmError`]<[`GetMnemonicError>`]> - Returns specific [`GetMnemonicError`] variants for different failure scenarios. +/// * [`MmError`]<[`MnemonicRpcError>`]> - Returns specific [`MnemonicRpcError`] variants for different failure scenarios. /// /// # Errors /// @@ -480,12 +510,12 @@ impl From for GetMnemonicError { /// Err(e) => println!("Error: {:?}", e), /// } /// ``` -pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { +pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { match req.mnemonic_format { MnemonicFormat::Encrypted => { let encrypted_mnemonic = read_encrypted_passphrase_if_available(&ctx) .await? - .ok_or_else(|| GetMnemonicError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; + .ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; Ok(GetMnemonicResponse { mnemonic: encrypted_mnemonic.into(), }) @@ -493,7 +523,7 @@ pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { let plaintext_mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &wallet_password) .await? - .ok_or_else(|| GetMnemonicError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; + .ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; Ok(GetMnemonicResponse { mnemonic: plaintext_mnemonic.into(), }) @@ -508,40 +538,13 @@ pub struct GetWalletNamesResponse { activated_wallet: Option, } -#[derive(Debug, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum GetWalletsError { - #[display(fmt = "Wallets storage error: {}", _0)] - WalletsStorageError(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), -} - -impl HttpStatusCode for GetWalletsError { - fn status_code(&self) -> StatusCode { - match self { - GetWalletsError::WalletsStorageError(_) | GetWalletsError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -#[cfg(not(target_arch = "wasm32"))] -impl From for GetWalletsError { - fn from(e: WalletsStorageError) -> Self { GetWalletsError::WalletsStorageError(e.to_string()) } -} - -#[cfg(target_arch = "wasm32")] -impl From for GetWalletsError { - fn from(e: WalletsDBError) -> Self { GetWalletsError::WalletsStorageError(e.to_string()) } -} - /// Retrieves all created wallets and the currently activated wallet. -pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult { +pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult { // We want to return wallet names in the same order for both native and wasm32 targets. let wallets = read_all_wallet_names(&ctx).await?.sorted().collect(); // Note: `ok_or` is used here on `Constructible>` to handle the case where the wallet name is not set. // `wallet_name` can be `None` in the case of no-login mode. - let activated_wallet = ctx.wallet_name.get().ok_or(GetWalletsError::Internal( + let activated_wallet = ctx.wallet_name.get().ok_or(MnemonicRpcError::Internal( "`wallet_name` not initialized yet!".to_string(), ))?; @@ -550,3 +553,46 @@ pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult MmResult<(), MnemonicRpcError> { + if req.new_password.is_empty() { + return MmError::err(MnemonicRpcError::PasswordPolicyViolation( + "`new_password` cannot be empty".to_string(), + )); + } + let is_weak_password_accepted = ctx.conf["allow_weak_password"].as_bool().unwrap_or(false); + if !is_weak_password_accepted { + password_policy(&req.new_password)?; + } + let wallet_name = ctx + .wallet_name + .get() + .ok_or(MnemonicRpcError::Internal( + "`wallet_name` not initialized yet!".to_string(), + ))? + .as_ref() + .ok_or_else(|| MnemonicRpcError::Internal("`wallet_name` cannot be None!".to_string()))?; + // read mnemonic for a wallet_name using current user's password. + let mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &req.current_password) + .await? + .ok_or(MmError::new(MnemonicRpcError::Internal(format!( + "{wallet_name}: wallet mnemonic file not found" + ))))?; + // encrypt mnemonic with new passphrase. + let encrypted_data = encrypt_mnemonic(&mnemonic, &req.new_password)?; + // save new encrypted mnemonic data with new password + save_encrypted_passphrase(&ctx, wallet_name, &encrypted_data).await?; + + Ok(()) +} diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs index e779f7b86a..25e77c27d1 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs @@ -1,7 +1,8 @@ use crypto::EncryptedData; -use mm2_core::mm_ctx::MmArc; +use mm2_core::mm_ctx::{MmArc, WALLET_FILE_EXTENSION}; use mm2_err_handle::prelude::*; use mm2_io::fs::{ensure_file_is_writable, list_files_by_extension}; +use std::path::PathBuf; type WalletsStorageResult = Result>; @@ -11,10 +12,33 @@ pub enum WalletsStorageError { FsWriteError(String), #[display(fmt = "Error reading from file: {}", _0)] FsReadError(String), + #[display(fmt = "Invalid wallet name: {}", _0)] + InvalidWalletName(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), } +fn wallet_file_path(ctx: &MmArc, wallet_name: &str) -> Result { + let wallet_name_trimmed = wallet_name.trim(); + if wallet_name_trimmed.is_empty() { + return Err("Wallet name cannot be empty or consist only of whitespace.".to_string()); + } + + if !wallet_name_trimmed + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == ' ') + { + return Err(format!( + "Invalid wallet name: '{}'. Only alphanumeric characters, spaces, dash and underscore are allowed.", + wallet_name_trimmed + )); + } + + Ok(ctx + .db_root() + .join(format!("{}.{}", wallet_name_trimmed, WALLET_FILE_EXTENSION))) +} + /// Saves the passphrase to a file associated with the given wallet name. /// /// # Returns @@ -24,7 +48,7 @@ pub(super) async fn save_encrypted_passphrase( wallet_name: &str, encrypted_passphrase_data: &EncryptedData, ) -> WalletsStorageResult<()> { - let wallet_path = ctx.wallet_file_path(wallet_name); + let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; ensure_file_is_writable(&wallet_path).map_to_mm(WalletsStorageError::FsWriteError)?; mm2_io::fs::write_json(encrypted_passphrase_data, &wallet_path, true) .await @@ -53,7 +77,7 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle ))? .clone() .ok_or_else(|| WalletsStorageError::Internal("`wallet_name` cannot be None!".to_string()))?; - let wallet_path = ctx.wallet_file_path(&wallet_name); + let wallet_path = wallet_file_path(ctx, &wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; mm2_io::fs::read_json(&wallet_path).await.mm_err(|e| { WalletsStorageError::FsReadError(format!( "Error reading passphrase from file {}: {}", @@ -64,7 +88,7 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle } pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsStorageResult> { - let wallet_names = list_files_by_extension(&ctx.db_root(), "dat", false) + let wallet_names = list_files_by_extension(&ctx.db_root(), WALLET_FILE_EXTENSION, false) .await .mm_err(|e| WalletsStorageError::FsReadError(format!("Error reading wallets directory: {}", e)))?; Ok(wallet_names) diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs index fa66cada1c..e4733a132d 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs @@ -112,7 +112,9 @@ pub(super) async fn save_encrypted_passphrase( } })?, }; - table.add_item(&mnemonics_table_item).await?; + table + .replace_item_by_unique_index("wallet_name", wallet_name, &mnemonics_table_item) + .await?; Ok(()) } diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 4876069e6d..9e7e465a78 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -42,15 +42,17 @@ #[cfg(not(target_arch = "wasm32"))] use common::block_on; use common::crash_reports::init_crash_reports; +use common::executor::Timer; use common::log; use common::log::LogLevel; use common::password_policy::password_policy; -use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; -#[cfg(feature = "custom-swap-locktime")] use common::log::warn; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] +use common::log::warn; +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use lp_swap::PAYMENT_LOCKTIME; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use std::sync::atomic::Ordering; use gstuff::slurp; @@ -81,11 +83,12 @@ pub mod lp_stats; pub mod lp_swap; pub mod lp_wallet; pub mod rpc; +mod swap_versioning; #[cfg(all(target_arch = "wasm32", test))] mod wasm_tests; pub const PASSWORD_MAXIMUM_CONSECUTIVE_CHARACTERS: usize = 3; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] const CUSTOM_PAYMENT_LOCKTIME_DEFAULT: u64 = 900; pub struct LpMainParams { @@ -102,7 +105,7 @@ impl LpMainParams { } } -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] /// Reads `payment_locktime` from conf arg and assigns it into `PAYMENT_LOCKTIME` in lp_swap. /// Assigns 900 if `payment_locktime` is invalid or not provided. fn initialize_payment_locktime(conf: &Json) { @@ -124,7 +127,7 @@ pub async fn lp_main( ctx_cb: &dyn Fn(u32), version: String, datetime: String, -) -> Result<(), String> { +) -> Result { let log_filter = params.filter.unwrap_or_default(); // Logger can be initialized once. // If `kdf` is linked as a library, and `kdf` is restarted, `init_logger` returns an error. @@ -150,7 +153,7 @@ pub async fn lp_main( } } - #[cfg(feature = "custom-swap-locktime")] + #[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] initialize_payment_locktime(&conf); let ctx = MmCtxBuilder::new() @@ -164,15 +167,28 @@ pub async fn lp_main( #[cfg(not(target_arch = "wasm32"))] spawn_ctrl_c_handler(ctx.clone()); - try_s!(lp_init(ctx, version, datetime).await); - Ok(()) + try_s!(lp_init(ctx.clone(), version, datetime).await); + Ok(ctx) +} + +pub async fn lp_run(ctx: MmArc) { + // In the mobile version we might depend on `lp_init` staying around until the context stops. + loop { + if ctx.is_stopping() { + break; + }; + Timer::sleep(0.2).await + } + + // Clearing up the running swaps removes any circular references that might prevent the context from being dropped. + lp_swap::clear_running_swaps(&ctx); } /// Handles CTRL-C signals and shutdowns the KDF runtime gracefully. /// /// It's important to spawn this task as soon as `Ctx` is in the correct state. #[cfg(not(target_arch = "wasm32"))] -fn spawn_ctrl_c_handler(ctx: mm2_core::mm_ctx::MmArc) { +fn spawn_ctrl_c_handler(ctx: MmArc) { use crate::lp_dispatcher::{dispatch_lp_event, StopCtxEvent}; common::executor::spawn(async move { @@ -367,7 +383,8 @@ pub fn run_lp_main( let log_filter = LogLevel::from_env(); let params = LpMainParams::with_conf(conf).log_filter(log_filter); - try_s!(block_on(lp_main(params, ctx_cb, version, datetime))); + let ctx = try_s!(block_on(lp_main(params, ctx_cb, version, datetime))); + block_on(lp_run(ctx)); Ok(()) } diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 3bf81d6370..7f88f129b3 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -39,6 +39,7 @@ fn test_match_maker_order_and_taker_request() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let request = TakerRequest { @@ -54,6 +55,7 @@ fn test_match_maker_order_and_taker_request() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let actual = maker.match_with_request(&request); @@ -77,6 +79,7 @@ fn test_match_maker_order_and_taker_request() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let request = TakerRequest { @@ -92,6 +95,7 @@ fn test_match_maker_order_and_taker_request() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let actual = maker.match_with_request(&request); @@ -115,6 +119,7 @@ fn test_match_maker_order_and_taker_request() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let request = TakerRequest { @@ -130,6 +135,7 @@ fn test_match_maker_order_and_taker_request() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let actual = maker.match_with_request(&request); @@ -153,6 +159,7 @@ fn test_match_maker_order_and_taker_request() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let request = TakerRequest { @@ -168,6 +175,7 @@ fn test_match_maker_order_and_taker_request() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let actual = maker.match_with_request(&request); @@ -191,6 +199,7 @@ fn test_match_maker_order_and_taker_request() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let request = TakerRequest { @@ -206,6 +215,7 @@ fn test_match_maker_order_and_taker_request() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let actual = maker.match_with_request(&request); @@ -229,6 +239,7 @@ fn test_match_maker_order_and_taker_request() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let request = TakerRequest { @@ -244,6 +255,7 @@ fn test_match_maker_order_and_taker_request() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let actual = maker.match_with_request(&request); @@ -269,6 +281,7 @@ fn test_match_maker_order_and_taker_request() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let request = TakerRequest { base: "KMD".to_owned(), @@ -283,6 +296,7 @@ fn test_match_maker_order_and_taker_request() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let actual = maker.match_with_request(&request); assert_eq!(actual, OrderMatchResult::NotMatched); @@ -309,6 +323,7 @@ fn test_match_maker_order_and_taker_request() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let request = TakerRequest { base: "REL".to_owned(), @@ -323,6 +338,7 @@ fn test_match_maker_order_and_taker_request() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let actual = maker.match_with_request(&request); let expected_base_amount = MmNumber::from(3); @@ -380,6 +396,7 @@ fn test_maker_order_available_amount() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; maker.matches.insert(new_uuid(), MakerMatch { request: TakerRequest { @@ -395,6 +412,7 @@ fn test_maker_order_available_amount() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }, reserved: MakerReserved { base: "BASE".into(), @@ -408,6 +426,7 @@ fn test_maker_order_available_amount() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }, connect: None, connected: None, @@ -427,6 +446,7 @@ fn test_maker_order_available_amount() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }, reserved: MakerReserved { base: "BASE".into(), @@ -440,6 +460,7 @@ fn test_maker_order_available_amount() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }, connect: None, connected: None, @@ -468,6 +489,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -495,6 +517,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -512,6 +535,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -539,6 +563,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -556,6 +581,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -583,6 +609,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -600,6 +627,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -627,6 +655,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -644,6 +673,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -671,6 +701,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -688,6 +719,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -715,6 +747,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -732,6 +765,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -759,6 +793,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -776,6 +811,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -803,6 +839,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -824,6 +861,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }, matches: HashMap::new(), order_type: OrderType::GoodTillCancelled, @@ -847,6 +885,7 @@ fn test_taker_match_reserved() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -867,6 +906,7 @@ fn test_taker_order_cancellable() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let order = TakerOrder { @@ -897,6 +937,7 @@ fn test_taker_order_cancellable() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let mut order = TakerOrder { @@ -926,6 +967,7 @@ fn test_taker_order_cancellable() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }, connect: TakerConnect { sender_pubkey: H256Json::default(), @@ -974,6 +1016,7 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }, None, ); @@ -996,6 +1039,7 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }, None, ); @@ -1018,6 +1062,7 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }, None, ); @@ -1037,6 +1082,7 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }, order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), @@ -1139,6 +1185,7 @@ fn test_taker_order_match_by() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; let mut order = TakerOrder { @@ -1166,6 +1213,7 @@ fn test_taker_order_match_by() { conf_settings: None, base_protocol_info: None, rel_protocol_info: None, + swap_version: SwapVersion::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -1206,6 +1254,7 @@ fn test_maker_order_was_updated() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let mut update_msg = MakerOrderUpdated::new(maker_order.uuid); update_msg.with_new_price(BigRational::from_integer(2.into())); @@ -2500,9 +2549,7 @@ fn test_orderbook_pubkey_sync_request_relay() { #[test] fn test_trie_diff_avoid_cycle_on_insertion() { - let mut history = TrieDiffHistory:: { - inner: TimeCache::new(Duration::from_secs(3600)), - }; + let mut history = TrieDiffHistory:: { inner: TimedMap::new() }; history.insert_new_diff([1; 8], TrieDiff { delta: vec![], next_root: [2; 8], @@ -2524,12 +2571,12 @@ fn test_trie_diff_avoid_cycle_on_insertion() { next_root: [2; 8], }); - let expected = HashMap::from_iter(iter::once(([1u8; 8], TrieDiff { + let expected = TrieDiff { delta: vec![], next_root: [2; 8], - }))); + }; - assert_eq!(expected, history.inner.as_hash_map()); + assert_eq!(&expected, history.inner.get(&[1u8; 8]).unwrap()); } #[test] @@ -2634,7 +2681,12 @@ fn check_if_orderbook_contains_only(orderbook: &Orderbook, pubkey: &str, orders: assert_eq!(orderbook.unordered, expected_unordered); // history - let actual_keys: HashSet<_> = pubkey_state.order_pairs_trie_state_history.keys().cloned().collect(); + let actual_keys: HashSet<_> = pubkey_state + .order_pairs_trie_state_history + .keys() + .iter() + .cloned() + .collect(); let expected_keys: HashSet<_> = orders .iter() .map(|order| alb_ordered_pair(&order.base, &order.rel)) @@ -3212,6 +3264,7 @@ fn test_maker_order_balance_loops() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; let morty_order = MakerOrder { @@ -3231,6 +3284,7 @@ fn test_maker_order_balance_loops() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; assert!(!maker_orders_ctx.balance_loop_exists(rick_ticker)); @@ -3263,6 +3317,7 @@ fn test_maker_order_balance_loops() { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + swap_version: SwapVersion::default(), }; maker_orders_ctx.add_order(ctx.weak(), rick_order_2.clone(), None); diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 4e4947e151..d3df2042db 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -38,7 +38,7 @@ use std::net::SocketAddr; cfg_native! { use hyper::{self, Body, Server}; use futures::channel::oneshot; - use mm2_net::sse_handler::{handle_sse, SSE_ENDPOINT}; + use mm2_net::event_streaming::sse_handler::{handle_sse, SSE_ENDPOINT}; } #[path = "rpc/dispatcher/dispatcher.rs"] mod dispatcher; @@ -46,8 +46,9 @@ cfg_native! { mod dispatcher_legacy; pub mod lp_commands; mod rate_limiter; +mod streaming_activations; -/// Lists the RPC method not requiring the "userpass" authentication. +/// Lists the RPC method not requiring the "userpass" authentication. /// None is also public to skip auth and display proper error in case of method is missing const PUBLIC_METHODS: &[Option<&str>] = &[ // Sorted alphanumerically (on the first letter) for readability. @@ -322,13 +323,15 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { req: Request, remote_addr: SocketAddr, ctx_h: u32, - is_event_stream_enabled: bool, ) -> Result, Infallible> { let (tx, rx) = oneshot::channel(); // We execute the request in a separate task to avoid it being left uncompleted if the client disconnects. - // So what's inside the spawn here will complete till completion (or panic). + // So what's inside the spawn here will run till completion (or panic). common::executor::spawn(async move { - if is_event_stream_enabled && req.uri().path() == SSE_ENDPOINT { + if req.uri().path() == SSE_ENDPOINT { + // TODO: We probably want to authenticate the SSE request here. + // Note though that whoever connects via SSE can't enable or disable any events + // without the password as this is done via RPC. (another client with the password can cross-enable events for them though). tx.send(handle_sse(req, ctx_h).await).ok(); } else { tx.send(rpc_service(req, ctx_h, remote_addr).await).ok(); @@ -352,7 +355,6 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { // cf. https://github.com/hyperium/hyper/pull/1640. let ctx = MmArc::from_ffi_handle(ctx_h).expect("No context"); - let is_event_stream_enabled = ctx.event_stream_configuration.is_some(); //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`. @@ -362,7 +364,7 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { let remote_addr = socket.remote_addr(); async move { Ok::<_, Infallible>(service_fn(move |req: Request| { - handle_request(req, remote_addr, ctx_h, is_event_stream_enabled) + handle_request(req, remote_addr, ctx_h) })) } }) @@ -410,7 +412,7 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { $port, now_sec() ); - let _ = $ctx.rpc_started.set(true); + let _ = $ctx.rpc_port.set($port); server }); } @@ -448,6 +450,7 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { // Create a TcpListener let incoming = AddrIncoming::bind(&rpc_ip_port).unwrap_or_else(|err| panic!("Can't bind on {}: {}", rpc_ip_port, err)); + let bound_to_addr = incoming.local_addr(); let acceptor = TlsAcceptor::builder() .with_single_cert(cert_chain, privkey) .unwrap_or_else(|err| panic!("Can't set certificate for TlsAcceptor: {}", err)) @@ -459,15 +462,16 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { .serve(make_svc!(TlsStream)) .with_graceful_shutdown(get_shutdown_future!(ctx)); - spawn_server!(server, ctx, rpc_ip_port.ip(), rpc_ip_port.port()); + spawn_server!(server, ctx, bound_to_addr.ip(), bound_to_addr.port()); } else { let server = Server::try_bind(&rpc_ip_port) - .unwrap_or_else(|err| panic!("Can't bind on {}: {}", rpc_ip_port, err)) + .unwrap_or_else(|err| panic!("Failed to bind rpc server on {}: {}", rpc_ip_port, err)) .http1_half_close(false) - .serve(make_svc!(AddrStream)) - .with_graceful_shutdown(get_shutdown_future!(ctx)); + .serve(make_svc!(AddrStream)); + let bound_to_addr = server.local_addr(); + let graceful_shutdown_server = server.with_graceful_shutdown(get_shutdown_future!(ctx)); - spawn_server!(server, ctx, rpc_ip_port.ip(), rpc_ip_port.port()); + spawn_server!(graceful_shutdown_server, ctx, bound_to_addr.ip(), bound_to_addr.port()); } } @@ -516,10 +520,6 @@ pub fn spawn_rpc(ctx_h: u32) { error!("'MmCtx::wasm_rpc' is initialized already"); return; }; - if ctx.rpc_started.set(true).is_err() { - error!("'MmCtx::rpc_started' is set already"); - return; - } log_tag!( ctx, diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 937db9631b..f3286e5ab5 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,3 +1,4 @@ +use super::streaming_activations; use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; use crate::lp_healthcheck::peer_connection_healthcheck_rpc; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; @@ -9,7 +10,7 @@ use crate::lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, s stop_version_stat_collection, update_version_stat_collection}; use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc}; use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}; -use crate::lp_wallet::{get_mnemonic_rpc, get_wallet_names_rpc}; +use crate::lp_wallet::{change_mnemonic_password, get_mnemonic_rpc, get_wallet_names_rpc}; use crate::rpc::lp_commands::db_id::get_shared_db_id; use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, one_inch_v6_0_classic_swap_create_rpc, @@ -21,14 +22,13 @@ use crate::rpc::lp_commands::tokens::get_token_info; use crate::rpc::lp_commands::tokens::{approve_token_rpc, get_token_allowance_rpc}; use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; +use coins::eth::fee_estimation::rpc::get_eth_estimated_fee_per_gas; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, get_enabled_coins::get_enabled_coins, - get_estimated_fees::{get_eth_estimated_fee_per_gas, start_eth_fee_estimator, - stop_eth_fee_estimator}, get_new_address::{cancel_get_new_address, get_new_address, init_get_new_address, init_get_new_address_status, init_get_new_address_user_action}, init_account_balance::{cancel_account_balance, init_account_balance, @@ -45,9 +45,10 @@ use coins::utxo::qtum::QtumCoin; use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::z_coin::ZCoin; -use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_infos, get_swap_transaction_fee_policy, - nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, sign_raw_transaction, - verify_message, withdraw}; +use coins::{add_delegation, claim_staking_rewards, delegations_info, get_my_address, get_raw_transaction, + get_swap_transaction_fee_policy, nft, ongoing_undelegations_info, remove_delegation, + set_swap_transaction_fee_policy, sign_message, sign_raw_transaction, validators_info, verify_message, + withdraw}; use coins_activation::{cancel_init_l2, cancel_init_platform_coin_with_tokens, cancel_init_standalone_coin, cancel_init_token, enable_platform_coin_with_tokens, enable_token, init_l2, init_l2_status, init_l2_user_action, init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, @@ -147,16 +148,43 @@ async fn auth(request: &MmRpcRequest, ctx: &MmArc, client: &SocketAddr) -> Dispa } } +/// Handles experimental RPCs. +/// +/// When an RPC is recently implemented and may go for breaking changes based on client feedback, +/// it should be handled in this dispatcher to apply the `experimental::` prefix to the RPC name. +async fn experimental_rpcs_dispatcher( + request: MmRpcRequest, + ctx: MmArc, + experimental_method: &str, +) -> DispatcherResult>> { + if let Some(staking_method) = experimental_method.strip_prefix("staking::") { + return staking_dispatcher(request, ctx, staking_method).await; + } + + MmError::err(DispatcherError::NoSuchMethod) +} + async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult>> { + if let Some(streaming_request) = request.method.strip_prefix("stream::") { + let streaming_request = streaming_request.to_string(); + return rpc_streaming_dispatcher(request, ctx, streaming_request).await; + } + if let Some(task_method) = request.method.strip_prefix("task::") { let task_method = task_method.to_string(); return rpc_task_dispatcher(request, ctx, task_method).await; } + if let Some(gui_storage_method) = request.method.strip_prefix("gui_storage::") { let gui_storage_method = gui_storage_method.to_owned(); return gui_storage_dispatcher(request, ctx, &gui_storage_method).await; } + if let Some(experimental_method) = request.method.strip_prefix("experimental::") { + let experimental_method = experimental_method.to_string(); + return experimental_rpcs_dispatcher(request, ctx, &experimental_method).await; + } + #[cfg(not(target_arch = "wasm32"))] if let Some(lightning_method) = request.method.strip_prefix("lightning::") { let lightning_method = lightning_method.to_owned(); @@ -166,7 +194,6 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, account_balance).await, "active_swaps" => handle_mmrpc(ctx, request, active_swaps_rpc).await, - "add_delegation" => handle_mmrpc(ctx, request, add_delegation).await, "add_node_to_version_stat" => handle_mmrpc(ctx, request, add_node_to_version_stat).await, "approve_token" => handle_mmrpc(ctx, request, approve_token_rpc).await, "get_token_allowance" => handle_mmrpc(ctx, request, get_token_allowance_rpc).await, @@ -194,7 +221,6 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, get_public_key_hash).await, "get_raw_transaction" => handle_mmrpc(ctx, request, get_raw_transaction).await, "get_shared_db_id" => handle_mmrpc(ctx, request, get_shared_db_id).await, - "get_staking_infos" => handle_mmrpc(ctx, request, get_staking_infos).await, "get_token_info" => handle_mmrpc(ctx, request, get_token_info).await, "get_wallet_names" => handle_mmrpc(ctx, request, get_wallet_names_rpc).await, "max_maker_vol" => handle_mmrpc(ctx, request, max_maker_vol).await, @@ -204,7 +230,6 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult 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, "sign_raw_transaction" => handle_mmrpc(ctx, request, sign_raw_transaction).await, @@ -215,6 +240,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult 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, + "change_mnemonic_password" => handle_mmrpc(ctx, request, change_mnemonic_password).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, @@ -222,8 +248,6 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, ibc_transfer_channels).await, "peer_connection_healthcheck" => handle_mmrpc(ctx, request, peer_connection_healthcheck_rpc).await, "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, - "start_eth_fee_estimator" => handle_mmrpc(ctx, request, start_eth_fee_estimator).await, - "stop_eth_fee_estimator" => handle_mmrpc(ctx, request, stop_eth_fee_estimator).await, "get_eth_estimated_fee_per_gas" => handle_mmrpc(ctx, request, get_eth_estimated_fee_per_gas).await, "get_swap_transaction_fee_policy" => handle_mmrpc(ctx, request, get_swap_transaction_fee_policy).await, "set_swap_transaction_fee_policy" => handle_mmrpc(ctx, request, set_swap_transaction_fee_policy).await, @@ -260,6 +284,10 @@ async fn rpc_task_dispatcher( "create_new_account::init" => handle_mmrpc(ctx, request, init_create_new_account).await, "create_new_account::status" => handle_mmrpc(ctx, request, init_create_new_account_status).await, "create_new_account::user_action" => handle_mmrpc(ctx, request, init_create_new_account_user_action).await, + "enable_bch::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, + "enable_bch::init" => handle_mmrpc(ctx, request, init_standalone_coin::).await, + "enable_bch::status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, + "enable_bch::user_action" => handle_mmrpc(ctx, request, init_standalone_coin_user_action::).await, "enable_qtum::cancel" => handle_mmrpc(ctx, request, cancel_init_standalone_coin::).await, "enable_qtum::init" => handle_mmrpc(ctx, request, init_standalone_coin::).await, "enable_qtum::status" => handle_mmrpc(ctx, request, init_standalone_coin_status::).await, @@ -280,6 +308,28 @@ async fn rpc_task_dispatcher( "enable_erc20::init" => handle_mmrpc(ctx, request, init_token::).await, "enable_erc20::status" => handle_mmrpc(ctx, request, init_token_status::).await, "enable_erc20::user_action" => handle_mmrpc(ctx, request, init_token_user_action::).await, + "enable_tendermint::cancel" => { + handle_mmrpc(ctx, request, cancel_init_platform_coin_with_tokens::).await + }, + "enable_tendermint::init" => handle_mmrpc(ctx, request, init_platform_coin_with_tokens::).await, + "enable_tendermint::status" => { + handle_mmrpc(ctx, request, init_platform_coin_with_tokens_status::).await + }, + "enable_tendermint::user_action" => { + handle_mmrpc( + ctx, + request, + init_platform_coin_with_tokens_user_action::, + ) + .await + }, + // // TODO: tendermint tokens + // "enable_tendermint_token::cancel" => handle_mmrpc(ctx, request, cancel_init_token::).await, + // "enable_tendermint_token::init" => handle_mmrpc(ctx, request, init_token::).await, + // "enable_tendermint_token::status" => handle_mmrpc(ctx, request, init_token_status::).await, + // "enable_tendermint_token::user_action" => { + // handle_mmrpc(ctx, request, init_token_user_action::).await + // }, "get_new_address::cancel" => handle_mmrpc(ctx, request, cancel_get_new_address).await, "get_new_address::init" => handle_mmrpc(ctx, request, init_get_new_address).await, "get_new_address::status" => handle_mmrpc(ctx, request, init_get_new_address_status).await, @@ -323,6 +373,25 @@ async fn rpc_task_dispatcher( } } +async fn rpc_streaming_dispatcher( + request: MmRpcRequest, + ctx: MmArc, + streaming_request: String, +) -> DispatcherResult>> { + match streaming_request.as_str() { + "balance::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_balance).await, + "network::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_network).await, + "heartbeat::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_heartbeat).await, + "fee_estimator::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_fee_estimation).await, + "swap_status::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_swap_status).await, + "order_status::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_order_status).await, + "tx_history::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_tx_history).await, + "orderbook::enable" => handle_mmrpc(ctx, request, streaming_activations::enable_orderbook).await, + "disable" => handle_mmrpc(ctx, request, streaming_activations::disable_streamer).await, + _ => MmError::err(DispatcherError::NoSuchMethod), + } +} + /// `gui_storage` dispatcher. /// /// # Note @@ -387,3 +456,34 @@ async fn lightning_dispatcher( _ => MmError::err(DispatcherError::NoSuchMethod), } } + +/// Dispatcher for `staking` namespace that handles all the staking related RPCs. +async fn staking_dispatcher( + request: MmRpcRequest, + ctx: MmArc, + staking_method: &str, +) -> DispatcherResult>> { + async fn query_dispatcher( + request: MmRpcRequest, + ctx: MmArc, + staking_query_method: &str, + ) -> DispatcherResult>> { + match staking_query_method { + "delegations" => handle_mmrpc(ctx, request, delegations_info).await, + "ongoing_undelegations" => handle_mmrpc(ctx, request, ongoing_undelegations_info).await, + "validators" => handle_mmrpc(ctx, request, validators_info).await, + _ => MmError::err(DispatcherError::NoSuchMethod), + } + } + + if let Some(query_method) = staking_method.strip_prefix("query::") { + return query_dispatcher(request, ctx, query_method).await; + } + + match staking_method { + "claim_rewards" => handle_mmrpc(ctx, request, claim_staking_rewards).await, + "delegate" => handle_mmrpc(ctx, request, add_delegation).await, + "undelegate" => handle_mmrpc(ctx, request, remove_delegation).await, + _ => MmError::err(DispatcherError::NoSuchMethod), + } +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs index a0c384463d..aab020e64e 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -2,7 +2,8 @@ use super::errors::ApiIntegrationRpcError; use super::types::{AggregationContractRequest, ClassicSwapCreateRequest, ClassicSwapLiquiditySourcesRequest, ClassicSwapLiquiditySourcesResponse, ClassicSwapQuoteRequest, ClassicSwapResponse, ClassicSwapTokensRequest, ClassicSwapTokensResponse}; -use coins::eth::{display_eth_address, wei_from_big_decimal, EthCoin, EthCoinType}; +use coins::eth::{wei_from_big_decimal, EthCoin, EthCoinType}; +use coins::hd_wallet::DisplayAddress; use coins::{lp_coinfind_or_err, CoinWithDerivationMethod, MmCoin, MmCoinEnum}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -74,7 +75,7 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( base_contract, rel_contract, sell_amount.to_string(), - display_eth_address(&single_address), + single_address.display_address(), req.slippage, ) .with_fee(req.fee) @@ -150,7 +151,7 @@ async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, }; let contract = match coin.coin_type { EthCoinType::Eth => ApiClient::eth_special_contract().to_owned(), - EthCoinType::Erc20 { token_addr, .. } => display_eth_address(&token_addr), + EthCoinType::Erc20 { token_addr, .. } => token_addr.display_address(), EthCoinType::Nft { .. } => return Err(MmError::new(ApiIntegrationRpcError::NftNotSupported)), }; Ok((coin, contract)) @@ -222,6 +223,8 @@ mod tests { "ticker": ticker_coin, "rpc_mode": "Default", "nodes": [ + {"url": "https://sepolia.drpc.org"}, + {"url": "https://ethereum-sepolia-rpc.publicnode.com"}, {"url": "https://rpc2.sepolia.org"}, {"url": "https://rpc.sepolia.org/"} ], diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs b/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs new file mode 100644 index 0000000000..76f9d594e1 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/balance.rs @@ -0,0 +1,100 @@ +//! RPC activation and deactivation for different balance event streamers. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use coins::eth::eth_balance_events::EthBalanceEventStreamer; +use coins::tendermint::tendermint_balance_events::TendermintBalanceEventStreamer; +use coins::utxo::utxo_balance_events::UtxoBalanceEventStreamer; +use coins::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; +use coins::{lp_coinfind, MmCoin, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use serde_json::Value as Json; + +#[derive(Deserialize)] +pub struct EnableBalanceStreamingRequest { + pub coin: String, + pub config: Option, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum BalanceStreamingRequestError { + EnableError(String), + CoinNotFound, + CoinNotSupported, + Internal(String), +} + +impl HttpStatusCode for BalanceStreamingRequestError { + fn status_code(&self) -> StatusCode { + match self { + BalanceStreamingRequestError::EnableError(_) => StatusCode::BAD_REQUEST, + BalanceStreamingRequestError::CoinNotFound => StatusCode::NOT_FOUND, + BalanceStreamingRequestError::CoinNotSupported => StatusCode::NOT_IMPLEMENTED, + BalanceStreamingRequestError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn enable_balance( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let coin = lp_coinfind(&ctx, &req.coin) + .await + .map_err(BalanceStreamingRequestError::Internal)? + .ok_or(BalanceStreamingRequestError::CoinNotFound)?; + + match coin { + MmCoinEnum::EthCoin(_) => (), + MmCoinEnum::ZCoin(_) + | MmCoinEnum::UtxoCoin(_) + | MmCoinEnum::Bch(_) + | MmCoinEnum::QtumCoin(_) + | MmCoinEnum::Tendermint(_) => { + if req.config.is_some() { + Err(BalanceStreamingRequestError::EnableError( + "Invalid config provided. No config needed".to_string(), + ))? + } + }, + _ => Err(BalanceStreamingRequestError::CoinNotSupported)?, + } + + let enable_result = match coin { + MmCoinEnum::UtxoCoin(coin) => { + let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::Bch(coin) => { + let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::QtumCoin(coin) => { + let streamer = UtxoBalanceEventStreamer::new(coin.clone().into()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::EthCoin(coin) => { + let streamer = EthBalanceEventStreamer::try_new(req.config, coin.clone()) + .map_to_mm(|e| BalanceStreamingRequestError::EnableError(format!("{e:?}")))?; + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::ZCoin(coin) => { + let streamer = ZCoinBalanceEventStreamer::new(coin.clone()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::Tendermint(coin) => { + let streamer = TendermintBalanceEventStreamer::new(coin.clone()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + _ => Err(BalanceStreamingRequestError::CoinNotSupported)?, + }; + + enable_result + .map(EnableStreamingResponse::new) + .map_to_mm(|e| BalanceStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs b/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs new file mode 100644 index 0000000000..9643e9e652 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs @@ -0,0 +1,50 @@ +//! The module for handling any event streaming deactivation requests. +//! +//! All event streamers are deactivated using the streamer ID only. + +use common::HttpStatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use http::StatusCode; + +/// The request used for any event streaming deactivation. +#[derive(Deserialize)] +pub struct DisableStreamingRequest { + pub client_id: u64, + pub streamer_id: String, +} + +/// The success/ok response for any event streaming deactivation request. +#[derive(Serialize)] +pub struct DisableStreamingResponse { + result: &'static str, +} + +impl DisableStreamingResponse { + fn new() -> Self { Self { result: "Success" } } +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +/// The error response for any event streaming deactivation request. +pub enum DisableStreamingRequestError { + DisableError(String), +} + +impl HttpStatusCode for DisableStreamingRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +/// Disables a streamer. +/// +/// This works for any streamer regarding of their type/usage. +pub async fn disable_streamer( + ctx: MmArc, + req: DisableStreamingRequest, +) -> MmResult { + ctx.event_stream_manager + .stop(req.client_id, &req.streamer_id) + .map_to_mm(|e| DisableStreamingRequestError::DisableError(format!("{e:?}")))?; + Ok(DisableStreamingResponse::new()) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs b/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs new file mode 100644 index 0000000000..ef36542716 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/fee_estimation.rs @@ -0,0 +1,58 @@ +//! RPC activation and deactivation for different fee estimation streamers. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use coins::eth::fee_estimation::eth_fee_events::{EthFeeEventStreamer, EthFeeStreamingConfig}; +use coins::{lp_coinfind, MmCoin, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +#[derive(Deserialize)] +pub struct EnableFeeStreamingRequest { + pub coin: String, + pub config: EthFeeStreamingConfig, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum FeeStreamingRequestError { + EnableError(String), + CoinNotFound, + CoinNotSupported, + Internal(String), +} + +impl HttpStatusCode for FeeStreamingRequestError { + fn status_code(&self) -> StatusCode { + match self { + FeeStreamingRequestError::EnableError(_) => StatusCode::BAD_REQUEST, + FeeStreamingRequestError::CoinNotFound => StatusCode::NOT_FOUND, + FeeStreamingRequestError::CoinNotSupported => StatusCode::NOT_IMPLEMENTED, + FeeStreamingRequestError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn enable_fee_estimation( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let coin = lp_coinfind(&ctx, &req.coin) + .await + .map_err(FeeStreamingRequestError::Internal)? + .ok_or(FeeStreamingRequestError::CoinNotFound)?; + + match coin { + MmCoinEnum::EthCoin(coin) => { + let eth_fee_estimator_streamer = EthFeeEventStreamer::new(req.config, coin.clone()); + ctx.event_stream_manager + .add(client_id, eth_fee_estimator_streamer, coin.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| FeeStreamingRequestError::EnableError(format!("{e:?}"))) + }, + _ => Err(FeeStreamingRequestError::CoinNotSupported)?, + } +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/heartbeat.rs b/mm2src/mm2_main/src/rpc/streaming_activations/heartbeat.rs new file mode 100644 index 0000000000..e3f4d06c5e --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/heartbeat.rs @@ -0,0 +1,36 @@ +//! RPC activation and deactivation for the heartbeats. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use crate::heartbeat_event::{HeartbeatEvent, HeartbeatEventConfig}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +#[derive(Deserialize)] +pub struct EnableHeartbeatRequest { + pub config: HeartbeatEventConfig, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum HeartbeatRequestError { + EnableError(String), +} + +impl HttpStatusCode for HeartbeatRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +pub async fn enable_heartbeat( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let heartbeat_streamer = HeartbeatEvent::new(req.config); + ctx.event_stream_manager + .add(client_id, heartbeat_streamer, ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| HeartbeatRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs b/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs new file mode 100644 index 0000000000..05d2848f97 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs @@ -0,0 +1,45 @@ +mod balance; +mod disable; +mod fee_estimation; +mod heartbeat; +mod network; +mod orderbook; +mod orders; +mod swaps; +mod tx_history; + +// Re-exports +pub use balance::*; +pub use disable::*; +pub use fee_estimation::*; +pub use heartbeat::*; +pub use network::*; +pub use orderbook::*; +pub use orders::*; +pub use swaps::*; +pub use tx_history::*; + +/// The general request for enabling any streamer. +/// `client_id` is common in each request, other data is request-specific. +#[derive(Deserialize)] +pub struct EnableStreamingRequest { + // If the client ID isn't included, assume it's 0. + #[serde(default)] + pub client_id: u64, + #[serde(flatten)] + inner: T, +} + +/// The success/ok response for any event streaming activation request. +#[derive(Serialize)] +pub struct EnableStreamingResponse { + pub streamer_id: String, + // TODO: If the the streamer was already running, it is probably running with different configuration. + // We might want to inform the client that the configuration they asked for wasn't applied and return + // the active configuration instead? + // pub config: Json, +} + +impl EnableStreamingResponse { + fn new(streamer_id: String) -> Self { Self { streamer_id } } +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/network.rs b/mm2src/mm2_main/src/rpc/streaming_activations/network.rs new file mode 100644 index 0000000000..11f9d0ed3b --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/network.rs @@ -0,0 +1,36 @@ +//! RPC activation and deactivation for the network event streamer. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; +use mm2_libp2p::application::network_event::{NetworkEvent, NetworkEventConfig}; + +#[derive(Deserialize)] +pub struct EnableNetworkStreamingRequest { + pub config: NetworkEventConfig, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum NetworkStreamingRequestError { + EnableError(String), +} + +impl HttpStatusCode for NetworkStreamingRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +pub async fn enable_network( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let network_steamer = NetworkEvent::new(req.config, ctx.clone()); + ctx.event_stream_manager + .add(client_id, network_steamer, ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| NetworkStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/orderbook.rs b/mm2src/mm2_main/src/rpc/streaming_activations/orderbook.rs new file mode 100644 index 0000000000..60805c4a54 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/orderbook.rs @@ -0,0 +1,37 @@ +//! RPC activation and deactivation of the orderbook streamer. +use super::EnableStreamingResponse; +use crate::lp_ordermatch::orderbook_events::OrderbookStreamer; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use common::HttpStatusCode; +use http::StatusCode; + +#[derive(Deserialize)] +pub struct EnableOrderbookStreamingRequest { + pub client_id: u64, + pub base: String, + pub rel: String, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum OrderbookStreamingRequestError { + EnableError(String), +} + +impl HttpStatusCode for OrderbookStreamingRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +pub async fn enable_orderbook( + ctx: MmArc, + req: EnableOrderbookStreamingRequest, +) -> MmResult { + let order_status_streamer = OrderbookStreamer::new(ctx.clone(), req.base, req.rel); + ctx.event_stream_manager + .add(req.client_id, order_status_streamer, ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| OrderbookStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/orders.rs b/mm2src/mm2_main/src/rpc/streaming_activations/orders.rs new file mode 100644 index 0000000000..08fd0a959a --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/orders.rs @@ -0,0 +1,30 @@ +//! RPC activation and deactivation of the order status streamer. +use super::{EnableStreamingRequest, EnableStreamingResponse}; +use crate::lp_ordermatch::order_events::OrderStatusStreamer; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use common::HttpStatusCode; +use http::StatusCode; + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum OrderStatusStreamingRequestError { + EnableError(String), +} + +impl HttpStatusCode for OrderStatusStreamingRequestError { + fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } +} + +pub async fn enable_order_status( + ctx: MmArc, + req: EnableStreamingRequest<()>, +) -> MmResult { + let order_status_streamer = OrderStatusStreamer::new(); + ctx.event_stream_manager + .add(req.client_id, order_status_streamer, ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| OrderStatusStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/swaps.rs b/mm2src/mm2_main/src/rpc/streaming_activations/swaps.rs new file mode 100644 index 0000000000..3d4aa2b93e --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/swaps.rs @@ -0,0 +1,33 @@ +//! RPC activation and deactivation of the swap status streamer. +use super::{EnableStreamingRequest, EnableStreamingResponse}; +use crate::lp_swap::swap_events::SwapStatusStreamer; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +use common::HttpStatusCode; +use http::StatusCode; + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum SwapStatusStreamingRequestError { + EnableError(String), +} + +impl HttpStatusCode for SwapStatusStreamingRequestError { + fn status_code(&self) -> StatusCode { + match self { + SwapStatusStreamingRequestError::EnableError(_) => StatusCode::BAD_REQUEST, + } + } +} + +pub async fn enable_swap_status( + ctx: MmArc, + req: EnableStreamingRequest<()>, +) -> MmResult { + ctx.event_stream_manager + .add(req.client_id, SwapStatusStreamer::new(), ctx.spawner()) + .await + .map(EnableStreamingResponse::new) + .map_to_mm(|e| SwapStatusStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs b/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs new file mode 100644 index 0000000000..ac37ca21b5 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/streaming_activations/tx_history.rs @@ -0,0 +1,76 @@ +//! RPC activation and deactivation for Tx history event streamers. +use super::{EnableStreamingRequest, EnableStreamingResponse}; + +use coins::utxo::tx_history_events::TxHistoryEventStreamer; +use coins::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; +use coins::{lp_coinfind, MmCoin, MmCoinEnum}; +use common::HttpStatusCode; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; + +#[derive(Deserialize)] +pub struct EnableTxHistoryStreamingRequest { + pub coin: String, +} + +#[derive(Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum TxHistoryStreamingRequestError { + EnableError(String), + CoinNotFound, + CoinNotSupported, + Internal(String), +} + +impl HttpStatusCode for TxHistoryStreamingRequestError { + fn status_code(&self) -> StatusCode { + match self { + TxHistoryStreamingRequestError::EnableError(_) => StatusCode::BAD_REQUEST, + TxHistoryStreamingRequestError::CoinNotFound => StatusCode::NOT_FOUND, + TxHistoryStreamingRequestError::CoinNotSupported => StatusCode::NOT_IMPLEMENTED, + TxHistoryStreamingRequestError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn enable_tx_history( + ctx: MmArc, + req: EnableStreamingRequest, +) -> MmResult { + let (client_id, req) = (req.client_id, req.inner); + let coin = lp_coinfind(&ctx, &req.coin) + .await + .map_err(TxHistoryStreamingRequestError::Internal)? + .ok_or(TxHistoryStreamingRequestError::CoinNotFound)?; + + let enable_result = match coin { + MmCoinEnum::UtxoCoin(coin) => { + let streamer = TxHistoryEventStreamer::new(req.coin); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::Bch(coin) => { + let streamer = TxHistoryEventStreamer::new(req.coin); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::QtumCoin(coin) => { + let streamer = TxHistoryEventStreamer::new(req.coin); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::Tendermint(coin) => { + // The tx history streamer is very primitive reactive streamer that only emits new txs. + // it's logic is exactly the same for utxo coins and tendermint coins as well. + let streamer = TxHistoryEventStreamer::new(req.coin); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + MmCoinEnum::ZCoin(coin) => { + let streamer = ZCoinTxHistoryEventStreamer::new(coin.clone()); + ctx.event_stream_manager.add(client_id, streamer, coin.spawner()).await + }, + _ => Err(TxHistoryStreamingRequestError::CoinNotSupported)?, + }; + + enable_result + .map(EnableStreamingResponse::new) + .map_to_mm(|e| TxHistoryStreamingRequestError::EnableError(format!("{e:?}"))) +} diff --git a/mm2src/mm2_main/src/swap_versioning.rs b/mm2src/mm2_main/src/swap_versioning.rs new file mode 100644 index 0000000000..5c21615219 --- /dev/null +++ b/mm2src/mm2_main/src/swap_versioning.rs @@ -0,0 +1,26 @@ +//! Swap Versioning Module +//! +//! This module provides a dedicated type for handling swap versioning + +#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct SwapVersion { + pub version: u8, +} + +impl Default for SwapVersion { + fn default() -> Self { + Self { + version: legacy_swap_version(), + } + } +} + +impl SwapVersion { + pub(crate) const fn is_legacy(&self) -> bool { self.version == legacy_swap_version() } +} + +impl From for SwapVersion { + fn from(version: u8) -> Self { Self { version } } +} + +pub(crate) const fn legacy_swap_version() -> u8 { 1 } diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index bd24bf4a4c..11d31fb91c 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -1,5 +1,6 @@ -use crate::lp_init; +use crate::{lp_init, lp_run}; use common::executor::{spawn, spawn_abortable, spawn_local_abortable, AbortOnDropHandle, Timer}; +use common::log::warn; use common::log::wasm_log::register_wasm_log; use mm2_core::mm_ctx::MmArc; use mm2_number::BigDecimal; @@ -21,7 +22,8 @@ const STOP_TIMEOUT_MS: u64 = 1000; /// Starts the WASM version of MM. fn wasm_start(ctx: MmArc) { spawn(async move { - lp_init(ctx, "TEST".into(), "TEST".into()).await.unwrap(); + lp_init(ctx.clone(), "TEST".into(), "TEST".into()).await.unwrap(); + lp_run(ctx).await; }) } @@ -241,21 +243,22 @@ async fn trade_v2_test_rick_and_morty() { #[wasm_bindgen_test] async fn activate_z_coin_light() { - let coins = json!([pirate_conf()]); - - let conf = Mm2TestConf::seednode(PIRATE_TEST_BALANCE_SEED, &coins); - let mm = MarketMakerIt::start_async(conf.conf, conf.rpc_password, Some(wasm_start)) - .await - .unwrap(); - - let activation_result = - enable_z_coin_light(&mm, ARRR, PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, None, None).await; - - let balance = match activation_result.wallet_balance { - EnableCoinBalance::Iguana(iguana) => iguana, - _ => panic!("Expected EnableCoinBalance::Iguana"), - }; - assert_eq!(balance.balance.spendable, BigDecimal::default()); + warn!("Skipping activate_z_coin_light since it's failing, check https://github.com/KomodoPlatform/komodo-defi-framework/issues/2366"); + // let coins = json!([pirate_conf()]); + // + // let conf = Mm2TestConf::seednode(PIRATE_TEST_BALANCE_SEED, &coins); + // let mm = MarketMakerIt::start_async(conf.conf, conf.rpc_password, Some(wasm_start)) + // .await + // .unwrap(); + // + // let activation_result = + // enable_z_coin_light(&mm, ARRR, PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, None, None).await; + // + // let balance = match activation_result.wallet_balance { + // EnableCoinBalance::Iguana(iguana) => iguana, + // _ => panic!("Expected EnableCoinBalance::Iguana"), + // }; + // assert_eq!(balance.balance.spendable, BigDecimal::default()); } #[wasm_bindgen_test] 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 3050f22826..253c8adb74 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -1,3 +1,4 @@ +use coins::z_coin::ZCoin; pub use common::{block_on, block_on_f01, now_ms, now_sec, wait_until_ms, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; pub use mm2_number::MmNumber; use mm2_rpc::data::legacy::BalanceResponse; @@ -8,6 +9,7 @@ pub use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, TAKER_SUCCESS_EVENTS}; use super::eth_docker_tests::{erc20_contract_checksum, fill_eth, fill_eth_erc20_with_private_key, swap_contract}; +use super::z_coin_docker_tests::z_coin_from_spending_key; use bitcrypto::{dhash160, ChecksumType}; use chain::TransactionOutput; use coins::eth::addr_from_raw_pubkey; @@ -22,7 +24,7 @@ use coins::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardC use coins::utxo::{coin_daemon_data_dir, sat_from_big_decimal, zcash_params_path, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps}; use coins::{ConfirmPaymentInput, MarketCoinOps, Transaction}; -use crypto::privkey::key_pair_from_seed; +use crypto::privkey::{key_pair_from_secret, key_pair_from_seed}; use crypto::Secp256k1Secret; use ethabi::Token; use ethereum_types::{H160 as H160Eth, U256}; @@ -39,6 +41,8 @@ use script::Builder; use secp256k1::Secp256k1; pub use secp256k1::{PublicKey, SecretKey}; use serde_json::{self as json, Value as Json}; +pub use std::cell::Cell; +use std::convert::TryFrom; use std::process::{Command, Stdio}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use std::str::FromStr; @@ -55,6 +59,7 @@ lazy_static! { static ref MY_COIN1_LOCK: Mutex<()> = Mutex::new(()); static ref QTUM_LOCK: Mutex<()> = Mutex::new(()); static ref FOR_SLP_LOCK: Mutex<()> = Mutex::new(()); + static ref ZOMBIE_LOCK: Mutex<()> = Mutex::new(()); pub static ref SLP_TOKEN_ID: Mutex = Mutex::new(H256::default()); // Private keys supplied with 1000 SLP tokens on tests initialization. // Due to the SLP protocol limitations only 19 outputs (18 + change) can be sent in one transaction, which is sufficient for now though. @@ -114,10 +119,19 @@ pub static GETH_RPC_URL: &str = "http://127.0.0.1:8545"; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static SEPOLIA_RPC_URL: &str = "https://ethereum-sepolia-rpc.publicnode.com"; +// use thread local to affect only the current running test +thread_local! { + /// Set test dex pubkey as Taker (to check DexFee::NoFee) + pub static SET_BURN_PUBKEY_TO_ALICE: Cell = Cell::new(false); +} + pub const UTXO_ASSET_DOCKER_IMAGE: &str = "docker.io/artempikulin/testblockchain"; pub const UTXO_ASSET_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/artempikulin/testblockchain:multiarch"; pub const GETH_DOCKER_IMAGE: &str = "docker.io/ethereum/client-go"; pub const GETH_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/ethereum/client-go:stable"; +pub const ZOMBIE_ASSET_DOCKER_IMAGE: &str = "docker.io/borngraced/zombietestrunner"; +pub const ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG: &str = "docker.io/borngraced/zombietestrunner:multiarch"; + #[allow(dead_code)] pub const SIA_DOCKER_IMAGE: &str = "docker.io/alrighttt/walletd-komodo"; #[allow(dead_code)] @@ -213,6 +227,24 @@ impl UtxoAssetDockerOps { } } +pub struct ZCoinAssetDockerOps { + #[allow(dead_code)] + ctx: MmArc, + coin: ZCoin, +} + +impl CoinDockerOps for ZCoinAssetDockerOps { + fn rpc_client(&self) -> &UtxoRpcClientEnum { &self.coin.as_ref().rpc_client } +} + +impl ZCoinAssetDockerOps { + pub fn new() -> ZCoinAssetDockerOps { + let (ctx, coin) = block_on(z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe")); + + ZCoinAssetDockerOps { ctx, coin } + } +} + pub struct BchDockerOps { #[allow(dead_code)] ctx: MmArc, @@ -297,7 +329,9 @@ impl BchDockerOps { let adex_slp = SlpToken::new( 8, "ADEXSLP".into(), - slp_genesis_tx.tx_hash_as_bytes().as_slice().into(), + <&[u8; 32]>::try_from(slp_genesis_tx.tx_hash_as_bytes().as_slice()) + .unwrap() + .into(), self.coin.clone(), 1, ) @@ -313,7 +347,9 @@ impl BchDockerOps { }; block_on_f01(self.coin.wait_for_confirmations(confirm_payment_input)).unwrap(); *SLP_TOKEN_OWNERS.lock().unwrap() = slp_privkeys; - *SLP_TOKEN_ID.lock().unwrap() = slp_genesis_tx.tx_hash_as_bytes().as_slice().into(); + *SLP_TOKEN_ID.lock().unwrap() = <[u8; 32]>::try_from(slp_genesis_tx.tx_hash_as_bytes().as_slice()) + .unwrap() + .into(); } } @@ -451,6 +487,38 @@ pub fn ibc_relayer_node(docker: &'_ Cli, runtime_dir: PathBuf) -> DockerNode<'_> } } +pub fn zombie_asset_docker_node(docker: &Cli, port: u16) -> DockerNode<'_> { + let image = GenericImage::new(ZOMBIE_ASSET_DOCKER_IMAGE, "multiarch") + .with_volume(zcash_params_path().display().to_string(), "/root/.zcash-params") + .with_env_var("COIN_RPC_PORT", port.to_string()) + .with_wait_for(WaitFor::message_on_stdout("config is ready")); + + let image = RunnableImage::from(image).with_mapped_port((port, port)); + let container = docker.run(image); + let config_ticker = "ZOMBIE"; + let mut conf_path = coin_daemon_data_dir(config_ticker, true); + + std::fs::create_dir_all(&conf_path).unwrap(); + conf_path.push(format!("{}.conf", config_ticker)); + Command::new("docker") + .arg("cp") + .arg(format!("{}:/data/node_0/{}.conf", container.id(), config_ticker)) + .arg(&conf_path) + .status() + .expect("Failed to execute docker command"); + + let timeout = wait_until_ms(3000); + while !conf_path.exists() { + assert!(now_ms() < timeout, "Test timed out"); + } + + DockerNode { + container, + ticker: config_ticker.into(), + port, + } +} + pub fn rmd160_from_priv(privkey: Secp256k1Secret) -> H160 { let secret = SecretKey::from_slice(privkey.as_slice()).unwrap(); let public = PublicKey::from_secret_key(&Secp256k1::new(), &secret); @@ -875,7 +943,17 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { let bob_priv_key = generate_and_fill_priv_key(base); let alice_priv_key = generate_and_fill_priv_key(rel); + let alice_pubkey_str = hex::encode( + key_pair_from_secret(&alice_priv_key) + .expect("valid test key pair") + .public() + .to_vec(), + ); + let mut envs = vec![]; + if SET_BURN_PUBKEY_TO_ALICE.get() { + envs.push(("TEST_BURN_ADDR_RAW_PUBKEY", alice_pubkey_str.as_str())); + } let confpath = unsafe { QTUM_CONF_PATH.as_ref().expect("Qtum config is not set yet") }; let coins = json! ([ eth_dev_conf(), @@ -886,12 +964,12 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { {"coin":"MYCOIN1","asset":"MYCOIN1","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, // TODO: check if we should fix protocol "type":"UTXO" to "QTUM" for this and other QTUM coin tests. // Maybe we should use a different coin for "UTXO" protocol and make new tests for "QTUM" protocol - {"coin":"QTUM","asset":"QTUM","required_confirmations":0,"decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, + {"coin":"QTUM","asset":"QTUM","required_confirmations":0,"decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"segwit":true,"txfee":0,"txfee_volatility_percent":0.1, "dust":72800, "mm2":1,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"},"bech32_hrp":"qcrt","address_format":{"format":"segwit"}}, {"coin":"FORSLP","asset":"FORSLP","required_confirmations":0,"txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"BCH","protocol_data":{"slp_prefix":"slptest"}}}, {"coin":"ADEXSLP","protocol":{"type":"SLPTOKEN","protocol_data":{"decimals":8,"token_id":get_slp_token_id(),"platform":"FORSLP"}}} ]); - let mut mm_bob = MarketMakerIt::start( + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( json! ({ "gui": "nogui", "netid": 9000, @@ -903,12 +981,13 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { }), "pass".to_string(), None, - ) + envs.as_slice(), + )) .unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); - let mut mm_alice = MarketMakerIt::start( + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( json! ({ "gui": "nogui", "netid": 9000, @@ -920,7 +999,8 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { }), "pass".to_string(), None, - ) + envs.as_slice(), + )) .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); @@ -1313,8 +1393,7 @@ pub fn init_geth_node() { thread::sleep(Duration::from_millis(100)); } - let dex_fee_addr = addr_from_raw_pubkey(&DEX_FEE_ADDR_RAW_PUBKEY).unwrap(); - let dex_fee_addr = Token::Address(dex_fee_addr); + let dex_fee_addr = Token::Address(GETH_ACCOUNT); let params = ethabi::encode(&[dex_fee_addr]); let taker_swap_v2_data = format!("{}{}", TAKER_SWAP_V2_BYTES, hex::encode(params)); @@ -1585,7 +1664,7 @@ pub fn init_geth_node() { SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 = EthAddress::from_str("0x9eb88cd58605d8fb9b14652d6152727f7e95fb4d").unwrap(); SEPOLIA_ERC20_CONTRACT = EthAddress::from_str("0xF7b5F8E8555EF7A743f24D3E974E23A3C6cB6638").unwrap(); - SEPOLIA_TAKER_SWAP_V2 = EthAddress::from_str("0x7Cc9F2c1c3B797D09B9d1CCd7FDcD2539a4b3874").unwrap(); + SEPOLIA_TAKER_SWAP_V2 = EthAddress::from_str("0x3B19873b81a6B426c8B2323955215F7e89CfF33F").unwrap(); // deploy tx https://sepolia.etherscan.io/tx/0x6f743d79ecb806f5899a6a801083e33eba9e6f10726af0873af9f39883db7f11 SEPOLIA_MAKER_SWAP_V2 = EthAddress::from_str("0xf9000589c66Df3573645B59c10aa87594Edc318F").unwrap(); } diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index 2a253760bc..ff7e6415fb 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -1,4 +1,5 @@ -use crate::docker_tests::docker_tests_common::{generate_utxo_coin_with_privkey, trade_base_rel, GETH_RPC_URL, MM_CTX}; +use crate::docker_tests::docker_tests_common::{generate_utxo_coin_with_privkey, trade_base_rel, GETH_RPC_URL, MM_CTX, + SET_BURN_PUBKEY_TO_ALICE}; use crate::docker_tests::eth_docker_tests::{erc20_coin_with_random_privkey, erc20_contract_checksum, fill_eth_erc20_with_private_key, swap_contract}; use crate::integration_tests_common::*; @@ -16,6 +17,7 @@ use common::{block_on, block_on_f01, executor::Timer, get_utc_timestamp, now_sec use crypto::privkey::key_pair_from_seed; use crypto::{CryptoCtx, DerivationPath, KeyPairPolicy}; use http::StatusCode; +use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; use mm2_number::{BigDecimal, BigRational, MmNumber}; use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, disable_coin_err, enable_eth_coin, enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, @@ -25,6 +27,7 @@ use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, di use mm2_test_helpers::{get_passphrase, structs::*}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::env; use std::iter::FromIterator; use std::str::FromStr; @@ -3890,6 +3893,13 @@ fn test_trade_base_rel_eth_erc20_coins() { trade_base_rel(("ETH", "ERC20DEV")); #[test] fn test_trade_base_rel_mycoin_mycoin1_coins() { trade_base_rel(("MYCOIN", "MYCOIN1")); } +// run swap with burn pubkey set to alice (no dex fee) +#[test] +fn test_trade_base_rel_mycoin_mycoin1_coins_burnkey_as_alice() { + SET_BURN_PUBKEY_TO_ALICE.set(true); + trade_base_rel(("MYCOIN", "MYCOIN1")); +} + fn withdraw_and_send( mm: &MarketMakerIt, coin: &str, @@ -5471,3 +5481,82 @@ fn test_approve_erc20() { block_on(mm.stop()).unwrap(); } + +#[test] +fn test_peer_time_sync_validation() { + let timeoffset_tolerable = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() - 1; + let timeoffset_too_big = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() + 1; + + let start_peers_with_time_offset = |offset: i64| -> (Json, Json) { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 10.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 10.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let bob_conf = Mm2TestConf::seednode(&hex::encode(bob_priv_key), &coins); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + bob_conf.conf, + bob_conf.rpc_password, + None, + &[], + )) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + let alice_conf = + Mm2TestConf::light_node(&hex::encode(alice_priv_key), &coins, &[mm_bob.ip.to_string().as_str()]); + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password, + None, + &[("TEST_TIMESTAMP_OFFSET", offset.to_string().as_str())], + )) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + let res_bob = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!(res_bob.0.is_success(), "!get_directly_connected_peers: {}", res_bob.1); + let bob_peers = serde_json::from_str::(&res_bob.1).unwrap(); + + let res_alice = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!( + res_alice.0.is_success(), + "!get_directly_connected_peers: {}", + res_alice.1 + ); + let alice_peers = serde_json::from_str::(&res_alice.1).unwrap(); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + (bob_peers, alice_peers) + }; + + // check with small time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_tolerable); + assert!( + bob_peers["result"].as_object().unwrap().len() == 1, + "bob must have one peer" + ); + assert!( + alice_peers["result"].as_object().unwrap().len() == 1, + "alice must have one peer" + ); + + // check with too big time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_too_big); + assert!( + bob_peers["result"].as_object().unwrap().is_empty(), + "bob must have no peers" + ); + assert!( + alice_peers["result"].as_object().unwrap().is_empty(), + "alice must have no peers" + ); +} diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index abcdc2ce9a..840e30279a 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -10,8 +10,9 @@ use crate::common::Future01CompatExt; use bitcrypto::{dhash160, sha256}; use coins::eth::gas_limit::ETH_MAX_TRADE_GAS; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivationV2Request, EthNode}; -use coins::eth::{checksum_address, eth_addr_to_hex, eth_coin_from_conf_and_request, EthCoin, EthCoinType, - EthPrivKeyBuildPolicy, SignedEthTx, SwapV2Contracts, ERC20_ABI}; +use coins::eth::{checksum_address, eth_coin_from_conf_and_request, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, + SignedEthTx, SwapV2Contracts, ERC20_ABI}; +use coins::hd_wallet::AddrToString; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use coins::{lp_coinfind, CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, @@ -28,12 +29,13 @@ use coins::{CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaym use common::{block_on, block_on_f01, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256; -#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_core::mm_ctx::MmArc; use mm2_number::{BigDecimal, BigUint}; -use mm2_test_helpers::for_tests::{account_balance, disable_coin, enable_erc20_token_v2, enable_eth_with_tokens_v2, - erc20_dev_conf, eth_dev_conf, get_new_address, get_token_info, nft_dev_conf, - MarketMakerIt, Mm2TestConf}; +use mm2_test_helpers::for_tests::{account_balance, active_swaps, coins_needed_for_kickstart, disable_coin, + enable_erc20_token_v2, enable_eth_coin_v2, enable_eth_with_tokens_v2, + erc20_dev_conf, eth1_dev_conf, eth_dev_conf, get_locked_amount, get_new_address, + get_token_info, mm_dump, my_swap_status, nft_dev_conf, start_swaps, MarketMakerIt, + Mm2TestConf, SwapV2TestContracts, TestNode}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalanceMap, EthWithTokensActivationResult, HDAccountAddressId, @@ -43,6 +45,7 @@ use serde_json::Value as Json; use std::str::FromStr; use std::thread; use std::time::Duration; +use uuid::Uuid; use web3::contract::{Contract, Options}; use web3::ethabi::Token; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] @@ -55,6 +58,7 @@ const SEPOLIA_MAKER_PRIV: &str = "6e2f3a6223b928a05a3a3622b0c3f3573d03663b704a61 const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba2110bddd281e711608f16"; const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; +const ETH1: &str = "ETH1"; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const ERC20: &str = "ERC20DEV"; @@ -262,7 +266,7 @@ pub(crate) async fn fill_erc1155_info(eth_coin: &EthCoin, token_address: Address contract_type: ContractType::Erc1155, amount: BigDecimal::from(amount), }; - let erc1155_address_str = eth_addr_to_hex(&token_address); + let erc1155_address_str = token_address.addr_to_string(); let erc1155_key = format!("{},{}", erc1155_address_str, token_id); nft_infos.insert(erc1155_key, erc1155_nft_info); } @@ -278,7 +282,7 @@ pub(crate) async fn fill_erc721_info(eth_coin: &EthCoin, token_address: Address, contract_type: ContractType::Erc721, amount: BigDecimal::from(1), }; - let erc721_address_str = eth_addr_to_hex(&token_address); + let erc721_address_str = token_address.addr_to_string(); let erc721_key = format!("{},{}", erc721_address_str, token_id); nft_infos.insert(erc721_key, erc721_nft_info); } @@ -1427,15 +1431,16 @@ impl SwapAddresses { } } -#[allow(dead_code)] /// Needed for eth or erc20 v2 activation in Geth tests fn eth_coin_v2_activation_with_random_privkey( + ctx: &MmArc, ticker: &str, conf: &Json, swap_addr: SwapAddresses, erc20: bool, -) -> EthCoin { - let build_policy = EthPrivKeyBuildPolicy::IguanaPrivKey(random_secp256k1_secret()); +) -> (EthCoin, Secp256k1Secret) { + let priv_key = random_secp256k1_secret(); + let build_policy = EthPrivKeyBuildPolicy::IguanaPrivKey(priv_key); let node = EthNode { url: GETH_RPC_URL.to_string(), komodo_proxy: false, @@ -1455,7 +1460,7 @@ fn eth_coin_v2_activation_with_random_privkey( gap_limit: None, }; let coin = block_on(eth_coin_from_conf_and_request_v2( - &MM_CTX1, + ctx, ticker, conf, platform_request, @@ -1471,9 +1476,9 @@ fn eth_coin_v2_activation_with_random_privkey( token_addr: erc20_contract(), }; let coin = block_on(coin.set_coin_type(coin_type)); - return coin; + return (coin, priv_key); } - coin + (coin, priv_key) } #[cfg(feature = "sepolia-taker-swap-v2-tests")] @@ -1484,10 +1489,10 @@ fn send_and_refund_taker_funding_by_secret_eth() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); - let taker_secret = vec![0; 32]; - let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; - let maker_secret_hash = sha256(&maker_secret).to_vec(); + let taker_secret = &[0; 32]; + let taker_secret_hash = sha256(taker_secret).to_vec(); + let maker_secret = &[1; 32]; + let maker_secret_hash = sha256(maker_secret).to_vec(); let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() + 1000; @@ -1519,7 +1524,7 @@ fn send_and_refund_taker_funding_by_secret_eth() { funding_time_lock, payment_time_lock, maker_pubkey: maker_pub, - taker_secret: &taker_secret, + taker_secret, taker_secret_hash: &taker_secret_hash, maker_secret_hash: &maker_secret_hash, dex_fee, @@ -1547,9 +1552,9 @@ fn send_and_refund_taker_funding_by_secret_erc20() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); - let taker_secret = vec![0; 32]; - let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let taker_secret = &[0; 32]; + let taker_secret_hash = sha256(taker_secret).to_vec(); + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let taker_address = block_on(taker_coin.my_addr()); @@ -1583,7 +1588,7 @@ fn send_and_refund_taker_funding_by_secret_erc20() { funding_time_lock, payment_time_lock, maker_pubkey: &taker_coin.derive_htlc_pubkey_v2(&[]), - taker_secret: &taker_secret, + taker_secret, taker_secret_hash: &taker_secret_hash, maker_secret_hash: &maker_secret_hash, dex_fee, @@ -1609,9 +1614,9 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_eth() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let taker_address = block_on(taker_coin.my_addr()); @@ -1674,10 +1679,10 @@ fn taker_send_approve_and_spend_eth() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); - let taker_secret = vec![0; 32]; - let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; - let maker_secret_hash = sha256(&maker_secret).to_vec(); + let taker_secret = &[0; 32]; + let taker_secret_hash = sha256(taker_secret).to_vec(); + let maker_secret = &[1; 32]; + let maker_secret_hash = sha256(maker_secret).to_vec(); let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() + 600; @@ -1700,6 +1705,7 @@ fn taker_send_approve_and_spend_eth() { swap_unique_data: &[], }; wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); + let taker_coin_start_block = block_on(taker_coin.current_block().compat()).unwrap(); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ETH funding, tx hash: {:02x}", funding_tx.tx_hash()); wait_for_confirmations(&taker_coin, &funding_tx, 100); @@ -1742,38 +1748,39 @@ fn taker_send_approve_and_spend_eth() { wait_for_confirmations(&taker_coin, &taker_approve_tx, 100); wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); - let check_taker_approved_tx = block_on(maker_coin.search_for_taker_funding_spend(&taker_approve_tx, 0u64, &[])) + let check_taker_approved_tx = block_on(maker_coin.search_for_taker_funding_spend(&funding_tx, 0u64, &[])) .unwrap() .unwrap(); match check_taker_approved_tx { FundingTxSpend::TransferredToTakerPayment(tx) => { - assert_eq!(tx, taker_approve_tx); + assert_eq!(tx, funding_tx); }, FundingTxSpend::RefundedTimelock(_) | FundingTxSpend::RefundedSecret { .. } => { panic!("Wrong FundingTxSpend variant, expected TransferredToTakerPayment") }, }; - let dex_fee_pub = sepolia_taker_swap_v2(); let spend_args = GenTakerPaymentSpendArgs { - taker_tx: &taker_approve_tx, + taker_tx: &funding_tx, time_lock: payment_time_lock, maker_secret_hash: &maker_secret_hash, maker_pub, maker_address: &maker_address, taker_pub, - dex_fee_pub: dex_fee_pub.as_bytes(), dex_fee, premium_amount: Default::default(), trading_amount, }; wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let spend_tx = - block_on(maker_coin.sign_and_broadcast_taker_payment_spend(&preimage, &spend_args, &maker_secret, &[])) - .unwrap(); + block_on(maker_coin.sign_and_broadcast_taker_payment_spend(None, &spend_args, maker_secret, &[])).unwrap(); log!("Maker spent ETH payment, tx hash: {:02x}", spend_tx.tx_hash()); wait_for_confirmations(&maker_coin, &spend_tx, 100); - block_on(taker_coin.wait_for_taker_payment_spend(&spend_tx, 0u64, payment_time_lock)).unwrap(); + let found_spend_tx = + block_on(taker_coin.find_taker_payment_spend_tx(&taker_approve_tx, taker_coin_start_block, payment_time_lock)) + .unwrap(); + let extracted_maker_secret = block_on(taker_coin.extract_secret_v2(&[], &found_spend_tx)).unwrap(); + assert_eq!(maker_secret, &extracted_maker_secret); } #[cfg(feature = "sepolia-taker-swap-v2-tests")] @@ -1785,9 +1792,9 @@ fn taker_send_approve_and_spend_erc20() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() + 600; @@ -1795,7 +1802,7 @@ fn taker_send_approve_and_spend_erc20() { let taker_address = block_on(taker_coin.my_addr()); let maker_address = block_on(maker_coin.my_addr()); - let dex_fee = &DexFee::Standard("0.00001".into()); + let dex_fee = &DexFee::NoFee; let trading_amount = BigDecimal::from_str("0.0001").unwrap(); let maker_pub = &maker_coin.derive_htlc_pubkey_v2(&[]); @@ -1811,6 +1818,7 @@ fn taker_send_approve_and_spend_erc20() { swap_unique_data: &[], }; wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); + let taker_coin_start_block = block_on(taker_coin.current_block().compat()).unwrap(); let funding_tx = block_on(taker_coin.send_taker_funding(payment_args)).unwrap(); log!("Taker sent ERC20 funding, tx hash: {:02x}", funding_tx.tx_hash()); wait_for_confirmations(&taker_coin, &funding_tx, 100); @@ -1853,37 +1861,38 @@ fn taker_send_approve_and_spend_erc20() { wait_for_confirmations(&taker_coin, &taker_approve_tx, 100); wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); - let check_taker_approved_tx = block_on(maker_coin.search_for_taker_funding_spend(&taker_approve_tx, 0u64, &[])) + let check_taker_approved_tx = block_on(maker_coin.search_for_taker_funding_spend(&funding_tx, 0u64, &[])) .unwrap() .unwrap(); match check_taker_approved_tx { FundingTxSpend::TransferredToTakerPayment(tx) => { - assert_eq!(tx, taker_approve_tx); + assert_eq!(tx, funding_tx); }, FundingTxSpend::RefundedTimelock(_) | FundingTxSpend::RefundedSecret { .. } => { panic!("Wrong FundingTxSpend variant, expected TransferredToTakerPayment") }, }; - let dex_fee_pub = sepolia_taker_swap_v2(); let spend_args = GenTakerPaymentSpendArgs { - taker_tx: &taker_approve_tx, + taker_tx: &funding_tx, time_lock: payment_time_lock, maker_secret_hash: &maker_secret_hash, maker_pub, maker_address: &maker_address, taker_pub, - dex_fee_pub: dex_fee_pub.as_bytes(), dex_fee, premium_amount: Default::default(), trading_amount, }; wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); let spend_tx = - block_on(maker_coin.sign_and_broadcast_taker_payment_spend(&preimage, &spend_args, &maker_secret, &[])) - .unwrap(); + block_on(maker_coin.sign_and_broadcast_taker_payment_spend(None, &spend_args, &maker_secret, &[])).unwrap(); log!("Maker spent ERC20 payment, tx hash: {:02x}", spend_tx.tx_hash()); - block_on(taker_coin.wait_for_taker_payment_spend(&spend_tx, 0u64, payment_time_lock)).unwrap(); + let found_spend_tx = + block_on(taker_coin.find_taker_payment_spend_tx(&taker_approve_tx, taker_coin_start_block, payment_time_lock)) + .unwrap(); + let extracted_maker_secret = block_on(taker_coin.extract_secret_v2(&[], &found_spend_tx)).unwrap(); + assert_eq!(maker_secret, extracted_maker_secret); } #[cfg(feature = "sepolia-taker-swap-v2-tests")] @@ -1894,9 +1903,9 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let funding_time_lock = now_sec() + 3000; let payment_time_lock = now_sec() - 1000; @@ -1979,9 +1988,9 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let funding_time_lock = now_sec() + 29; let payment_time_lock = now_sec() + 15; @@ -2065,9 +2074,9 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let taker_address = block_on(taker_coin.my_addr()); @@ -2130,9 +2139,9 @@ fn send_maker_payment_and_refund_timelock_eth() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let payment_time_lock = now_sec() - 1000; @@ -2185,9 +2194,9 @@ fn send_maker_payment_and_refund_timelock_erc20() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let payment_time_lock = now_sec() - 1000; @@ -2239,10 +2248,10 @@ fn send_maker_payment_and_refund_secret_eth() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); - let taker_secret = vec![0; 32]; - let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; - let maker_secret_hash = sha256(&maker_secret).to_vec(); + let taker_secret = &[0; 32]; + let taker_secret_hash = sha256(taker_secret).to_vec(); + let maker_secret = &[1; 32]; + let maker_secret_hash = sha256(maker_secret).to_vec(); let payment_time_lock = now_sec() + 1000; let maker_address = block_on(maker_coin.my_addr()); @@ -2268,7 +2277,7 @@ fn send_maker_payment_and_refund_secret_eth() { time_lock: payment_time_lock, taker_secret_hash: &taker_secret_hash, maker_secret_hash: &maker_secret_hash, - taker_secret: &taker_secret, + taker_secret, taker_pub, swap_unique_data: &[], amount: trading_amount, @@ -2291,10 +2300,10 @@ fn send_maker_payment_and_refund_secret_erc20() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); - let taker_secret = vec![0; 32]; - let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; - let maker_secret_hash = sha256(&maker_secret).to_vec(); + let taker_secret = &[0; 32]; + let taker_secret_hash = sha256(taker_secret).to_vec(); + let maker_secret = &[1; 32]; + let maker_secret_hash = sha256(maker_secret).to_vec(); let payment_time_lock = now_sec() + 1000; let maker_address = block_on(maker_coin.my_addr()); @@ -2320,7 +2329,7 @@ fn send_maker_payment_and_refund_secret_erc20() { time_lock: payment_time_lock, taker_secret_hash: &taker_secret_hash, maker_secret_hash: &maker_secret_hash, - taker_secret: &taker_secret, + taker_secret, taker_pub, swap_unique_data: &[], amount: trading_amount, @@ -2342,9 +2351,9 @@ fn send_and_spend_maker_payment_eth() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let payment_time_lock = now_sec() + 1000; @@ -2385,7 +2394,7 @@ fn send_and_spend_maker_payment_eth() { time_lock: payment_time_lock, taker_secret_hash: &taker_secret_hash, maker_secret_hash: &maker_secret_hash, - maker_secret: &maker_secret, + maker_secret, maker_pub, swap_unique_data: &[], amount: trading_amount, @@ -2405,9 +2414,9 @@ fn send_and_spend_maker_payment_erc20() { let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); - let taker_secret = vec![0; 32]; + let taker_secret = [0; 32]; let taker_secret_hash = sha256(&taker_secret).to_vec(); - let maker_secret = vec![1; 32]; + let maker_secret = [1; 32]; let maker_secret_hash = sha256(&maker_secret).to_vec(); let payment_time_lock = now_sec() + 1000; @@ -2448,7 +2457,7 @@ fn send_and_spend_maker_payment_erc20() { time_lock: payment_time_lock, taker_secret_hash: &taker_secret_hash, maker_secret_hash: &maker_secret_hash, - maker_secret: &maker_secret, + maker_secret, maker_pub, swap_unique_data: &[], amount: trading_amount, @@ -2740,3 +2749,128 @@ fn test_enable_custom_erc20_with_duplicate_contract_in_config() { // Disable the custom token, this to check that it was enabled correctly block_on(disable_coin(&mm_hd, &config_ticker, true)); } + +#[test] +fn test_v2_eth_eth_kickstart() { + // Initialize swap addresses and configurations + let swap_addresses = SwapAddresses::init(); + let contracts = SwapV2TestContracts { + maker_swap_v2_contract: swap_addresses.swap_v2_contracts.maker_swap_v2_contract.addr_to_string(), + taker_swap_v2_contract: swap_addresses.swap_v2_contracts.taker_swap_v2_contract.addr_to_string(), + nft_maker_swap_v2_contract: swap_addresses + .swap_v2_contracts + .nft_maker_swap_v2_contract + .addr_to_string(), + }; + let swap_contract_address = swap_addresses.swap_contract_address.addr_to_string(); + let node = TestNode { + url: GETH_RPC_URL.to_string(), + }; + + // Helper function for activating coins + let enable_coins = |mm: &MarketMakerIt, coins: &[&str]| { + for &coin in coins { + log!( + "{:?}", + block_on(enable_eth_coin_v2( + mm, + coin, + &swap_contract_address, + contracts.clone(), + None, + &[node.clone()] + )) + ); + } + }; + + // start Bob and Alice + let (_, bob_priv_key) = + eth_coin_v2_activation_with_random_privkey(&MM_CTX, ETH, ð_dev_conf(), swap_addresses, false); + let (_, alice_priv_key) = + eth_coin_v2_activation_with_random_privkey(&MM_CTX1, ETH1, ð1_dev_conf(), swap_addresses, false); + let coins = json!([eth_dev_conf(), eth1_dev_conf()]); + + let mut bob_conf = Mm2TestConf::seednode_trade_v2(&format!("0x{}", hex::encode(bob_priv_key)), &coins); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf.clone(), bob_conf.rpc_password.clone(), None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + log!("Bob log path: {}", mm_bob.log_path.display()); + + let mut alice_conf = + Mm2TestConf::light_node_trade_v2(&format!("0x{}", hex::encode(alice_priv_key)), &coins, &[&mm_bob + .ip + .to_string()]); + 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_dump(&mm_alice.log_path); + log!("Alice log path: {}", mm_alice.log_path.display()); + + // Enable ETH and ETH1 for both Bob and Alice + enable_coins(&mm_bob, &[ETH, ETH1]); + enable_coins(&mm_alice, &[ETH, ETH1]); + + let uuids = block_on(start_swaps(&mut mm_bob, &mut mm_alice, &[(ETH, ETH1)], 1.0, 1.0, 77.)); + log!("{:?}", uuids); + let parsed_uuids: Vec = uuids.iter().map(|u| u.parse().unwrap()).collect(); + + for uuid in uuids.iter() { + log_swap_status_before_stop(&mm_bob, uuid, "Maker"); + log_swap_status_before_stop(&mm_alice, uuid, "Taker"); + } + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + + // Restart Bob and Alice + bob_conf.conf["dbdir"] = mm_bob.folder.join("DB").to_str().unwrap().into(); + bob_conf.conf["log"] = mm_bob.folder.join("mm2_dup.log").to_str().unwrap().into(); + + let mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + log!("Bob log path: {}", mm_bob.log_path.display()); + + alice_conf.conf["dbdir"] = mm_alice.folder.join("DB").to_str().unwrap().into(); + alice_conf.conf["log"] = mm_alice.folder.join("mm2_dup.log").to_str().unwrap().into(); + alice_conf.conf["seednodes"] = vec![mm_bob.ip.to_string()].into(); + + let mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + log!("Alice log path: {}", mm_alice.log_path.display()); + + verify_coins_needed_for_kickstart(&mm_bob, &[ETH, ETH1]); + verify_coins_needed_for_kickstart(&mm_alice, &[ETH, ETH1]); + + enable_coins(&mm_bob, &[ETH, ETH1]); + enable_coins(&mm_alice, &[ETH, ETH1]); + + // give swaps 1 second to restart + thread::sleep(Duration::from_secs(1)); + + verify_active_swaps(&mm_bob, &parsed_uuids); + verify_active_swaps(&mm_alice, &parsed_uuids); + + // coins must be virtually locked after kickstart until swap transactions are sent + verify_locked_amount(&mm_alice, "Taker", ETH1); + verify_locked_amount(&mm_bob, "Maker", ETH); +} + +fn log_swap_status_before_stop(mm: &MarketMakerIt, uuid: &str, role: &str) { + let status = block_on(my_swap_status(mm, uuid)); + log!("{} swap {} status before stop: {:?}", role, uuid, status); +} + +fn verify_coins_needed_for_kickstart(mm: &MarketMakerIt, expected_coins: &[&str]) { + let mut coins_needed = block_on(coins_needed_for_kickstart(mm)); + coins_needed.sort(); + assert_eq!(coins_needed, expected_coins); +} + +fn verify_active_swaps(mm: &MarketMakerIt, expected_uuids: &[Uuid]) { + let active_swaps = block_on(active_swaps(mm)); + assert_eq!(active_swaps.uuids, expected_uuids); +} + +fn verify_locked_amount(mm: &MarketMakerIt, role: &str, coin: &str) { + let locked = block_on(get_locked_amount(mm, coin)); + log!("{} {} locked amount: {:?}", role, coin, locked.locked_amount); + assert_eq!(locked.coin, coin); +} diff --git a/mm2src/mm2_main/tests/docker_tests/mod.rs b/mm2src/mm2_main/tests/docker_tests/mod.rs index 4b969b9fec..8dce2cdfb8 100644 --- a/mm2src/mm2_main/tests/docker_tests/mod.rs +++ b/mm2src/mm2_main/tests/docker_tests/mod.rs @@ -11,6 +11,7 @@ mod swap_watcher_tests; mod swaps_confs_settings_sync_tests; mod swaps_file_lock_tests; mod tendermint_tests; +mod z_coin_docker_tests; // dummy test helping IDE to recognize this as test module #[test] diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index cfbd2df664..7f759443a5 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -6,17 +6,16 @@ use coins::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin}; use coins::utxo::rpc_clients::UtxoRpcClientEnum; use coins::utxo::utxo_common::big_decimal_from_sat; use coins::utxo::{UtxoActivationParams, UtxoCommonOps}; -use coins::{CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MarketCoinOps, - MmCoin, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, TradePreimageValue, TransactionEnum, ValidateFeeArgs, ValidatePaymentInput, - WaitForHTLCTxSpendArgs}; -use common::log::debug; +use coins::{CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, DexFeeBurnDestination, FeeApproxStage, + FoundSwapTxSpend, MarketCoinOps, MmCoin, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, + SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TradePreimageValue, TransactionEnum, ValidateFeeArgs, + ValidatePaymentInput, WaitForHTLCTxSpendArgs}; use common::{block_on_f01, temp_dir, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::Secp256k1Secret; use ethereum_types::H160; use http::StatusCode; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; -use mm2_main::lp_swap::{dex_fee_amount, max_taker_vol_from_available}; +use mm2_main::lp_swap::max_taker_vol_from_available; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::{CoinInitResponse, OrderbookResponse}; use mm2_test_helpers::structs::{trade_preimage_error, RpcErrorResponse, RpcSuccessResponse, TransactionDetails}; @@ -977,7 +976,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ let coins = json! ([ {"coin":"MYCOIN","asset":"MYCOIN","txversion":4,"overwintered":1,"txfee":1000,"protocol":{"type":"UTXO"}}, {"coin":"QTUM","decimals":8,"pubtype":120,"p2shtype":110,"wiftype":128,"txfee":0,"txfee_volatility_percent":0.1, - "mm2":1,"mature_confirmations":500,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"}}, + "mm2":1,"mature_confirmations":500,"network":"regtest","confpath":confpath,"protocol":{"type":"UTXO"}, "dust": 72800}, ]); let mut mm = MarketMakerIt::start( json! ({ @@ -1013,13 +1012,13 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ )) .expect("!get_sender_trade_fee"); let max_trade_fee = max_trade_fee.amount.to_decimal(); - debug!("max_trade_fee: {}", max_trade_fee); + log!("max_trade_fee: {}", max_trade_fee); // - `max_possible_2 = balance - locked_amount - max_trade_fee`, where `locked_amount = 0` let max_possible_2 = &qtum_balance - &max_trade_fee; // - `max_dex_fee = dex_fee(max_possible_2)` - let max_dex_fee = dex_fee_amount("QTUM", "MYCOIN", &MmNumber::from(max_possible_2), &qtum_min_tx_amount); - debug!("max_dex_fee: {:?}", max_dex_fee.fee_amount().to_fraction()); + let max_dex_fee = DexFee::new_from_taker_coin(&coin, "MYCOIN", &MmNumber::from(max_possible_2)); + log!("max_dex_fee: {:?}", max_dex_fee.fee_amount().to_fraction()); // - `max_fee_to_send_taker_fee = fee_to_send_taker_fee(max_dex_fee)` // `taker_fee` is sent using general withdraw, and the fee get be obtained from withdraw result @@ -1027,19 +1026,17 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ block_on(coin.get_fee_to_send_taker_fee(max_dex_fee, FeeApproxStage::TradePreimage)) .expect("!get_fee_to_send_taker_fee"); let max_fee_to_send_taker_fee = max_fee_to_send_taker_fee.amount.to_decimal(); - debug!("max_fee_to_send_taker_fee: {}", max_fee_to_send_taker_fee); + log!("max_fee_to_send_taker_fee: {}", max_fee_to_send_taker_fee); // and then calculate `min_max_val = balance - locked_amount - max_trade_fee - max_fee_to_send_taker_fee - dex_fee(max_val)` using `max_taker_vol_from_available()` // where `available = balance - locked_amount - max_trade_fee - max_fee_to_send_taker_fee` let available = &qtum_balance - &max_trade_fee - &max_fee_to_send_taker_fee; - debug!("total_available: {}", available); - #[allow(clippy::redundant_clone)] // This is a false-possitive bug from clippy - let min_tx_amount = qtum_min_tx_amount.clone(); + log!("total_available: {}", available); let expected_max_taker_vol = - max_taker_vol_from_available(MmNumber::from(available), "QTUM", "MYCOIN", &min_tx_amount) + max_taker_vol_from_available(MmNumber::from(available), "QTUM", "MYCOIN", &qtum_min_tx_amount) .expect("max_taker_vol_from_available"); - let real_dex_fee = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol, &qtum_min_tx_amount).fee_amount(); - debug!("real_max_dex_fee: {:?}", real_dex_fee.to_fraction()); + let real_dex_fee = DexFee::new_from_taker_coin(&coin, "MYCOIN", &expected_max_taker_vol).fee_amount(); + log!("real_max_dex_fee: {:?}", real_dex_fee.to_fraction()); // check if the actual max_taker_vol equals to the expected let rc = block_on(mm.rpc(&json! ({ @@ -1071,9 +1068,8 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ let timelock = now_sec() - 200; let secret_hash = &[0; 20]; - let dex_fee = dex_fee_amount("QTUM", "MYCOIN", &expected_max_taker_vol, &qtum_min_tx_amount); - let _taker_fee_tx = - block_on(coin.send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, &[], timelock)).expect("!send_taker_fee"); + let dex_fee = DexFee::new_from_taker_coin(&coin, "MYCOIN", &expected_max_taker_vol); + let _taker_fee_tx = block_on(coin.send_taker_fee(dex_fee, &[], timelock)).expect("!send_taker_fee"); let taker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, @@ -1100,7 +1096,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ /// Generate the Qtum coin with a random balance and start the `test_get_max_taker_vol_and_trade_with_dynamic_trade_fee` test. #[test] fn test_max_taker_vol_dynamic_trade_fee() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 2 Qtums let (_ctx, coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", 2.into(), Some(0)); let my_address = coin.my_address().expect("!my_address"); @@ -1127,7 +1123,7 @@ fn test_max_taker_vol_dynamic_trade_fee() { /// This test checks if the fee returned from `get_sender_trade_fee` should include the change output anyway. #[test] fn test_trade_preimage_fee_includes_change_output_anyway() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 2 Qtums let (_ctx, coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", 2.into(), Some(0)); let my_address = coin.my_address().expect("!my_address"); @@ -1143,7 +1139,7 @@ fn test_trade_preimage_fee_includes_change_output_anyway() { } #[test] fn test_trade_preimage_not_sufficient_base_coin_balance_for_ticker() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QRC20 coin(QICK) fill the wallet with 10 QICK // fill QTUM balance with 0.005 QTUM which is will be than expected transaction fee just to get our desired output for this test. let qick_balance = MmNumber::from("10").to_decimal(); @@ -1205,7 +1201,7 @@ fn test_trade_preimage_not_sufficient_base_coin_balance_for_ticker() { #[test] fn test_trade_preimage_dynamic_fee_not_sufficient_balance() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums let qtum_balance = MmNumber::from("0.5").to_decimal(); let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", qtum_balance.clone(), Some(0)); @@ -1266,7 +1262,7 @@ fn test_trade_preimage_dynamic_fee_not_sufficient_balance() { /// so we have to receive the `NotSufficientBalance` error. #[test] fn test_trade_preimage_deduct_fee_from_output_failed() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.00073 Qtums (that is little greater than dust 0.000728) let qtum_balance = MmNumber::from("0.00073").to_decimal(); let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", qtum_balance.clone(), Some(0)); @@ -1326,7 +1322,7 @@ fn test_trade_preimage_deduct_fee_from_output_failed() { #[test] fn test_segwit_native_balance() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums let (_ctx, _coin, priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); @@ -1372,7 +1368,7 @@ fn test_segwit_native_balance() { #[test] fn test_withdraw_and_send_from_segwit() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.7 Qtums let (_ctx, _coin, priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.7).unwrap(), Some(0)); @@ -1420,7 +1416,7 @@ fn test_withdraw_and_send_from_segwit() { #[test] fn test_withdraw_and_send_legacy_to_segwit() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.7 Qtums let (_ctx, _coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.7).unwrap(), Some(0)); @@ -1465,7 +1461,7 @@ fn test_withdraw_and_send_legacy_to_segwit() { #[test] fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run let (_ctx, coin, _) = generate_segwit_qtum_coin_with_random_privkey("QTUM", 1000u64.into(), Some(0)); let my_public_key = coin.my_public_key().unwrap(); @@ -1533,7 +1529,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { #[test] fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); let timeout = wait_until_sec(120); // timeout if test takes more than 120 seconds to run let (_ctx, coin, _) = generate_segwit_qtum_coin_with_random_privkey("QTUM", 1000u64.into(), Some(0)); let my_public_key = coin.my_public_key().unwrap(); @@ -1619,7 +1615,7 @@ pub async fn enable_native_segwit(mm: &MarketMakerIt, coin: &str) -> Json { #[test] #[ignore] fn segwit_address_in_the_orderbook() { - wait_for_estimate_smart_fee(30).expect("!wait_for_estimate_smart_fee"); + wait_for_estimate_smart_fee(60).expect("!wait_for_estimate_smart_fee"); // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums let (_ctx, coin, priv_key) = generate_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); @@ -1699,15 +1695,39 @@ fn test_trade_qrc20_utxo() { trade_base_rel(("QICK", "MYCOIN")); } fn test_trade_utxo_qrc20() { trade_base_rel(("MYCOIN", "QICK")); } #[test] -fn test_send_taker_fee_qtum() { +fn test_send_standard_taker_fee_qtum() { // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums let (_ctx, coin, _priv_key) = generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); let amount = BigDecimal::from_str("0.01").unwrap(); + let tx = block_on(coin.send_taker_fee(DexFee::Standard(amount.clone().into()), &[], 0)).expect("!send_taker_fee"); + assert!(matches!(tx, TransactionEnum::UtxoTx(_)), "Expected UtxoTx"); + + block_on(coin.validate_fee(ValidateFeeArgs { + fee_tx: &tx, + expected_sender: coin.my_public_key().unwrap(), + dex_fee: &DexFee::Standard(amount.into()), + min_block_number: 0, + uuid: &[], + })) + .expect("!validate_fee"); +} + +#[test] +fn test_send_taker_fee_with_burn_qtum() { + // generate QTUM coin with the dynamic fee and fill the wallet by 0.5 Qtums + let (_ctx, coin, _priv_key) = + generate_segwit_qtum_coin_with_random_privkey("QTUM", BigDecimal::try_from(0.5).unwrap(), Some(0)); + + let fee_amount = BigDecimal::from_str("0.0075").unwrap(); + let burn_amount = BigDecimal::from_str("0.0025").unwrap(); let tx = block_on(coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - DexFee::Standard(amount.clone().into()), + DexFee::WithBurn { + fee_amount: fee_amount.clone().into(), + burn_amount: burn_amount.clone().into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }, &[], 0, )) @@ -1717,8 +1737,11 @@ fn test_send_taker_fee_qtum() { block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: coin.my_public_key().unwrap(), - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, - dex_fee: &DexFee::Standard(amount.into()), + dex_fee: &DexFee::WithBurn { + fee_amount: fee_amount.into(), + burn_amount: burn_amount.into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }, min_block_number: 0, uuid: &[], })) @@ -1734,19 +1757,12 @@ fn test_send_taker_fee_qrc20() { ); let amount = BigDecimal::from_str("0.01").unwrap(); - let tx = block_on(coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - DexFee::Standard(amount.clone().into()), - &[], - 0, - )) - .expect("!send_taker_fee"); + let tx = block_on(coin.send_taker_fee(DexFee::Standard(amount.clone().into()), &[], 0)).expect("!send_taker_fee"); assert!(matches!(tx, TransactionEnum::UtxoTx(_)), "Expected UtxoTx"); block_on(coin.validate_fee(ValidateFeeArgs { fee_tx: &tx, expected_sender: coin.my_public_key().unwrap(), - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee: &DexFee::Standard(amount.into()), min_block_number: 0, uuid: &[], diff --git a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs index 304f6f4819..7816d62028 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs @@ -1,4 +1,4 @@ -use crate::{generate_utxo_coin_with_random_privkey, MYCOIN, MYCOIN1}; +use crate::{generate_utxo_coin_with_random_privkey, MYCOIN, MYCOIN1, SET_BURN_PUBKEY_TO_ALICE}; use bitcrypto::dhash160; use coins::utxo::UtxoCommonOps; use coins::{ConfirmPaymentInput, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, @@ -6,7 +6,9 @@ use coins::{ConfirmPaymentInput, DexFee, FundingTxSpend, GenTakerFundingSpendArg RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, SendMakerPaymentArgs, SendTakerFundingArgs, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, Transaction, ValidateMakerPaymentArgs, ValidateTakerFundingArgs}; -use common::{block_on, block_on_f01, now_sec, DEX_FEE_ADDR_RAW_PUBKEY}; +use crypto::privkey::key_pair_from_secret; +//use futures01::Future; +use common::{block_on, block_on_f01, now_sec}; use mm2_number::MmNumber; use mm2_test_helpers::for_tests::{active_swaps, check_recent_swaps, coins_needed_for_kickstart, disable_coin, disable_coin_err, enable_native, get_locked_amount, mm_dump, my_swap_status, @@ -107,8 +109,8 @@ fn send_and_refund_taker_funding_secret() { let (_mm_arc, coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let funding_time_lock = now_sec() - 1000; - let taker_secret = [0; 32]; - let taker_secret_hash_owned = dhash160(&taker_secret); + let taker_secret = &[0; 32]; + let taker_secret_hash_owned = dhash160(taker_secret); let taker_secret_hash = taker_secret_hash_owned.as_slice(); let maker_pub = coin.my_public_key().unwrap(); let dex_fee = &DexFee::Standard("0.01".into()); @@ -158,7 +160,7 @@ fn send_and_refund_taker_funding_secret() { funding_time_lock, payment_time_lock: 0, maker_pubkey: maker_pub, - taker_secret: &taker_secret, + taker_secret, taker_secret_hash, maker_secret_hash: &[], dex_fee, @@ -186,7 +188,7 @@ fn send_and_refund_taker_funding_secret() { match found_refund_tx { Some(FundingTxSpend::RefundedSecret { tx, secret }) => { assert_eq!(refund_tx, tx); - assert_eq!(taker_secret, secret); + assert_eq!(taker_secret, &secret); }, unexpected => panic!("Got unexpected FundingTxSpend variant {:?}", unexpected), } @@ -280,7 +282,7 @@ fn send_and_spend_taker_funding() { } #[test] -fn send_and_spend_taker_payment_dex_fee_burn() { +fn send_and_spend_taker_payment_dex_fee_burn_kmd() { let (_mm_arc, taker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let (_mm_arc, maker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); @@ -294,7 +296,7 @@ fn send_and_spend_taker_payment_dex_fee_burn() { let taker_pub = taker_coin.my_public_key().unwrap(); let maker_pub = maker_coin.my_public_key().unwrap(); - let dex_fee = &DexFee::with_burn("0.75".into(), "0.25".into()); + let dex_fee = &DexFee::create_from_fields("0.75".into(), "0.25".into(), "KMD"); let send_args = SendTakerFundingArgs { funding_time_lock, @@ -357,7 +359,6 @@ fn send_and_spend_taker_payment_dex_fee_burn() { maker_pub, maker_address: &block_on(maker_coin.my_addr()), taker_pub, - dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, premium_amount: 0.into(), trading_amount: 777.into(), @@ -377,7 +378,7 @@ fn send_and_spend_taker_payment_dex_fee_burn() { .unwrap(); let taker_payment_spend = block_on(maker_coin.sign_and_broadcast_taker_payment_spend( - &taker_payment_spend_preimage, + Some(&taker_payment_spend_preimage), &gen_taker_payment_spend_args, maker_secret, &[], @@ -387,7 +388,7 @@ fn send_and_spend_taker_payment_dex_fee_burn() { } #[test] -fn send_and_spend_taker_payment_standard_dex_fee() { +fn send_and_spend_taker_payment_dex_fee_burn_non_kmd() { let (_mm_arc, taker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let (_mm_arc, maker_coin, _privkey) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); @@ -401,7 +402,7 @@ fn send_and_spend_taker_payment_standard_dex_fee() { let taker_pub = taker_coin.my_public_key().unwrap(); let maker_pub = maker_coin.my_public_key().unwrap(); - let dex_fee = &DexFee::Standard(1.into()); + let dex_fee = &DexFee::create_from_fields("0.75".into(), "0.25".into(), "MYCOIN"); let send_args = SendTakerFundingArgs { funding_time_lock, @@ -464,7 +465,6 @@ fn send_and_spend_taker_payment_standard_dex_fee() { maker_pub, maker_address: &block_on(maker_coin.my_addr()), taker_pub, - dex_fee_pub: &DEX_FEE_ADDR_RAW_PUBKEY, dex_fee, premium_amount: 0.into(), trading_amount: 777.into(), @@ -472,9 +472,12 @@ fn send_and_spend_taker_payment_standard_dex_fee() { let taker_payment_spend_preimage = block_on(taker_coin.gen_taker_payment_spend_preimage(&gen_taker_payment_spend_args, &[])).unwrap(); - // tx must have 1 output: dex fee - assert_eq!(taker_payment_spend_preimage.preimage.outputs.len(), 1); - assert_eq!(taker_payment_spend_preimage.preimage.outputs[0].value, 100000000); + // tx must have 3 outputs: dex fee, burn (for non-kmd too), and maker amount + // because of the burn output we can't use SIGHASH_SINGLE and taker must add the maker output + assert_eq!(taker_payment_spend_preimage.preimage.outputs.len(), 3); + assert_eq!(taker_payment_spend_preimage.preimage.outputs[0].value, 75_000_000); + assert_eq!(taker_payment_spend_preimage.preimage.outputs[1].value, 25_000_000); + assert_eq!(taker_payment_spend_preimage.preimage.outputs[2].value, 77699998000); block_on( maker_coin.validate_taker_payment_spend_preimage(&gen_taker_payment_spend_args, &taker_payment_spend_preimage), @@ -482,7 +485,7 @@ fn send_and_spend_taker_payment_standard_dex_fee() { .unwrap(); let taker_payment_spend = block_on(maker_coin.sign_and_broadcast_taker_payment_spend( - &taker_payment_spend_preimage, + Some(&taker_payment_spend_preimage), &gen_taker_payment_spend_args, maker_secret, &[], @@ -619,13 +622,39 @@ fn send_and_refund_maker_payment_taker_secret() { } #[test] -fn test_v2_swap_utxo_utxo() { +fn test_v2_swap_utxo_utxo() { test_v2_swap_utxo_utxo_impl(); } + +// test a swap when taker is burn pubkey (no dex fee should be paid) +#[test] +fn test_v2_swap_utxo_utxo_burnkey_as_alice() { + SET_BURN_PUBKEY_TO_ALICE.set(true); + test_v2_swap_utxo_utxo_impl(); +} + +fn test_v2_swap_utxo_utxo_impl() { let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey(MYCOIN, 1000.into()); let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey(MYCOIN1, 1000.into()); let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let alice_pubkey_str = hex::encode( + key_pair_from_secret(&alice_priv_key) + .expect("valid test key pair") + .public() + .to_vec(), + ); + let mut envs = vec![]; + if SET_BURN_PUBKEY_TO_ALICE.get() { + envs.push(("TEST_BURN_ADDR_RAW_PUBKEY", alice_pubkey_str.as_str())); + } + let bob_conf = Mm2TestConf::seednode_trade_v2(&format!("0x{}", hex::encode(bob_priv_key)), &coins); - let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + bob_conf.conf, + bob_conf.rpc_password, + None, + &envs, + )) + .unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); log!("Bob log path: {}", mm_bob.log_path.display()); @@ -633,7 +662,13 @@ fn test_v2_swap_utxo_utxo() { Mm2TestConf::light_node_trade_v2(&format!("0x{}", hex::encode(alice_priv_key)), &coins, &[&mm_bob .ip .to_string()]); - let mut mm_alice = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password, + None, + &envs, + )) + .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); log!("Alice log path: {}", mm_alice.log_path.display()); @@ -681,7 +716,11 @@ fn test_v2_swap_utxo_utxo() { let locked_alice = block_on(get_locked_amount(&mm_alice, MYCOIN1)); assert_eq!(locked_alice.coin, MYCOIN1); - let expected: MmNumberMultiRepr = MmNumber::from("778.00001").into(); + let expected: MmNumberMultiRepr = if SET_BURN_PUBKEY_TO_ALICE.get() { + MmNumber::from("777.00001").into() // no dex fee if dex pubkey is alice + } else { + MmNumber::from("778.00001").into() + }; assert_eq!(locked_alice.locked_amount, expected); // amount must unlocked after funding tx is sent 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 43eb715324..fcb111dca3 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -4,20 +4,21 @@ use crate::docker_tests::eth_docker_tests::{erc20_coin_with_random_privkey, erc2 use crate::integration_tests_common::*; use crate::{generate_utxo_coin_with_privkey, generate_utxo_coin_with_random_privkey, random_secp256k1_secret}; use coins::coin_errors::ValidatePaymentError; -use coins::eth::checksum_address; +use coins::eth::{checksum_address, EthCoin}; +use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::utxo::{dhash160, UtxoCommonOps}; -use coins::{ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, RewardTarget, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, ValidateWatcherSpendInput, WatcherOps, WatcherSpendType, +use coins::{ConfirmPaymentInput, DexFee, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, + RewardTarget, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SwapOps, + SwapTxTypeWithSecretHash, TestCoin, ValidateWatcherSpendInput, WatcherOps, WatcherSpendType, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; -use common::{block_on, block_on_f01, now_sec, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; +use common::{block_on, block_on_f01, now_sec, wait_until_sec}; use crypto::privkey::{key_pair_from_secret, key_pair_from_seed}; -use mm2_main::lp_swap::{dex_fee_amount, dex_fee_amount_from_taker_coin, generate_secret, get_payment_locktime, - MAKER_PAYMENT_SENT_LOG, MAKER_PAYMENT_SPEND_FOUND_LOG, MAKER_PAYMENT_SPEND_SENT_LOG, - REFUND_TEST_FAILURE_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, WATCHER_MESSAGE_SENT_LOG}; +use mm2_main::lp_swap::{generate_secret, get_payment_locktime, MAKER_PAYMENT_SENT_LOG, MAKER_PAYMENT_SPEND_FOUND_LOG, + MAKER_PAYMENT_SPEND_SENT_LOG, REFUND_TEST_FAILURE_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, + WATCHER_MESSAGE_SENT_LOG}; use mm2_number::BigDecimal; use mm2_number::MmNumber; use mm2_test_helpers::for_tests::{enable_eth_coin, erc20_dev_conf, eth_dev_conf, eth_jst_testnet_conf, mm_dump, @@ -26,6 +27,7 @@ use mm2_test_helpers::for_tests::{enable_eth_coin, erc20_dev_conf, eth_dev_conf, DEFAULT_RPC_PASSWORD}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::WatcherConf; +use mocktopus::mocking::*; use num_traits::{One, Zero}; use primitives::hash::H256; use serde_json::Value; @@ -92,6 +94,7 @@ fn start_swaps_and_get_balances( alice_privkey: &str, bob_privkey: &str, watcher_privkey: &str, + custom_locktime: Option, ) -> BalanceResult { let coins = json!([ eth_dev_conf(), @@ -100,7 +103,10 @@ fn start_swaps_and_get_balances( mycoin1_conf(1000) ]); - let alice_conf = Mm2TestConf::seednode(&format!("0x{}", alice_privkey), &coins); + let mut alice_conf = Mm2TestConf::seednode(&format!("0x{}", alice_privkey), &coins); + if let Some(locktime) = custom_locktime { + alice_conf.conf["payment_locktime"] = locktime.into(); + } let mut mm_alice = block_on(MarketMakerIt::start_with_envs( alice_conf.conf.clone(), alice_conf.rpc_password.clone(), @@ -111,7 +117,10 @@ 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_conf = Mm2TestConf::light_node(&format!("0x{}", bob_privkey), &coins, &[&mm_alice.ip.to_string()]); + let mut bob_conf = Mm2TestConf::light_node(&format!("0x{}", bob_privkey), &coins, &[&mm_alice.ip.to_string()]); + if let Some(locktime) = custom_locktime { + bob_conf.conf["payment_locktime"] = locktime.into(); + } let mut mm_bob = block_on(MarketMakerIt::start_with_envs( bob_conf.conf.clone(), bob_conf.rpc_password, @@ -157,13 +166,16 @@ fn start_swaps_and_get_balances( ), }; - let watcher_conf = Mm2TestConf::watcher_light_node( + let mut watcher_conf = Mm2TestConf::watcher_light_node( &format!("0x{}", watcher_privkey), &coins, &[&mm_alice.ip.to_string()], watcher_conf, ) .conf; + if let Some(locktime) = custom_locktime { + watcher_conf["payment_locktime"] = locktime.into(); + } let mut mm_watcher = block_on(MarketMakerIt::start_with_envs( watcher_conf, @@ -270,9 +282,17 @@ fn check_actual_events(mm_alice: &MarketMakerIt, uuid: &str, expected_events: &[ status_response } -fn run_taker_node(coins: &Value, envs: &[(&str, &str)], seednodes: &[&str]) -> (MarketMakerIt, Mm2TestConf) { +fn run_taker_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + custom_locktime: Option, +) -> (MarketMakerIt, Mm2TestConf) { let privkey = hex::encode(random_secp256k1_secret()); - let conf = Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes); + let mut conf = Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes); + if let Some(locktime) = custom_locktime { + conf.conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf.conf.clone(), conf.rpc_password.clone(), @@ -309,13 +329,21 @@ fn restart_taker_and_wait_until(conf: &Mm2TestConf, envs: &[(&str, &str)], wait_ mm_alice } -fn run_maker_node(coins: &Value, envs: &[(&str, &str)], seednodes: &[&str]) -> MarketMakerIt { +fn run_maker_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + custom_locktime: Option, +) -> MarketMakerIt { let privkey = hex::encode(random_secp256k1_secret()); - let conf = if seednodes.is_empty() { + let mut conf = if seednodes.is_empty() { Mm2TestConf::seednode(&format!("0x{}", privkey), coins) } else { Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes) }; + if let Some(locktime) = custom_locktime { + conf.conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf.conf.clone(), conf.rpc_password, @@ -339,9 +367,13 @@ fn run_watcher_node( envs: &[(&str, &str)], seednodes: &[&str], watcher_conf: WatcherConf, + custom_locktime: Option, ) -> MarketMakerIt { let privkey = hex::encode(random_secp256k1_secret()); - let conf = Mm2TestConf::watcher_light_node(&format!("0x{}", privkey), coins, seednodes, watcher_conf).conf; + let mut conf = Mm2TestConf::watcher_light_node(&format!("0x{}", privkey), coins, seednodes, watcher_conf).conf; + if let Some(locktime) = custom_locktime { + conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf, DEFAULT_RPC_PASSWORD.to_string(), @@ -363,11 +395,13 @@ fn run_watcher_node( #[test] fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = - run_taker_node(&coins, &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], &[ - &mm_bob.ip.to_string(), - ]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], + &[&mm_bob.ip.to_string()], + None, + ); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -375,7 +409,7 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker refund_start_factor: 1.5, search_interval: 1.0, }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -422,11 +456,13 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker #[test] fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = - run_taker_node(&coins, &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], &[&mm_bob - .ip - .to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], + &[&mm_bob.ip.to_string()], + None, + ); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -434,7 +470,7 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_ refund_start_factor: 1.5, search_interval: 1.0, }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -477,15 +513,13 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_ #[test] fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_wait_for_taker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_seednode = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[]); - let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_seednode.ip.to_string()]); + let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); + let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); let (mut mm_alice, mut alice_conf) = run_taker_node( &coins, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic"), - ], + &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], &[&mm_seednode.ip.to_string()], + Some(60), ); let watcher_conf = WatcherConf { @@ -494,12 +528,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa refund_start_factor: 0., search_interval: 1., }; - let mut mm_watcher = run_watcher_node( - &coins, - &[("USE_TEST_LOCKTIME", "")], - &[&mm_seednode.ip.to_string()], - watcher_conf, - ); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); let uuids = block_on(start_swaps( &mut mm_bob, @@ -521,11 +550,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa block_on(mm_alice.stop()).unwrap(); - let mm_alice = restart_taker_and_wait_until( - &alice_conf, - &[("USE_TEST_LOCKTIME", "")], - &format!("[swap uuid={}] Finished", &uuids[0]), - ); + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); let expected_events = [ "Started", @@ -550,15 +575,13 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa #[test] fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_taker_payment_refund() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_seednode = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[]); - let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_seednode.ip.to_string()]); + let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); + let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); let (mut mm_alice, mut alice_conf) = run_taker_node( &coins, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TAKER_FAIL_AT", "taker_payment_refund_panic"), - ], + &[("TAKER_FAIL_AT", "taker_payment_refund_panic")], &[&mm_seednode.ip.to_string()], + Some(60), ); let watcher_conf = WatcherConf { @@ -567,12 +590,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa refund_start_factor: 0., search_interval: 1., }; - let mut mm_watcher = run_watcher_node( - &coins, - &[("USE_TEST_LOCKTIME", "")], - &[&mm_seednode.ip.to_string()], - watcher_conf, - ); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); let uuids = block_on(start_swaps( &mut mm_bob, @@ -594,11 +612,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa block_on(mm_alice.stop()).unwrap(); - let mm_alice = restart_taker_and_wait_until( - &alice_conf, - &[("USE_TEST_LOCKTIME", "")], - &format!("[swap uuid={}] Finished", &uuids[0]), - ); + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); let expected_events = [ "Started", @@ -626,8 +640,8 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa #[test] fn test_taker_completes_swap_after_restart() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -671,8 +685,8 @@ fn test_taker_completes_swap_after_restart() { #[test] fn test_taker_completes_swap_after_taker_payment_spent_while_offline() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -736,6 +750,7 @@ fn test_watcher_spends_maker_payment_utxo_utxo() { &alice_privkey, &bob_privkey, &watcher_privkey, + None, ); let acoin_volume = BigDecimal::from_str("50").unwrap(); @@ -776,6 +791,7 @@ fn test_watcher_spends_maker_payment_utxo_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -805,14 +821,17 @@ fn test_watcher_spends_maker_payment_eth_utxo() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let eth_volume = BigDecimal::from_str("0.01").unwrap(); let mycoin_volume = BigDecimal::from_str("1").unwrap(); - let min_tx_amount = BigDecimal::from_str("0.00001").unwrap().into(); + let min_tx_amount = BigDecimal::from_str("0.00001").unwrap(); - let dex_fee: BigDecimal = dex_fee_amount("MYCOIN", "ETH", &MmNumber::from(mycoin_volume.clone()), &min_tx_amount) - .fee_amount() + let coin = TestCoin::new("MYCOIN"); + TestCoin::min_tx_amount.mock_safe(move |_| MockResult::Return(min_tx_amount.clone())); + let dex_fee: BigDecimal = DexFee::new_from_taker_coin(&coin, "ETH", &MmNumber::from(mycoin_volume.clone())) + .fee_amount() // returns Standard fee (default for TestCoin) .into(); let alice_mycoin_reward_sent = balances.alice_acoin_balance_before - balances.alice_acoin_balance_after.clone() @@ -847,6 +866,7 @@ fn test_watcher_spends_maker_payment_eth_erc20() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let eth_volume = BigDecimal::from_str("0.01").unwrap(); @@ -880,6 +900,7 @@ fn test_watcher_spends_maker_payment_erc20_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let jst_volume = BigDecimal::from_str("1").unwrap(); @@ -910,6 +931,7 @@ fn test_watcher_spends_maker_payment_utxo_erc20() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -943,20 +965,18 @@ fn test_watcher_spends_maker_payment_erc20_utxo() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); let jst_volume = BigDecimal::from_str("1").unwrap(); - let min_tx_amount = BigDecimal::from_str("0.00001").unwrap().into(); - let dex_fee: BigDecimal = dex_fee_amount( - "MYCOIN", - "ERC20DEV", - &MmNumber::from(mycoin_volume.clone()), - &min_tx_amount, - ) - .fee_amount() - .into(); + let min_tx_amount = BigDecimal::from_str("0.00001").unwrap(); + let coin = TestCoin::new("MYCOIN"); + TestCoin::min_tx_amount.mock_safe(move |_| MockResult::Return(min_tx_amount.clone())); + let dex_fee: BigDecimal = DexFee::new_from_taker_coin(&coin, "ERC20DEV", &MmNumber::from(mycoin_volume.clone())) + .fee_amount() // returns Standard fee (default for TestCoin) + .into(); let alice_mycoin_reward_sent = balances.alice_acoin_balance_before - balances.alice_acoin_balance_after.clone() - mycoin_volume.clone() @@ -991,11 +1011,12 @@ fn test_watcher_refunds_taker_payment_utxo() { 25., 25., 2., - &[("USE_TEST_LOCKTIME", "")], + &[], SwapFlow::WatcherRefundsTakerPayment, alice_privkey, bob_privkey, watcher_privkey, + Some(60), ); assert_eq!( @@ -1017,11 +1038,12 @@ fn test_watcher_refunds_taker_payment_eth() { 0.01, 0.01, 1., - &[("USE_TEST_LOCKTIME", ""), ("USE_WATCHER_REWARD", "")], + &[("USE_WATCHER_REWARD", "")], SwapFlow::WatcherRefundsTakerPayment, &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + Some(60), ); assert_eq!(balances.alice_bcoin_balance_after, balances.alice_bcoin_balance_before); @@ -1040,15 +1062,12 @@ fn test_watcher_refunds_taker_payment_erc20() { 100., 100., 0.01, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TEST_COIN_PRICE", "0.01"), - ("USE_WATCHER_REWARD", ""), - ], + &[("TEST_COIN_PRICE", "0.01"), ("USE_WATCHER_REWARD", "")], SwapFlow::WatcherRefundsTakerPayment, &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + Some(60), ); let erc20_volume = BigDecimal::from_str("1").unwrap(); @@ -1080,6 +1099,7 @@ fn test_watcher_waits_for_taker_utxo() { alice_privkey, bob_privkey, watcher_privkey, + None, ); } @@ -1100,6 +1120,7 @@ fn test_watcher_waits_for_taker_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); } @@ -1204,15 +1225,9 @@ fn test_watcher_validate_taker_fee_utxo() { let taker_pubkey = taker_coin.my_public_key().unwrap(); let taker_amount = MmNumber::from((10, 1)); - let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, maker_coin.ticker(), &taker_amount); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, maker_coin.ticker(), &taker_amount); - let taker_fee = block_on(taker_coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - fee_amount, - Uuid::new_v4().as_bytes(), - lock_duration, - )) - .unwrap(); + let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: taker_fee.tx_hex(), @@ -1228,7 +1243,6 @@ fn test_watcher_validate_taker_fee_utxo() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })); assert!(validate_taker_fee_res.is_ok()); @@ -1237,7 +1251,6 @@ fn test_watcher_validate_taker_fee_utxo() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: maker_coin.my_public_key().unwrap().to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1255,7 +1268,6 @@ fn test_watcher_validate_taker_fee_utxo() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: std::u64::MAX, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1275,7 +1287,6 @@ fn test_watcher_validate_taker_fee_utxo() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration: 0, })) .unwrap_err() @@ -1288,11 +1299,14 @@ fn test_watcher_validate_taker_fee_utxo() { _ => panic!("Expected `WrongPaymentTx` transaction too old, found {:?}", error), } + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey + .mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: taker_pubkey.to_vec(), lock_duration, })) .unwrap_err() @@ -1319,14 +1333,8 @@ fn test_watcher_validate_taker_fee_eth() { let taker_pubkey = taker_keypair.public(); let taker_amount = MmNumber::from((1, 1)); - let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, "ETH", &taker_amount); - let taker_fee = block_on(taker_coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - fee_amount, - Uuid::new_v4().as_bytes(), - lock_duration, - )) - .unwrap(); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, "ETH", &taker_amount); + let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: taker_fee.tx_hex(), @@ -1341,17 +1349,15 @@ fn test_watcher_validate_taker_fee_eth() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })); assert!(validate_taker_fee_res.is_ok()); - let wrong_keypair = key_pair_from_secret(random_secp256k1_secret().as_slice()).unwrap(); + let wrong_keypair = key_pair_from_secret(&random_secp256k1_secret().take()).unwrap(); let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: wrong_keypair.public().to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1369,7 +1375,6 @@ fn test_watcher_validate_taker_fee_eth() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: std::u64::MAX, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1385,11 +1390,13 @@ fn test_watcher_validate_taker_fee_eth() { ), } + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey.mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: taker_pubkey.to_vec(), lock_duration, })) .unwrap_err() @@ -1404,6 +1411,7 @@ fn test_watcher_validate_taker_fee_eth() { error ), } + ::dex_pubkey.clear_mock(); } #[test] @@ -1416,14 +1424,8 @@ fn test_watcher_validate_taker_fee_erc20() { let taker_pubkey = taker_keypair.public(); let taker_amount = MmNumber::from((1, 1)); - let fee_amount = dex_fee_amount_from_taker_coin(&taker_coin, "ETH", &taker_amount); - let taker_fee = block_on(taker_coin.send_taker_fee( - &DEX_FEE_ADDR_RAW_PUBKEY, - fee_amount, - Uuid::new_v4().as_bytes(), - lock_duration, - )) - .unwrap(); + let dex_fee = DexFee::new_from_taker_coin(&taker_coin, "ETH", &taker_amount); + let taker_fee = block_on(taker_coin.send_taker_fee(dex_fee, Uuid::new_v4().as_bytes(), lock_duration)).unwrap(); let confirm_payment_input = ConfirmPaymentInput { payment_tx: taker_fee.tx_hex(), @@ -1438,17 +1440,15 @@ fn test_watcher_validate_taker_fee_erc20() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })); assert!(validate_taker_fee_res.is_ok()); - let wrong_keypair = key_pair_from_secret(random_secp256k1_secret().as_slice()).unwrap(); + let wrong_keypair = key_pair_from_secret(&random_secp256k1_secret().take()).unwrap(); let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: wrong_keypair.public().to_vec(), min_block_number: 0, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1466,7 +1466,6 @@ fn test_watcher_validate_taker_fee_erc20() { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: std::u64::MAX, - fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, })) .unwrap_err() @@ -1482,11 +1481,13 @@ fn test_watcher_validate_taker_fee_erc20() { ), } + let mock_pubkey = taker_pubkey.to_vec(); + ::dex_pubkey.mock_safe(move |_| MockResult::Return(Box::leak(Box::new(mock_pubkey.clone())))); + let error = block_on_f01(taker_coin.watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash_as_bytes().into_vec(), sender_pubkey: taker_pubkey.to_vec(), min_block_number: 0, - fee_addr: taker_pubkey.to_vec(), lock_duration, })) .unwrap_err() @@ -1501,6 +1502,7 @@ fn test_watcher_validate_taker_fee_erc20() { error ), } + ::dex_pubkey.clear_mock(); } #[test] diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 9fe3858736..45e3b4a03e 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,12 +5,18 @@ use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_ enable_tendermint_token, enable_tendermint_without_balance, get_tendermint_my_tx_history, ibc_withdraw, iris_ibc_nucleus_testnet_conf, my_balance, nucleus_testnet_conf, orderbook, orderbook_v2, send_raw_transaction, - set_price, withdraw_v1, MarketMakerIt, Mm2TestConf}; + set_price, tendermint_add_delegation, tendermint_delegations, + tendermint_ongoing_undelegations, tendermint_remove_delegation, + tendermint_remove_delegation_raw, tendermint_validators, withdraw_v1, MarketMakerIt, + Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, - TendermintActivationResult, TransactionDetails}; + TendermintActivationResult, TransactionDetails, TransactionType}; use serde_json::json; use std::collections::HashSet; use std::iter::FromIterator; +use std::sync::Mutex; +use std::thread; +use std::time::Duration; const TENDERMINT_TEST_SEED: &str = "tendermint test seed"; const TENDERMINT_CONSTANT_BALANCE_SEED: &str = "tendermint constant balance seed"; @@ -21,6 +27,12 @@ const NUCLEUS_TESTNET_RPC_URLS: &[&str] = &["http://localhost:26657"]; const TENDERMINT_TEST_BIP39_SEED: &str = "emerge canoe salmon dolphin glow priority random become gasp sell blade argue"; +lazy_static! { + /// Makes sure that tests sending transactions run sequentially to prevent account sequence + /// mismatches as some addresses are used in multiple tests. + static ref SEQUENCE_LOCK: Mutex<()> = Mutex::new(()); +} + #[test] fn test_tendermint_balance() { let coins = json!([atom_testnet_conf()]); @@ -159,6 +171,7 @@ fn test_tendermint_hd_address() { #[test] fn test_tendermint_withdraw() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "cosmos150evuj4j7k9kgu38e453jdv9m3u0ft2n53flg6"; let coins = json!([atom_testnet_conf()]); @@ -216,6 +229,7 @@ fn test_tendermint_withdraw() { #[test] fn test_tendermint_withdraw_hd() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "cosmos134h9tv7866jcuw708w5w76lcfx7s3x2ysyalxy"; let coins = json!([atom_testnet_conf()]); @@ -313,6 +327,7 @@ fn test_custom_gas_limit_on_tendermint_withdraw() { #[test] fn test_tendermint_ibc_withdraw() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels const IBC_SOURCE_CHANNEL: &str = "channel-3"; @@ -359,6 +374,7 @@ fn test_tendermint_ibc_withdraw() { #[test] fn test_tendermint_ibc_withdraw_hd() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels const IBC_SOURCE_CHANNEL: &str = "channel-3"; @@ -406,6 +422,7 @@ fn test_tendermint_ibc_withdraw_hd() { #[test] fn test_tendermint_token_withdraw() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; let coins = json!([nucleus_testnet_conf(), iris_ibc_nucleus_testnet_conf()]); @@ -651,6 +668,147 @@ fn test_passive_coin_and_force_disable() { block_on(disable_coin_err(&mm, token, false)); } +#[test] +fn test_tendermint_validators_rpc() { + let coins = json!([nucleus_testnet_conf()]); + let platform_coin = coins[0]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(TENDERMINT_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint( + &mm, + platform_coin, + &[], + NUCLEUS_TESTNET_RPC_URLS, + false, + )); + assert!(&activation_res.get("result").unwrap().get("address").is_some()); + + let validators_raw_response = block_on(tendermint_validators(&mm, platform_coin, "All", 10, 1)); + + assert_eq!( + validators_raw_response["result"]["validators"][0]["operator_address"], + "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu" + ); + assert_eq!(validators_raw_response["result"]["validators"][0]["jailed"], false); +} + +#[test] +fn test_tendermint_add_delegation() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); + const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; + const VALIDATOR_ADDRESS: &str = "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu"; + + let coins = json!([nucleus_testnet_conf()]); + let coin_ticker = coins[0]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(TENDERMINT_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint( + &mm, + coin_ticker, + &[], + NUCLEUS_TESTNET_RPC_URLS, + false, + )); + + log!( + "Activation with assets {}", + serde_json::to_string(&activation_res).unwrap() + ); + + let tx_details = block_on(tendermint_add_delegation(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.5")); + + assert_eq!(tx_details.to, vec![VALIDATOR_ADDRESS.to_owned()]); + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + assert_eq!(tx_details.transaction_type, TransactionType::StakingDelegation); + + let send_raw_tx = block_on(send_raw_transaction(&mm, coin_ticker, &tx_details.tx_hex)); + log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); +} + +#[test] +fn test_tendermint_remove_delegation() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); + const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; + const VALIDATOR_ADDRESS: &str = "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu"; + + let coins = json!([nucleus_testnet_conf()]); + let coin_ticker = coins[0]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(TENDERMINT_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint( + &mm, + coin_ticker, + &[], + NUCLEUS_TESTNET_RPC_URLS, + false, + )); + + log!( + "Activation with assets {}", + serde_json::to_string(&activation_res).unwrap() + ); + + let tx_details = block_on(tendermint_add_delegation(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.5")); + + assert_eq!(tx_details.to, vec![VALIDATOR_ADDRESS.to_owned()]); + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + assert_eq!(tx_details.transaction_type, TransactionType::StakingDelegation); + + let send_raw_tx = block_on(send_raw_transaction(&mm, coin_ticker, &tx_details.tx_hex)); + log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); + + thread::sleep(Duration::from_secs(1)); + + let r = block_on(tendermint_delegations(&mm, coin_ticker)); + let delegation_info = r["result"]["delegations"].as_array().unwrap().last().unwrap(); + assert_eq!(delegation_info["validator_address"], VALIDATOR_ADDRESS); + + // Try to undelegate more than the total delegated amount + let raw_response = block_on(tendermint_remove_delegation_raw( + &mm, + coin_ticker, + VALIDATOR_ADDRESS, + "3.4", + )); + assert_eq!(raw_response.0, http::StatusCode::BAD_REQUEST); + + // Track this type here to enforce compiler to help us to update this test coverage + // whenever this type is removed/renamed. + let _ = coins::DelegationError::TooMuchToUndelegate { + available: BigDecimal::default(), + requested: BigDecimal::default(), + }; + assert!(raw_response.1.contains("TooMuchToUndelegate")); + + let tx_details = block_on(tendermint_remove_delegation( + &mm, + coin_ticker, + VALIDATOR_ADDRESS, + "0.15", + )); + + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + assert!(tx_details.to.is_empty()); + assert_eq!(tx_details.transaction_type, TransactionType::RemoveDelegation); + + let send_raw_tx = block_on(send_raw_transaction(&mm, coin_ticker, &tx_details.tx_hex)); + log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); + + thread::sleep(Duration::from_secs(1)); + + let r = block_on(tendermint_ongoing_undelegations(&mm, coin_ticker)); + let undelegation_info = r["result"]["ongoing_undelegations"].as_array().unwrap().last().unwrap(); + assert_eq!(undelegation_info["validator_address"], VALIDATOR_ADDRESS); + let undelegation_entry = undelegation_info["entries"].as_array().unwrap().last().unwrap(); + assert_eq!(undelegation_entry["balance"], "0.15"); +} + mod swap { use super::*; @@ -659,21 +817,22 @@ mod swap { use crate::integration_tests_common::enable_electrum; use common::executor::Timer; use common::log; + use compatible_time::Duration; use ethereum_types::{Address, U256}; - use instant::Duration; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, doc_conf, enable_eth_coin, iris_ibc_nucleus_testnet_conf, nucleus_testnet_conf, wait_check_stats_swap_status, DOC_ELECTRUM_ADDRS}; use std::convert::TryFrom; + use std::env; use std::str::FromStr; - use std::{env, thread}; const BOB_PASSPHRASE: &str = "iris test seed"; const ALICE_PASSPHRASE: &str = "iris test2 seed"; #[test] fn swap_nucleus_with_doc() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); let bob_passphrase = String::from(BOB_PASSPHRASE); let alice_passphrase = String::from(ALICE_PASSPHRASE); @@ -752,6 +911,7 @@ mod swap { #[test] fn swap_nucleus_with_eth() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); let bob_passphrase = String::from(BOB_PASSPHRASE); let alice_passphrase = String::from(ALICE_PASSPHRASE); const BOB_ETH_ADDRESS: &str = "0x7b338250f990954E3Ab034ccD32a917c2F607C2d"; @@ -858,6 +1018,7 @@ mod swap { #[test] fn swap_doc_with_iris_ibc_nucleus() { + let _lock = SEQUENCE_LOCK.lock().unwrap(); let bob_passphrase = String::from(BOB_PASSPHRASE); let alice_passphrase = String::from(ALICE_PASSPHRASE); diff --git a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs new file mode 100644 index 0000000000..9abcfbe506 --- /dev/null +++ b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs @@ -0,0 +1,238 @@ +use bitcrypto::dhash160; +use coins::z_coin::{z_coin_from_conf_and_params_with_docker, z_send_dex_fee, ZCoin, ZcoinActivationParams, + ZcoinRpcMode}; +use coins::{coin_errors::ValidatePaymentError, CoinProtocol, DexFee, PrivKeyBuildPolicy, RefundPaymentArgs, + SendPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, ValidateFeeArgs}; +use common::now_sec; +use lazy_static::lazy_static; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_number::MmNumber; +use mm2_test_helpers::for_tests::{new_mm2_temp_folder_path, zombie_conf_for_docker}; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use tokio::sync::Mutex; + +// https://github.com/KomodoPlatform/librustzcash/blob/4e030a0f44cc17f100bf5f019563be25c5b8755f/zcash_client_backend/src/data_api/wallet.rs#L72-L73 +lazy_static! { + static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); +} + +/// Build asset `ZCoin` from ticker and spendingkey str without filling the balance. +pub async fn z_coin_from_spending_key(spending_key: &str) -> (MmArc, ZCoin) { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let mut conf = zombie_conf_for_docker(); + let params = ZcoinActivationParams { + mode: ZcoinRpcMode::Native, + ..Default::default() + }; + let pk_data = [1; 32]; + let salt: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(4) + .map(char::from) + .collect(); + let db_folder = new_mm2_temp_folder_path(None).join(format!("ZOMBIE_DB_{}", salt)); + std::fs::create_dir_all(&db_folder).unwrap(); + let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; + + let coin = z_coin_from_conf_and_params_with_docker( + &ctx, + "ZOMBIE", + &conf, + ¶ms, + PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), + db_folder, + protocol_info, + spending_key, + ) + .await + .unwrap(); + + (ctx, coin) +} + +#[ignore] +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_and_refund_maker_payment() { + let _lock = TEST_MUTEX.lock().await; + + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + let time_lock = now_sec() - 3600; + let secret_hash = [0; 20]; + + let maker_uniq_data = [3; 32]; + + let taker_uniq_data = [5; 32]; + let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); + let taker_pub = taker_key_pair.public(); + + let args = SendPaymentArgs { + time_lock_duration: 0, + time_lock, + other_pubkey: taker_pub, + secret_hash: &secret_hash, + amount: "0.01".parse().unwrap(), + swap_contract_address: &None, + swap_unique_data: maker_uniq_data.as_slice(), + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let tx = coin.send_maker_payment(args).await.unwrap(); + log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); + + let refund_args = RefundPaymentArgs { + payment_tx: &tx.tx_hex(), + time_lock, + other_pubkey: taker_pub, + tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &secret_hash, + }, + swap_contract_address: &None, + swap_unique_data: maker_uniq_data.as_slice(), + watcher_reward: false, + }; + let refund_tx = coin.send_maker_refunds_payment(refund_args).await.unwrap(); + log!("refund tx {}", hex::encode(refund_tx.tx_hash_as_bytes().0)); + drop(_lock); +} + +#[ignore] +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_and_spend_maker_payment() { + let _lock = TEST_MUTEX.lock().await; + + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + + let lock_time = now_sec() - 1000; + let secret = [0; 32]; + let secret_hash = dhash160(&secret); + + let maker_uniq_data = [3; 32]; + let maker_key_pair = coin.derive_htlc_key_pair(maker_uniq_data.as_slice()); + let maker_pub = maker_key_pair.public(); + + let taker_uniq_data = [5; 32]; + let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); + let taker_pub = taker_key_pair.public(); + + let maker_payment_args = SendPaymentArgs { + time_lock_duration: 0, + time_lock: lock_time, + other_pubkey: taker_pub, + secret_hash: secret_hash.as_slice(), + amount: "0.01".parse().unwrap(), + swap_contract_address: &None, + swap_unique_data: maker_uniq_data.as_slice(), + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + + let tx = coin.send_maker_payment(maker_payment_args).await.unwrap(); + log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); + let spends_payment_args = SpendPaymentArgs { + other_payment_tx: &tx.tx_hex(), + time_lock: lock_time, + other_pubkey: maker_pub, + secret: &secret, + secret_hash: secret_hash.as_slice(), + swap_contract_address: &None, + swap_unique_data: taker_uniq_data.as_slice(), + watcher_reward: false, + }; + let spend_tx = coin.send_taker_spends_maker_payment(spends_payment_args).await.unwrap(); + log!("spend tx {}", hex::encode(spend_tx.tx_hash_as_bytes().0)); + drop(_lock); +} + +#[ignore] +#[tokio::test(flavor = "multi_thread")] +async fn prepare_zombie_sapling_cache() { + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + + assert!(coin.is_sapling_state_synced().await); +} + +#[ignore] +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_send_dex_fee() { + let _lock = TEST_MUTEX.lock().await; + + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + + let dex_fee = DexFee::Standard("0.01".into()); + let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); + log!("dex fee tx {}", tx.txid()); + drop(_lock); +} + +#[ignore] +#[tokio::test(flavor = "multi_thread")] +async fn zombie_coin_validate_dex_fee() { + let _lock = TEST_MUTEX.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + + // let balance = coin.my_balance().compat().await; + + let dex_fee = DexFee::Standard("0.01".into()); + let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); + log!("dex fee tx {}", tx.txid()); + let tx = tx.into(); + + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &[], + dex_fee: &DexFee::Standard(MmNumber::from("0.001")), + min_block_number: 4, + uuid: &[1; 16], + }; + // Invalid amount should return an error + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid amount")), + _ => panic!("Expected `WrongPaymentTx`: {:?}", err), + } + + // Invalid memo should return an error + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &[], + dex_fee: &DexFee::Standard(MmNumber::from("0.01")), + min_block_number: 10, + uuid: &[2; 16], + }; + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid memo")), + _ => panic!("Expected `WrongPaymentTx`: {:?}", err), + } + + // Confirmed before min block + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &[], + dex_fee: &DexFee::Standard(MmNumber::from("0.01")), + min_block_number: 20000, + uuid: &[1; 16], + }; + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min block")), + _ => panic!("Expected `WrongPaymentTx`: {:?}", err), + } + + // Success validation + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &[], + dex_fee: &DexFee::Standard(MmNumber::from("0.01")), + min_block_number: 20, + uuid: &[1; 16], + }; + coin.validate_fee(validate_fee_args).await.unwrap(); + drop(_lock); +} diff --git a/mm2src/mm2_main/tests/docker_tests_main.rs b/mm2src/mm2_main/tests/docker_tests_main.rs index 707f558631..e5fdaafa8b 100644 --- a/mm2src/mm2_main/tests/docker_tests_main.rs +++ b/mm2src/mm2_main/tests/docker_tests_main.rs @@ -6,6 +6,7 @@ #![feature(drain_filter)] #![feature(hash_raw_entry)] #![cfg(not(target_arch = "wasm32"))] +#![feature(local_key_cell_methods)] // for setting global vars in tests #[cfg(test)] #[macro_use] @@ -54,6 +55,7 @@ pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { const IMAGES: &[&str] = &[ UTXO_ASSET_DOCKER_IMAGE_WITH_TAG, QTUM_REGTEST_DOCKER_IMAGE_WITH_TAG, + ZOMBIE_ASSET_DOCKER_IMAGE_WITH_TAG, GETH_DOCKER_IMAGE_WITH_TAG, NUCLEUS_IMAGE, ATOM_IMAGE_WITH_TAG, @@ -75,12 +77,15 @@ pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { let qtum_node = qtum_docker_node(&docker, 9000); let for_slp_node = utxo_asset_docker_node(&docker, "FORSLP", 10000); let geth_node = geth_docker_node(&docker, "ETH", 8545); + let zombie_node = zombie_asset_docker_node(&docker, 7090); let utxo_ops = UtxoAssetDockerOps::from_ticker("MYCOIN"); let utxo_ops1 = UtxoAssetDockerOps::from_ticker("MYCOIN1"); let qtum_ops = QtumDockerOps::new(); let for_slp_ops = BchDockerOps::from_ticker("FORSLP"); + let zombie_ops = ZCoinAssetDockerOps::new(); + zombie_ops.wait_ready(4); qtum_ops.wait_ready(2); qtum_ops.initialize_contracts(); for_slp_ops.wait_ready(4); @@ -92,13 +97,14 @@ pub fn docker_tests_runner(tests: &[&TestDescAndFn]) { init_geth_node(); prepare_ibc_channels(ibc_relayer_node.container.id()); - thread::sleep(Duration::from_secs(10)); + thread::sleep(Duration::from_secs(12)); wait_until_relayer_container_is_ready(ibc_relayer_node.container.id()); containers.push(utxo_node); containers.push(utxo_node1); containers.push(qtum_node); containers.push(for_slp_node); + containers.push(zombie_node); containers.push(geth_node); containers.push(nucleus_node); containers.push(atom_node); diff --git a/mm2src/mm2_main/tests/docker_tests_sia_unique.rs b/mm2src/mm2_main/tests/docker_tests_sia_unique.rs index 521da60e01..a176277c64 100644 --- a/mm2src/mm2_main/tests/docker_tests_sia_unique.rs +++ b/mm2src/mm2_main/tests/docker_tests_sia_unique.rs @@ -7,6 +7,7 @@ #![feature(drain_filter)] #![feature(hash_raw_entry)] #![cfg(not(target_arch = "wasm32"))] +#![feature(local_key_cell_methods)] #[cfg(test)] #[macro_use] diff --git a/mm2src/mm2_main/tests/integration_tests_common/mod.rs b/mm2src/mm2_main/tests/integration_tests_common/mod.rs index 0c22f3c8bd..170915f681 100644 --- a/mm2src/mm2_main/tests/integration_tests_common/mod.rs +++ b/mm2src/mm2_main/tests/integration_tests_common/mod.rs @@ -2,7 +2,7 @@ use common::executor::Timer; use common::log::LogLevel; use common::{block_on, log, now_ms, wait_until_ms}; use crypto::privkey::key_pair_from_seed; -use mm2_main::{lp_main, LpMainParams}; +use mm2_main::{lp_main, lp_run, LpMainParams}; use mm2_rpc::data::legacy::CoinInitResponse; use mm2_test_helpers::electrums::{doc_electrums, marty_electrums}; use mm2_test_helpers::for_tests::{create_new_account_status, enable_native as enable_native_impl, @@ -26,7 +26,8 @@ pub fn test_mm_start_impl() { log!("test_mm_start] Starting the MarketMaker..."); let conf: Json = json::from_str(&conf).unwrap(); let params = LpMainParams::with_conf(conf).log_filter(Some(filter)); - block_on(lp_main(params, &|_ctx| (), "TEST".into(), "TEST".into())).unwrap() + let ctx = block_on(lp_main(params, &|_ctx| (), "TEST".into(), "TEST".into())).unwrap(); + block_on(lp_run(ctx)) } } } diff --git a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs index 4614bfdafb..cc1094e668 100644 --- a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs @@ -2,21 +2,23 @@ use common::custom_futures::repeatable::{Ready, Retry}; use common::{block_on, log, repeatable}; use http::StatusCode; use itertools::Itertools; -use mm2_test_helpers::for_tests::{disable_coin, electrum_servers_rpc, enable_bch_with_tokens, enable_slp, - my_tx_history_v2, sign_message, tbch_for_slp_conf, tbch_usdf_conf, verify_message, - MarketMakerIt, Mm2TestConf, UtxoRpcMode, T_BCH_ELECTRUMS}; +use mm2_test_helpers::for_tests::{electrum_servers_rpc, enable_bch_with_tokens, enable_slp, my_tx_history_v2, + sign_message, tbch_for_slp_conf, tbch_usdf_conf, verify_message, MarketMakerIt, + Mm2TestConf, UtxoRpcMode, T_BCH_ELECTRUMS}; use mm2_test_helpers::structs::{Bip44Chain, EnableBchWithTokensResponse, HDAccountAddressId, RpcV2Response, SignatureResponse, StandardHistoryV2Res, UtxoFeeDetails, VerificationResponse}; use serde_json::{self as json, json, Value as Json}; use std::env; -use std::thread; -use std::time::Duration; const BIP39_PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; #[test] -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(target_os = "macos")))] // https://github.com/KomodoPlatform/komodo-defi-framework/issues/1712#issuecomment-2669920708 fn test_withdraw_cashaddresses() { + use mm2_test_helpers::for_tests::disable_coin; + use std::thread; + use std::time::Duration; + let coins = json!([ {"coin":"BCH","pubtype":0,"p2shtype":5,"mm2":1,"fork_id": "0x40","protocol":{"type":"UTXO"}, "address_format":{"format":"cashaddress","network":"bchtest"}}, 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 c8d3252ad4..a2451ac362 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -20,7 +20,7 @@ use mm2_test_helpers::for_tests::{account_balance, btc_segwit_conf, btc_with_spv test_qrc20_history_impl, tqrc20_conf, verify_message, wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, - DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODE, ETH_MAINNET_SWAP_CONTRACT, ETH_SEPOLIA_NODES, + DOC_ELECTRUM_ADDRS, ETH_MAINNET_NODES, ETH_MAINNET_SWAP_CONTRACT, ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT, MARTY_ELECTRUM_ADDRS, MORTY, QRC20_ELECTRUMS, RICK, RICK_ELECTRUM_ADDRS, TBTC_ELECTRUMS, T_BCH_ELECTRUMS}; use mm2_test_helpers::get_passphrase; @@ -2792,15 +2792,16 @@ fn test_add_delegation_qtum() { ])); log!("{}", json.balance); + let rpc_endpoint = "experimental::staking::delegate"; let rc = block_on(mm.rpc(&json!({ "userpass": "pass", "mmrpc": "2.0", - "method": "add_delegation", + "method": rpc_endpoint, "params": { "coin": "tQTUM", "staking_details": { "type": "Qtum", - "address": "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE" + "validator_address": "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE" } }, "id": 0 @@ -2809,18 +2810,18 @@ fn test_add_delegation_qtum() { assert_eq!( rc.0, StatusCode::OK, - "RPC «add_delegation» failed with status «{}»", + "RPC «{rpc_endpoint}» failed with status «{}»", rc.0 ); let rc = block_on(mm.rpc(&json!({ "userpass": "pass", "mmrpc": "2.0", - "method": "add_delegation", + "method": rpc_endpoint, "params": { "coin": "tQTUM", "staking_details": { "type": "Qtum", - "address": "fake_address" + "validator_address": "fake_address" } }, "id": 0 @@ -2828,7 +2829,7 @@ fn test_add_delegation_qtum() { .unwrap(); assert!( rc.0.is_client_error(), - "!add_delegation success but should be error: {}", + "!{rpc_endpoint} success but should be error: {}", rc.1 ); } @@ -2870,17 +2871,14 @@ fn test_remove_delegation_qtum() { ) .unwrap(); - let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ - "electrum1.cipig.net:10071", - "electrum2.cipig.net:10071", - "electrum3.cipig.net:10071", - ])); + let json = block_on(enable_electrum_json(&mm, "tQTUM", false, tqtum_electrums())); log!("{}", json.balance); + let rpc_endpoint = "experimental::staking::undelegate"; let rc = block_on(mm.rpc(&json!({ "userpass": "pass", "mmrpc": "2.0", - "method": "remove_delegation", + "method": rpc_endpoint, "params": {"coin": "tQTUM"}, "id": 0 }))) @@ -2888,14 +2886,14 @@ fn test_remove_delegation_qtum() { assert_eq!( rc.0, StatusCode::OK, - "RPC «remove_delegation» failed with status «{}»", + "RPC «{rpc_endpoint}» failed with status «{}»", rc.0 ); } #[test] #[cfg(not(target_arch = "wasm32"))] -fn test_get_staking_infos_qtum() { +fn test_query_delegations_info_qtum() { let coins = json!([{ "coin": "tQTUM", "name": "qtumtest", @@ -2937,18 +2935,24 @@ fn test_get_staking_infos_qtum() { ])); log!("{}", json.balance); + let rpc_endpoint = "experimental::staking::query::delegations"; let rc = block_on(mm.rpc(&json!({ "userpass": "pass", "mmrpc": "2.0", - "method": "get_staking_infos", - "params": {"coin": "tQTUM"}, + "method": rpc_endpoint, + "params": { + "coin": "tQTUM", + "info_details": { + "type": "Qtum" + } + }, "id": 0 }))) .unwrap(); assert_eq!( rc.0, StatusCode::OK, - "RPC «get_staking_infos» failed with status «{}»", + "RPC «{rpc_endpoint}» failed with status «{}»", rc.0 ); } @@ -3579,7 +3583,7 @@ fn test_get_raw_transaction() { "userpass": mm.userpass, "method": "enable", "coin": "ETH", - "urls": &[ETH_MAINNET_NODE], + "urls": ETH_MAINNET_NODES, // Dev chain swap contract address "swap_contract_address": ETH_MAINNET_SWAP_CONTRACT, "mm2": 1, @@ -5853,6 +5857,76 @@ fn test_get_wallet_names() { block_on(mm_wallet_2.stop()).unwrap(); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_change_mnemonic_password_rpc() { + let coins = json!([]); + // Initialize wallet with current_password. + let old_password = "helloworld"; + let wallet_1 = Mm2TestConf::seednode_with_wallet_name(&coins, "wallet_1", old_password); + let mm = MarketMakerIt::start(wallet_1.conf, wallet_1.rpc_password, None).unwrap(); + + // Retrieve all wallet names(should succeed). + let get_wallet_names_1 = block_on(get_wallet_names(&mm)); + assert_eq!(get_wallet_names_1.wallet_names, vec!["wallet_1"]); + assert_eq!(get_wallet_names_1.activated_wallet.unwrap(), "wallet_1"); + + // STAGE 1: send update mnemonic password using new rpc(must succeed). + let new_password_stage_1 = "worldhello"; + let request = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "change_mnemonic_password", + "mmrpc": "2.0", + "params": { + "current_password": old_password, + "new_password": new_password_stage_1 + } + }))) + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'change_mnemonic_password' failed: {}", + request.1 + ); + + // STAGE 2: Try changing wallet password using old_password(Should fail!) + let request = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "change_mnemonic_password", + "mmrpc": "2.0", + "params": { + "current_password": old_password, + "new_password": "password2" + } + }))) + .unwrap(); + assert_eq!( + request.0, + StatusCode::INTERNAL_SERVER_ERROR, + "'change_mnemonic_password' failed: {}", + request.1 + ); + + // STAGE 3: try updating password again using new_password_stage_1 password(Should pass!) + let request = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "change_mnemonic_password", + "mmrpc": "2.0", + "params": { + "current_password": new_password_stage_1, + "new_password": "password3" + } + }))) + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'change_mnemonic_password' failed: {}", + request.1 + ); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_raw_transaction_rick() { @@ -6031,7 +6105,7 @@ mod trezor_tests { withdraw_status, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use mm2_test_helpers::structs::{InitTaskResult, RpcV2Response, TransactionDetails, WithdrawStatus}; - use rpc_task::{rpc_common::RpcTaskStatusRequest, RpcTaskStatus}; + use rpc_task::{rpc_common::RpcTaskStatusRequest, RpcInitReq, RpcTaskStatus}; use serde_json::{self as json, json, Value as Json}; use std::io::{stdin, stdout, BufRead, Write}; @@ -6048,7 +6122,7 @@ mod trezor_tests { let ctx = mm_ctx_with_custom_db_with_conf(Some(conf)); CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123456").unwrap(); // for now we need passphrase seed for init - let req: InitHwRequest = serde_json::from_value(json!({ "device_pubkey": null })).unwrap(); + let req: RpcInitReq = serde_json::from_value(json!({ "device_pubkey": null })).unwrap(); let res = match init_trezor(ctx.clone(), req).await { Ok(res) => res, _ => { @@ -6360,6 +6434,8 @@ mod trezor_tests { "ticker": ticker_coin, "rpc_mode": "Default", "nodes": [ + {"url": "https://sepolia.drpc.org"}, + {"url": "https://ethereum-sepolia-rpc.publicnode.com"}, {"url": "https://rpc2.sepolia.org"}, {"url": "https://rpc.sepolia.org/"} ], diff --git a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs index 004ee27cac..d99290c3dc 100644 --- a/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/z_coin_tests.rs @@ -17,8 +17,8 @@ use std::str::FromStr; use std::thread; use std::time::Duration; -const ZOMBIE_TEST_BIP39_ACTIVATION_SEED: &str = "course flock lucky cereal hamster novel team never metal bean behind cute cruel matrix symptom fault harsh fashion impact prison glove then tree chef"; -const ZOMBIE_TEST_BALANCE_SEED: &str = "zombie test seed"; +const ARRR_TEST_BIP39_ACTIVATION_SEED: &str = "course flock lucky cereal hamster novel team never metal bean behind cute cruel matrix symptom fault harsh fashion impact prison glove then tree chef"; +const ARRR_TEST_BALANCE_SEED: &str = "zombie test seed"; const ARRR_TEST_ACTIVATION_SEED: &str = "arrr test activation seed"; const ZOMBIE_TEST_HISTORY_SEED: &str = "zombie test history seed"; const ZOMBIE_TEST_WITHDRAW_SEED: &str = "zombie withdraw test seed"; @@ -48,16 +48,16 @@ async fn withdraw(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) -> Tra #[test] fn activate_z_coin_light() { - let coins = json!([zombie_conf()]); + let coins = json!([pirate_conf()]); - let conf = Mm2TestConf::seednode(ZOMBIE_TEST_BALANCE_SEED, &coins); + let conf = Mm2TestConf::seednode(ARRR_TEST_BALANCE_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_result = block_on(enable_z_coin_light( &mm, - ZOMBIE_TICKER, - ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, + ARRR, + PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, None, None, )); @@ -71,16 +71,16 @@ fn activate_z_coin_light() { #[test] fn activate_z_coin_light_with_changing_height() { - let coins = json!([zombie_conf()]); + let coins = json!([pirate_conf()]); - let conf = Mm2TestConf::seednode_with_hd_account(ZOMBIE_TEST_BIP39_ACTIVATION_SEED, &coins); + let conf = Mm2TestConf::seednode_with_hd_account(ARRR_TEST_BIP39_ACTIVATION_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_result = block_on(enable_z_coin_light( &mm, - ZOMBIE_TICKER, - ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, + ARRR, + PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, None, None, )); @@ -93,7 +93,7 @@ fn activate_z_coin_light_with_changing_height() { assert_eq!(balance.balance.spendable, BigDecimal::default()); // disable coin - block_on(disable_coin(&mm, ZOMBIE_TICKER, true)); + block_on(disable_coin(&mm, ARRR, true)); // Perform activation with changed height // Calculate timestamp for 2 days ago @@ -106,9 +106,9 @@ fn activate_z_coin_light_with_changing_height() { let activation_result = block_on(enable_z_coin_light( &mm, - ZOMBIE_TICKER, - ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, + ARRR, + PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, None, Some(two_days_ago), )); @@ -132,17 +132,17 @@ fn activate_z_coin_light_with_changing_height() { #[test] fn activate_z_coin_with_hd_account() { - let coins = json!([zombie_conf()]); + let coins = json!([pirate_conf()]); let hd_account_id = 0; - let conf = Mm2TestConf::seednode_with_hd_account(ZOMBIE_TEST_BIP39_ACTIVATION_SEED, &coins); + let conf = Mm2TestConf::seednode_with_hd_account(ARRR_TEST_BIP39_ACTIVATION_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_result = block_on(enable_z_coin_light( &mm, - ZOMBIE_TICKER, - ZOMBIE_ELECTRUMS, - ZOMBIE_LIGHTWALLETD_URLS, + ARRR, + PIRATE_ELECTRUMS, + PIRATE_LIGHTWALLETD_URLS, Some(hd_account_id), None, )); diff --git a/mm2src/mm2_net/src/event_streaming/mod.rs b/mm2src/mm2_net/src/event_streaming/mod.rs new file mode 100644 index 0000000000..001424f5f4 --- /dev/null +++ b/mm2src/mm2_net/src/event_streaming/mod.rs @@ -0,0 +1,2 @@ +#[cfg(not(target_arch = "wasm32"))] pub mod sse_handler; +#[cfg(target_arch = "wasm32")] pub mod wasm_event_stream; diff --git a/mm2src/mm2_net/src/event_streaming/sse_handler.rs b/mm2src/mm2_net/src/event_streaming/sse_handler.rs new file mode 100644 index 0000000000..e6d061004e --- /dev/null +++ b/mm2src/mm2_net/src/event_streaming/sse_handler.rs @@ -0,0 +1,70 @@ +use http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_TYPE}; +use hyper::{body::Bytes, Body, Request, Response}; +use mm2_core::mm_ctx::MmArc; +use serde_json::json; + +pub const SSE_ENDPOINT: &str = "/event-stream"; + +/// Handles broadcasted messages from `mm2_event_stream` continuously. +pub async fn handle_sse(request: Request, ctx_h: u32) -> Response { + let ctx = match MmArc::from_ffi_handle(ctx_h) { + Ok(ctx) => ctx, + Err(err) => return handle_internal_error(err).await, + }; + + let Some(event_streaming_config) = ctx.event_streaming_configuration() else { + return handle_internal_error("Event streaming is disabled".to_string()).await; + }; + + let client_id = match request.uri().query().and_then(|query| { + query + .split('&') + .find(|param| param.starts_with("id=")) + .map(|id_param| id_param.trim_start_matches("id=").parse::()) + }) { + Some(Ok(id)) => id, + // Default to zero when client ID isn't passed, most of the cases we will have a single user/client. + _ => 0, + }; + + let event_stream_manager = ctx.event_stream_manager.clone(); + let Ok(mut rx) = event_stream_manager.new_client(client_id) else { + return handle_internal_error("ID already in use".to_string()).await; + }; + let body = Body::wrap_stream(async_stream::stream! { + while let Some(event) = rx.recv().await { + // The event's filter will decide whether to expose the event data to this client or not. + // This happens based on the events that this client has subscribed to. + let (event_type, message) = event.get(); + let data = json!({ + "_type": event_type, + "message": message, + }); + + yield Ok::<_, hyper::Error>(Bytes::from(format!("data: {data} \n\n"))); + } + }); + + let response = Response::builder() + .status(200) + .header(CONTENT_TYPE, "text/event-stream") + .header(CACHE_CONTROL, "no-cache") + .header( + ACCESS_CONTROL_ALLOW_ORIGIN, + event_streaming_config.access_control_allow_origin, + ) + .body(body); + + match response { + Ok(res) => res, + Err(err) => handle_internal_error(err.to_string()).await, + } +} + +/// Fallback function for handling errors in SSE connections +async fn handle_internal_error(message: String) -> Response { + Response::builder() + .status(500) + .body(Body::from(message)) + .expect("Returning 500 should never fail.") +} diff --git a/mm2src/mm2_net/src/wasm_event_stream.rs b/mm2src/mm2_net/src/event_streaming/wasm_event_stream.rs similarity index 67% rename from mm2src/mm2_net/src/wasm_event_stream.rs rename to mm2src/mm2_net/src/event_streaming/wasm_event_stream.rs index dcd6da33e2..c10f838a70 100644 --- a/mm2src/mm2_net/src/wasm_event_stream.rs +++ b/mm2src/mm2_net/src/event_streaming/wasm_event_stream.rs @@ -11,21 +11,9 @@ struct SendableMessagePort(web_sys::MessagePort); unsafe impl Send for SendableMessagePort {} /// Handles broadcasted messages from `mm2_event_stream` continuously for WASM. -pub async fn handle_worker_stream(ctx: MmArc) { - let config = ctx - .event_stream_configuration - .as_ref() - .expect("Event stream configuration couldn't be found. This should never happen."); - - let mut channel_controller = ctx.stream_channel_controller.clone(); - let mut rx = channel_controller.create_channel(config.total_active_events()); - - let worker_path = config - .worker_path - .to_str() - .expect("worker_path contains invalid UTF-8 characters"); +pub async fn handle_worker_stream(ctx: MmArc, worker_path: String) { let worker = SendableSharedWorker( - SharedWorker::new(worker_path).unwrap_or_else(|_| { + SharedWorker::new(&worker_path).unwrap_or_else(|_| { panic!( "Failed to create a new SharedWorker with path '{}'.\n\ This could be due to the file missing or the browser being incompatible.\n\ @@ -38,13 +26,18 @@ pub async fn handle_worker_stream(ctx: MmArc) { let port = SendableMessagePort(worker.0.port()); port.0.start(); + let event_stream_manager = ctx.event_stream_manager.clone(); + let mut rx = event_stream_manager + .new_client(0) + .expect("A different wasm client is already listening. Only one client is allowed at a time."); + while let Some(event) = rx.recv().await { + let (event_type, message) = event.get(); let data = json!({ - "_type": event.event_type(), - "message": event.message(), + "_type": event_type, + "message": message, }); let message_js = wasm_bindgen::JsValue::from_str(&data.to_string()); - port.0.post_message(&message_js) .expect("Failed to post a message to the SharedWorker.\n\ This could be due to the browser being incompatible.\n\ diff --git a/mm2src/mm2_net/src/ip_addr.rs b/mm2src/mm2_net/src/ip_addr.rs index 5b46f54891..4c4c77f448 100644 --- a/mm2src/mm2_net/src/ip_addr.rs +++ b/mm2src/mm2_net/src/ip_addr.rs @@ -1,5 +1,6 @@ use crate::transport::slurp_url; use common::log; +use derive_more::Display; use gstuff::try_s; use gstuff::{ERR, ERRL}; use mm2_core::mm_ctx::MmArc; @@ -10,6 +11,9 @@ use std::io::Read; use std::net::{IpAddr, Ipv4Addr}; use std::path::Path; +use mm2_err_handle::prelude::MmError; +use std::net::ToSocketAddrs; + const IP_PROVIDERS: [&str; 2] = ["http://checkip.amazonaws.com/", "http://api.ipify.org"]; /// Tries to serve on the given IP to check if it's available. @@ -27,7 +31,6 @@ const IP_PROVIDERS: [&str; 2] = ["http://checkip.amazonaws.com/", "http://api.ip /// Dropping or using that Sender will stop the HTTP fallback server. /// /// Also the port of the HTTP fallback server is returned. -#[cfg(not(target_arch = "wasm32"))] fn test_ip(ctx: &MmArc, ip: IpAddr) -> Result<(), String> { let netid = ctx.netid(); @@ -66,7 +69,6 @@ fn simple_ip_extractor(ip: &str) -> Result { } /// Detect the outer IP address, visible to the internet. -#[cfg(not(target_arch = "wasm32"))] pub async fn fetch_external_ip() -> Result { for url in IP_PROVIDERS.iter() { log::info!("Trying to fetch the real IP from '{}' ...", url); @@ -105,7 +107,6 @@ pub async fn fetch_external_ip() -> Result { /// Later we'll try to *bind* on this IP address, /// and this will break under NAT or forwarding because the internal IP address will be different. /// Which might be a good thing, allowing us to detect the likehoodness of NAT early. -#[cfg(not(target_arch = "wasm32"))] async fn detect_myipaddr(ctx: MmArc) -> Result { let ip = try_s!(fetch_external_ip().await); @@ -148,7 +149,6 @@ async fn detect_myipaddr(ctx: MmArc) -> Result { Ok(all_interfaces) } -#[cfg(not(target_arch = "wasm32"))] pub async fn myipaddr(ctx: MmArc) -> Result { let myipaddr: IpAddr = if Path::new("myipaddr").exists() { match fs::File::open("myipaddr") { @@ -169,3 +169,48 @@ pub async fn myipaddr(ctx: MmArc) -> Result { }; Ok(myipaddr) } + +#[derive(Debug, Display)] +pub enum ParseAddressError { + #[display(fmt = "Address/Seed {} resolved to IPv6 which is not supported", _0)] + UnsupportedIPv6Address(String), + #[display(fmt = "Address/Seed {} to_socket_addrs empty iter", _0)] + EmptyIterator(String), + #[display(fmt = "Couldn't resolve '{}' Address/Seed: {}", _0, _1)] + UnresolvedAddress(String, String), +} + +pub fn addr_to_ipv4_string(address: &str) -> Result> { + // Remove "https:// or http://" etc.. from address str + let formated_address = address.split("://").last().unwrap_or(address); + + let address_with_port = format!( + "{formated_address}{}", + if formated_address.contains(':') { "" } else { ":0" } + ); + + match address_with_port.as_str().to_socket_addrs() { + Ok(mut iter) => match iter.next() { + Some(addr) => { + if addr.is_ipv4() { + Ok(addr.ip().to_string()) + } else { + log::warn!( + "Address/Seed {} resolved to IPv6 {} which is not supported", + address, + addr + ); + MmError::err(ParseAddressError::UnsupportedIPv6Address(address.into())) + } + }, + None => { + log::warn!("Address/Seed {} to_socket_addrs empty iter", address); + MmError::err(ParseAddressError::EmptyIterator(address.into())) + }, + }, + Err(e) => { + log::error!("Couldn't resolve '{}' seed: {}", address, e); + MmError::err(ParseAddressError::UnresolvedAddress(address.into(), e.to_string())) + }, + } +} diff --git a/mm2src/mm2_net/src/lib.rs b/mm2src/mm2_net/src/lib.rs index 4ae26ca182..68ed4284f3 100644 --- a/mm2src/mm2_net/src/lib.rs +++ b/mm2src/mm2_net/src/lib.rs @@ -1,9 +1,9 @@ +pub mod event_streaming; pub mod grpc_web; +pub mod ip_addr; 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(not(target_arch = "wasm32"))] pub mod sse_handler; + #[cfg(target_arch = "wasm32")] pub mod wasm; -#[cfg(target_arch = "wasm32")] pub mod wasm_event_stream; diff --git a/mm2src/mm2_net/src/native_http.rs b/mm2src/mm2_net/src/native_http.rs index 94f37ef65c..6837c04655 100644 --- a/mm2src/mm2_net/src/native_http.rs +++ b/mm2src/mm2_net/src/native_http.rs @@ -262,7 +262,7 @@ mod tests { #[test] fn test_slurp_req() { - let (status, headers, body) = block_on(slurp_url("https://httpbin.org/get")).unwrap(); + let (status, headers, body) = block_on(slurp_url("https://postman-echo.com/get")).unwrap(); assert!(status.is_success(), "{:?} {:?} {:?}", status, headers, body); } } diff --git a/mm2src/mm2_net/src/sse_handler.rs b/mm2src/mm2_net/src/sse_handler.rs deleted file mode 100644 index 568bfc98c0..0000000000 --- a/mm2src/mm2_net/src/sse_handler.rs +++ /dev/null @@ -1,75 +0,0 @@ -use hyper::{body::Bytes, Body, Request, Response}; -use mm2_core::mm_ctx::MmArc; -use serde_json::json; - -pub const SSE_ENDPOINT: &str = "/event-stream"; - -/// Handles broadcasted messages from `mm2_event_stream` continuously. -pub async fn handle_sse(request: Request, ctx_h: u32) -> Response { - // This is only called once for per client on the initialization, - // meaning this is not a resource intensive computation. - let ctx = match MmArc::from_ffi_handle(ctx_h) { - Ok(ctx) => ctx, - Err(err) => return handle_internal_error(err).await, - }; - - let config = match &ctx.event_stream_configuration { - Some(config) => config, - None => { - return handle_internal_error( - "Event stream configuration couldn't be found. This should never happen.".to_string(), - ) - .await - }, - }; - - let filtered_events = request - .uri() - .query() - .and_then(|query| { - query - .split('&') - .find(|param| param.starts_with("filter=")) - .map(|param| param.trim_start_matches("filter=")) - }) - .map_or(Vec::new(), |events_param| { - events_param.split(',').map(|event| event.to_string()).collect() - }); - - let mut channel_controller = ctx.stream_channel_controller.clone(); - let mut rx = channel_controller.create_channel(config.total_active_events()); - let body = Body::wrap_stream(async_stream::stream! { - while let Some(event) = rx.recv().await { - // If there are no filtered events, that means we want to - // stream out all the events. - if filtered_events.is_empty() || filtered_events.contains(&event.event_type().to_owned()) { - let data = json!({ - "_type": event.event_type(), - "message": event.message(), - }); - - yield Ok::<_, hyper::Error>(Bytes::from(format!("data: {data} \n\n"))); - } - } - }); - - let response = Response::builder() - .status(200) - .header("Content-Type", "text/event-stream") - .header("Cache-Control", "no-cache") - .header("Access-Control-Allow-Origin", &config.access_control_allow_origin) - .body(body); - - match response { - Ok(res) => res, - Err(err) => handle_internal_error(err.to_string()).await, - } -} - -/// Fallback function for handling errors in SSE connections -async fn handle_internal_error(message: String) -> Response { - Response::builder() - .status(500) - .body(Body::from(message)) - .expect("Returning 500 should never fail.") -} diff --git a/mm2src/mm2_number/src/mm_number.rs b/mm2src/mm2_number/src/mm_number.rs index 6e3c5896a6..3f731e6436 100644 --- a/mm2src/mm2_number/src/mm_number.rs +++ b/mm2src/mm2_number/src/mm_number.rs @@ -103,6 +103,10 @@ impl From for MmNumber { fn from(n: BigDecimal) -> MmNumber { from_dec_to_ratio(&n).into() } } +impl From<&BigDecimal> for MmNumber { + fn from(n: &BigDecimal) -> MmNumber { from_dec_to_ratio(n).into() } +} + impl From for MmNumber { fn from(r: BigRational) -> MmNumber { MmNumber(r) } } diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 80465a254d..6cc38db4d6 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [features] -default = [] +default = ["application"] application = ["dep:mm2_number"] [lib] @@ -13,6 +13,7 @@ doctest = false [dependencies] async-trait = "0.1" common = { path = "../common" } +compatible-time = { version = "1.1.0", package = "web-time" } derive_more = "0.99" futures = { version = "0.3.1", default-features = false } futures-ticker = "0.0.3" @@ -21,6 +22,7 @@ lazy_static = "1.4" log = "0.4" mm2_core = { path = "../mm2_core" } mm2_event_stream = { path = "../mm2_event_stream" } +mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number", optional = true } parking_lot = { version = "0.12.0", features = ["nightly"] } rand = { version = "0.7", default-features = false, features = ["wasm-bindgen"] } @@ -37,15 +39,16 @@ void = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] futures-rustls = "0.24" -instant = "0.1.12" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } +timed-map = { version = "1.3", features = ["rustc-hash"] } tokio = { version = "1.20", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] futures-rustls = "0.22" -instant = { version = "0.1.12", features = ["wasm-bindgen"] } -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } +timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } [dev-dependencies] async-std = "1.6.2" env_logger = "0.9.3" +common = { path = "../common", features = ["for-tests"] } diff --git a/mm2src/mm2_p2p/src/application/network_event.rs b/mm2src/mm2_p2p/src/application/network_event.rs index c3c0a0eb5c..fa152469d1 100644 --- a/mm2src/mm2_p2p/src/application/network_event.rs +++ b/mm2src/mm2_p2p/src/application/network_event.rs @@ -1,30 +1,55 @@ -use async_trait::async_trait; -use common::{executor::{SpawnFuture, Timer}, - log::info}; -use futures::channel::oneshot::{self, Receiver, Sender}; - +use common::executor::Timer; use mm2_core::mm_ctx::MmArc; -pub use mm2_event_stream::behaviour::EventBehaviour; -use mm2_event_stream::{behaviour::EventInitStatus, Event, EventName, EventStreamConfiguration}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; + +use async_trait::async_trait; +use futures::channel::oneshot; +use serde::Deserialize; use serde_json::json; +#[derive(Deserialize)] +#[serde(deny_unknown_fields, default)] +pub struct NetworkEventConfig { + /// The time in seconds to wait after sending network info before sending another one. + pub stream_interval_seconds: f64, + /// Always (force) send network info data, even if it's the same as the previous one sent. + pub always_send: bool, +} + +impl Default for NetworkEventConfig { + fn default() -> Self { + Self { + stream_interval_seconds: 5.0, + always_send: false, + } + } +} + pub struct NetworkEvent { + config: NetworkEventConfig, ctx: MmArc, } impl NetworkEvent { - pub fn new(ctx: MmArc) -> Self { Self { ctx } } + pub fn new(config: NetworkEventConfig, ctx: MmArc) -> Self { Self { config, ctx } } } #[async_trait] -impl EventBehaviour for NetworkEvent { - fn event_name() -> EventName { EventName::NETWORK } +impl EventStreamer for NetworkEvent { + type DataInType = NoDataIn; - async fn handle(self, interval: f64, tx: oneshot::Sender) { + fn streamer_id(&self) -> String { "NETWORK".to_string() } + + async fn handle( + self, + broadcaster: Broadcaster, + ready_tx: oneshot::Sender>, + _: impl StreamHandlerInput, + ) { let p2p_ctx = crate::p2p_ctx::P2PContext::fetch_from_mm_arc(&self.ctx); let mut previously_sent = json!({}); - tx.send(EventInitStatus::Success).unwrap(); + ready_tx.send(Ok(())).unwrap(); loop { let p2p_cmd_tx = p2p_ctx.cmd_tx.lock().clone(); @@ -43,34 +68,13 @@ impl EventBehaviour for NetworkEvent { "relay_mesh": relay_mesh, }); - if previously_sent != event_data { - self.ctx - .stream_channel_controller - .broadcast(Event::new(Self::event_name().to_string(), event_data.to_string())) - .await; + if previously_sent != event_data || self.config.always_send { + broadcaster.broadcast(Event::new(self.streamer_id(), event_data.clone())); previously_sent = event_data; } - Timer::sleep(interval).await; - } - } - - async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(&Self::event_name()) { - info!( - "NETWORK event is activated with {} seconds interval.", - event.stream_interval_seconds - ); - - let (tx, rx): (Sender, Receiver) = oneshot::channel(); - self.ctx.spawner().spawn(self.handle(event.stream_interval_seconds, tx)); - - rx.await.unwrap_or_else(|e| { - EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) - }) - } else { - EventInitStatus::Inactive + Timer::sleep(self.config.stream_interval_seconds).await; } } } diff --git a/mm2src/mm2_p2p/src/application/request_response/network_info.rs b/mm2src/mm2_p2p/src/application/request_response/network_info.rs index c8dece2ef5..4d610d932c 100644 --- a/mm2src/mm2_p2p/src/application/request_response/network_info.rs +++ b/mm2src/mm2_p2p/src/application/request_response/network_info.rs @@ -6,4 +6,6 @@ use serde::{Deserialize, Serialize}; pub enum NetworkInfoRequest { /// Get MM2 version of nodes added to stats collection GetMm2Version, + /// Get UTC timestamp in seconds from the target peer + GetPeerUtcTimestamp, } diff --git a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs index 9d58da4e1e..ad649328b4 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -1,4 +1,5 @@ use common::executor::SpawnFuture; +use compatible_time::Duration; use derive_more::Display; use futures::channel::mpsc::{channel, Receiver, Sender}; use futures::{channel::oneshot, @@ -6,7 +7,7 @@ use futures::{channel::oneshot, Future, FutureExt, SinkExt, StreamExt}; use futures_rustls::rustls; use futures_ticker::Ticker; -use instant::Duration; +use lazy_static::lazy_static; use libp2p::core::transport::Boxed as BoxedTransport; use libp2p::core::{ConnectedPoint, Endpoint}; use libp2p::floodsub::{Floodsub, FloodsubEvent, Topic as FloodsubTopic}; @@ -23,16 +24,20 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::iter; use std::net::IpAddr; +use std::sync::{Mutex, MutexGuard}; use std::task::{Context, Poll}; +use timed_map::{MapKind, TimedMap}; use super::peers_exchange::{PeerAddresses, PeersExchange, PeersExchangeRequest, PeersExchangeResponse}; use super::ping::AdexPing; use super::request_response::{build_request_response_behaviour, PeerRequest, PeerResponse, RequestResponseBehaviour, RequestResponseSender}; +use crate::application::request_response::network_info::NetworkInfoRequest; +use crate::application::request_response::P2PRequest; use crate::network::{get_all_network_seednodes, DEFAULT_NETID}; use crate::relay_address::{RelayAddress, RelayAddressError}; use crate::swarm_runtime::SwarmRuntime; -use crate::{NetworkInfo, NetworkPorts, RequestResponseBehaviourEvent}; +use crate::{decode_message, encode_message, NetworkInfo, NetworkPorts, RequestResponseBehaviourEvent}; pub use libp2p::gossipsub::{Behaviour as Gossipsub, IdentTopic, MessageAuthenticity, MessageId, Topic, TopicHash}; pub use libp2p::gossipsub::{ConfigBuilder as GossipsubConfigBuilder, Event as GossipsubEvent, @@ -50,6 +55,21 @@ const ANNOUNCE_INTERVAL: Duration = Duration::from_secs(600); const ANNOUNCE_INITIAL_DELAY: Duration = Duration::from_secs(60); const CHANNEL_BUF_SIZE: usize = 1024 * 8; +/// Used in time validation logic for each peer which runs immediately after the +/// `ConnectionEstablished` event. +/// +/// Be careful when updating this value, we have some defaults (like for swaps) +/// depending on this. +pub const MAX_TIME_GAP_FOR_CONNECTED_PEER: u64 = 20; + +/// Used for storing peers in [`RECENTLY_DIALED_PEERS`]. +const DIAL_RETRY_DELAY: Duration = Duration::from_secs(60 * 5); + +lazy_static! { + /// Tracks recently dialed peers to avoid repeated connection attempts. + static ref RECENTLY_DIALED_PEERS: Mutex> = Mutex::new(TimedMap::new_with_map_kind(MapKind::FxHashMap)); +} + pub const DEPRECATED_NETID_LIST: &[u16] = &[ 7777, // TODO: keep it inaccessible until Q2 of 2024. ]; @@ -162,6 +182,21 @@ pub enum AdexBehaviourCmd { }, } +/// Determines if a dial attempt to the remote should be made. +/// +/// Returns `false` if a dial attempt to the given address has already been made, +/// in which case the caller must skip the dial attempt. +fn check_and_mark_dialed(recently_dialed_peers: &mut MutexGuard>, addr: &Multiaddr) -> bool { + if recently_dialed_peers.get(addr).is_some() { + info!("Connection attempt was already made recently to '{addr}'."); + return false; + } + + recently_dialed_peers.insert_expirable(addr.clone(), (), DIAL_RETRY_DELAY); + + true +} + /// Returns info about directly connected peers. pub async fn get_directly_connected_peers(mut cmd_tx: AdexCmdTx) -> HashMap> { let (result_tx, rx) = oneshot::channel(); @@ -199,6 +234,44 @@ pub async fn get_relay_mesh(mut cmd_tx: AdexCmdTx) -> Vec { rx.await.expect("Tx should be present") } +async fn validate_peer_time(peer: PeerId, mut response_tx: Sender, rp_sender: RequestResponseSender) { + let request = P2PRequest::NetworkInfo(NetworkInfoRequest::GetPeerUtcTimestamp); + let encoded_request = encode_message(&request) + .expect("Static type `PeerInfoRequest::GetPeerUtcTimestamp` should never fail in serialization."); + + match request_one_peer(peer, encoded_request, rp_sender).await { + PeerResponse::Ok { res } => { + if let Ok(timestamp) = decode_message::(&res) { + let now = common::get_utc_timestamp(); + let now: u64 = now + .try_into() + .unwrap_or_else(|_| panic!("`common::get_utc_timestamp` returned invalid data: {}", now)); + + let diff = now.abs_diff(timestamp); + + // If time diff is in the acceptable gap, end the validation here. + if diff <= MAX_TIME_GAP_FOR_CONNECTED_PEER { + debug!( + "Peer '{peer}' is within the acceptable time gap ({MAX_TIME_GAP_FOR_CONNECTED_PEER} seconds); time difference is {diff} seconds." + ); + return; + } + }; + }, + other => { + error!("Unexpected response `{other:?}` from peer `{peer}`"); + // TODO: Ideally, we should send `peer` to end the connection, + // but we don't want to cause a breaking change yet. + return; + }, + } + + // If the function reaches this point, this means validation has failed. + // Send the peer ID to disconnect from it. + error!("Failed to validate the time for peer `{peer}`; disconnecting."); + response_tx.send(peer).await.unwrap(); +} + async fn request_one_peer(peer: PeerId, req: Vec, mut request_response_tx: RequestResponseSender) -> PeerResponse { // Use the internal receiver to receive a response to this request. let (internal_response_tx, internal_response_rx) = oneshot::channel(); @@ -711,12 +784,18 @@ fn start_gossipsub( _ => (), } + let mut recently_dialed_peers = RECENTLY_DIALED_PEERS.lock().unwrap(); for relay in bootstrap.choose_multiple(&mut rng, mesh_n) { + if !check_and_mark_dialed(&mut recently_dialed_peers, relay) { + continue; + } + match libp2p::Swarm::dial(&mut swarm, relay.clone()) { Ok(_) => info!("Dialed {}", relay), Err(e) => error!("Dial {:?} failed: {:?}", relay, e), } } + drop(recently_dialed_peers); let mut check_connected_relays_interval = Ticker::new_with_next(CONNECTED_RELAYS_CHECK_INTERVAL, CONNECTED_RELAYS_CHECK_INTERVAL); @@ -724,6 +803,7 @@ fn start_gossipsub( let mut announce_interval = Ticker::new_with_next(ANNOUNCE_INTERVAL, ANNOUNCE_INITIAL_DELAY); let mut listening = false; + let (timestamp_tx, mut timestamp_rx) = futures::channel::mpsc::channel(mesh_n_high); let polling_fut = poll_fn(move |cx: &mut Context| { loop { match swarm.behaviour_mut().cmd_rx.poll_next_unpin(cx) { @@ -733,11 +813,27 @@ fn start_gossipsub( } } + while let Poll::Ready(Some(peer_id)) = timestamp_rx.poll_next_unpin(cx) { + if swarm.disconnect_peer_id(peer_id).is_err() { + error!("Disconnection from `{peer_id}` failed unexpectedly, which should never happen."); + } + } + loop { match swarm.poll_next_unpin(cx) { Poll::Ready(Some(event)) => { debug!("Swarm event {:?}", event); + if let SwarmEvent::ConnectionEstablished { peer_id, .. } = &event { + info!("Validating time data for peer `{peer_id}`."); + let future = validate_peer_time( + *peer_id, + timestamp_tx.clone(), + swarm.behaviour().core.request_response.sender(), + ); + swarm.behaviour().spawn(future); + } + if let SwarmEvent::Behaviour(event) = event { if swarm.behaviour_mut().netid != DEFAULT_NETID { if let AdexBehaviourEvent::Floodsub(FloodsubEvent::Message(message)) = &event { @@ -798,19 +894,29 @@ fn maintain_connection_to_relays(swarm: &mut AtomicDexSwarm, bootstrap_addresses let mut rng = rand::thread_rng(); if connected_relays.len() < mesh_n_low { + let mut recently_dialed_peers = RECENTLY_DIALED_PEERS.lock().unwrap(); let to_connect_num = mesh_n - connected_relays.len(); - let to_connect = swarm - .behaviour_mut() - .core - .peers_exchange - .get_random_peers(to_connect_num, |peer| !connected_relays.contains(peer)); + let to_connect = + swarm + .behaviour_mut() + .core + .peers_exchange + .get_random_peers(to_connect_num, |peer, addresses| { + !connected_relays.contains(peer) + && addresses + .iter() + .any(|addr| check_and_mark_dialed(&mut recently_dialed_peers, addr)) + }); // choose some random bootstrap addresses to connect if peers exchange returned not enough peers if to_connect.len() < to_connect_num { let connect_bootstrap_num = to_connect_num - to_connect.len(); for addr in bootstrap_addresses .iter() - .filter(|addr| !swarm.behaviour().core.gossipsub.is_connected_to_addr(addr)) + .filter(|addr| { + !swarm.behaviour().core.gossipsub.is_connected_to_addr(addr) + && check_and_mark_dialed(&mut recently_dialed_peers, addr) + }) .collect::>() .choose_multiple(&mut rng, connect_bootstrap_num) { @@ -824,11 +930,13 @@ fn maintain_connection_to_relays(swarm: &mut AtomicDexSwarm, bootstrap_addresses if swarm.behaviour().core.gossipsub.is_connected_to_addr(&addr) { continue; } + if let Err(e) = libp2p::Swarm::dial(swarm, addr.clone()) { error!("Peer {} address {} dial error {}", peer, addr, e); } } } + drop(recently_dialed_peers); } if connected_relays.len() > max_n { diff --git a/mm2src/mm2_p2p/src/behaviours/mod.rs b/mm2src/mm2_p2p/src/behaviours/mod.rs index cdfda38c8d..8d8b4442b1 100644 --- a/mm2src/mm2_p2p/src/behaviours/mod.rs +++ b/mm2src/mm2_p2p/src/behaviours/mod.rs @@ -16,7 +16,7 @@ mod tests { use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; - #[cfg(not(windows))] use std::sync::Mutex; + #[cfg(target_os = "linux")] use std::sync::Mutex; use std::time::Duration; use crate::behaviours::peers_exchange::{PeerIdSerde, PeersExchange}; @@ -106,17 +106,16 @@ mod tests { let node1_port = next_port(); let node1 = Node::spawn(node1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; request_received_cpy.store(true, Ordering::Relaxed); - assert_eq!(request, b"test request"); let res = AdexResponse::Ok { response: b"test response".to_vec(), @@ -146,7 +145,7 @@ mod tests { } #[tokio::test] - #[cfg(not(windows))] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 + #[cfg(target_os = "linux")] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 async fn test_request_response_ok_three_peers() { let _ = env_logger::try_init(); @@ -157,19 +156,17 @@ mod tests { impl RequestHandler { fn handle(&mut self, mut cmd_tx: mpsc::Sender, event: AdexBehaviourEvent) { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; self.requests += 1; - assert_eq!(request, b"test request"); - // the first time we should respond the none if self.requests == 1 { let res = AdexResponse::None; @@ -249,17 +246,16 @@ mod tests { let node1_port = next_port(); let _node1 = Node::spawn(node1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; request_received_cpy.store(true, Ordering::Relaxed); - assert_eq!(request, b"test request"); let res = AdexResponse::None; cmd_tx @@ -293,17 +289,15 @@ mod tests { let receiver1_port = next_port(); let receiver1 = Node::spawn(receiver1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::None; cmd_tx .try_send(AdexBehaviourCmd::SendResponse { res, response_channel }) @@ -313,17 +307,15 @@ mod tests { let receiver2_port = next_port(); let receiver2 = Node::spawn(receiver2_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::Err { error: "test error".into(), }; @@ -335,17 +327,15 @@ mod tests { let receiver3_port = next_port(); let receiver3 = Node::spawn(receiver3_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::Ok { response: b"test response".to_vec(), }; diff --git a/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs b/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs index 412fa16355..1bede91995 100644 --- a/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs +++ b/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs @@ -330,11 +330,18 @@ impl PeersExchange { pub fn get_random_peers( &mut self, num: usize, - mut filter: impl FnMut(&PeerId) -> bool, + mut filter: impl FnMut(&PeerId, HashSet) -> bool, ) -> HashMap { let mut result = HashMap::with_capacity(num); let mut rng = rand::thread_rng(); - let peer_ids = self.known_peers.iter().filter(|peer| filter(peer)).collect::>(); + let peer_ids = self + .known_peers + .iter() + .filter(|peer| { + let addresses = self.request_response.addresses_of_peer(peer).into_iter().collect(); + filter(peer, addresses) + }) + .collect::>(); for peer_id in peer_ids.choose_multiple(&mut rng, num) { let addresses = self.request_response.addresses_of_peer(peer_id).into_iter().collect(); diff --git a/mm2src/mm2_p2p/src/behaviours/request_response.rs b/mm2src/mm2_p2p/src/behaviours/request_response.rs index adffafa644..0d6dfd69cd 100644 --- a/mm2src/mm2_p2p/src/behaviours/request_response.rs +++ b/mm2src/mm2_p2p/src/behaviours/request_response.rs @@ -1,10 +1,10 @@ use async_trait::async_trait; +use compatible_time::{Duration, Instant}; use futures::channel::{mpsc, oneshot}; use futures::io::{AsyncRead, AsyncWrite}; use futures::task::Poll; use futures::StreamExt; use futures_ticker::Ticker; -use instant::{Duration, Instant}; use libp2p::core::upgrade::{read_length_prefixed, write_length_prefixed}; use libp2p::core::Endpoint; use libp2p::request_response::{InboundFailure, Message, OutboundFailure, ProtocolSupport}; diff --git a/mm2src/mm2_p2p/src/network.rs b/mm2src/mm2_p2p/src/network.rs index 70978a3301..6d5524a31e 100644 --- a/mm2src/mm2_p2p/src/network.rs +++ b/mm2src/mm2_p2p/src/network.rs @@ -5,75 +5,57 @@ pub const DEFAULT_NETID: u16 = 8762; pub struct SeedNodeInfo { pub id: &'static str, - pub ip: &'static str, pub domain: &'static str, } impl SeedNodeInfo { - pub const fn new(id: &'static str, ip: &'static str, domain: &'static str) -> Self { Self { id, ip, domain } } + pub const fn new(id: &'static str, domain: &'static str) -> Self { Self { id, domain } } } #[cfg_attr(target_arch = "wasm32", allow(dead_code))] const ALL_DEFAULT_NETID_SEEDNODES: &[SeedNodeInfo] = &[ SeedNodeInfo::new( "12D3KooWHKkHiNhZtKceQehHhPqwU5W1jXpoVBgS1qst899GjvTm", - "168.119.236.251", "viserion.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWAToxtunEBWCoAHjefSv74Nsmxranw8juy3eKEdrQyGRF", - "168.119.236.240", "rhaegal.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWSmEi8ypaVzFA1AGde2RjxNW5Pvxw3qa2fVe48PjNs63R", - "168.119.236.239", "drogon.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWMrjLmrv8hNgAoVf1RfumfjyPStzd4nv5XL47zN4ZKisb", - "168.119.237.8", "falkor.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWEWzbYcosK2JK9XpFXzumfgsWJW1F7BZS15yLTrhfjX2Z", - "65.21.51.47", "smaug.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWJWBnkVsVNjiqUEPjLyHpiSmQVAJ5t6qt1Txv5ctJi9Xd", - "135.181.34.220", "balerion.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWPR2RoPi19vQtLugjCdvVmCcGLP2iXAzbDfP3tp81ZL4d", - "168.119.237.13", "kalessin.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWJDoV9vJdy6PnzwVETZ3fWGMhV41VhSbocR1h2geFqq9Y", - "65.108.90.210", "icefyre.dragon-seed.com", ), SeedNodeInfo::new( "12D3KooWEaZpH61H4yuQkaNG5AsyGdpBhKRppaLdAY52a774ab5u", - "46.4.78.11", - "fr1.cipig.net", + "seed01.kmdefi.net", + ), + SeedNodeInfo::new( + "12D3KooWAd5gPXwX7eDvKWwkr2FZGfoJceKDCA53SHmTFFVkrN7Q", + "seed02.kmdefi.net", ), ]; -// TODO: Uncomment these once re-enabled on the main network. -// Operated by Dragonhound, still on NetID 7777. Domains will update after netid migration. -// SeedNodeInfo::new("12D3KooWEsuiKcQaBaKEzuMtT6uFjs89P1E8MK3wGRZbeuCbCw6P", "168.119.236.241", "seed1.komodo.earth"), // tintaglia.dragon-seed.com -// SeedNodeInfo::new("12D3KooWHBeCnJdzNk51G4mLnao9cDsjuqiMTEo5wMFXrd25bd1F", "168.119.236.243", "seed2.komodo.earth"), // mercor.dragon-seed.com -// SeedNodeInfo::new("12D3KooWKxavLCJVrQ5Gk1kd9m6cohctGQBmiKPS9XQFoXEoyGmS", "168.119.236.249", "seed3.komodo.earth"), // karrigvestrit.dragon-seed.com -// SeedNodeInfo::new("12D3KooWGrUpCAbkxhPRioNs64sbUmPmpEcou6hYfrqQvxfWDEuf", "135.181.35.77", "seed4.komodo.earth"), // sintara.dragon-seed.com -// SeedNodeInfo::new("12D3KooWKu8pMTgteWacwFjN7zRWWHb3bctyTvHU3xx5x4x6qDYY", "65.21.56.210", "seed6.komodo.earth"), // heeby.dragon-seed.com -// SeedNodeInfo::new("12D3KooW9soGyPfX6kcyh3uVXNHq1y2dPmQNt2veKgdLXkBiCVKq", "168.119.236.246", "seed7.komodo.earth"), // kalo.dragon-seed.com -// SeedNodeInfo::new("12D3KooWL6yrrNACb7t7RPyTEPxKmq8jtrcbkcNd6H5G2hK7bXaL", "168.119.236.233", "seed8.komodo.earth"), // relpda.dragon-seed.com -// Operated by Cipi, still on NetID 7777 -// SeedNodeInfo::new("12D3KooWAd5gPXwX7eDvKWwkr2FZGfoJceKDCA53SHmTFFVkrN7Q", "46.4.87.18", "fr2.cipig.net"), - #[cfg(target_arch = "wasm32")] pub fn get_all_network_seednodes(_netid: u16) -> Vec<(PeerId, RelayAddress, String)> { Vec::new() } @@ -86,9 +68,11 @@ pub fn get_all_network_seednodes(netid: u16) -> Vec<(PeerId, RelayAddress, Strin } ALL_DEFAULT_NETID_SEEDNODES .iter() - .map(|SeedNodeInfo { id, ip, domain }| { - let peer_id = PeerId::from_str(id).expect("valid peer id"); - let address = RelayAddress::IPv4(ip.to_string()); + .map(|SeedNodeInfo { id, domain }| { + let peer_id = PeerId::from_str(id).unwrap_or_else(|e| panic!("Valid peer id {id}: {e}")); + let ip = + mm2_net::ip_addr::addr_to_ipv4_string(domain).unwrap_or_else(|e| panic!("Valid domain {domain}: {e}")); + let address = RelayAddress::IPv4(ip); let domain = domain.to_string(); (peer_id, address, domain) }) diff --git a/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs b/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs index d585424c57..a14f5edc13 100644 --- a/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs +++ b/mm2src/mm2_rpc/src/data/legacy/activation/utxo.rs @@ -11,23 +11,17 @@ pub struct UtxoMergeParams { } #[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] /// Deserializable Electrum protocol representation for RPC -#[derive(Default)] pub enum ElectrumProtocol { /// TCP - #[default] + #[cfg_attr(not(target_arch = "wasm32"), default)] TCP, /// SSL/TLS SSL, /// Insecure WebSocket. WS, /// Secure WebSocket. + #[cfg_attr(target_arch = "wasm32", default)] WSS, } - -#[cfg(not(target_arch = "wasm32"))] -#[cfg(target_arch = "wasm32")] -impl Default for ElectrumProtocol { - fn default() -> Self { ElectrumProtocol::WS } -} diff --git a/mm2src/mm2_test_helpers/dummy_files/iris-ibc-nucleus-history.json b/mm2src/mm2_test_helpers/dummy_files/iris-ibc-nucleus-history.json index 48049cfef4..c9e68d327c 100644 --- a/mm2src/mm2_test_helpers/dummy_files/iris-ibc-nucleus-history.json +++ b/mm2src/mm2_test_helpers/dummy_files/iris-ibc-nucleus-history.json @@ -21,9 +21,9 @@ "gas_limit": 125000 }, "coin": "IRIS-IBC-NUCLEUS-TEST", - "internal_id": "3935464134384431413046454634343232384335344245390000000000000000", + "internal_id": "373935464134384431413046454634343232384335344245390000000000000000", "transaction_type": { - "TokenTransfer": "da5368780890dadaff39ad08ae29eaf6ae408fc4f8ece2f1336975ec2e70095f" + "TokenTransfer": "7527813a107d05cfb44b0a6d79798e838e09ff1362e193d654e19afe53b20f6d" }, "memo": "" } diff --git a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json index 4400c318e0..0c26bd77b3 100644 --- a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json +++ b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json @@ -21,7 +21,7 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "4335303135373938433145384342333931424145373846420000000000000000", + "internal_id": "314335303135373938433145384342333931424145373846420000000000000000", "transaction_type": "StandardTransfer", "memo": "rly(2.5.2-28-gdf42391)" }, @@ -47,8 +47,12 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "3031414542453143353638454230314344383437414235390000000000000000", - "transaction_type": "StandardTransfer", + "internal_id": "413031414542453143353638454230314344383437414235390000000000000000", + "transaction_type": { + "TendermintIBCTransfer": { + "token_id": null + } + }, "memo": "" }, { @@ -71,7 +75,7 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "3233413532364335463338414434423844384230374337310000000000000000", + "internal_id": "443233413532364335463338414434423844384230374337310000000000000000", "transaction_type": { "CustomTendermintMsg": { "msg_type": "SignClaimHtlc", @@ -100,7 +104,7 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "4544364132384233444536373846304539333433343132330000000000000000", + "internal_id": "444544364132384233444536373846304539333433343132330000000000000000", "transaction_type": { "CustomTendermintMsg": { "msg_type": "SendHtlcAmount", @@ -129,7 +133,7 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "3838433336433830323231414344314341393532313738410000000000000000", + "internal_id": "323838433336433830323231414344314341393532313738410000000000000000", "transaction_type": { "CustomTendermintMsg": { "msg_type": "SignClaimHtlc", @@ -158,7 +162,7 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "4630343833303245373434314242423633373841343639370000000000000000", + "internal_id": "414630343833303245373434314242423633373841343639370000000000000000", "transaction_type": { "CustomTendermintMsg": { "msg_type": "SendHtlcAmount", @@ -187,7 +191,7 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "0000000000000000394542343543383232343446454630413144383441463539", + "internal_id": "000000000000000039454234354338323234344645463041314438344146353937", "transaction_type": "FeeForTokenTx", "memo": "" }, @@ -211,7 +215,7 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "4137314330463745363846424446433646343945323537330000000000000000", + "internal_id": "424137314330463745363846424446433646343945323537330000000000000000", "transaction_type": { "CustomTendermintMsg": { "msg_type": "SendHtlcAmount", @@ -242,7 +246,7 @@ "gas_limit": 125000 }, "coin": "NUCLEUS-TEST", - "internal_id": "3841453143323545314533393830423532444343383932450000000000000000", + "internal_id": "303841453143323545314533393830423532444343383932450000000000000000", "transaction_type": "StandardTransfer", "memo": "" } diff --git a/mm2src/mm2_test_helpers/src/electrums.rs b/mm2src/mm2_test_helpers/src/electrums.rs index d1673b2f53..03eaac51e6 100644 --- a/mm2src/mm2_test_helpers/src/electrums.rs +++ b/mm2src/mm2_test_helpers/src/electrums.rs @@ -73,19 +73,16 @@ pub fn tbtc_electrums() -> Vec { } #[cfg(target_arch = "wasm32")] -pub fn qtum_electrums() -> Vec { +pub fn tqtum_electrums() -> Vec { vec![ - json!({ "url": "electrum1.cipig.net:30071", "protocol": "WSS" }), - json!({ "url": "electrum2.cipig.net:30071", "protocol": "WSS" }), json!({ "url": "electrum3.cipig.net:30071", "protocol": "WSS" }), ] } #[cfg(not(target_arch = "wasm32"))] -pub fn qtum_electrums() -> Vec { +pub fn tqtum_electrums() -> Vec { vec![ - json!({ "url": "electrum1.cipig.net:10071" }), - json!({ "url": "electrum2.cipig.net:10071" }), + json!({ "url": "electrum3.cipig.net:20071", "protocol": "SSL" }), json!({ "url": "electrum3.cipig.net:10071" }), ] } diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 7592384696..9285768bc1 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -2,7 +2,7 @@ #![allow(missing_docs)] -use crate::electrums::qtum_electrums; +use crate::electrums::tqtum_electrums; use crate::structs::*; use common::custom_futures::repeatable::{Ready, Retry}; use common::executor::Timer; @@ -199,21 +199,21 @@ pub const MARTY_ELECTRUM_ADDRS: &[&str] = &[ "electrum3.cipig.net:10021", ]; pub const ZOMBIE_TICKER: &str = "ZOMBIE"; +#[cfg(not(target_arch = "wasm32"))] +pub const ZOMBIE_ELECTRUMS: &[&str] = &["zombie.dragonhound.info:10033", "zombie.dragonhound.info:10133"]; +#[cfg(target_arch = "wasm32")] +pub const ZOMBIE_ELECTRUMS: &[&str] = &["zombie.dragonhound.info:30058", "zombie.dragonhound.info:30059"]; +pub const ZOMBIE_LIGHTWALLETD_URLS: &[&str] = &[ + "https://zombie.dragonhound.info:443", + "https://zombie.dragonhound.info:1443", +]; pub const ARRR: &str = "ARRR"; -pub const ZOMBIE_ELECTRUMS: &[&str] = &[ +#[cfg(not(target_arch = "wasm32"))] +pub const PIRATE_ELECTRUMS: &[&str] = &[ "electrum1.cipig.net:10008", "electrum2.cipig.net:10008", "electrum3.cipig.net:10008", ]; -pub const ZOMBIE_LIGHTWALLETD_URLS: &[&str] = &[ - "https://lightd1.pirate.black:443", - "https://piratelightd1.cryptoforge.cc:443", - "https://piratelightd2.cryptoforge.cc:443", - "https://piratelightd3.cryptoforge.cc:443", - "https://piratelightd4.cryptoforge.cc:443", -]; -#[cfg(not(target_arch = "wasm32"))] -pub const PIRATE_ELECTRUMS: &[&str] = &["node1.chainkeeper.pro:10132"]; #[cfg(target_arch = "wasm32")] pub const PIRATE_ELECTRUMS: &[&str] = &[ "electrum3.cipig.net:30008", @@ -221,7 +221,13 @@ pub const PIRATE_ELECTRUMS: &[&str] = &[ "electrum2.cipig.net:30008", ]; #[cfg(not(target_arch = "wasm32"))] -pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &["http://node1.chainkeeper.pro:443"]; +pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &[ + "https://lightd1.pirate.black:443", + "https://piratelightd1.cryptoforge.cc:443", + "https://piratelightd2.cryptoforge.cc:443", + "https://piratelightd3.cryptoforge.cc:443", + "https://piratelightd4.cryptoforge.cc:443", +]; #[cfg(target_arch = "wasm32")] pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &["https://pirate.battlefield.earth:8581"]; pub const DEFAULT_RPC_PASSWORD: &str = "pass"; @@ -231,20 +237,22 @@ pub const QRC20_ELECTRUMS: &[&str] = &[ "electrum3.cipig.net:10071", ]; pub const T_BCH_ELECTRUMS: &[&str] = &["tbch.loping.net:60001", "bch0.kister.net:51001"]; -pub const TBTC_ELECTRUMS: &[&str] = &[ - "electrum1.cipig.net:10068", - "electrum2.cipig.net:10068", - "electrum3.cipig.net:10068", -]; +pub const TBTC_ELECTRUMS: &[&str] = &["electrum3.cipig.net:10068", "testnet.aranguren.org:51001"]; -pub const ETH_MAINNET_NODE: &str = "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b"; +pub const ETH_MAINNET_NODES: &[&str] = &[ + "https://mainnet.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b", + "https://ethereum-rpc.publicnode.com", + "https://eth.drpc.org", +]; pub const ETH_MAINNET_CHAIN_ID: u64 = 1; pub const ETH_MAINNET_SWAP_CONTRACT: &str = "0x24abe4c71fc658c91313b6552cd40cd808b3ea80"; pub const ETH_SEPOLIA_NODES: &[&str] = &[ + "https://sepolia.drpc.org", "https://ethereum-sepolia-rpc.publicnode.com", "https://rpc2.sepolia.org", "https://1rpc.io/sepolia", + "https://sepolia.drpc.org", ]; pub const ETH_SEPOLIA_CHAIN_ID: u64 = 11155111; pub const ETH_SEPOLIA_SWAP_CONTRACT: &str = "0xeA6D65434A15377081495a9E7C5893543E7c32cB"; @@ -465,14 +473,18 @@ pub enum Mm2InitPrivKeyPolicy { GlobalHDAccount, } -pub fn zombie_conf() -> Json { +pub fn zombie_conf() -> Json { zombie_conf_inner(None) } + +pub fn zombie_conf_for_docker() -> Json { zombie_conf_inner(Some(10)) } + +pub fn zombie_conf_inner(custom_blocktime: Option) -> Json { json!({ "coin":"ZOMBIE", "asset":"ZOMBIE", "txversion":4, - "overwintered":1, + "overwintered": 1, "mm2":1, - "avg_blocktime": 60, + "avg_blocktime": custom_blocktime.unwrap_or(60), "protocol":{ "type":"ZHTLC", "protocol_data": { @@ -521,9 +533,11 @@ pub fn pirate_conf() -> Json { "b58_pubkey_address_prefix": [ 28, 184 ], "b58_script_address_prefix": [ 28, 189 ] }, + "z_derivation_path": "m/32'/133'", } }, - "required_confirmations":0 + "required_confirmations":0, + "derivation_path": "m/44'/133'", }) } @@ -819,9 +833,13 @@ pub fn eth_testnet_conf_trezor() -> Json { } /// ETH configuration used for dockerized Geth dev node -pub fn eth_dev_conf() -> Json { +pub fn eth_dev_conf() -> Json { eth_conf("ETH") } + +pub fn eth1_dev_conf() -> Json { eth_conf("ETH1") } + +fn eth_conf(coin: &str) -> Json { json!({ - "coin": "ETH", + "coin": coin, "name": "ethereum", "mm2": 1, "chain_id": 1337, @@ -1426,6 +1444,7 @@ impl MarketMakerIt { local: Option, db_namespace_id: Option, ) -> Result { + conf["allow_weak_password"] = true.into(); if conf["p2p_in_memory"].is_null() { conf["p2p_in_memory"] = Json::Bool(true); } @@ -2041,6 +2060,56 @@ pub async fn enable_eth_coin( json::from_str(&enable.1).unwrap() } +#[derive(Clone)] +pub struct SwapV2TestContracts { + pub maker_swap_v2_contract: String, + pub taker_swap_v2_contract: String, + pub nft_maker_swap_v2_contract: String, +} + +#[derive(Clone)] +pub struct TestNode { + pub url: String, +} + +pub async fn enable_eth_coin_v2( + mm: &MarketMakerIt, + ticker: &str, + swap_contract_address: &str, + swap_v2_contracts: SwapV2TestContracts, + fallback_swap_contract: Option<&str>, + nodes: &[TestNode], +) -> Json { + let enable = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "enable_eth_with_tokens", + "mmrpc": "2.0", + "params": { + "ticker": ticker, + "mm2": 1, + "swap_contract_address": swap_contract_address, + "swap_v2_contracts": { + "maker_swap_v2_contract": swap_v2_contracts.maker_swap_v2_contract, + "taker_swap_v2_contract": swap_v2_contracts.taker_swap_v2_contract, + "nft_maker_swap_v2_contract": swap_v2_contracts.nft_maker_swap_v2_contract + }, + "fallback_swap_contract": fallback_swap_contract, + "nodes": nodes.iter().map(|node| json!({ "url": node.url })).collect::>(), + "erc20_tokens_requests": [] + } + })) + .await + .unwrap(); + assert_eq!( + enable.0, + StatusCode::OK, + "'enable_eth_with_tokens' failed: {}", + enable.1 + ); + json::from_str(&enable.1).unwrap() +} + pub async fn enable_slp(mm: &MarketMakerIt, coin: &str) -> Json { let enable = mm .rpc(&json!({ @@ -3068,6 +3137,52 @@ pub async fn get_tendermint_my_tx_history(mm: &MarketMakerIt, coin: &str, limit: json::from_str(&request.1).unwrap() } +pub async fn tendermint_delegations(mm: &MarketMakerIt, coin: &str) -> Json { + let rpc_endpoint = "experimental::staking::query::delegations"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "info_details": { + "type": "Cosmos", + "limit": 0, + "page_number": 1 + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, StatusCode::OK, "'{rpc_endpoint}' failed: {}", request.1); + log!("{rpc_endpoint} response {}", request.1); + json::from_str(&request.1).unwrap() +} + +pub async fn tendermint_ongoing_undelegations(mm: &MarketMakerIt, coin: &str) -> Json { + let rpc_endpoint = "experimental::staking::query::ongoing_undelegations"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "info_details": { + "type": "Cosmos", + "limit": 0, + "page_number": 1 + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, StatusCode::OK, "'{rpc_endpoint}' failed: {}", request.1); + log!("{rpc_endpoint} response {}", request.1); + json::from_str(&request.1).unwrap() +} + pub async fn enable_tendermint_token(mm: &MarketMakerIt, coin: &str) -> Json { let request = json!({ "userpass": mm.userpass, @@ -3091,6 +3206,106 @@ pub async fn enable_tendermint_token(mm: &MarketMakerIt, coin: &str) -> Json { json::from_str(&request.1).unwrap() } +pub async fn tendermint_validators( + mm: &MarketMakerIt, + coin: &str, + filter_by_status: &str, + limit: usize, + page_number: usize, +) -> Json { + let rpc_endpoint = "experimental::staking::query::validators"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "info_details": { + "type": "Cosmos", + "filter_by_status": filter_by_status, + "limit": limit, + "page_number": page_number + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let response = mm.rpc(&request).await.unwrap(); + assert_eq!(response.0, StatusCode::OK, "{rpc_endpoint} failed: {}", response.1); + log!("{rpc_endpoint} response {}", response.1); + json::from_str(&response.1).unwrap() +} + +pub async fn tendermint_add_delegation( + mm: &MarketMakerIt, + coin: &str, + validator_address: &str, + amount: &str, +) -> TransactionDetails { + let rpc_endpoint = "experimental::staking::delegate"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "staking_details": { + "type": "Cosmos", + "validator_address": validator_address, + "amount": amount, + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let response = mm.rpc(&request).await.unwrap(); + assert_eq!(response.0, StatusCode::OK, "{rpc_endpoint} failed: {}", response.1); + log!("{rpc_endpoint} response {}", response.1); + + let json: Json = json::from_str(&response.1).unwrap(); + json::from_value(json["result"].clone()).unwrap() +} + +pub async fn tendermint_remove_delegation_raw( + mm: &MarketMakerIt, + coin: &str, + validator_address: &str, + amount: &str, +) -> (StatusCode, String, HeaderMap) { + let rpc_endpoint = "experimental::staking::undelegate"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "coin": coin, + "staking_details": { + "type": "Cosmos", + "validator_address": validator_address, + "amount": amount, + } + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + mm.rpc(&request).await.unwrap() +} + +pub async fn tendermint_remove_delegation( + mm: &MarketMakerIt, + coin: &str, + validator_address: &str, + amount: &str, +) -> TransactionDetails { + let rpc_endpoint = "experimental::staking::undelegate"; + let response = tendermint_remove_delegation_raw(mm, coin, validator_address, amount).await; + assert_eq!(response.0, StatusCode::OK, "{rpc_endpoint} failed: {}", response.1); + log!("{rpc_endpoint} response {}", response.1); + + let json: Json = json::from_str(&response.1).unwrap(); + json::from_value(json["result"].clone()).unwrap() +} + pub async fn init_utxo_electrum( mm: &MarketMakerIt, coin: &str, @@ -3271,18 +3486,19 @@ async fn init_erc20_token( protocol: Option, path_to_address: Option, ) -> Result<(StatusCode, Json), Json> { - let (status, response, _) = mm.rpc(&json!({ - "userpass": mm.userpass, - "method": "task::enable_erc20::init", - "mmrpc": "2.0", - "params": { - "ticker": ticker, - "protocol": protocol, - "activation_params": { - "path_to_address": path_to_address.unwrap_or_default(), + let (status, response, _) = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "task::enable_erc20::init", + "mmrpc": "2.0", + "params": { + "ticker": ticker, + "protocol": protocol, + "activation_params": { + "path_to_address": path_to_address.unwrap_or_default(), + } } - } - })) + })) .await .unwrap(); @@ -3352,12 +3568,7 @@ pub async fn get_token_info(mm: &MarketMakerIt, protocol: Json) -> TokenInfoResp })) .await .unwrap(); - assert_eq!( - response.0, - StatusCode::OK, - "'get_token_info' failed: {}", - response.1 - ); + assert_eq!(response.0, StatusCode::OK, "'get_token_info' failed: {}", response.1); let response_json: Json = json::from_str(&response.1).unwrap(); json::from_value(response_json["result"].clone()).unwrap() } @@ -3528,7 +3739,7 @@ pub async fn test_qrc20_history_impl(local_start: Option) { "userpass": mm.userpass, "method": "electrum", "coin": "QRC20", - "servers": qtum_electrums(), + "servers": tqtum_electrums(), "mm2": 1, "tx_history": true, "swap_contract_address": "0xd362e096e873eb7907e205fadc6175c6fec7bc44", diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index baba173461..b5d3be28f5 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -463,7 +463,9 @@ pub enum TransactionType { msg_type: CustomTendermintMsgType, token_id: Option, }, - TendermintIBCTransfer, + TendermintIBCTransfer { + token_id: Option, + }, } #[derive(Debug, Deserialize, PartialEq, Serialize)] @@ -995,7 +997,6 @@ pub enum MyTxHistoryTarget { Iguana, AccountId { account_id: u32 }, AddressId(HDAccountAddressId), - AddressDerivationPath(String), } #[derive(Debug, Deserialize)] diff --git a/mm2src/proxy_signature/Cargo.toml b/mm2src/proxy_signature/Cargo.toml index bbad60cbb8..260aa1a568 100644 --- a/mm2src/proxy_signature/Cargo.toml +++ b/mm2src/proxy_signature/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] chrono = "0.4" http = "0.2" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify"] } serde = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/rpc_task/Cargo.toml b/mm2src/rpc_task/Cargo.toml index 4bf524f86a..c542159f25 100644 --- a/mm2src/rpc_task/Cargo.toml +++ b/mm2src/rpc_task/Cargo.toml @@ -10,9 +10,11 @@ doctest = false async-trait = "0.1" common = { path = "../common" } mm2_err_handle = { path = "../mm2_err_handle" } +mm2_event_stream = { path = "../mm2_event_stream" } derive_more = "0.99" futures = "0.3" ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } serde = "1" serde_derive = "1" +serde_json = "1" diff --git a/mm2src/rpc_task/src/lib.rs b/mm2src/rpc_task/src/lib.rs index f5861f37cc..2e8f703d87 100644 --- a/mm2src/rpc_task/src/lib.rs +++ b/mm2src/rpc_task/src/lib.rs @@ -16,7 +16,7 @@ mod task; pub use handle::{RpcTaskHandle, RpcTaskHandleShared}; pub use manager::{RpcTaskManager, RpcTaskManagerShared}; -pub use task::{RpcTask, RpcTaskTypes}; +pub use task::{RpcInitReq, RpcTask, RpcTaskTypes}; pub type RpcTaskResult = Result>; pub type TaskId = u64; diff --git a/mm2src/rpc_task/src/manager.rs b/mm2src/rpc_task/src/manager.rs index 950eac97f4..b5c43a04b6 100644 --- a/mm2src/rpc_task/src/manager.rs +++ b/mm2src/rpc_task/src/manager.rs @@ -2,10 +2,11 @@ use crate::task::RpcTaskTypes; use crate::{AtomicTaskId, RpcTask, RpcTaskError, RpcTaskHandle, RpcTaskResult, RpcTaskStatus, RpcTaskStatusAlias, TaskAbortHandle, TaskAbortHandler, TaskId, TaskStatus, TaskStatusError, UserActionSender}; use common::executor::SpawnFuture; -use common::log::{debug, info}; +use common::log::{debug, info, trace, warn}; use futures::channel::oneshot; use futures::future::{select, Either}; use mm2_err_handle::prelude::*; +use mm2_event_stream::{Event, StreamingManager, StreamingManagerError}; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::atomic::Ordering; @@ -29,26 +30,29 @@ static NEXT_RPC_TASK_ID: AtomicTaskId = AtomicTaskId::new(0); fn next_rpc_task_id() -> TaskId { NEXT_RPC_TASK_ID.fetch_add(1, Ordering::Relaxed) } pub struct RpcTaskManager { + /// A map of task IDs to their statuses and abort handlers. tasks: HashMap>, -} - -impl Default for RpcTaskManager { - fn default() -> Self { RpcTaskManager { tasks: HashMap::new() } } + /// A copy of the MM2's streaming manager to broadcast task status updates to interested parties. + streaming_manager: StreamingManager, } impl RpcTaskManager { /// Create new instance of `RpcTaskHandle` attached to the only one `RpcTask`. /// This function registers corresponding RPC task in the `RpcTaskManager` and returns the task id. - pub fn spawn_rpc_task(this: &RpcTaskManagerShared, spawner: &F, mut task: Task) -> RpcTaskResult + pub fn spawn_rpc_task( + this: &RpcTaskManagerShared, + spawner: &F, + mut task: Task, + client_id: u64, + ) -> RpcTaskResult where F: SpawnFuture, { - let initial_task_status = task.initial_status(); let (task_id, task_abort_handler) = { let mut task_manager = this .lock() .map_to_mm(|e| RpcTaskError::Internal(format!("RpcTaskManager is not available: {}", e)))?; - task_manager.register_task(initial_task_status)? + task_manager.register_task(&task, client_id)? }; let task_handle = Arc::new(RpcTaskHandle { task_manager: RpcTaskManagerShared::downgrade(this), @@ -103,10 +107,26 @@ impl RpcTaskManager { Some(rpc_status) } - pub fn new_shared() -> RpcTaskManagerShared { Arc::new(Mutex::new(Self::default())) } + pub fn new(streaming_manager: StreamingManager) -> Self { + RpcTaskManager { + tasks: HashMap::new(), + streaming_manager, + } + } + + pub fn new_shared(streaming_manager: StreamingManager) -> RpcTaskManagerShared { + Arc::new(Mutex::new(Self::new(streaming_manager))) + } pub fn contains(&self, task_id: TaskId) -> bool { self.tasks.contains_key(&task_id) } + fn get_client_id(&self, task_id: TaskId) -> Option { + self.tasks.get(&task_id).and_then(|task| match task { + TaskStatusExt::InProgress { client_id, .. } | TaskStatusExt::Awaiting { client_id, .. } => Some(*client_id), + _ => None, + }) + } + /// Cancel task if it's in progress. pub fn cancel_task(&mut self, task_id: TaskId) -> RpcTaskResult<()> { let task = self.tasks.remove(&task_id); @@ -138,18 +158,16 @@ impl RpcTaskManager { } } - pub(crate) fn register_task( - &mut self, - task_initial_in_progress_status: Task::InProgressStatus, - ) -> RpcTaskResult<(TaskId, TaskAbortHandler)> { + pub(crate) fn register_task(&mut self, task: &Task, client_id: u64) -> RpcTaskResult<(TaskId, TaskAbortHandler)> { let task_id = next_rpc_task_id(); let (abort_handle, abort_handler) = oneshot::channel(); match self.tasks.entry(task_id) { Entry::Occupied(_entry) => unexpected_task_status!(task_id, actual = InProgress, expected = Idle), Entry::Vacant(entry) => { entry.insert(TaskStatusExt::InProgress { - status: task_initial_in_progress_status, + status: task.initial_status(), abort_handle, + client_id, }); Ok((task_id, abort_handler)) }, @@ -157,7 +175,9 @@ impl RpcTaskManager { } pub(crate) fn update_task_status(&mut self, task_id: TaskId, status: TaskStatus) -> RpcTaskResult<()> { - match status { + // Get the client ID before updating the task status because not all task status variants store the ID. + let client_id = self.get_client_id(task_id); + let update_result = match status { TaskStatus::Ok(result) => self.on_task_finished(task_id, Ok(result)), TaskStatus::Error(error) => self.on_task_finished(task_id, Err(error)), TaskStatus::InProgress(in_progress) => self.update_in_progress_status(task_id, in_progress), @@ -165,7 +185,29 @@ impl RpcTaskManager { awaiting_status, user_action_tx, } => self.set_task_is_waiting_for_user_action(task_id, awaiting_status, user_action_tx), - } + }; + // If the status was updated successfully, we need to inform the client about the new status. + if update_result.is_ok() { + if let Some(client_id) = client_id { + // Note that this should really always be `Some`, since we updated the status *successfully*. + if let Some(new_status) = self.task_status(task_id, false) { + let event = Event::new( + format!("TASK:{task_id}"), + serde_json::to_value(new_status).expect("Serialization shouldn't fail."), + ); + if let Err(e) = self.streaming_manager.broadcast_to(event, client_id) { + match e { + StreamingManagerError::UnknownClient => { + // TODO: Set this log to warn level once we stop setting a default client ID in task managed requests (i.e. after migrating event streaming to WS). + trace!("Failed to send task status update to the client (ID={client_id}): {e:?}") + }, + _ => warn!("Failed to send task status update to the client (ID={client_id}): {e:?}"), + } + } + }; + } + }; + update_result } pub(crate) fn on_task_cancelling_finished(&mut self, task_id: TaskId) -> RpcTaskResult<()> { @@ -196,11 +238,22 @@ impl RpcTaskManager { fn update_in_progress_status(&mut self, task_id: TaskId, status: Task::InProgressStatus) -> RpcTaskResult<()> { match self.tasks.remove(&task_id) { - Some(TaskStatusExt::InProgress { abort_handle, .. }) - | Some(TaskStatusExt::Awaiting { abort_handle, .. }) => { + Some(TaskStatusExt::InProgress { + abort_handle, + client_id, + .. + }) + | Some(TaskStatusExt::Awaiting { + abort_handle, + client_id, + .. + }) => { // Insert new in-progress status to the tasks container. - self.tasks - .insert(task_id, TaskStatusExt::InProgress { status, abort_handle }); + self.tasks.insert(task_id, TaskStatusExt::InProgress { + status, + abort_handle, + client_id, + }); Ok(()) }, Some(cancelling @ TaskStatusExt::Cancelling { .. }) => { @@ -227,13 +280,15 @@ impl RpcTaskManager { Some(TaskStatusExt::InProgress { status: next_in_progress_status, abort_handle, + client_id, }) => { // Insert new awaiting status to the tasks container. self.tasks.insert(task_id, TaskStatusExt::Awaiting { status, - abort_handle, action_sender, next_in_progress_status, + abort_handle, + client_id, }); Ok(()) }, @@ -259,8 +314,9 @@ impl RpcTaskManager { match self.tasks.remove(&task_id) { Some(TaskStatusExt::Awaiting { action_sender, - abort_handle, next_in_progress_status: status, + abort_handle, + client_id, .. }) => { let result = action_sender @@ -268,8 +324,11 @@ impl RpcTaskManager { // The task seems to be canceled/aborted for some reason. .map_to_mm(|_user_action| RpcTaskError::Cancelled); // Insert new in-progress status to the tasks container. - self.tasks - .insert(task_id, TaskStatusExt::InProgress { status, abort_handle }); + self.tasks.insert(task_id, TaskStatusExt::InProgress { + status, + abort_handle, + client_id, + }); result }, Some(unexpected) => { @@ -298,12 +357,16 @@ enum TaskStatusExt { InProgress { status: Task::InProgressStatus, abort_handle: TaskAbortHandle, + /// The ID of the client requesting the task. To stream out the updates & results for them. + client_id: u64, }, Awaiting { status: Task::AwaitingStatus, action_sender: UserActionSender, - abort_handle: TaskAbortHandle, next_in_progress_status: Task::InProgressStatus, + abort_handle: TaskAbortHandle, + /// The ID of the client requesting the task. To stream out the updates & results for them. + client_id: u64, }, /// `Cancelling` status is set on [`RpcTaskManager::cancel_task`]. /// This status is used to save the task state before it's actually canceled on [`RpcTaskHandle::on_canceled`], diff --git a/mm2src/rpc_task/src/task.rs b/mm2src/rpc_task/src/task.rs index 6c38f75050..3f5ceca1bd 100644 --- a/mm2src/rpc_task/src/task.rs +++ b/mm2src/rpc_task/src/task.rs @@ -6,8 +6,8 @@ use serde::Serialize; pub trait RpcTaskTypes { type Item: Serialize + Clone + Send + Sync + 'static; type Error: SerMmErrorType + Clone + Send + Sync + 'static; - type InProgressStatus: Clone + Send + Sync + 'static; - type AwaitingStatus: Clone + Send + Sync + 'static; + type InProgressStatus: Serialize + Clone + Send + Sync + 'static; + type AwaitingStatus: Serialize + Clone + Send + Sync + 'static; type UserAction: NotMmError + Send + Sync + 'static; } @@ -20,3 +20,16 @@ pub trait RpcTask: RpcTaskTypes + Sized + Send + 'static { async fn run(&mut self, task_handle: RpcTaskHandleShared) -> Result>; } + +/// The general request for initializing an RPC Task. +/// +/// `client_id` is used to identify the client to which the task should stream out update events +/// to and is common in each request. Other data is request-specific. +#[derive(Deserialize)] +pub struct RpcInitReq { + // If the client ID isn't included, assume it's 0. + #[serde(default)] + pub client_id: u64, + #[serde(flatten)] + pub inner: T, +} diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 4fd9514fb9..4f714ed34d 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -23,6 +23,7 @@ url = { version = "2.2.2", features = ["serde"] } [features] test-ext-api = [] # use test config to connect to an external api +for-tests = ["dep:mocktopus"] [dev-dependencies] mocktopus = { version = "0.8.0" } \ No newline at end of file diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index 9c7136148a..2825d930b5 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -10,7 +10,7 @@ use mm2_net::transport::slurp_url_with_headers; use serde::de::DeserializeOwned; use url::Url; -#[cfg(any(test, feature = "mocktopus"))] +#[cfg(any(test, feature = "for-tests"))] use mocktopus::macros::*; const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; @@ -78,7 +78,7 @@ impl<'a> UrlBuilder<'a> { let url = self .base_url .join(self.endpoint)? - .join(&format!("{}/", self.chain_id.to_string()))? + .join(&format!("{}/", self.chain_id))? .join(self.method_name.as_str())?; Ok(Url::parse_with_params( url.as_str(), @@ -96,7 +96,7 @@ pub struct ApiClient { } #[allow(clippy::swap_ptr_to_ref)] // need for moctopus -#[cfg_attr(any(test, feature = "mocktopus"), mockable)] +#[cfg_attr(any(test, feature = "for-tests"), mockable)] impl ApiClient { #[allow(unused_variables)] #[allow(clippy::result_large_err)] diff --git a/mm2src/trading_api/src/one_inch_api/errors.rs b/mm2src/trading_api/src/one_inch_api/errors.rs index d92f8e144b..70264a9b89 100644 --- a/mm2src/trading_api/src/one_inch_api/errors.rs +++ b/mm2src/trading_api/src/one_inch_api/errors.rs @@ -76,7 +76,7 @@ impl NativeError { match serde_json::from_value(body) { Ok(err) => Self::HttpError400(err), Err(err) => Self::ParseError { - error_msg: format!("could not parse error response: {}", err.to_string()), + error_msg: format!("could not parse error response: {}", err), }, } } else {