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..cc3b541969 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,29 +37,26 @@ 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/ @@ -70,7 +66,7 @@ jobs: 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/ @@ -90,7 +86,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,29 +107,26 @@ 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/ @@ -143,7 +136,7 @@ jobs: 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/ @@ -172,29 +165,26 @@ 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/ @@ -204,7 +194,7 @@ jobs: 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/ @@ -232,31 +222,26 @@ 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/ @@ -266,7 +251,7 @@ jobs: 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/ @@ -295,29 +280,26 @@ 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 @@ -328,7 +310,7 @@ jobs: 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/ @@ -368,31 +350,29 @@ 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/ @@ -422,29 +402,26 @@ 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 @@ -455,7 +432,7 @@ jobs: 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/ @@ -497,22 +474,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,7 +495,7 @@ 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 @@ -532,7 +506,7 @@ jobs: 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/ @@ -574,22 +548,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,7 +569,7 @@ 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 @@ -609,7 +580,7 @@ jobs: 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/ @@ -639,13 +610,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 +628,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..b5212320dd 100644 --- a/.github/workflows/fmt-and-lint.yml +++ b/.github/workflows/fmt-and-lint.yml @@ -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 59e176a5e0..6ce7dedd0a 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -22,6 +22,7 @@ jobs: types: | feat fix + improvement chore docs deps @@ -38,16 +39,18 @@ jobs: TITLE: ${{ github.event.pull_request.title }} run: | title_length=${#TITLE} - if [ $title_length -gt 72 ] + if [ $title_length -gt 85 ] then - echo "PR title is too long (greater than 72 characters)" + echo "PR title is too long (greater than 85 characters)" exit 1 fi - 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..f1cdda82df 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,33 +37,30 @@ 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/ - 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/ @@ -102,33 +98,30 @@ 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/ - 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/ @@ -157,33 +150,30 @@ 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/ - 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/ @@ -211,35 +201,30 @@ 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/ - 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/ @@ -267,26 +252,23 @@ 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 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 @@ -294,7 +276,7 @@ jobs: - 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/ @@ -334,28 +316,26 @@ 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/ @@ -385,26 +365,23 @@ 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 @@ -412,7 +389,7 @@ jobs: - 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/ @@ -454,28 +431,25 @@ 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 @@ -483,7 +457,7 @@ jobs: - 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/ @@ -525,28 +499,25 @@ 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 @@ -554,7 +525,7 @@ jobs: - 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/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12a60bbc3c..53debcb4b2 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 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..56e7a26582 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 @@ -74,9 +70,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/Cargo.lock b/Cargo.lock index 2bc1c3d543..0768065e77 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]] @@ -921,6 +927,7 @@ dependencies = [ "spv_validation", "tendermint-rpc", "time 0.3.20", + "timed-map", "tokio", "tokio-rustls 0.24.1", "tokio-tungstenite-wasm", @@ -1013,6 +1020,7 @@ dependencies = [ "log", "parking_lot", "parking_lot_core 0.6.2", + "paste", "primitive-types", "rand 0.7.3", "regex", @@ -1826,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", @@ -1851,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]] @@ -2182,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]] @@ -2499,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" @@ -2775,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", @@ -2796,6 +2804,10 @@ dependencies = [ "if-addrs", "ipnet", "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", "rtnetlink", "system-configuration", "tokio", @@ -3072,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", @@ -3080,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" @@ -3099,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -3131,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3142,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3153,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "fnv", @@ -3180,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "futures 0.3.28", "libp2p-core", @@ -3194,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", "cuckoofilter", @@ -3214,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", "base64 0.21.7", @@ -3245,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", "either", @@ -3285,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "data-encoding", "futures 0.3.28", @@ -3305,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "instant", "libp2p-core", @@ -3321,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +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", @@ -3345,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "futures 0.3.28", @@ -3362,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "async-trait", "futures 0.3.28", @@ -3379,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "fnv", @@ -3401,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "heck", "proc-macro-warning", @@ -3413,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "futures 0.3.28", "futures-timer", @@ -3429,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "futures 0.3.28", "js-sys", @@ -3442,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "futures 0.3.28", @@ -3461,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "either", "futures 0.3.28", @@ -3621,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" @@ -3840,7 +3852,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "2.2.0-beta" +version = "2.3.0-beta" dependencies = [ "chrono", "common", @@ -3889,6 +3901,7 @@ dependencies = [ "serde", "serde_json", "shared_ref_counter", + "timed-map", "tokio", "uuid", "wasm-bindgen-test", @@ -3962,6 +3975,7 @@ dependencies = [ "futures 0.3.28", "parking_lot", "serde", + "serde_json", "tokio", "wasm-bindgen-test", ] @@ -4076,6 +4090,7 @@ dependencies = [ "num-traits", "parity-util-mem", "parking_lot", + "primitive-types", "primitives", "prost", "prost-build", @@ -4104,7 +4119,9 @@ dependencies = [ "sp-trie", "spv_validation", "testcontainers", + "timed-map", "tokio", + "trading_api", "trie-db", "trie-root 0.16.0", "url", @@ -4247,6 +4264,7 @@ dependencies = [ "sha2 0.10.7", "smallvec 1.6.1", "syn 2.0.38", + "timed-map", "tokio", "void", ] @@ -4377,7 +4395,7 @@ checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" [[package]] name = "multistream-select" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -4389,24 +4407,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", @@ -4427,9 +4444,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", @@ -4464,11 +4481,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", ] @@ -4553,7 +4570,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", ] @@ -4849,18 +4866,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]] @@ -5109,7 +5125,7 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", "bytes 1.4.0", @@ -5376,7 +5392,7 @@ version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c49596760fce12ca21550ac21dc5a9617b2ea4b6e0aa7d8dab8ff2824fc2bba" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -5412,7 +5428,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -5638,22 +5654,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", @@ -5675,7 +5696,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", @@ -5710,9 +5731,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" @@ -5744,7 +5765,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", @@ -5754,16 +5775,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]] @@ -5849,7 +5869,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.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "futures 0.3.28", "pin-project", @@ -6013,7 +6033,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", @@ -6453,7 +6473,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", @@ -6731,20 +6751,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", @@ -6952,6 +6972,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" @@ -7265,6 +7295,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trading_api" +version = "0.1.0" +dependencies = [ + "common", + "derive_more", + "enum_derives", + "ethereum-types", + "lazy_static", + "mm2_core", + "mm2_err_handle", + "mm2_net", + "mm2_number", + "mocktopus", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "trezor" version = "0.1.1" @@ -7751,6 +7801,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" @@ -7867,15 +7927,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]] @@ -7924,6 +8000,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" @@ -7954,6 +8039,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" @@ -7967,16 +8068,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" @@ -7991,16 +8092,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" @@ -8015,16 +8116,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" @@ -8039,16 +8146,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" @@ -8062,6 +8169,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" @@ -8075,16 +8188,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" @@ -8098,6 +8211,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" @@ -8191,7 +8310,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", @@ -8216,7 +8335,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", @@ -8238,7 +8357,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", @@ -8254,7 +8373,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", @@ -8268,7 +8387,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", @@ -8298,7 +8417,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/Cargo.toml b/Cargo.toml index ab18c83da1..507c2e5c31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ members = [ "mm2src/proxy_signature", "mm2src/rpc_task", "mm2src/trezor", + "mm2src/trading_api", ] exclude = [ 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/adex_cli/src/scenarios/init_mm2_cfg.rs b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs index d58b035f73..1fef03bf30 100644 --- a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs +++ b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs @@ -107,7 +107,7 @@ impl Mm2Cfg { self.dbdir = CustomType::>::new("What is dbdir") .with_placeholder(DEFAULT_OPTION_PLACEHOLDER) - .with_help_message("AtomicDEX API database path. Optional, defaults to a subfolder named DB in the path of your mm2 binary") + .with_help_message("Komodo DeFi Framework database path. Optional, defaults to a subfolder named DB in the path of your mm2 binary") .with_validator(is_reachable_dir) .prompt() .map_err(|error| @@ -128,7 +128,7 @@ impl Mm2Cfg { fn inquire_net_id(&mut self) -> Result<()> { self.netid = CustomType::::new("What is the network `mm2` is going to be a part, netid:") .with_default(DEFAULT_NET_ID) - .with_help_message(r#"Network ID number, telling the AtomicDEX API which network to join. 8762 is the current main network, though alternative netids can be used for testing or "private" trades"#) + .with_help_message(r#"Network ID number, telling the Komodo DeFi Framework which network to join. 8762 is the current main network, though alternative netids can be used for testing or "private" trades"#) .with_placeholder(format!("{DEFAULT_NET_ID}").as_str()) .prompt() .map_err(|error| @@ -268,7 +268,7 @@ impl Mm2Cfg { .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) .with_default_value_formatter(DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER) .with_default(InquireOption::None) - .with_help_message("If false the AtomicDEX API will allow rpc methods sent from external IP addresses. Optional, defaults to true. Warning: Only use this if you know what you are doing, and have put the appropriate security measures in place.") + .with_help_message("If false the Komodo DeFi Framework will allow rpc methods sent from external IP addresses. Optional, defaults to true. Warning: Only use this if you know what you are doing, and have put the appropriate security measures in place.") .prompt() .map_err(|error| error_anyhow!("Failed to get rpc_local_only: {error}") @@ -283,7 +283,7 @@ impl Mm2Cfg { .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) .with_default_value_formatter(DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER) .with_default(InquireOption::None) - .with_help_message("Runs AtomicDEX API as a seed node mode (acting as a relay for AtomicDEX API clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (8762) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other AtomicDEX API clients using the same netID.") + .with_help_message("Runs Komodo DeFi Framework as a seed node mode (acting as a relay for Komodo DeFi Framework clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (8762) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other Komodo DeFi Framework clients using the same netID.") .prompt() .map_err(|error| error_anyhow!("Failed to get i_am_a_seed: {error}") diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 290a0dd7f5..b75e994b96 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -69,7 +69,7 @@ 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" @@ -110,9 +110,9 @@ uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } # We don't need the default web3 features at all since we added our own web3 transport using shared HYPER instance. web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.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" @@ -125,13 +125,14 @@ 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" } @@ -148,18 +149,20 @@ 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" } +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/eth.rs b/mm2src/coins/eth.rs index 7934bdf485..6509027c08 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -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,7 @@ use crypto::privkey::key_pair_from_secret; use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; use derive_more::Display; use enum_derives::EnumFromStringify; + use ethabi::{Contract, Function, Token}; use ethcore_transaction::tx_builders::TxBuilderError; use ethcore_transaction::{Action, TransactionWrapper, TransactionWrapperBuilder as UnSignedEthTxBuilder, @@ -77,7 +78,6 @@ 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::*; @@ -109,30 +109,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, + CoinBalance, 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, 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,14 +153,12 @@ 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}; @@ -524,20 +522,6 @@ pub type Web3RpcFut = Box> pub type Web3RpcResult = Result>; type EthPrivKeyPolicy = PrivKeyPolicy; -#[macro_export] -macro_rules! wei_from_gwei_decimal { - ($big_decimal: expr) => { - $crate::eth::wei_from_big_decimal($big_decimal, $crate::eth::ETH_GWEI_DECIMALS) - }; -} - -#[macro_export] -macro_rules! wei_to_gwei_decimal { - ($gwei: expr) => { - $crate::eth::u256_to_big_decimal($gwei, $crate::eth::ETH_GWEI_DECIMALS) - }; -} - #[derive(Clone, Debug)] pub(crate) struct LegacyGasPrice { pub(crate) gas_price: U256, @@ -582,11 +566,11 @@ impl TryFrom for PayForGasOption { fn try_from(param: PayForGasParams) -> Result { match param { PayForGasParams::Legacy(legacy) => Ok(Self::Legacy(LegacyGasPrice { - gas_price: wei_from_gwei_decimal!(&legacy.gas_price)?, + gas_price: wei_from_gwei_decimal(&legacy.gas_price)?, })), PayForGasParams::Eip1559(eip1559) => Ok(Self::Eip1559(Eip1559FeePerGas { - max_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_from_gwei_decimal(&eip1559.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&eip1559.max_priority_fee_per_gas)?, })), } } @@ -594,7 +578,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), @@ -828,29 +813,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, @@ -891,8 +853,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 @@ -1082,6 +1042,9 @@ impl EthCoinImpl { let guard = self.erc20_tokens_infos.lock().unwrap(); (*guard).clone() } + + #[inline(always)] + pub fn chain_id(&self) -> u64 { self.chain_id } } async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> RawTransactionResult { @@ -1204,8 +1167,8 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; Ok(TransactionNftDetails { - tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), + 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()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc1155, @@ -1296,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()), + 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()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc721, @@ -1511,7 +1474,7 @@ impl SwapOps for EthCoin { _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)); @@ -1533,7 +1496,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 @@ -1583,7 +1546,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!(), @@ -1591,19 +1554,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, } } @@ -2456,7 +2420,7 @@ impl MarketCoinOps for EthCoin { let fut = async move { coin.send_raw_transaction(bytes.into()) .await - .map(|res| format!("{:02x}", res)) + .map(|res| format!("{:02x}", res)) // TODO: add 0x hash (use unified hash format for eth wherever it is returned) .map_err(|e| ERRL!("{}", e)) }; @@ -4762,7 +4726,7 @@ impl EthCoin { self.call(request, Some(BlockId::Number(BlockNumber::Latest))).await } - fn allowance(&self, spender: Address) -> Web3RpcFut { + pub fn allowance(&self, spender: Address) -> Web3RpcFut { let coin = self.clone(); let fut = async move { match coin.coin_type { @@ -4827,7 +4791,7 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - fn approve(&self, spender: Address, amount: U256) -> EthTxFut { + pub fn approve(&self, spender: Address, amount: U256) -> EthTxFut { let coin = self.clone(); let fut = async move { let token_addr = match coin.coin_type { @@ -5369,14 +5333,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()))); @@ -5413,7 +5377,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, @@ -5569,16 +5533,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( @@ -5705,7 +5659,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()) @@ -6194,6 +6148,12 @@ pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu U256::from_dec_str(&amount).map_to_mm(|e| NumConversError::new(format!("{:?}", e))) } +pub fn wei_from_gwei_decimal(bigdec: &BigDecimal) -> NumConversResult { + wei_from_big_decimal(bigdec, ETH_GWEI_DECIMALS) +} + +pub fn wei_to_gwei_decimal(wei: U256) -> NumConversResult { u256_to_big_decimal(wei, ETH_GWEI_DECIMALS) } + impl Transaction for SignedEthTx { fn tx_hex(&self) -> Vec { rlp::encode(self).to_vec() } @@ -6573,7 +6533,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)?; @@ -6601,16 +6560,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 @@ -7419,7 +7374,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 4d33781f39..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::{Web3RpcError, Web3RpcResult}; -use crate::{wei_from_gwei_decimal, wei_to_gwei_decimal, 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..aeeeffb209 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -1,21 +1,50 @@ +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::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_number::BigDecimal; + 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 futures::{channel::oneshot, stream::FuturesUnordered, StreamExt}; use instant::Instant; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::MmError; -use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - ErrorEventName, Event, EventName, EventStreamConfiguration}; -use mm2_number::BigDecimal; +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_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 922e219fbd..3dc6711126 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -241,8 +241,8 @@ impl EthCoin { .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } - /// Get chain id - pub(crate) async fn chain_id(&self) -> Result { + /// Get chain id from network + pub(crate) async fn network_chain_id(&self) -> Result { self.try_rpc_send("eth_chainId", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 19f45d10ae..f1bd022ba9 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,13 +1,13 @@ use super::*; use crate::IguanaPrivKey; -use common::{block_on, block_on_f01}; +use common::block_on; use mm2_core::mm_ctx::MmCtxBuilder; cfg_native!( use crate::eth::for_tests::{eth_coin_for_test, eth_coin_from_keypair}; use crate::DexFee; - use common::now_sec; + 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, ETH_SEPOLIA_TOKEN_CONTRACT}; @@ -216,16 +216,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 +262,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(); @@ -1040,3 +1034,10 @@ fn test_gas_limit_conf() { && eth_coin.gas_limit.eth_max_trade_gas == 150_000 ); } + +#[test] +fn test_h256_to_str() { + let h = H256::from_str("5136701f11060010841c9708c3eb26f6606a070b8ae43f4b98b6d7b10a545258").unwrap(); + let b: BytesJson = h.0.to_vec().into(); + 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..fd98b99a08 --- /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 futures::channel::oneshot; +use instant::Instant; +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/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index cee2313ba2..5ba8811954 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -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..6a7d084d54 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -11,7 +11,6 @@ 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 futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::oneshot; @@ -25,6 +24,7 @@ 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/storage/sqlite_storage.rs b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs index 898f4c8823..f430eac042 100644 --- a/mm2src/coins/hd_wallet/storage/sqlite_storage.rs +++ b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs @@ -101,7 +101,7 @@ impl HDWalletStorageInternalOps for HDWalletSqliteStorage { where Self: Sized, { - let shared = ctx.shared_sqlite_conn.as_option().or_mm_err(|| { + let shared = ctx.shared_sqlite_conn.get().or_mm_err(|| { HDWalletStorageError::Internal("'MmCtx::shared_sqlite_conn' is not initialized".to_owned()) })?; let storage = HDWalletSqliteStorage { diff --git a/mm2src/coins/hd_wallet/withdraw_ops.rs b/mm2src/coins/hd_wallet/withdraw_ops.rs index 7f1aa8b19c..b7bd7e04da 100644 --- a/mm2src/coins/hd_wallet/withdraw_ops.rs +++ b/mm2src/coins/hd_wallet/withdraw_ops.rs @@ -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. diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 67b27ba8f4..a4b604975c 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -15,8 +15,8 @@ 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, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, @@ -27,7 +27,8 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFut, + WithdrawRequest}; use async_trait::async_trait; use bitcoin::bech32::ToBase32; use bitcoin::hashes::Hash; @@ -73,7 +74,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; @@ -793,13 +794,13 @@ impl SwapOps for LightningCoin { _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 +858,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] @@ -1046,7 +1047,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 +1060,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) } @@ -1261,7 +1264,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/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 5b4ac5698d..79868908fa 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -87,6 +87,7 @@ pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult std::fmt::Result { write!(f, "{}", self.get_plain_text_format()) } +} + #[derive(Debug, PartialEq)] pub enum FoundSwapTxSpend { Spent(TransactionEnum), @@ -1116,7 +1117,7 @@ pub trait SwapOps { secret_hash: &[u8], spend_tx: &[u8], watcher_reward: bool, - ) -> Result, String>; + ) -> Result<[u8; 32], String>; fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result>; @@ -1148,7 +1149,7 @@ 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>; @@ -2109,7 +2110,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, @@ -2132,6 +2133,7 @@ pub struct WithdrawRequest { #[serde(tag = "type")] pub enum StakingDetails { Qtum(QtumDelegationRequest), + Cosmos(Box), } #[allow(dead_code)] @@ -2145,6 +2147,7 @@ pub struct AddDelegateRequest { #[derive(Deserialize)] pub struct RemoveDelegateRequest { pub coin: String, + pub staking_details: Option, } #[derive(Deserialize)] @@ -2170,15 +2173,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() } } } @@ -2760,6 +2757,24 @@ 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 = "{}", _0)] CannotInteractWithSmartContract(String), #[from_stringify("ScriptHashTypeNotSupported")] @@ -2771,6 +2786,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), @@ -3288,8 +3305,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; @@ -3415,43 +3432,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 { @@ -3705,11 +3685,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")] @@ -4851,12 +4831,35 @@ 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(), + }) + }, }, } } @@ -4875,17 +4878,26 @@ pub async fn get_staking_infos(ctx: MmArc, req: GetStakingInfosRequest) -> Staki pub async fn add_delegation(ctx: MmArc, req: AddDelegateRequest) -> DelegationResult { 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.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 + }, } } @@ -5677,7 +5689,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 @@ -5689,17 +5701,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/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index b10f68d05d..e1412933d4 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -734,14 +734,15 @@ impl NftCtx { /// If an `NftCtx` instance doesn't already exist in the MM context, it gets created and cached for subsequent use. #[cfg(not(target_arch = "wasm32"))] pub(crate) fn from_ctx(ctx: &MmArc) -> Result, String> { - Ok(try_s!(from_ctx(&ctx.nft_ctx, move || { + from_ctx(&ctx.nft_ctx, move || { let async_sqlite_connection = ctx .async_sqlite_connection + .get() .ok_or("async_sqlite_connection is not initialized".to_owned())?; Ok(NftCtx { nft_cache_db: async_sqlite_connection.clone(), }) - }))) + }) } #[cfg(target_arch = "wasm32")] diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index df14fea09a..8cbf8a8786 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -16,20 +16,21 @@ 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, 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, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, + WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -992,7 +993,7 @@ impl SwapOps for Qrc20Coin { secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { self.extract_secret_impl(secret_hash, spend_tx) } @@ -1034,7 +1035,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) } @@ -1287,7 +1288,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..23977f001b 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -88,13 +88,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 +128,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(); @@ -481,8 +474,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 +498,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 +562,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 b62e572756..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::{EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; -use crate::{lp_coinfind_or_err, wei_to_gwei_decimal, 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..0b67aad92b 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 }) } 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..3ca77b0295 --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -0,0 +1,164 @@ +use common::{HttpStatusCode, PagingOptions, StatusCode}; +use cosmrs::staking::{Commission, Description, Validator}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; +use mm2_number::BigDecimal; + +use crate::{hd_wallet::WithdrawFrom, lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum, WithdrawFee}; + +/// Represents current status of the validator. +#[derive(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(Deserialize)] +pub struct ValidatorsRPC { + #[serde(rename = "ticker")] + coin: String, + #[serde(flatten)] + paging: PagingOptions, + #[serde(default)] + filter_by_status: ValidatorStatus, +} + +#[derive(Clone, Serialize)] +pub struct ValidatorsRPCResponse { + validators: Vec, +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ValidatorsRPCError { + #[display(fmt = "Coin '{ticker}' could not be found in coins configuration.")] + CoinNotFound { ticker: String }, + #[display(fmt = "'{ticker}' is not a Cosmos coin.")] + UnexpectedCoinType { ticker: String }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for ValidatorsRPCError { + fn status_code(&self) -> common::StatusCode { + match self { + ValidatorsRPCError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE, + ValidatorsRPCError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ValidatorsRPCError::CoinNotFound { .. } => StatusCode::NOT_FOUND, + ValidatorsRPCError::UnexpectedCoinType { .. } => StatusCode::BAD_REQUEST, + } + } +} + +impl From for ValidatorsRPCError { + fn from(e: TendermintCoinRpcError) -> Self { + match e { + TendermintCoinRpcError::InvalidResponse(e) + | TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) => ValidatorsRPCError::Transport(e), + TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => ValidatorsRPCError::InternalError(e), + TendermintCoinRpcError::UnexpectedAccountType { .. } => ValidatorsRPCError::InternalError( + "RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal." + .into(), + ), + } + } +} + +pub async fn validators_rpc( + ctx: MmArc, + req: ValidatorsRPC, +) -> 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 lp_coinfind_or_err(&ctx, &req.coin).await { + Ok(MmCoinEnum::Tendermint(coin)) => coin.validators_list(req.filter_by_status, req.paging).await?, + Ok(MmCoinEnum::TendermintToken(token)) => { + token + .platform_coin + .validators_list(req.filter_by_status, req.paging) + .await? + }, + Ok(_) => return MmError::err(ValidatorsRPCError::UnexpectedCoinType { ticker: req.coin }), + Err(_) => return MmError::err(ValidatorsRPCError::CoinNotFound { ticker: req.coin }), + }; + + Ok(ValidatorsRPCResponse { + 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, +} diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index bc57aaaf10..46fe63deba 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1,17 +1,16 @@ 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, - TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, ValidatePaymentResult, ValidateWatcherSpendInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawRequest}; +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, + DexFee, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicy, + RawTransactionResult, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, + SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, ValidatePaymentResult, ValidateWatcherSpendInput, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use common::executor::AbortedError; pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature}; @@ -213,7 +212,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!() } @@ -487,7 +486,7 @@ impl SwapOps for SiaCoin { _secret_hash: &[u8], _spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { unimplemented!() } @@ -504,7 +503,7 @@ impl SwapOps for SiaCoin { 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!() } diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index 78009b5db8..9f9a5b29a9 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; 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..cf20c37e81 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,6 +6,7 @@ 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::{DelegationPayload, ValidatorStatus}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -14,7 +15,7 @@ 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, + CoinBalance, ConfirmPaymentInput, DelegationError, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, @@ -27,7 +28,7 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; + WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; use bip32::DerivationPath; @@ -35,17 +36,21 @@ 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 common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY}; use cosmrs::bank::MsgSend; use cosmrs::crypto::secp256k1::SigningKey; use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}; use cosmrs::proto::cosmos::bank::v1beta1::{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::staking::v1beta1::{QueryDelegationRequest, QueryDelegationResponse, QueryValidatorsRequest, + QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, 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; @@ -73,7 +78,7 @@ 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; @@ -89,6 +94,8 @@ 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"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -373,7 +380,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, } @@ -423,6 +430,8 @@ pub enum TendermintInitErrorKind { 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 +458,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 +483,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")) @@ -775,7 +790,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 +840,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 +871,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 +881,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 +906,7 @@ impl TendermintCoin { tx_payload: Any, fee: Fee, timeout_height: u64, - memo: String, + memo: &str, timeout: Duration, ) -> Result<(String, Raw), TransactionErr> { #[derive(Deserialize)] @@ -935,7 +950,7 @@ 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 { @@ -953,13 +968,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,7 +1029,7 @@ 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 { @@ -1031,7 +1040,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( @@ -1138,11 +1147,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 +1160,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 +1190,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 +1280,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 +1295,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 +1327,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 +1346,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 @@ -1515,7 +1523,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 +1534,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 @@ -1572,7 +1580,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 +1590,7 @@ impl TendermintCoin { tx_payload.clone(), fee.clone(), timeout_height, - memo.clone(), + &memo, Duration::from_secs(timeout) ) .await @@ -1822,7 +1830,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 +1881,7 @@ impl TendermintCoin { self.activation_policy.activated_key(), msg_send, timeout_height, - TX_DEFAULT_MEMO.to_owned(), + TX_DEFAULT_MEMO, None, ) .await?; @@ -2080,6 +2088,390 @@ impl TendermintCoin { None } + + 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, + }), + }; + + 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?; + + 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(typed_response.validators) + } + + 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() + } + + /// 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, + }; + + if is_max { + if balance_u64 < fee_u64 { + return Err(not_sufficient(fee_decimal)); + } + + let amount_u64 = balance_u64 - fee_u64; + return Ok((amount_u64, balance_decimal)); + } + + let total = &request_amount + &fee_decimal; + if balance_decimal < total { + return Err(not_sufficient(total)); + } + + let amount_u64 = sat_from_big_decimal(&request_amount, coin.decimals) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + Ok((amount_u64, total)) + } + + let validator_address = + AccountId::from_str(&req.validator_address).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; + + let (delegator_address, maybe_priv_key) = self + .extract_account_id_and_private_key(req.withdraw_from) + .map_err(|e| DelegationError::InternalError(e.to_string()))?; + + let (balance_u64, balance_dec) = self + .get_balance_as_unsigned_and_decimal(&delegator_address, &self.denom, self.decimals()) + .await?; + + 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()))? + }; + + // 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 timeout_height = self + .current_block() + .compat() + .await + .map_to_mm(DelegationError::Transport)? + + TIMEOUT_HEIGHT_DELTA; + + // `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); + + 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), + }) + } + + pub(crate) 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)) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { @@ -2171,7 +2563,7 @@ impl MmCoin for TendermintCoin { wallet_only_conf || self.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 coin = self.clone(); @@ -2181,7 +2573,9 @@ impl MmCoin for TendermintCoin { let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); - let (account_id, maybe_pk) = coin.account_id_and_pk_for_withdraw(req.from)?; + 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()) @@ -2244,10 +2638,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 +2701,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 = { @@ -2359,8 +2753,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(), @@ -2764,13 +3162,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 +3171,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 +3218,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 +3227,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 @@ -2920,7 +3308,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,7 +3318,7 @@ impl SwapOps for TendermintCoin { )); let htlc = try_s!(ClaimHtlcMsg::try_from(htlc_proto)); - Ok(try_s!(hex::decode(htlc.secret()))) + Ok(try_s!(try_s!(hex::decode(htlc.secret())).as_slice().try_into())) } fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { @@ -2957,17 +3345,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> { @@ -3105,7 +3496,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()), })?; @@ -3454,7 +3845,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 +3856,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 +3885,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), ); diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index e5cc90f895..8eeb0cd09b 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -6,18 +6,19 @@ 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, + CheckIfMyPaymentSentArgs, CoinBalance, 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}; + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFut, + WithdrawRequest}; use crate::{DexFee, MmCoinEnum, PaymentInstructionArgs, ValidateWatcherSpendInput, WatcherReward, WatcherRewardError}; use async_trait::async_trait; use bitcrypto::sha256; @@ -238,7 +239,7 @@ 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 @@ -271,7 +272,7 @@ 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) } @@ -500,7 +501,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 +512,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 +595,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 +623,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 = { diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 3dc95e7443..22caa0554b 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; @@ -37,6 +39,8 @@ const CLAIM_HTLC_EVENT: &str = "claim_htlc"; 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 ACCEPTED_EVENTS: &[&str] = &[ TRANSFER_EVENT, @@ -45,6 +49,8 @@ const ACCEPTED_EVENTS: &[&str] = &[ IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT, + DELEGATE_EVENT, + UNDELEGATE_EVENT, ]; const RECEIVER_TAG_KEY: &str = "receiver"; @@ -56,6 +62,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"; @@ -131,7 +143,7 @@ impl CoinWithTxHistoryV2 for TendermintToken { _target: MyTxHistoryTarget, ) -> MmResult { let denom_hash = sha256(self.denom.to_string().as_bytes()); - let id = H256::from(denom_hash.as_slice()); + let id = H256::from(denom_hash.take()); Ok(GetTxHistoryFilters::for_address(self.platform_coin.account_id.to_string()).with_token_id(id.to_string())) } @@ -140,6 +152,7 @@ impl CoinWithTxHistoryV2 for TendermintToken { struct TendermintTxHistoryStateMachine { coin: Coin, storage: Storage, + streaming_manager: StreamingManager, balances: AllBalancesResult, last_received_page: u32, last_spent_page: u32, @@ -403,6 +416,8 @@ where ClaimHtlc, IBCSend, IBCReceive, + Delegate, + Undelegate, } #[derive(Clone)] @@ -470,96 +485,158 @@ where 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 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 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 amount_with_denoms = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + AMOUNT_TAG_KEY, + AMOUNT_TAG_KEY_BASE64 + )); - 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(), - }; + 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()..]; + 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 { + from, + to, + denom: denom.to_owned(), + 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) = 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); + } + + 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); - } + 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 { + from, + to, + denom: denom.to_owned(), + amount, + transfer_event_type: TransferEventType::Delegate, + }; + + 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 - }); + 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 { + from, + to: String::default(), + denom: denom.to_owned(), + amount: 0, + transfer_event_type: TransferEventType::Undelegate, + }; + + 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 => { + log::warn!( + "Found an unrecognized event '{unrecognized}' in transaction history processing." + ); + }, + }; } } transfer_details_list } + 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(tx_events: Vec, fee_amount_with_denom: String) -> Vec { // 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 + let is_undelegate_tx = events.iter().any(|e| e.kind == UNDELEGATE_EVENT); + events.retain(|event| { + // We only interested `UNDELEGATE_EVENT` events for undelegation transactions, + // so we drop the rest. + if is_undelegate_tx && event.kind != UNDELEGATE_EVENT { + return false; + } + + // 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 }); } @@ -584,6 +661,8 @@ where }, token_id, }, + (TransferEventType::Delegate, _) => TransactionType::StakingDelegation, + (TransferEventType::Undelegate, _) => TransactionType::RemoveDelegation, (_, Some(token_id)) => TransactionType::TokenTransfer(token_id), _ => TransactionType::StandardTransfer, } @@ -604,9 +683,13 @@ where } }, TransferEventType::ClaimHtlc => Some((vec![my_address], vec![])), - TransferEventType::Standard | TransferEventType::IBCSend | TransferEventType::IBCReceive => { + TransferEventType::Standard + | TransferEventType::IBCSend + | TransferEventType::IBCReceive + | TransferEventType::Delegate => { Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()])) }, + TransferEventType::Undelegate => Some((vec![my_address], vec![])), } } @@ -614,6 +697,7 @@ where address: String, coin: &Coin, storage: &Storage, + streaming_manager: &StreamingManager, query: String, from_height: u64, page: &mut u32, @@ -703,8 +787,28 @@ where 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 len = internal_id_hash.len(); + // Todo: This truncates `internal_id_hash` to 32 bytes instead of using all 33 bytes (index + tx_hash). + // This is a limitation kept for backward compatibility. Changing to 33 bytes would + // alter the internal_id calculation, causing existing wallets to see duplicate transactions + // in their history. A proper migration would be needed to safely transition to using the full 33 bytes. + let internal_id_hash: [u8; 32] = match internal_id_hash + .get(..32) + .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 = H256::from(internal_id_hash.as_slice()).reversed().to_vec().into(); + let internal_id = H256::from(internal_id_hash).reversed().to_vec().into(); if let Ok(Some(_)) = storage .get_tx_from_history(&coin.history_wallet_id(), &internal_id) @@ -744,7 +848,7 @@ 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()) + Some(H256::from(denom_hash.take()).to_vec().into()) }, false => None, }; @@ -786,7 +890,7 @@ where fee_tx_details.my_balance_change = BigDecimal::default() - &fee_details.amount; fee_tx_details.coin = coin.platform_ticker().to_string(); // 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 = H256::from(internal_id_hash).to_vec().into(); fee_tx_details.transaction_type = TransactionType::FeeForTokenTx; tx_details.push(fee_tx_details); @@ -797,6 +901,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 +932,7 @@ where self.address.clone(), &ctx.coin, &ctx.storage, + &ctx.streaming_manager, q, self.from_block_height, &mut ctx.last_spent_page, @@ -844,6 +955,7 @@ where self.address.clone(), &ctx.coin, &ctx.storage, + &ctx.streaming_manager, q, self.from_block_height, &mut ctx.last_received_page, @@ -957,7 +1069,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 +1083,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..308868dc3c 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -4,20 +4,20 @@ use super::{CoinBalance, CommonSwapOpsV2, FundingTxSpend, HistorySyncState, Mark RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee, TransactionEnum, TransactionFut, WaitForPaymentSpendError}; 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, - ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, 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, ValidateTakerFundingSpendPreimageResult, + ValidateTakerPaymentSpendPreimageResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WeakSpawner, WithdrawFut, WithdrawRequest}; use crate::{DexFee, ToBytes, ValidateWatcherSpendInput}; use async_trait::async_trait; use common::executor::AbortedError; @@ -196,7 +196,7 @@ impl SwapOps for TestCoin { secret_hash: &[u8], spend_tx: &[u8], watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { unimplemented!() } @@ -213,7 +213,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!() } @@ -343,7 +343,7 @@ impl WatcherOps for TestCoin { 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!() } diff --git a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs index cf0575f973..f5b1312a65 100644 --- a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -376,11 +376,12 @@ pub struct SqliteTxHistoryStorage(Arc>); impl SqliteTxHistoryStorage { pub fn new(ctx: &MmArc) -> Result> { - let sqlite_connection = ctx - .sqlite_connection - .ok_or(MmError::new(CreateTxHistoryStorageError::Internal( - "sqlite_connection is not initialized".to_owned(), - )))?; + let sqlite_connection = + ctx.sqlite_connection + .get() + .ok_or(MmError::new(CreateTxHistoryStorageError::Internal( + "sqlite_connection is not initialized".to_owned(), + )))?; Ok(SqliteTxHistoryStorage(sqlite_connection.clone())) } } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 6d98451c7f..c78d927e6d 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,12 +103,12 @@ 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::utxo::tx_cache::UtxoVerboseCacheShared; @@ -144,9 +145,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 +602,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)] @@ -1158,7 +1155,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 +1302,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 +1316,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..1a37738db0 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -45,8 +45,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 +83,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 +162,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 +640,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) @@ -1002,7 +1007,7 @@ impl SwapOps for BchCoin { secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -1033,7 +1038,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) } @@ -1284,7 +1289,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 +1415,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..dc5fc2a934 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -488,7 +488,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 @@ -642,7 +642,7 @@ impl SwapOps for QtumCoin { secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -673,7 +673,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) } @@ -904,7 +904,7 @@ impl MarketCoinOps for QtumCoin { 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/rpc_clients/electrum_rpc/client.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs index ce0498cc31..84e39997e4 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; 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,12 +179,25 @@ 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))?; - } + self.streaming_manager + .send( + &UtxoBalanceEventStreamer::derive_streamer_id(&self.coin_ticker), + ScripthashNotification::SubscribeToAddresses(addresses), + ) + .map_err(|e| ERRL!("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> { + self.streaming_manager + .send( + &UtxoBalanceEventStreamer::derive_streamer_id(&self.coin_ticker), + ScripthashNotification::Triggered(script_hash), + ) + .map_err(|e| ERRL!("Failed sending scripthash message. {:?}", e))?; Ok(()) } @@ -203,9 +210,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 +220,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 +279,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..da3ed33ea5 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/connection.rs @@ -6,12 +6,12 @@ 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}; @@ -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/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..695e1c6889 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -13,9 +13,9 @@ 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, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, 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, @@ -25,7 +25,7 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use base64::engine::general_purpose::STANDARD; @@ -54,7 +54,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; @@ -922,7 +922,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 +936,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 +1130,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 @@ -1415,7 +1416,7 @@ impl SwapOps for SlpToken { secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -1439,7 +1440,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) } @@ -1601,7 +1602,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 +1645,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( 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..5de90555e4 100644 --- a/mm2src/coins/utxo/utxo_balance_events.rs +++ b/mm2src/coins/utxo/utxo_balance_events.rs @@ -1,19 +1,18 @@ -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 mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; use std::collections::{BTreeMap, HashMap, HashSet}; macro_rules! try_or_continue { @@ -28,14 +27,37 @@ macro_rules! try_or_continue { }; } +pub struct UtxoBalanceEventStreamer { + coin: UtxoStandardCoin, +} + +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), + } + } + + pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } +} + #[async_trait] -impl EventBehaviour for UtxoStandardCoin { - fn event_name() -> EventName { EventName::CoinBalance } +impl EventStreamer for UtxoBalanceEventStreamer { + type DataInType = ScripthashNotification; - fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } + fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } - async fn handle(self, _interval: f64, tx: oneshot::Sender) { + 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; async fn subscribe_to_addresses( utxo: &UtxoCoinFields, @@ -66,44 +88,25 @@ impl EventBehaviour for UtxoStandardCoin { } } - 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); - }, - }; - - 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); - }, - }; + if coin.as_ref().rpc_client.is_native() { + let msg = "Native RPC client is not supported for UtxoBalanceEventStreamer."; + ready_tx.send(Err(msg.to_string())).expect(RECEIVER_DROPPED_MSG); + panic!("{}", msg); + } - tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + ready_tx.send(Ok(())).expect(RECEIVER_DROPPED_MSG); 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 { + match subscribe_to_addresses(coin.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; + broadcaster.broadcast(Event::err(streamer_id.clone(), json!({ "error": e }))); }, }; @@ -113,7 +116,7 @@ impl EventBehaviour for UtxoStandardCoin { 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 +149,26 @@ 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; - } - } - - 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 + broadcaster.broadcast(Event::new(streamer_id.clone(), json!(vec![payload]))); } } } diff --git a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs index 89266af2f6..c065016176 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs @@ -27,7 +27,7 @@ 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.ok_or(BlockHeaderStorageError::Internal( + let sqlite_connection = ctx.sqlite_connection.get().ok_or(BlockHeaderStorageError::Internal( "sqlite_connection is not initialized".to_owned(), ))?; Ok(BlockHeaderStorage { @@ -50,8 +50,11 @@ impl BlockHeaderStorage { use db_common::sqlite::rusqlite::Connection; use std::sync::{Arc, Mutex}; - let conn = Arc::new(Mutex::new(Connection::open_in_memory().unwrap())); - let conn = ctx.sqlite_connection.clone_or(conn); + let conn = ctx + .sqlite_connection + .get() + .cloned() + .unwrap_or_else(|| Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); Ok(BlockHeaderStorage { inner: Box::new(SqliteBlockHeadersStorage { ticker, conn }), 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..2722dfcdeb 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -60,6 +60,7 @@ 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; use utxo_signer::with_key_pair::{calc_and_sign_sighash, p2sh_spend, signature_hash_to_sign, SIGHASH_ALL, @@ -2042,19 +2043,26 @@ pub fn watcher_validate_taker_fee( 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 { @@ -2582,11 +2590,12 @@ 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))); 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() { @@ -2596,7 +2605,7 @@ pub fn extract_secret(secret_hash: &[u8], spend_tx: &[u8]) -> Result, St 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())); } } } @@ -2644,7 +2653,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 +2798,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) @@ -2980,9 +3000,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 +3357,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 +3391,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 +3555,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, }); @@ -4748,8 +4772,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> { 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_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index f5a02f5095..401be98080 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -281,7 +281,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 @@ -435,7 +435,7 @@ impl SwapOps for UtxoStandardCoin { secret_hash: &[u8], spend_tx: &[u8], _watcher_reward: bool, - ) -> Result, String> { + ) -> Result<[u8; 32], String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -466,7 +466,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) } @@ -977,7 +977,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..188ce519d4 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,17 @@ 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_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 +87,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 +105,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 +124,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 +153,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 +484,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 +606,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 +652,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 +701,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 +752,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 +803,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 +841,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 +895,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 +908,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 +986,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 +1521,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(), )) }, @@ -1624,13 +1602,13 @@ fn test_spam_rick() { fn test_one_unavailable_electrum_proto_version() { // Patch the electurm client construct to require protocol version 1.4 only. 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(), )) }, @@ -1933,7 +1911,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 +2500,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))) }); @@ -3204,13 +3181,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 +3235,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 +3291,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 +3347,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 +3398,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 +4037,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 +4179,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 +4879,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..3f44593643 100644 --- a/mm2src/coins/utxo/utxo_tx_history_v2.rs +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -4,16 +4,18 @@ use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, TxHistoryStor 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/z_coin.rs b/mm2src/coins/z_coin.rs index b8c7c8c944..b2b9f34e72 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -1,7 +1,10 @@ pub mod storage; -mod z_balance_streaming; +pub mod tx_history_events; +#[cfg_attr(not(target_arch = "wasm32"), cfg(test))] +mod tx_streaming_tests; +pub mod z_balance_streaming; mod z_coin_errors; -#[cfg(all(test, feature = "zhtlc-native-tests"))] +#[cfg(all(test, not(target_arch = "wasm32"), feature = "zhtlc-native-tests"))] mod z_coin_native_tests; mod z_htlc; mod z_rpc; @@ -24,11 +27,11 @@ use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxDa UtxoCommonOps, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; use crate::utxo::{UnsupportedAddr, UtxoFeeDetails}; use crate::z_coin::storage::{BlockDbImpl, WalletDbShared}; -use crate::z_coin::z_balance_streaming::ZBalanceEventHandler; + use crate::z_coin::z_tx_history::{fetch_tx_history_from_db, ZCoinTxHistoryItem}; -use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, - DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, - MmCoinEnum, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, + NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyActivationPolicy, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, @@ -39,15 +42,14 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Coi ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; + WatcherValidateTakerFeeInput, WeakSpawner, WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcrypto::dhash256; use chain::constants::SEQUENCE_FINAL; use chain::{Transaction as UtxoTx, TransactionOutput}; -use common::calc_total_pages; use common::executor::{AbortableSystem, AbortedError}; -use common::{log, one_thousand_u32}; +use common::{calc_total_pages, log}; use crypto::privkey::{key_pair_from_secret, secp_privkey_from_hash}; use crypto::HDPathToCoin; use crypto::{Bip32DerPathOps, GlobalHDAccountArc}; @@ -59,7 +61,6 @@ use keys::hash::H256; use keys::{KeyPair, Message, Public}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; use primitives::bytes::Bytes; @@ -70,6 +71,7 @@ use serialization::CoinVariant; use std::collections::{HashMap, HashSet}; use std::convert::TryInto; use std::iter; +use std::num::NonZeroU32; use std::num::TryFromIntError; use std::path::PathBuf; use std::sync::Arc; @@ -93,6 +95,8 @@ use zcash_primitives::{constants::mainnet as z_mainnet_constants, sapling::Payme zip32::ExtendedFullViewingKey, zip32::ExtendedSpendingKey}; use zcash_proofs::prover::LocalTxProver; +use self::storage::store_change_output; + cfg_native!( use common::{async_blocking, sha256_digest}; use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; @@ -209,7 +213,6 @@ pub struct ZCoinFields { light_wallet_db: WalletDbShared, consensus_params: ZcoinConsensusParams, sync_state_connector: AsyncMutex, - z_balance_event_handler: Option, } impl Transaction for ZTransaction { @@ -474,6 +477,12 @@ impl ZCoin { .await? .tx_result?; + // Store any change outputs we created in this transaction by decrypting them with our keys + // and saving them to the wallet database for future spends + store_change_output(self.consensus_params_ref(), &self.z_fields.light_wallet_db, &tx) + .await + .map_to_mm(GenTxError::SaveChangeNotesError)?; + let additional_data = AdditionalTxData { received_by_me, spent_by_me: sat_from_big_decimal(&total_input_amount, self.decimals())?, @@ -520,7 +529,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 +541,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 +631,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 +650,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 { @@ -660,17 +667,6 @@ impl ZCoin { paging_options: request.paging_options, }) } - - 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); - } - } - - Ok(()) - } } impl AsRef for ZCoin { @@ -768,19 +764,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, @@ -897,24 +912,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 +931,6 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { sync_params, skip_sync_params.unwrap_or_default(), &z_spending_key, - z_balance_event_sender, ) .await? }, @@ -945,16 +946,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 }) } } @@ -1513,7 +1507,7 @@ 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) } @@ -1542,12 +1536,16 @@ 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] @@ -1677,7 +1675,7 @@ impl WatcherOps for ZCoin { 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..dd3042a169 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::*; @@ -9,8 +10,9 @@ pub mod walletdb; pub(crate) use z_params::ZcashParamsWasmImpl; pub use walletdb::*; +use zcash_extras::wallet::decrypt_and_store_transaction; +use zcash_primitives::transaction::Transaction; -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 +62,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 +121,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 +186,22 @@ 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) +} + +/// Processes and stores any change outputs created in the transaction by: +/// - Decrypting outputs using wallet viewing keys +/// - Adding decrypted change notes to the wallet database +/// - Making change notes available for future spends +pub(crate) async fn store_change_output( + params: &ZcoinConsensusParams, + shared_db: &WalletDbShared, + tx: &Transaction, +) -> Result<(), String> { + let mut data = try_s!(shared_db.db.get_update_ops()); + try_s!(decrypt_and_store_transaction(params, &mut data, tx).await); + + Ok(()) } 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 7523360807..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::*; @@ -49,10 +50,9 @@ impl BlockDbImpl { async_blocking(move || { let conn = Connection::open(path).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; let conn = Arc::new(Mutex::new(conn)); - let conn_clone = conn.clone(); - let conn_clone = conn_clone.lock().unwrap(); - run_optimization_pragmas(&conn_clone).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; - conn_clone + let conn_lock = conn.lock().unwrap(); + run_optimization_pragmas(&conn_lock).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + conn_lock .execute( "CREATE TABLE IF NOT EXISTS compactblocks ( height INTEGER PRIMARY KEY, @@ -61,6 +61,7 @@ impl BlockDbImpl { [], ) .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + drop(conn_lock); Ok(Self { db: conn, ticker }) }) @@ -73,11 +74,12 @@ impl BlockDbImpl { async_blocking(move || { let conn = ctx .sqlite_connection - .clone_or(Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); - let conn_clone = conn.clone(); - let conn_clone = conn_clone.lock().unwrap(); - run_optimization_pragmas(&conn_clone).map_err(|err| ZcoinStorageError::DbError(err.to_string()))?; - conn_clone + .get() + .cloned() + .unwrap_or_else(|| Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); + let conn_lock = conn.lock().unwrap(); + run_optimization_pragmas(&conn_lock).map_err(|err| ZcoinStorageError::DbError(err.to_string()))?; + conn_lock .execute( "CREATE TABLE IF NOT EXISTS compactblocks ( height INTEGER PRIMARY KEY, @@ -86,6 +88,7 @@ impl BlockDbImpl { [], ) .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + drop(conn_lock); Ok(BlockDbImpl { db: conn, ticker }) }) @@ -225,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..1b3676dbbd 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, } 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..457162c80f --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/mod.rs @@ -0,0 +1,30 @@ +#[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}; + +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..cc5ecc5812 --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/native.rs @@ -0,0 +1,73 @@ +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; + +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, MarketCoinOps, MmCoin, PrivKeyBuildPolicy}; + +#[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, "0.0001".parse().unwrap(), &[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..192db3c7a9 --- /dev/null +++ b/mm2src/coins/z_coin/tx_streaming_tests/wasm.rs @@ -0,0 +1,74 @@ +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 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() { + 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..f7e3d65a5d 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -129,8 +129,8 @@ pub enum GenTxError { LightClientErr(String), FailedToCreateNote, SpendableNotesError(String), - #[cfg(target_arch = "wasm32")] Internal(String), + SaveChangeNotesError(String), } impl From for GenTxError { @@ -177,9 +177,9 @@ 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(_) + | GenTxError::SaveChangeNotesError(_) => WithdrawError::InternalError(gen_tx.to_string()), } } } @@ -301,6 +301,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..0cde681ee8 100644 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ b/mm2src/coins/z_coin/z_coin_native_tests.rs @@ -13,11 +13,18 @@ use crate::DexFee; use crate::{CoinProtocol, SwapTxTypeWithSecretHash}; use mm2_number::MmNumber; +fn native_zcoin_activation_params() -> ZcoinActivationParams { + ZcoinActivationParams { + mode: ZcoinRpcMode::Native, + ..Default::default() + } +} + #[test] 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(); @@ -76,7 +83,7 @@ fn zombie_coin_send_and_refund_maker_payment() { 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(); @@ -138,7 +145,7 @@ fn zombie_coin_send_and_spend_maker_payment() { 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(); @@ -167,7 +174,7 @@ fn zombie_coin_send_dex_fee() { 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(); @@ -197,7 +204,7 @@ fn prepare_zombie_sapling_cache() { 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(); @@ -280,15 +287,3 @@ fn zombie_coin_validate_dex_fee() { }; block_on(coin.validate_fee(validate_fee_args)).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, - } -} diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index c2fba37a88..bd32389f51 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -5,6 +5,7 @@ // taker payment spend - https://zombie.explorer.lordofthechains.com/tx/af6bb0f99f9a5a070a0c1f53d69e4189b0e9b68f9d66e69f201a6b6d9f93897e // maker payment spend - https://rick.explorer.dexstats.info/tx/6a2dcc866ad75cebecb780a02320073a88bcf5e57ddccbe2657494e7747d591e +use super::storage::store_change_output; use super::{GenTxError, ZCoin}; use crate::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcError}; use crate::utxo::utxo_common::payment_script; @@ -190,6 +191,12 @@ pub async fn z_p2sh_spend( let mut tx_buffer = Vec::with_capacity(1024); zcash_tx.write(&mut tx_buffer)?; + // Store any change outputs we created in this transaction by decrypting them with our keys + // and saving them to the wallet database for future spends + store_change_output(coin.consensus_params_ref(), &coin.z_fields.light_wallet_db, &zcash_tx) + .await + .map_to_mm(|err| ZP2SHSpendError::GenTxError(GenTxError::SaveChangeNotesError(err)))?; + coin.utxo_rpc_client() .send_raw_transaction(tx_buffer.into()) .compat() diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index bd71d554c6..7bfd299bdc 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 { @@ -842,7 +841,7 @@ impl SaplingSyncLoopHandle { 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), ) 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/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 7bc62b444a..88889426e4 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -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; @@ -449,13 +448,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..d62c9ebd8b 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; } @@ -368,22 +368,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 6a5395b360..7f5b4803ed 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] @@ -34,9 +35,10 @@ lazy_static = "1.4" log = "0.4.17" parking_lot = { version = "0.12.0", features = ["nightly"] } 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" diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index de201856d8..d9b984a118 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -128,12 +128,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"] @@ -152,6 +151,7 @@ use futures01::{future, Future}; use http::header::CONTENT_TYPE; use http::Response; use parking_lot::{Mutex as PaMutex, MutexGuard as PaMutexGuard}; +pub use paste::paste; use rand::RngCore; use rand::{rngs::SmallRng, SeedableRng}; use serde::{de, ser}; @@ -161,14 +161,14 @@ use std::convert::TryInto; use std::fmt::Write as FmtWrite; use std::fs::File; use std::future::Future as Future03; -use std::io::{BufReader, Read, Write}; +use std::io::{self, BufReader, Read, Write}; use std::iter::Peekable; use std::mem::{forget, zeroed}; use std::num::{NonZeroUsize, TryFromIntError}; use std::ops::{Add, Deref, Div, RangeInclusive}; use std::os::raw::c_void; use std::panic::{set_hook, PanicInfo}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::ptr::read_volatile; use std::sync::atomic::Ordering; use std::time::{Duration, SystemTime, SystemTimeError}; @@ -791,7 +791,7 @@ pub fn kdf_app_dir() -> Option { } /// Returns path of the coins file. -pub fn kdf_coins_file() -> PathBuf { +pub fn kdf_coins_file() -> Result { #[cfg(not(target_arch = "wasm32"))] let value_from_env = env::var("MM_COINS_PATH").ok(); @@ -802,7 +802,7 @@ pub fn kdf_coins_file() -> PathBuf { } /// Returns path of the config file. -pub fn kdf_config_file() -> PathBuf { +pub fn kdf_config_file() -> Result { #[cfg(not(target_arch = "wasm32"))] let value_from_env = env::var("MM_CONF_PATH").ok(); @@ -818,16 +818,41 @@ pub fn kdf_config_file() -> PathBuf { /// 1- From the environment variable. /// 2- From the current directory where app is called. /// 3- From the root application directory. -pub fn find_kdf_dependency_file(value_from_env: Option, path_leaf: &str) -> PathBuf { +fn find_kdf_dependency_file(value_from_env: Option, path_leaf: &str) -> Result { if let Some(path) = value_from_env { - return PathBuf::from(path); + let path = PathBuf::from(path); + require_file(&path)?; + return Ok(path); } let from_current_dir = PathBuf::from(path_leaf); - if from_current_dir.exists() { + + let path = if from_current_dir.exists() { from_current_dir } else { kdf_app_dir().unwrap_or_default().join(path_leaf) + }; + + require_file(&path)?; + return Ok(path); + + fn require_file(path: &Path) -> Result<(), io::Error> { + if path.is_dir() { + // TODO: use `IsADirectory` variant which is stabilized with 1.83 + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Expected file but '{}' is a directory.", path.display()), + )); + } + + if !path.exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("File '{}' is not present.", path.display()), + )); + } + + Ok(()) } } @@ -1054,7 +1079,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() } @@ -1129,6 +1164,37 @@ pub fn http_uri_to_ws_address(uri: http::Uri) -> String { format!("{}{}{}{}", address_prefix, host_address, port, path) } +/// If 0x prefix exists in an str strip it or return the str as-is +#[macro_export] +macro_rules! str_strip_0x { + ($s: expr) => { + $s.strip_prefix("0x").unwrap_or($s) + }; +} + +/// If value is 'some' push key and value (as string) into an array containing (key, value) elements +#[macro_export] +macro_rules! push_if_some { + ($arr: expr, $k: expr, $v: expr) => { + if let Some(v) = $v { + $arr.push(($k, v.to_string())) + } + }; +} + +/// Define 'with_...' method to set a parameter with an optional value in a builder +#[macro_export] +macro_rules! def_with_opt_param { + ($var: ident, $var_type: ty) => { + $crate::paste! { + pub fn [](&mut self, $var: Option<$var_type>) -> &mut Self { + self.$var = $var; + self + } + } + }; +} + #[test] fn test_http_uri_to_ws_address() { let uri = "https://cosmos-rpc.polkachu.com".parse::().unwrap(); 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/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/crypto_ctx.rs b/mm2src/crypto/src/crypto_ctx.rs index 92ac1f2196..ffc83603a6 100644 --- a/mm2src/crypto/src/crypto_ctx.rs +++ b/mm2src/crypto/src/crypto_ctx.rs @@ -316,10 +316,12 @@ impl CryptoCtx { *ctx_field = Some(result.clone()); drop(ctx_field); - ctx.rmd160.pin(rmd160).map_to_mm(CryptoInitError::Internal)?; + ctx.rmd160 + .set(rmd160) + .map_to_mm(|_| CryptoInitError::Internal("Already Initialized".to_string()))?; ctx.shared_db_id - .pin(shared_db_id) - .map_to_mm(CryptoInitError::Internal)?; + .set(shared_db_id) + .map_to_mm(|_| CryptoInitError::Internal("Already Initialized".to_string()))?; info!("Public key hash: {rmd160}"); info!("Shared Database ID: {shared_db_id}"); 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/mnemonic.rs b/mm2src/crypto/src/mnemonic.rs index c92e23c05b..922b292da0 100644 --- a/mm2src/crypto/src/mnemonic.rs +++ b/mm2src/crypto/src/mnemonic.rs @@ -104,7 +104,7 @@ pub fn encrypt_mnemonic(mnemonic: &str, password: &str) -> MmResult MmResult { +pub fn decrypt_mnemonic(encrypted_data: &EncryptedData, password: &str) -> MmResult { // Re-create the salts from Base64-encoded strings let (salt_aes, salt_hmac) = match &encrypted_data.key_derivation_details { KeyDerivationDetails::Argon2 { @@ -126,8 +126,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 +143,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 +172,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 +185,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/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index 7415f21b4f..3019fb6add 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "mm2_bin_lib" -version = "2.2.0-beta" +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"] 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 c78233e64a..cbf46087a4 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,7 @@ fn mm2_status() -> MainStatus { Err(_) => return MainStatus::NoRpc, }; - if ctx.rpc_started.copy_or(false) { + if *ctx.rpc_started.get().unwrap_or(&false) { MainStatus::RpcIsUp } else { MainStatus::NoRpc 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..f08a0f756b 100644 --- a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs @@ -70,7 +70,8 @@ pub unsafe extern "C" fn mm2_main(conf: *const c_char, log_cb: extern "C" fn(lin 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())) { + match catch_unwind(move || mm2_main::run_lp_main(Some(&conf), ctx_cb, KDF_VERSION.into(), KDF_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)), diff --git a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs index ee56bd4045..deaec8fedb 100644 --- a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs @@ -115,7 +115,7 @@ pub fn mm2_main(params: JsValue, log_cb: js_sys::Function) -> Result<(), JsValue // 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 { + match mm2_main::lp_main(params, &ctx_cb, KDF_VERSION.into(), KDF_DATETIME.into()).await { Ok(()) => console_info!("run_lp_main finished"), Err(err) => console_err!("run_lp_main error: {}", err), }; @@ -216,7 +216,7 @@ pub async fn mm2_rpc(payload: JsValue) -> Result { Err(_) => return Err(Mm2RpcErr::NotRunning.into()), }; - let wasm_rpc = ctx.wasm_rpc.ok_or(JsValue::from(Mm2RpcErr::NotRunning))?; + let wasm_rpc = ctx.wasm_rpc.get().ok_or(JsValue::from(Mm2RpcErr::NotRunning))?; let response: Mm2RpcResponse = wasm_rpc.request(request_json).await.into(); serialize_to_js(&response).map_err(|e| { @@ -242,8 +242,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/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/lib.rs b/mm2src/mm2_bitcoin/rpc/src/lib.rs index d901dbdb22..70efe15dc7 100644 --- a/mm2src/mm2_bitcoin/rpc/src/lib.rs +++ b/mm2src/mm2_bitcoin/rpc/src/lib.rs @@ -1,4 +1,3 @@ -extern crate core; #[cfg(test)] extern crate lazy_static; extern crate log; extern crate rustc_hex as hex; diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs index 9ff3d3e36e..5368985a08 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs @@ -85,8 +85,8 @@ impl ops::Deref for Bytes { fn deref(&self) -> &Self::Target { &self.0 } } -impl ::core::fmt::LowerHex for Bytes { - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { +impl ::std::fmt::LowerHex for Bytes { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { for i in &self.0[..] { write!(f, "{:02x}", i)?; } diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs index 1911d11781..00ecf4f52f 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs @@ -147,8 +147,8 @@ macro_rules! impl_hash { } } - impl ::core::fmt::LowerHex for $name { - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + impl ::std::fmt::LowerHex for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { for i in &self.0[..] { write!(f, "{:02x}", i)?; } diff --git a/mm2src/mm2_bitcoin/script/src/script.rs b/mm2src/mm2_bitcoin/script/src/script.rs index 9244f043f7..4653919fd8 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}; @@ -350,7 +351,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 +426,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 +455,18 @@ 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::CallSender => { Ok(vec![]) // TODO }, diff --git a/mm2src/mm2_bitcoin/script/src/sign.rs b/mm2src/mm2_bitcoin/script/src/sign.rs index a70e423eea..95b9d5a0ea 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"; @@ -466,7 +467,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 +481,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 +495,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 +503,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 +516,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 +534,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 +545,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 +561,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 +609,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)] @@ -783,7 +785,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 d0df9dbe7c..980adfc50c 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -15,9 +15,10 @@ common = { path = "../common" } 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.4", 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" } @@ -31,13 +32,13 @@ shared_ref_counter = { path = "../common/shared_ref_counter" } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } 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 } -gstuff = { version = "0.7", features = ["nightly"] } 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..33e3d24b6f 100644 --- a/mm2src/mm2_core/src/data_asker.rs +++ b/mm2src/mm2_core/src/data_asker.rs @@ -1,4 +1,4 @@ -use common::expirable_map::ExpirableMap; +use common::custom_futures::timeout::FutureTimerExt; use common::{HttpStatusCode, StatusCode}; use derive_more::Display; use futures::channel::oneshot; @@ -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), } } } diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 8c417f2ce1..32afffb12f 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, Constructible, ERR, ERRL}; +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}; +use std::sync::{Arc, Mutex, OnceLock}; +use timed_map::{MapKind, TimedMap}; use crate::data_asker::DataAsker; @@ -76,20 +75,18 @@ pub struct MmCtx { /// Should be refactored away in the future. State should always be valid. /// If there are things that are loaded in background then they should be separately optional, /// without invalidating the entire state. - pub initialized: Constructible, + pub initialized: OnceLock, /// True if the RPC HTTP server was started. - pub rpc_started: Constructible, - /// Controller for continuously streaming data using streaming channels of `mm2_event_stream`. - pub stream_channel_controller: Controller, + pub rpc_started: 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: Constructible, + pub stop: OnceLock, /// Unique context identifier, allowing us to more easily pass the context through the FFI boundaries. /// 0 if the handler ID is allocated yet. - pub ffi_handle: Constructible, + pub ffi_handle: OnceLock, /// The context belonging to the `ordermatch` mod: `OrdermatchContext`. pub ordermatch_ctx: Mutex>>, pub rate_limit_ctx: Mutex>>, @@ -104,10 +101,10 @@ pub struct MmCtx { pub crypto_ctx: Mutex>>, /// RIPEMD160(SHA256(x)) where x is secp256k1 pubkey derived from passphrase. /// This hash is **unique** among Iguana and each HD accounts derived from the same passphrase. - pub rmd160: Constructible, + pub rmd160: OnceLock, /// A shared DB identifier - RIPEMD160(SHA256(x)) where x is secp256k1 pubkey derived from (passphrase + magic salt). /// This hash is **the same** for Iguana and all HD accounts derived from the same passphrase. - pub shared_db_id: Constructible, + pub shared_db_id: OnceLock, /// Coins that should be enabled to kick start the interrupted swaps and orders. pub coins_needed_for_kick_start: Mutex>, /// The context belonging to the `lp_swap` mod: `SwapsContext`. @@ -115,19 +112,19 @@ pub struct MmCtx { /// The context belonging to the `lp_stats` mod: `StatsContext` pub stats_ctx: Mutex>>, /// Wallet name for this mm2 instance. Optional for backwards compatibility. - pub wallet_name: Constructible>, + pub wallet_name: OnceLock>, /// The context belonging to the `lp_wallet` mod: `WalletsContext`. #[cfg(target_arch = "wasm32")] pub wallets_ctx: Mutex>>, /// The RPC sender forwarding requests to writing part of underlying stream. #[cfg(target_arch = "wasm32")] - pub wasm_rpc: Constructible, + pub wasm_rpc: OnceLock, /// Deprecated, please use `async_sqlite_connection` for new implementations. #[cfg(not(target_arch = "wasm32"))] - pub sqlite_connection: Constructible>>, + pub sqlite_connection: OnceLock>>, /// 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: Constructible>>, + pub shared_sqlite_conn: OnceLock>>, pub mm_version: String, pub datetime: String, pub mm_init_ctx: Mutex>>, @@ -144,9 +141,9 @@ pub struct MmCtx { pub nft_ctx: Mutex>>, /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] - pub async_sqlite_connection: Constructible>>, + 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 { @@ -155,13 +152,12 @@ impl MmCtx { conf: Json::Object(json::Map::new()), log: log::LogArc::new(log), metrics: MetricsArc::new(), - initialized: Constructible::default(), - rpc_started: Constructible::default(), - stream_channel_controller: Controller::new(), + initialized: OnceLock::default(), + rpc_started: OnceLock::default(), data_asker: DataAsker::default(), - event_stream_configuration: None, - stop: Constructible::default(), - ffi_handle: Constructible::default(), + event_stream_manager: Default::default(), + stop: OnceLock::default(), + ffi_handle: OnceLock::default(), ordermatch_ctx: Mutex::new(None), rate_limit_ctx: Mutex::new(None), simple_market_maker_bot_ctx: Mutex::new(None), @@ -172,20 +168,20 @@ impl MmCtx { coins_ctx: Mutex::new(None), coins_activation_ctx: Mutex::new(None), crypto_ctx: Mutex::new(None), - rmd160: Constructible::default(), - shared_db_id: Constructible::default(), + rmd160: OnceLock::default(), + shared_db_id: OnceLock::default(), coins_needed_for_kick_start: Mutex::new(HashSet::new()), swaps_ctx: Mutex::new(None), stats_ctx: Mutex::new(None), - wallet_name: Constructible::default(), + wallet_name: OnceLock::default(), #[cfg(target_arch = "wasm32")] wallets_ctx: Mutex::new(None), #[cfg(target_arch = "wasm32")] - wasm_rpc: Constructible::default(), + wasm_rpc: OnceLock::default(), #[cfg(not(target_arch = "wasm32"))] - sqlite_connection: Constructible::default(), + sqlite_connection: OnceLock::default(), #[cfg(not(target_arch = "wasm32"))] - shared_sqlite_conn: Constructible::default(), + shared_sqlite_conn: OnceLock::default(), mm_version: "".into(), datetime: "".into(), mm_init_ctx: Mutex::new(None), @@ -195,8 +191,10 @@ impl MmCtx { db_namespace: DbNamespaceId::Main, nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] - async_sqlite_connection: Constructible::default(), - healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), + async_sqlite_connection: OnceLock::default(), + healthcheck_response_handler: AsyncMutex::new( + TimedMap::new_with_map_kind(MapKind::FxHashMap).expiration_tick_cap(3), + ), } } @@ -204,14 +202,14 @@ impl MmCtx { lazy_static! { static ref DEFAULT: H160 = [0; 20].into(); } - self.rmd160.or(&|| &*DEFAULT) + self.rmd160.get().unwrap_or(&*DEFAULT) } pub fn shared_db_id(&self) -> &H160 { lazy_static! { static ref DEFAULT: H160 = [0; 20].into(); } - self.shared_db_id.or(&|| &*DEFAULT) + self.shared_db_id.get().unwrap_or(&*DEFAULT) } #[cfg(not(target_arch = "wasm32"))] @@ -342,11 +340,16 @@ 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.copy_or(false) } + pub fn is_stopping(&self) -> bool { *self.stop.get().unwrap_or(&false) } pub fn gui(&self) -> Option<&str> { self.conf["gui"].as_str() } @@ -357,7 +360,10 @@ impl MmCtx { let sqlite_file_path = self.dbdir().join("MM2.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let connection = try_s!(Connection::open(sqlite_file_path)); - try_s!(self.sqlite_connection.pin(Arc::new(Mutex::new(connection)))); + try_s!(self + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } @@ -366,7 +372,10 @@ impl MmCtx { let sqlite_file_path = self.shared_dbdir().join("MM2-shared.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let connection = try_s!(Connection::open(sqlite_file_path)); - try_s!(self.shared_sqlite_conn.pin(Arc::new(Mutex::new(connection)))); + try_s!(self + .shared_sqlite_conn + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } @@ -375,19 +384,23 @@ impl MmCtx { let sqlite_file_path = self.dbdir().join("KOMODEFI.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let async_conn = try_s!(AsyncConnection::open(sqlite_file_path).await); - try_s!(self.async_sqlite_connection.pin(Arc::new(AsyncMutex::new(async_conn)))); + try_s!(self + .async_sqlite_connection + .set(Arc::new(AsyncMutex::new(async_conn))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } #[cfg(not(target_arch = "wasm32"))] pub fn sqlite_conn_opt(&self) -> Option> { - self.sqlite_connection.as_option().map(|conn| conn.lock().unwrap()) + self.sqlite_connection.get().map(|conn| conn.lock().unwrap()) } #[cfg(not(target_arch = "wasm32"))] pub fn sqlite_connection(&self) -> MutexGuard { self.sqlite_connection - .or(&|| panic!("sqlite_connection is not initialized")) + .get() + .expect("sqlite_connection is not initialized") .lock() .unwrap() } @@ -395,7 +408,8 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] pub fn shared_sqlite_conn(&self) -> MutexGuard { self.shared_sqlite_conn - .or(&|| panic!("shared_sqlite_conn is not initialized")) + .get() + .expect("shared_sqlite_conn is not initialized") .lock() .unwrap() } @@ -409,7 +423,7 @@ impl Drop for MmCtx { fn drop(&mut self) { let ffi_handle = self .ffi_handle - .as_option() + .get() .map(|handle| handle.to_string()) .unwrap_or_else(|| "UNKNOWN".to_owned()); log::info!("MmCtx ({}) has been dropped", ffi_handle) @@ -512,7 +526,7 @@ impl MmArc { #[cfg(not(target_arch = "wasm32"))] try_s!(self.close_async_connection().await); - try_s!(self.stop.pin(true)); + try_s!(self.stop.set(true)); // Notify shutdown listeners. self.graceful_shutdown_registry.abort_all().warn_log(); @@ -527,7 +541,7 @@ impl MmArc { #[cfg(not(target_arch = "wasm32"))] async fn close_async_connection(&self) -> Result<(), db_common::async_sql_conn::AsyncConnError> { - if let Some(async_conn) = self.async_sqlite_connection.as_option() { + if let Some(async_conn) = self.async_sqlite_connection.get() { let mut conn = async_conn.lock().await; conn.close().await?; } @@ -560,7 +574,7 @@ impl MmArc { /// Unique context identifier, allowing us to more easily pass the context through the FFI boundaries. pub fn ffi_handle(&self) -> Result { let mut mm_ctx_ffi = try_s!(MM_CTX_FFI.lock()); - if let Some(have) = self.ffi_handle.as_option() { + if let Some(have) = self.ffi_handle.get() { return Ok(*have); } let mut tries = 0; @@ -579,7 +593,7 @@ impl MmArc { Entry::Occupied(_) => continue, // Try another ID. Entry::Vacant(ve) => { ve.insert(self.weak()); - try_s!(self.ffi_handle.pin(rid)); + try_s!(self.ffi_handle.set(rid)); return Ok(rid); }, } @@ -656,67 +670,23 @@ 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`. /// * `constructor` - Generates the initial crate context. -pub fn from_ctx( - ctx_field: &Mutex>>, - constructor: C, -) -> Result, String> +pub fn from_ctx(ctx: &Mutex>>, init: F) -> Result, String> where - C: FnOnce() -> Result, T: 'static + Send + Sync, + F: FnOnce() -> Result, { - let mut ctx_field = try_s!(ctx_field.lock()); - if let Some(ref ctx) = *ctx_field { - let ctx: Arc = match ctx.clone().downcast() { - Ok(p) => p, - Err(_) => return ERR!("Error casting the context field"), - }; - return Ok(ctx); + let mut guard = try_s!(ctx.lock()); + if let Some(ctx) = guard.as_ref() { + return ctx.clone().downcast().map_err(|_| "Context type mismatch".to_string()); } - let arc = Arc::new(try_s!(constructor())); - *ctx_field = Some(arc.clone()); - Ok(arc) + + let new_ctx = Arc::new(init()?); + *guard = Some(new_ctx.clone()); + Ok(new_ctx) } #[derive(Default)] @@ -779,14 +749,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..f9c895a2d1 --- /dev/null +++ b/mm2src/mm2_event_stream/src/manager.rs @@ -0,0 +1,543 @@ +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()); + }); + + 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()); + }); + + 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_gui_storage/src/account/storage/sqlite_storage.rs b/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs index 916854de63..4e2be2acac 100644 --- a/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs +++ b/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs @@ -118,7 +118,7 @@ impl SqliteAccountStorage { pub(crate) fn new(ctx: &MmArc) -> AccountStorageResult { let shared = ctx .sqlite_connection - .as_option() + .get() .or_mm_err(|| AccountStorageError::Internal("'MmCtx::sqlite_connection' is not initialized".to_owned()))?; Ok(SqliteAccountStorage { conn: Arc::clone(shared), diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 6900009b2f..856cd244ac 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -24,6 +24,7 @@ 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"] [dependencies] async-std = { version = "1.5", features = ["unstable"] } @@ -65,16 +66,18 @@ 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" } +trading_api = { path = "../trading_api" } num-traits = "0.2" parity-util-mem = "0.11" parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } +primitive-types = "0.11.1" prost = "0.12" rand = { version = "0.7", features = ["std", "small_rng"] } rand6 = { version = "0.6", package = "rand" } @@ -102,6 +105,7 @@ 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" } @@ -114,6 +118,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] @@ -122,7 +127,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"] } 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"] } diff --git a/mm2src/mm2_main/src/ext_api.rs b/mm2src/mm2_main/src/ext_api.rs new file mode 100644 index 0000000000..f1b92c145f --- /dev/null +++ b/mm2src/mm2_main/src/ext_api.rs @@ -0,0 +1,3 @@ +//! 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..ed2440a7a1 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -1,7 +1,6 @@ use async_std::prelude::FutureExt; use chrono::Utc; use common::executor::SpawnFuture; -use common::expirable_map::ExpirableEntry; use common::{log, HttpStatusCode, StatusCode}; use derive_more::Display; use futures::channel::oneshot::{self, Receiver, Sender}; @@ -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 8e1e91ec13..878a74ea6b 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -28,8 +28,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, @@ -46,13 +44,13 @@ 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_swap; +use crate::lp_swap::swap_kick_starts; use crate::lp_wallet::{initialize_wallet_passphrase, WalletInitError}; use crate::rpc::spawn_rpc; @@ -67,7 +65,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; @@ -205,10 +203,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), @@ -427,25 +423,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)); } } @@ -476,14 +458,14 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { let balance_update_ordermatch_handler = BalanceUpdateOrdermatchHandler::new(ctx.clone()); register_balance_update_handler(ctx.clone(), Box::new(balance_update_ordermatch_handler)).await; - ctx.initialized.pin(true).map_to_mm(MmInitError::Internal)?; + ctx.initialized + .set(true) + .map_to_mm(|_| MmInitError::Internal("Already Initialized".to_string()))?; // launch kickstart threads before RPC is available, this will prevent the API user to place // 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())); @@ -533,14 +515,9 @@ pub async fn lp_init(ctx: MmArc, version: String, datetime: String) -> MmInitRes }; 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); - // wait for swaps to stop - loop { - if running_swaps_num(&ctx) == 0 { - break; - }; - Timer::sleep(0.2).await - } Ok(()) } diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index b2ef53f3fb..6ed8719fa5 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -238,9 +238,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 { diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 620cb79bfb..de75e4ed3b 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -29,7 +29,6 @@ use coins::{coin_conf, find_pair, lp_coinfind, BalanceTradeFeeUpdatedHandler, Co 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,6 +55,8 @@ 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 serde_json::{self as json, Value as Json}; @@ -67,6 +69,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; @@ -102,8 +105,10 @@ pub use lp_bot::{start_simple_market_maker_bot, stop_simple_market_maker_bot, St 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"] @@ -365,7 +370,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); @@ -436,11 +443,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 +525,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 +653,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 } => { @@ -2345,7 +2359,7 @@ struct TrieDiff { #[derive(Debug)] struct TrieDiffHistory { - inner: TimeCache>, + inner: TimedMap>, } impl TrieDiffHistory { @@ -2355,30 +2369,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 +2403,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 +2411,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 +2439,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 +2452,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 +2477,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 +2499,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 +2511,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 +2572,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 +2645,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 +2700,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 +2820,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 +2850,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(), @@ -2929,7 +2984,7 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO // 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(), @@ -3086,7 +3141,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(); @@ -3569,6 +3624,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 +3679,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(), @@ -3723,6 +3792,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 +3863,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 +3953,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 +3963,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, @@ -3981,7 +4064,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, 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/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..3aedc1cb5c 100644 --- a/mm2src/mm2_main/src/lp_stats.rs +++ b/mm2src/mm2_main/src/lp_stats.rs @@ -11,6 +11,7 @@ use mm2_libp2p::application::request_response::network_info::NetworkInfoRequest; use mm2_libp2p::{encode_message, NetworkInfo, PeerId, RelayAddress, RelayAddressError}; 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, @@ -170,16 +171,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()) } -pub fn process_info_request(ctx: MmArc, request: NetworkInfoRequest) -> Result>, String> { - log::debug!("Got stats request {:?}", request); +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> { 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..f2486f03e5 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -65,7 +65,6 @@ use bitcrypto::{dhash160, sha256}; use coins::{lp_coinfind, lp_coinfind_or_err, CoinFindError, DexFee, MmCoin, 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}, @@ -74,11 +73,12 @@ 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_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}; @@ -87,11 +87,11 @@ 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 +106,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; @@ -155,12 +156,13 @@ 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; +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; @@ -420,13 +422,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 +437,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 +518,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 +534,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 +617,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 +652,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 +666,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 +686,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 +704,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))); @@ -853,7 +846,7 @@ pub struct NegotiationDataV1 { started_at: u64, payment_locktime: u64, secret_hash: [u8; 20], - persistent_pubkey: Vec, + persistent_pubkey: H264, } #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] @@ -861,7 +854,7 @@ pub struct NegotiationDataV2 { started_at: u64, payment_locktime: u64, secret_hash: Vec, - persistent_pubkey: Vec, + persistent_pubkey: H264, maker_coin_swap_contract: Vec, taker_coin_swap_contract: Vec, } @@ -873,8 +866,8 @@ 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, + maker_coin_htlc_pub: H264, + taker_coin_htlc_pub: H264, } #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] @@ -910,7 +903,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 +911,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, @@ -2072,14 +2065,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 +2086,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 +2095,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 +2109,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 +2126,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 +2341,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 +2362,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, diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 0eb72b8a71..d5c8bf2582 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -14,13 +14,14 @@ use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_e 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}; + 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::{now_sec, wait_until_sec}; @@ -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}; @@ -426,7 +428,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 +440,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(), }) } } @@ -569,8 +571,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), }; @@ -658,7 +660,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 +671,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 +689,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()), }), ])) } @@ -793,83 +801,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 +856,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); @@ -1341,7 +1374,9 @@ impl MakerSwap { taker.bytes = data.taker.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, @@ -2095,17 +2130,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); + // 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,6 +2155,13 @@ 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"); @@ -2162,6 +2206,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 { 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..8b2a05f935 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -1,3 +1,4 @@ +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}; @@ -46,7 +47,7 @@ cfg_wasm32!( #[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 +59,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. @@ -718,12 +719,17 @@ 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, diff --git a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs index e4e430c71c..4d9b6c18fc 100644 --- a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs +++ b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs @@ -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/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..ea459082a0 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_common.rs @@ -37,7 +37,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, diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 312f62e5c5..6c0e4f5995 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -12,6 +12,7 @@ use common::executor::{AbortSettings, SpawnAbortable, Timer}; use common::log::{debug, error, info}; use common::{now_sec, DEX_FEE_ADDR_RAW_PUBKEY}; use futures::compat::Future01CompatExt; +use instant::Duration; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MapToMmResult; use mm2_libp2p::{decode_signed, pub_sub_topic, TopicPrefix}; @@ -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; @@ -258,10 +259,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 +379,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 +449,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 +561,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 +581,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 +607,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..55d7677e73 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -12,6 +12,7 @@ use super::{broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_msg 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, @@ -20,7 +21,7 @@ use coins::lp_price::fetch_swap_coins_price; use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, 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}; @@ -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}; @@ -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); - + // 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,6 +483,13 @@ 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"); @@ -491,10 +503,10 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { } 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,6 +534,8 @@ 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)] @@ -971,7 +985,7 @@ impl TakerSwap { 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 +996,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(), }) } } @@ -1131,8 +1145,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 +1229,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(), )])); @@ -1287,8 +1307,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()), }, )])) } @@ -1506,6 +1526,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.use_watchers() + && 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 +1632,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 +1666,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 +1728,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)) @@ -1733,11 +1789,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 +1828,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,7 +2092,10 @@ 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; @@ -2846,7 +2901,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 +3028,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(|_, _| { @@ -3276,8 +3331,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..8801d32cfc 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -1,3 +1,4 @@ +use super::swap_events::{SwapStatusEvent, SwapStatusStreamer}; use super::swap_v2_common::*; use super::{LockedAmount, LockedAmountInfo, SavedTradeFee, SwapsContext, TakerSwapPreparedParams, NEGOTIATE_SEND_INTERVAL, NEGOTIATION_TIMEOUT_SEC}; @@ -47,7 +48,7 @@ cfg_wasm32!( #[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 +60,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. @@ -838,12 +839,17 @@ 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, .. } => { diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index 559821a26a..abe5663024 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -139,7 +139,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), } @@ -214,7 +214,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 => { @@ -305,8 +305,8 @@ fn initialize_crypto_context(ctx: &MmArc, passphrase: &str) -> WalletInitResult< pub(crate) async fn initialize_wallet_passphrase(ctx: &MmArc) -> WalletInitResult<()> { let (wallet_name, passphrase) = deserialize_wallet_config(ctx)?; ctx.wallet_name - .pin(wallet_name.clone()) - .map_to_mm(WalletInitError::InternalError)?; + .set(wallet_name.clone()) + .map_to_mm(|_| WalletInitError::InternalError("Already Initialized".to_string()))?; let passphrase = process_passphrase_logic(ctx, wallet_name, passphrase).await?; if let Some(passphrase) = passphrase { @@ -541,7 +541,7 @@ pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult>` 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.ok_or(GetWalletsError::Internal( + let activated_wallet = ctx.wallet_name.get().ok_or(GetWalletsError::Internal( "`wallet_name` not initialized yet!".to_string(), ))?; diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs index c873b2d5ff..e779f7b86a 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs @@ -47,6 +47,7 @@ pub(super) async fn save_encrypted_passphrase( pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsStorageResult> { let wallet_name = ctx .wallet_name + .get() .ok_or(WalletsStorageError::Internal( "`wallet_name` not initialized yet!".to_string(), ))? 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 a815bfcca1..fa66cada1c 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs @@ -126,6 +126,7 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle let wallet_name = ctx .wallet_name + .get() .ok_or(WalletsDBError::Internal( "`wallet_name` not initialized yet!".to_string(), ))? diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index dd7c7bed27..151b6de1eb 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -47,10 +47,11 @@ use common::log::LogLevel; use common::password_policy::password_policy; use mm2_core::mm_ctx::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; @@ -85,7 +86,7 @@ pub mod rpc; 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 +103,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) { @@ -150,7 +151,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() @@ -277,7 +278,7 @@ pub fn mm2_main(version: String, datetime: String) { } if first_arg == Some("--version") || first_arg == Some("-v") || first_arg == Some("version") { - println!("AtomicDEX API: {version}"); + println!("Komodo DeFi Framework: {version}"); return; } @@ -291,7 +292,7 @@ pub fn mm2_main(version: String, datetime: String) { return; } - log!("AtomicDEX API {} DT {}", version, datetime); + log!("Komodo DeFi Framework {} DT {}", version, datetime); if let Err(err) = run_lp_main(first_arg, &|_| (), version, datetime) { log!("{}", err); @@ -303,22 +304,23 @@ pub fn mm2_main(version: String, datetime: String) { /// Parses and returns the `first_arg` as JSON. /// Attempts to load the config from `MM2.json` file if `first_arg` is None pub fn get_mm2config(first_arg: Option<&str>) -> Result { - let conf_path = common::kdf_config_file(); - let conf_from_file = slurp(&conf_path); let conf = match first_arg { - Some(s) => s, + Some(s) => s.to_owned(), None => { + let conf_path = common::kdf_config_file().map_err(|e| e.to_string())?; + let conf_from_file = slurp(&conf_path); + if conf_from_file.is_empty() { return ERR!( "Config is not set from command line arg and {} file doesn't exist.", conf_path.display() ); } - try_s!(std::str::from_utf8(&conf_from_file)) + try_s!(String::from_utf8(conf_from_file)) }, }; - let mut conf: Json = match json::from_str(conf) { + let mut conf: Json = match json::from_str(&conf) { Ok(json) => json, // Syntax or io errors may include the conf string in the error message so we don't want to take risks and show these errors internals in the log. // If new variants are added to the Error enum, there can be a risk of exposing the conf string in the error message when updating serde_json so @@ -327,7 +329,7 @@ pub fn get_mm2config(first_arg: Option<&str>) -> Result { }; if conf["coins"].is_null() { - let coins_path = common::kdf_coins_file(); + let coins_path = common::kdf_coins_file().map_err(|e| e.to_string())?; let coins_from_file = slurp(&coins_path); if coins_from_file.is_empty() { diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 1ac83697af..56a9c6a135 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -1055,7 +1055,10 @@ fn test_cancel_by_single_coin() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); @@ -1074,7 +1077,10 @@ fn test_cancel_by_pair() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); @@ -1097,7 +1103,10 @@ fn test_cancel_by_all() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); @@ -2491,9 +2500,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], @@ -2515,12 +2522,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] @@ -2625,7 +2632,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)) diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 1f0afd3234..90233e34a4 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -22,10 +22,10 @@ use crate::rpc::rate_limiter::RateLimitError; use common::log::{error, info}; -use common::{err_to_rpc_json_string, err_tp_rpc_json, HttpStatusCode, APPLICATION_JSON}; +use common::{err_to_rpc_json_string, err_tp_rpc_json, HttpStatusCode}; use derive_more::Display; use futures::future::{join_all, FutureExt}; -use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE}; +use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN}; use http::request::Parts; use http::{Method, Request, Response, StatusCode}; use mm2_core::mm_ctx::MmArc; @@ -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. @@ -203,8 +204,6 @@ async fn process_single_request(ctx: MmArc, req: Json, client: SocketAddr) -> Re #[cfg(not(target_arch = "wasm32"))] async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Response { - const NON_ALLOWED_CHARS: &[char] = &['<', '>', '&']; - /// Unwraps a result or propagates its error 500 response with the specified headers (if they are present). macro_rules! try_sf { ($value: expr $(, $header_key:expr => $header_val:expr)*) => { @@ -263,19 +262,6 @@ async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Resp let req_json = { let req_bytes = try_sf!(hyper::body::to_bytes(req_body).await, ACCESS_CONTROL_ALLOW_ORIGIN => rpc_cors); - let req_str = String::from_utf8_lossy(req_bytes.as_ref()); - let is_invalid_input = req_str.chars().any(|c| NON_ALLOWED_CHARS.contains(&c)); - if is_invalid_input { - return Response::builder() - .status(500) - .header(ACCESS_CONTROL_ALLOW_ORIGIN, rpc_cors) - .header(CONTENT_TYPE, APPLICATION_JSON) - .body(Body::from(err_to_rpc_json_string(&format!( - "Invalid input: contains one or more of the following non-allowed characters: {:?}", - NON_ALLOWED_CHARS - )))) - .unwrap(); - } try_sf!(json::from_slice(&req_bytes), ACCESS_CONTROL_ALLOW_ORIGIN => rpc_cors) }; @@ -337,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(); @@ -367,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`. @@ -377,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) })) } }) @@ -425,7 +412,7 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { $port, now_sec() ); - let _ = $ctx.rpc_started.pin(true); + let _ = $ctx.rpc_started.set(true); server }); } @@ -494,7 +481,7 @@ pub fn spawn_rpc(ctx_h: u32) { use std::sync::Mutex; let ctx = MmArc::from_ffi_handle(ctx_h).expect("No context"); - if ctx.wasm_rpc.is_some() { + if ctx.wasm_rpc.get().is_some() { error!("RPC is initialized already"); return; } @@ -527,12 +514,12 @@ pub fn spawn_rpc(ctx_h: u32) { ctx.spawner().spawn(fut); // even if the [`MmCtx::wasm_rpc`] is initialized already, the spawned future above will be shutdown - if let Err(e) = ctx.wasm_rpc.pin(request_tx) { - error!("'MmCtx::wasm_rpc' is initialized already: {}", e); + if ctx.wasm_rpc.set(request_tx).is_err() { + error!("'MmCtx::wasm_rpc' is initialized already"); return; }; - if let Err(e) = ctx.rpc_started.pin(true) { - error!("'MmCtx::rpc_started' is set already: {}", e); + if ctx.rpc_started.set(true).is_err() { + error!("'MmCtx::rpc_started' is set already"); return; } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 6b68666d6b..f72b2be399 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}; @@ -11,18 +12,24 @@ use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swa 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::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, + one_inch_v6_0_classic_swap_liquidity_sources_rpc, + one_inch_v6_0_classic_swap_quote_rpc, + one_inch_v6_0_classic_swap_tokens_rpc}; use crate::rpc::lp_commands::pubkey::*; 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::staking::validators_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, @@ -142,6 +149,10 @@ async fn auth(request: &MmRpcRequest, ctx: &MmArc, client: &SocketAddr) -> Dispa } 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; @@ -162,6 +173,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult 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, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, @@ -204,6 +217,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, start_version_stat_collection).await, "stop_simple_market_maker_bot" => handle_mmrpc(ctx, request, stop_simple_market_maker_bot).await, "stop_version_stat_collection" => handle_mmrpc(ctx, request, stop_version_stat_collection).await, + "tendermint_validators" => handle_mmrpc(ctx, request, validators_rpc).await, "trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await, "trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await, "update_nft" => handle_mmrpc(ctx, request, update_nft).await, @@ -214,13 +228,18 @@ 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, "send_asked_data" => handle_mmrpc(ctx, request, send_asked_data_rpc).await, "z_coin_tx_history" => handle_mmrpc(ctx, request, coins::my_tx_history_v2::z_coin_tx_history_rpc).await, + "1inch_v6_0_classic_swap_contract" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_contract_rpc).await, + "1inch_v6_0_classic_swap_quote" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_quote_rpc).await, + "1inch_v6_0_classic_swap_create" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_create_rpc).await, + "1inch_v6_0_classic_swap_liquidity_sources" => { + handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_liquidity_sources_rpc).await + }, + "1inch_v6_0_classic_swap_tokens" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_tokens_rpc).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } @@ -245,6 +264,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, @@ -265,6 +288,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, @@ -308,6 +353,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 diff --git a/mm2src/mm2_main/src/rpc/lp_commands/mod.rs b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs index 002066c836..e61d5aead8 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/mod.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod db_id; pub mod legacy; +pub(crate) mod one_inch; pub(crate) mod pubkey; pub(crate) mod tokens; pub(crate) mod trezor; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs new file mode 100644 index 0000000000..3d47853294 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs @@ -0,0 +1,5 @@ +//! RPC implementation for integration with 1inch swap API provider. + +pub mod errors; +pub mod rpcs; +pub mod types; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs new file mode 100644 index 0000000000..8ee65af984 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs @@ -0,0 +1,99 @@ +use coins::{eth::u256_to_big_decimal, NumConversError}; +use common::{HttpStatusCode, StatusCode}; +use enum_derives::EnumFromStringify; +use mm2_number::BigDecimal; +use ser_error_derive::SerializeErrorType; +use serde::Serialize; +use trading_api::one_inch_api::errors::ApiClientError; + +#[derive(Debug, Display, Serialize, SerializeErrorType, EnumFromStringify)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ApiIntegrationRpcError { + #[from_stringify("coins::CoinFindError")] + NoSuchCoin(String), + #[display(fmt = "EVM token needed")] + CoinTypeError, + #[display(fmt = "NFT not supported")] + NftNotSupported, + #[display(fmt = "Chain not supported")] + ChainNotSupported, + #[display(fmt = "Must be same chain")] + DifferentChains, + #[from_stringify("coins::UnexpectedDerivationMethod")] + MyAddressError(String), + InvalidParam(String), + #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] + OutOfBounds { + param: String, + value: String, + min: String, + max: String, + }, + #[display(fmt = "allowance not enough for 1inch contract, available: {allowance}, needed: {amount}")] + OneInchAllowanceNotEnough { + allowance: BigDecimal, + amount: BigDecimal, + }, + #[display(fmt = "1inch API error: {}", _0)] + OneInchError(ApiClientError), + ApiDataError(String), +} + +impl HttpStatusCode for ApiIntegrationRpcError { + fn status_code(&self) -> StatusCode { + match self { + ApiIntegrationRpcError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, + ApiIntegrationRpcError::CoinTypeError + | ApiIntegrationRpcError::NftNotSupported + | ApiIntegrationRpcError::ChainNotSupported + | ApiIntegrationRpcError::DifferentChains + | ApiIntegrationRpcError::MyAddressError(_) + | ApiIntegrationRpcError::InvalidParam(_) + | ApiIntegrationRpcError::OutOfBounds { .. } + | ApiIntegrationRpcError::OneInchAllowanceNotEnough { .. } => StatusCode::BAD_REQUEST, + ApiIntegrationRpcError::OneInchError(_) | ApiIntegrationRpcError::ApiDataError(_) => { + StatusCode::BAD_GATEWAY + }, + } + } +} + +impl ApiIntegrationRpcError { + pub(crate) fn from_api_error(error: ApiClientError, decimals: Option) -> Self { + match error { + ApiClientError::InvalidParam(error) => ApiIntegrationRpcError::InvalidParam(error), + ApiClientError::OutOfBounds { param, value, min, max } => { + ApiIntegrationRpcError::OutOfBounds { param, value, min, max } + }, + ApiClientError::TransportError(_) + | ApiClientError::ParseBodyError { .. } + | ApiClientError::GeneralApiError { .. } => ApiIntegrationRpcError::OneInchError(error), + ApiClientError::AllowanceNotEnough { allowance, amount, .. } => { + ApiIntegrationRpcError::OneInchAllowanceNotEnough { + allowance: u256_to_big_decimal(allowance, decimals.unwrap_or_default()).unwrap_or_default(), + amount: u256_to_big_decimal(amount, decimals.unwrap_or_default()).unwrap_or_default(), + } + }, + } + } +} + +/// Error aggregator for errors of conversion of api returned values +#[derive(Debug, Display, Serialize)] +pub(crate) struct FromApiValueError(String); + +impl From for FromApiValueError { + fn from(err: NumConversError) -> Self { Self(err.to_string()) } +} + +impl From for FromApiValueError { + fn from(err: primitive_types::Error) -> Self { Self(format!("{:?}", err)) } +} + +impl From for FromApiValueError { + fn from(err: hex::FromHexError) -> Self { Self(err.to_string()) } +} + +impl From for FromApiValueError { + fn from(err: ethereum_types::FromDecStrErr) -> Self { Self(err.to_string()) } +} 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 new file mode 100644 index 0000000000..a0c384463d --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -0,0 +1,439 @@ +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::{lp_coinfind_or_err, CoinWithDerivationMethod, MmCoin, MmCoinEnum}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use trading_api::one_inch_api::client::ApiClient; +use trading_api::one_inch_api::types::{ClassicSwapCreateParams, ClassicSwapQuoteParams, ProtocolsResponse, + TokensResponse}; + +/// "1inch_v6_0_classic_swap_contract" rpc impl +/// used to get contract address (for e.g. to approve funds) +pub async fn one_inch_v6_0_classic_swap_contract_rpc( + _ctx: MmArc, + _req: AggregationContractRequest, +) -> MmResult { + Ok(ApiClient::classic_swap_contract().to_owned()) +} + +/// "1inch_classic_swap_quote" rpc impl +pub async fn one_inch_v6_0_classic_swap_quote_rpc( + ctx: MmArc, + req: ClassicSwapQuoteRequest, +) -> MmResult { + let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; + let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + api_supports_pair(&base, &rel)?; + let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) + .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; + let query_params = ClassicSwapQuoteParams::new(base_contract, rel_contract, sell_amount.to_string()) + .with_fee(req.fee) + .with_protocols(req.protocols) + .with_gas_price(req.gas_price) + .with_complexity_level(req.complexity_level) + .with_parts(req.parts) + .with_main_route_parts(req.main_route_parts) + .with_gas_limit(req.gas_limit) + .with_include_tokens_info(Some(req.include_tokens_info)) + .with_include_protocols(Some(req.include_protocols)) + .with_include_gas(Some(req.include_gas)) + .with_connector_tokens(req.connector_tokens) + .build_query_params() + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; + let quote = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? + .call_swap_api( + base.chain_id(), + ApiClient::get_quote_method().to_owned(), + Some(query_params), + ) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_classic_swap_data(quote, rel.decimals()) // use 'rel' as quote value is in the dst coin + .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) +} + +/// "1inch_classic_swap_create" rpc implementation +/// This rpc actually returns a transaction to call the 1inch swap aggregation contract. GUI should sign it and send to the chain. +/// We don't verify the transaction in any way and trust the 1inch api. +pub async fn one_inch_v6_0_classic_swap_create_rpc( + ctx: MmArc, + req: ClassicSwapCreateRequest, +) -> MmResult { + let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; + let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + api_supports_pair(&base, &rel)?; + let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) + .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; + let single_address = base.derivation_method().single_addr_or_err().await?; + + let query_params = ClassicSwapCreateParams::new( + base_contract, + rel_contract, + sell_amount.to_string(), + display_eth_address(&single_address), + req.slippage, + ) + .with_fee(req.fee) + .with_protocols(req.protocols) + .with_gas_price(req.gas_price) + .with_complexity_level(req.complexity_level) + .with_parts(req.parts) + .with_main_route_parts(req.main_route_parts) + .with_gas_limit(req.gas_limit) + .with_include_tokens_info(Some(req.include_tokens_info)) + .with_include_protocols(Some(req.include_protocols)) + .with_include_gas(Some(req.include_gas)) + .with_connector_tokens(req.connector_tokens) + .with_excluded_protocols(req.excluded_protocols) + .with_permit(req.permit) + .with_compatibility(req.compatibility) + .with_receiver(req.receiver) + .with_referrer(req.referrer) + .with_disable_estimate(req.disable_estimate) + .with_allow_partial_fill(req.allow_partial_fill) + .with_use_permit2(req.use_permit2) + .build_query_params() + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; + let swap_with_tx = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? + .call_swap_api( + base.chain_id(), + ApiClient::get_swap_method().to_owned(), + Some(query_params), + ) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_classic_swap_data(swap_with_tx, base.decimals()) // use 'base' as we spend in the src coin + .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) +} + +/// "1inch_v6_0_classic_swap_liquidity_sources" rpc implementation. +/// Returns list of DEX available for routing with the 1inch Aggregation contract +pub async fn one_inch_v6_0_classic_swap_liquidity_sources_rpc( + ctx: MmArc, + req: ClassicSwapLiquiditySourcesRequest, +) -> MmResult { + let response: ProtocolsResponse = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))? + .call_swap_api(req.chain_id, ApiClient::get_liquidity_sources_method().to_owned(), None) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))?; + Ok(ClassicSwapLiquiditySourcesResponse { + protocols: response.protocols, + }) +} + +/// "1inch_classic_swap_tokens" rpc implementation. +/// Returns list of tokens available for 1inch classic swaps +pub async fn one_inch_v6_0_classic_swap_tokens_rpc( + ctx: MmArc, + req: ClassicSwapTokensRequest, +) -> MmResult { + let response: TokensResponse = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))? + .call_swap_api(req.chain_id, ApiClient::get_tokens_method().to_owned(), None) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))?; + Ok(ClassicSwapTokensResponse { + tokens: response.tokens, + }) +} + +async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, String), ApiIntegrationRpcError> { + let coin = match lp_coinfind_or_err(ctx, ticker).await? { + MmCoinEnum::EthCoin(coin) => coin, + _ => return Err(MmError::new(ApiIntegrationRpcError::CoinTypeError)), + }; + let contract = match coin.coin_type { + EthCoinType::Eth => ApiClient::eth_special_contract().to_owned(), + EthCoinType::Erc20 { token_addr, .. } => display_eth_address(&token_addr), + EthCoinType::Nft { .. } => return Err(MmError::new(ApiIntegrationRpcError::NftNotSupported)), + }; + Ok((coin, contract)) +} + +#[allow(clippy::result_large_err)] +fn api_supports_pair(base: &EthCoin, rel: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { + if !ApiClient::is_chain_supported(base.chain_id()) { + return MmError::err(ApiIntegrationRpcError::ChainNotSupported); + } + if base.chain_id() != rel.chain_id() { + return MmError::err(ApiIntegrationRpcError::DifferentChains); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::rpc::lp_commands::one_inch::{rpcs::{one_inch_v6_0_classic_swap_create_rpc, + one_inch_v6_0_classic_swap_quote_rpc}, + types::{ClassicSwapCreateRequest, ClassicSwapQuoteRequest}}; + use coins::eth::EthCoin; + use coins_activation::platform_for_tests::init_platform_coin_with_tokens_loop; + use common::block_on; + use crypto::CryptoCtx; + use mm2_core::mm_ctx::MmCtxBuilder; + use mm2_number::{BigDecimal, MmNumber}; + use mocktopus::mocking::{MockResult, Mockable}; + use std::str::FromStr; + use trading_api::one_inch_api::{client::ApiClient, types::ClassicSwapData}; + + #[test] + fn test_classic_swap_response_conversion() { + let ticker_coin = "ETH".to_owned(); + let ticker_token = "JST".to_owned(); + let eth_conf = json!({ + "coin": ticker_coin, + "name": "ethereum", + "derivation_path": "m/44'/1'", + "chain_id": 1, + "protocol": { + "type": "ETH" + }, + "trezor_coin": "Ethereum" + }); + let jst_conf = json!({ + "coin": ticker_token, + "name": "jst", + "chain_id": 1, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "ETH", + "contract_address": "0x09d0d71FBC00D7CCF9CFf132f5E6825C88293F19" + } + }, + }); + + let conf = json!({ + "coins": [eth_conf, jst_conf], + "1inch_api": "https://api.1inch.dev" + }); + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123").unwrap(); + + block_on(init_platform_coin_with_tokens_loop::( + ctx.clone(), + serde_json::from_value(json!({ + "ticker": ticker_coin, + "rpc_mode": "Default", + "nodes": [ + {"url": "https://rpc2.sepolia.org"}, + {"url": "https://rpc.sepolia.org/"} + ], + "swap_contract_address": "0xeA6D65434A15377081495a9E7C5893543E7c32cB", + "erc20_tokens_requests": [{"ticker": ticker_token}], + "priv_key_policy": "ContextPrivKey" + })) + .unwrap(), + )) + .unwrap(); + + let response_quote_raw = json!({ + "dstAmount": "13", + "srcToken": { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": ticker_coin, + "name": "Ether", + "decimals": 18, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png", + "tags": [ + "crosschain", + "GROUP:ETH", + "native", + "PEG:ETH" + ] + }, + "dstToken": { + "address": "0x1234567890123456789012345678901234567890", + "symbol": ticker_token, + "name": "Test just token", + "decimals": 6, + "eip2612": false, + "isFoT": false, + "logoURI": "https://example.org/0x1234567890123456789012345678901234567890.png", + "tags": [ + "crosschain", + "GROUP:JSTT", + "PEG:JST", + "tokens" + ] + }, + "protocols": [ + [ + [ + { + "name": "SUSHI", + "part": 100, + "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "toTokenAddress": "0xf16e81dce15b08f326220742020379b855b87df9" + } + ], + [ + { + "name": "ONE_INCH_LIMIT_ORDER_V3", + "part": 100, + "fromTokenAddress": "0xf16e81dce15b08f326220742020379b855b87df9", + "toTokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] + ] + ], + "gas": 452704 + }); + + let response_create_raw = json!({ + "dstAmount": "13", + "tx": { + "from": "0x590559f6fb7720f24ff3e2fccf6015b466e9c92c", + "to": "0x111111125421ca6dc452d289314280a0f8842a65", + "data": "0x07ed23790000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000590559f6fb7720f24ff3e2fccf6015b466e9c92c0000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000648e8755f7ac30b5e4fa3f9c00e2cb6667501797b8bc01a7a367a4b2889ca6a05d9c31a31a781c12a4c3bdfc2ef1e02942e388b6565989ebe860bd67925bda74fbe0000000000000000000000000000000000000000000000000005ea0005bc00a007e5c0d200000000000000000000000000000000059800057e00018500009500001a4041c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2d0e30db00c20c02aaa39b223fe8d0a0e5c4f27ead9083c756cc27b73644935b8e68019ac6356c40661e1bc3158606ae4071118002dc6c07b73644935b8e68019ac6356c40661e1bc3158600000000000000000000000000000000000000000000000000294932ccadc9c58c02aaa39b223fe8d0a0e5c4f27ead9083c756cc251204dff5675ecff96b565ba3804dd4a63799ccba406761d38e5ddf6ccf6cf7c55759d5210750b5d60f30044e331d039000000000000000000000000761d38e5ddf6ccf6cf7c55759d5210750b5d60f3000000000000000000000000111111111117dc0aa78b770fa6a738034120c302000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002f8a744a79be00000000000000000000000042f527f50f16a103b6ccab48bccca214500c10210000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec00a0860a32ec00000000000000000000000000000000000000000000000000003005635d54300003d05120ead050515e10fdb3540ccd6f8236c46790508a76111111111117dc0aa78b770fa6a738034120c30200c4e525b10b000000000000000000000000000000000000000000000000000000000000002000000000000000000000000022b1a53ac4be63cdc1f47c99572290eff1edd8020000000000000000000000006a32cc044dd6359c27bb66e7b02dce6dd0fda2470000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000111111111117dc0aa78b770fa6a738034120c302000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003005635d5430000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000067138e8c00000000000000000000000000000000000000000000000000030fb9b1525d8185f8d63fbcbe42e5999263c349cb5d81000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026000000000000000000000000067297ee4eb097e072b4ab6f1620268061ae8046400000000000000000000000060cba82ddbf4b5ddcd4398cdd05354c6a790c309000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041d26038ef66344af785ff342b86db3da06c4cc6a62f0ca80ffd78affc0a95ccad44e814acebb1deda729bbfe3050bec14a47af487cc1cadc75f43db2d073016c31c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a66cd52a747c5f60b9db637ffe30d0e413ec87858101832b4c5c1ae154bf247f3717c8ed4133e276ddf68d43a827f280863c91d6c42bc6ad1ec7083b2315b6fd1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020d6bdbf78dac17f958d2ee523a2206206994597c13d831ec780a06c4eca27dac17f958d2ee523a2206206994597c13d831ec7111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000c095c0a2", + "value": "10000000", + "gas": 721429, + "gasPrice": "9525172167" + }, + "srcToken": { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": ticker_coin, + "name": "Ether", + "decimals": 18, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png", + "tags": [ + "crosschain", + "GROUP:ETH", + "native", + "PEG:ETH" + ] + }, + "dstToken": { + "address": "0x1234567890123456789012345678901234567890", + "symbol": ticker_token, + "name": "Just Token", + "decimals": 6, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0x1234567890123456789012345678901234567890.png", + "tags": [ + "crosschain", + "GROUP:USDT", + "PEG:USD", + "tokens" + ] + }, + "protocols": [ + [ + [ + { + "name": "UNISWAP_V2", + "part": 100, + "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "toTokenAddress": "0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3" + } + ], + [ + { + "name": "ONE_INCH_LP_1_1", + "part": 100, + "fromTokenAddress": "0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3", + "toTokenAddress": "0x111111111117dc0aa78b770fa6a738034120c302" + } + ], + [ + { + "name": "PMM11", + "part": 100, + "fromTokenAddress": "0x111111111117dc0aa78b770fa6a738034120c302", + "toTokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] + ] + ] + }); + + let quote_req = ClassicSwapQuoteRequest { + base: ticker_coin.clone(), + rel: ticker_token.clone(), + amount: MmNumber::from("1.0"), + fee: None, + protocols: None, + gas_price: None, + complexity_level: None, + parts: None, + main_route_parts: None, + gas_limit: None, + include_tokens_info: true, + include_protocols: true, + include_gas: true, + connector_tokens: None, + }; + + let create_req = ClassicSwapCreateRequest { + base: ticker_coin.clone(), + rel: ticker_token.clone(), + amount: MmNumber::from("1.0"), + fee: None, + protocols: None, + gas_price: None, + complexity_level: None, + parts: None, + main_route_parts: None, + gas_limit: None, + include_tokens_info: true, + include_protocols: true, + include_gas: true, + connector_tokens: None, + slippage: 0.0, + excluded_protocols: None, + permit: None, + compatibility: None, + receiver: None, + referrer: None, + disable_estimate: None, + allow_partial_fill: None, + use_permit2: None, + }; + + ApiClient::call_swap_api::.mock_safe(move |_, _, _, _| { + let response_quote_raw = response_quote_raw.clone(); + MockResult::Return(Box::pin(async move { + Ok(serde_json::from_value::(response_quote_raw).unwrap()) + })) + }); + + let quote_response = block_on(one_inch_v6_0_classic_swap_quote_rpc(ctx.clone(), quote_req)).unwrap(); + assert_eq!( + quote_response.dst_amount.amount, + BigDecimal::from_str("0.000000000000000013").unwrap() + ); + assert_eq!(quote_response.src_token.as_ref().unwrap().symbol, ticker_coin); + assert_eq!(quote_response.src_token.as_ref().unwrap().decimals, 18); + assert_eq!(quote_response.dst_token.as_ref().unwrap().symbol, ticker_token); + assert_eq!(quote_response.dst_token.as_ref().unwrap().decimals, 6); + assert_eq!(quote_response.gas.unwrap(), 452704_u128); + + ApiClient::call_swap_api::.mock_safe(move |_, _, _, _| { + let response_create_raw = response_create_raw.clone(); + MockResult::Return(Box::pin(async move { + Ok(serde_json::from_value::(response_create_raw).unwrap()) + })) + }); + let create_response = block_on(one_inch_v6_0_classic_swap_create_rpc(ctx, create_req)).unwrap(); + assert_eq!( + create_response.dst_amount.amount, + BigDecimal::from_str("0.000000000000000013").unwrap() + ); + assert_eq!(create_response.src_token.as_ref().unwrap().symbol, ticker_coin); + assert_eq!(create_response.src_token.as_ref().unwrap().decimals, 18); + assert_eq!(create_response.dst_token.as_ref().unwrap().symbol, ticker_token); + assert_eq!(create_response.dst_token.as_ref().unwrap().decimals, 6); + assert_eq!(create_response.tx.as_ref().unwrap().data.len(), 1960); + } +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs new file mode 100644 index 0000000000..202eb0dcf2 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs @@ -0,0 +1,213 @@ +use crate::rpc::lp_commands::one_inch::errors::FromApiValueError; +use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; +use common::true_f; +use ethereum_types::{Address, U256}; +use mm2_err_handle::prelude::*; +use mm2_number::{construct_detailed, BigDecimal, MmNumber}; +use rpc::v1::types::Bytes as BytesJson; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use trading_api::one_inch_api::{self, + types::{ProtocolImage, ProtocolInfo, TokenInfo}}; + +construct_detailed!(DetailedAmount, amount); + +#[derive(Clone, Debug, Deserialize)] +pub struct AggregationContractRequest {} + +/// Request to get quote for 1inch classic swap. +/// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/quote_params +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ClassicSwapQuoteRequest { + /// Base coin ticker + pub base: String, + /// Rel coin ticker + pub rel: String, + /// Swap amount in coins (with fraction) + pub amount: MmNumber, + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Should be the same for quote and swap rpc. Default is 0 + pub fee: Option, + /// Specify liquidity sources + /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX + /// (by default - all used) + pub protocols: Option, + /// Network price per gas, in Gwei for this rpc. + /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. + /// If not set the 'fast' network gas price will be used + pub gas_price: Option, + /// Maximum number of token-connectors to be used in a transaction, min: 0; max: 3; default: 2 + pub complexity_level: Option, + /// Limit maximum number of parts each main route parts can be split into. + /// Should be the same for a quote and swap. Default: 20; max: 100 + pub parts: Option, + /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; + pub main_route_parts: Option, + /// Maximum amount of gas for a swap. + /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 + pub gas_limit: Option, + /// Return fromToken and toToken info in response (default is true) + #[serde(default = "true_f")] + pub include_tokens_info: bool, + /// Return used swap protocols in response (default is true) + #[serde(default = "true_f")] + pub include_protocols: bool, + /// Include estimated gas in return value (default is true) + #[serde(default = "true_f")] + pub include_gas: bool, + /// Token-connectors can be specified via this parameter. If not set, default token-connectors will be used + pub connector_tokens: Option, +} + +/// Request to create transaction for 1inch classic swap. +/// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/swap_params +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ClassicSwapCreateRequest { + /// Base coin ticker + pub base: String, + /// Rel coin ticker + pub rel: String, + /// Swap amount in coins (with fraction) + pub amount: MmNumber, + /// Allowed slippage, min: 0; max: 50 + pub slippage: f32, + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Should be the same for quote and swap rpc. Default is 0 + pub fee: Option, + /// Specify liquidity sources + /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX + /// (by default - all used) + pub protocols: Option, + /// Network price per gas, in Gwei for this rpc. + /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. + /// If not set the 'fast' network gas price will be used + pub gas_price: Option, + /// Maximum number of token-connectors to be used in a transaction, min: 0; max: 3; default: 2 + pub complexity_level: Option, + /// Limit maximum number of parts each main route parts can be split into. + /// Should be the same for a quote and swap. Default: 20; max: 100 + pub parts: Option, + /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; + pub main_route_parts: Option, + /// Maximum amount of gas for a swap. + /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 + pub gas_limit: Option, + /// Return fromToken and toToken info in response (default is true) + #[serde(default = "true_f")] + pub include_tokens_info: bool, + /// Return used swap protocols in response (default is true) + #[serde(default = "true_f")] + pub include_protocols: bool, + /// Include estimated gas in response (default is true) + #[serde(default = "true_f")] + pub include_gas: bool, + /// Token-connectors can be specified via this parameter. If not set, default token-connectors will be used + pub connector_tokens: Option, + /// Excluded supported liquidity sources. Should be the same for a quote and swap, max: 5 + pub excluded_protocols: Option, + /// Used according https://eips.ethereum.org/EIPS/eip-2612 + pub permit: Option, + /// Exclude the Unoswap method + pub compatibility: Option, + /// This address will receive funds after the swap. By default same address as 'my address' + pub receiver: Option, + /// Address to receive the partner fee. Must be set explicitly if fee is also set + pub referrer: Option, + /// if true, disable most of the checks, default: false + pub disable_estimate: Option, + /// if true, the algorithm can cancel part of the route, if the rate has become less attractive. + /// Unswapped tokens will return to 'my address'. Default: true + pub allow_partial_fill: Option, + /// Enable this flag for auto approval by Permit2 contract if you did an approval to Uniswap Permit2 smart contract for this token. + /// Default is false + pub use_permit2: Option, +} + +/// Response for both classic swap quote or create swap calls +#[derive(Serialize, Debug)] +pub struct ClassicSwapResponse { + /// Destination token amount, in coins (with fraction) + pub dst_amount: DetailedAmount, + /// Source (base) token info + #[serde(skip_serializing_if = "Option::is_none")] + pub src_token: Option, + /// Destination (rel) token info + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_token: Option, + /// Used liquidity sources + #[serde(skip_serializing_if = "Option::is_none")] + pub protocols: Option>>>, + /// Swap tx fields (returned only for create swap rpc) + #[serde(skip_serializing_if = "Option::is_none")] + pub tx: Option, + /// Estimated (returned only for quote rpc) + pub gas: Option, +} + +impl ClassicSwapResponse { + pub(crate) fn from_api_classic_swap_data( + data: one_inch_api::types::ClassicSwapData, + decimals: u8, + ) -> MmResult { + Ok(Self { + dst_amount: MmNumber::from(u256_to_big_decimal(U256::from_dec_str(&data.dst_amount)?, decimals)?).into(), + src_token: data.src_token, + dst_token: data.dst_token, + protocols: data.protocols, + tx: data + .tx + .map(|tx| TxFields::from_api_tx_fields(tx, decimals)) + .transpose()?, + gas: data.gas, + }) + } +} + +#[derive(Serialize, Debug)] +pub struct TxFields { + pub from: Address, + pub to: Address, + pub data: BytesJson, + pub value: BigDecimal, + /// Estimated gas price in gwei + pub gas_price: BigDecimal, + pub gas: u128, // TODO: in eth EthTxFeeDetails rpc we use u64. Better have identical u128 everywhere +} + +impl TxFields { + pub(crate) fn from_api_tx_fields( + tx_fields: one_inch_api::types::TxFields, + decimals: u8, + ) -> MmResult { + Ok(Self { + from: tx_fields.from, + to: tx_fields.to, + data: BytesJson::from(hex::decode(str_strip_0x!(tx_fields.data.as_str()))?), + value: u256_to_big_decimal(U256::from_dec_str(&tx_fields.value)?, decimals)?, + gas_price: wei_to_gwei_decimal(U256::from_dec_str(&tx_fields.gas_price)?)?, + gas: tx_fields.gas, + }) + } +} + +#[derive(Deserialize)] +pub struct ClassicSwapLiquiditySourcesRequest { + pub chain_id: u64, +} + +#[derive(Serialize)] +pub struct ClassicSwapLiquiditySourcesResponse { + pub protocols: Vec, +} + +#[derive(Deserialize)] +pub struct ClassicSwapTokensRequest { + pub chain_id: u64, +} + +#[derive(Serialize)] +pub struct ClassicSwapTokensResponse { + pub tokens: HashMap, +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs index c72e772a81..78697530c1 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs @@ -1,10 +1,18 @@ +//! This source file is for RPCs specific for EVM platform use coins::eth::erc20::{get_erc20_ticker_by_contract_address, get_erc20_token_info, Erc20TokenInfo}; use coins::eth::valid_addr_from_str; -use coins::{lp_coinfind_or_err, CoinFindError, CoinProtocol, MmCoinEnum}; +use coins::eth::{u256_to_big_decimal, wei_from_big_decimal, EthCoin, Web3RpcError}; +use coins::{lp_coinfind_or_err, CoinFindError, CoinProtocol, MmCoin, MmCoinEnum, NumConversError, Transaction, + TransactionErr}; use common::HttpStatusCode; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethereum_types::Address as EthAddress; +use futures::compat::Future01CompatExt; use http::StatusCode; use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmError, prelude::MmResult}; +use mm2_number::BigDecimal; #[derive(Deserialize)] pub struct TokenInfoRequest { @@ -93,3 +101,73 @@ pub async fn get_token_info(ctx: MmArc, req: TokenInfoRequest) -> MmResult StatusCode { + match self { + Erc20CallError::NoSuchCoin { .. } + | Erc20CallError::CoinNotSupported { .. } + | Erc20CallError::InvalidParam(_) => StatusCode::BAD_REQUEST, + Erc20CallError::TransactionError(_) | Erc20CallError::Web3RpcError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct Erc20AllowanceRequest { + coin: String, + spender: EthAddress, +} + +/// Call allowance method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). +/// Returns BigDecimal allowance value. +pub async fn get_token_allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let wei = eth_coin.allowance(req.spender).compat().await?; + let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; + Ok(amount) +} + +#[derive(Debug, Deserialize)] +pub struct Erc20ApproveRequest { + coin: String, + spender: EthAddress, + amount: BigDecimal, +} + +/// Call approve method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#allowance). +/// Returns approval transaction hash. +pub async fn approve_token_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; + let tx = eth_coin.approve(req.spender, amount).compat().await?; + Ok(format!("0x{:02x}", tx.tx_hash_as_bytes())) +} + +async fn find_erc20_eth_coin(ctx: &MmArc, coin: &str) -> Result> { + match lp_coinfind_or_err(ctx, coin).await { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin), + Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { + coin: coin.to_string(), + })), + Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: coin.to_string() })), + } +} 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/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index 3050f22826..e67eaaeaaa 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -39,6 +39,7 @@ use script::Builder; use secp256k1::Secp256k1; pub use secp256k1::{PublicKey, SecretKey}; use serde_json::{self as json, Value as Json}; +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; @@ -297,7 +298,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 +316,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(); } } 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 1757f97d36..b4f074857f 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -16,15 +16,17 @@ 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, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, - MarketMakerIt, Mm2TestConf}; + MarketMakerIt, Mm2TestConf, DEFAULT_RPC_PASSWORD}; 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; @@ -5396,3 +5398,157 @@ fn test_orderbook_depth() { block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } + +#[test] +fn test_approve_erc20() { + let privkey = random_secp256k1_secret(); + fill_eth_erc20_with_private_key(privkey); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let mm = MarketMakerIt::start( + Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins).conf, + DEFAULT_RPC_PASSWORD.to_string(), + None, + ) + .unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("Node log path: {}", mm.log_path.display()); + + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + let _eth_enable = block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + let _erc20_enable = block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"approve_token", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + "amount": BigDecimal::from_str("11.0").unwrap(), + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "approve_token error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert!( + hex::decode(str_strip_0x!(res["result"].as_str().unwrap())).is_ok(), + "approve_token result incorrect" + ); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"get_token_allowance", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "get_token_allowance error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert_eq!( + BigDecimal::from_str(res["result"].as_str().unwrap()).unwrap(), + BigDecimal::from_str("11.0").unwrap(), + "get_token_allowance result incorrect" + ); + + block_on(mm.stop()).unwrap(); +} + +#[test] +fn test_peer_time_sync_validation() { + let timeoffset_tolerable = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() - 1; + let timeoffset_too_big = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() + 1; + + let start_peers_with_time_offset = |offset: i64| -> (Json, Json) { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 10.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 10.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let bob_conf = Mm2TestConf::seednode(&hex::encode(bob_priv_key), &coins); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + bob_conf.conf, + bob_conf.rpc_password, + None, + &[], + )) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + let alice_conf = + Mm2TestConf::light_node(&hex::encode(alice_priv_key), &coins, &[mm_bob.ip.to_string().as_str()]); + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password, + None, + &[("TEST_TIMESTAMP_OFFSET", offset.to_string().as_str())], + )) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + let res_bob = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!(res_bob.0.is_success(), "!get_directly_connected_peers: {}", res_bob.1); + let bob_peers = serde_json::from_str::(&res_bob.1).unwrap(); + + let res_alice = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!( + res_alice.0.is_success(), + "!get_directly_connected_peers: {}", + res_alice.1 + ); + let alice_peers = serde_json::from_str::(&res_alice.1).unwrap(); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + (bob_peers, alice_peers) + }; + + // check with small time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_tolerable); + assert!( + bob_peers["result"].as_object().unwrap().len() == 1, + "bob must have one peer" + ); + assert!( + alice_peers["result"].as_object().unwrap().len() == 1, + "alice must have one peer" + ); + + // check with too big time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_too_big); + assert!( + bob_peers["result"].as_object().unwrap().is_empty(), + "bob must have no peers" + ); + assert!( + alice_peers["result"].as_object().unwrap().is_empty(), + "alice must have no peers" + ); +} diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 7ea038a8f7..5844459b9f 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -13,18 +13,18 @@ use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivation use coins::eth::{checksum_address, eth_addr_to_hex, eth_coin_from_conf_and_request, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, SignedEthTx, SwapV2Contracts, ERC20_ABI}; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; -use coins::{lp_coinfind, CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, - DerivationMethod, Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, - ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, - SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, - SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, ToBytes, Transaction, - ValidateNftMakerPaymentArgs}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] -use coins::{CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, +use coins::{lp_coinfind, CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoinEnum, MmCoinStruct, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, SendMakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, TakerCoinSwapOpsV2, TxPreimageWithSig, ValidateMakerPaymentArgs, ValidateTakerFundingArgs}; +use coins::{CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, DerivationMethod, + Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, ParseCoinAssocTypes, + ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, + SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, ToBytes, Transaction, + ValidateNftMakerPaymentArgs}; use common::{block_on, block_on_f01, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256; @@ -55,6 +55,7 @@ const SEPOLIA_MAKER_PRIV: &str = "6e2f3a6223b928a05a3a3622b0c3f3573d03663b704a61 const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba2110bddd281e711608f16"; const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const ERC20: &str = "ERC20DEV"; /// # Safety 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 49abc5c77f..723ce0bfe5 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -92,6 +92,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 +101,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 +115,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 +164,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 +280,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 +327,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 +365,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 +393,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 +407,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 +454,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 +468,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 +511,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 +526,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 +548,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 +573,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 +588,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 +610,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 +638,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 +683,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 +748,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 +789,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,6 +819,7 @@ 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(); @@ -847,6 +862,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 +896,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 +927,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,6 +961,7 @@ 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(); @@ -991,11 +1010,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 +1037,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 +1061,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 +1098,7 @@ fn test_watcher_waits_for_taker_utxo() { alice_privkey, bob_privkey, watcher_privkey, + None, ); } @@ -1100,6 +1119,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, ); } @@ -1346,7 +1366,7 @@ fn test_watcher_validate_taker_fee_eth() { })); 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(), @@ -1443,7 +1463,7 @@ fn test_watcher_validate_taker_fee_erc20() { })); 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(), @@ -3300,5 +3320,5 @@ fn test_watcher_reward() { let watcher_reward = block_on(utxo_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoin(utxo_coin.clone()), None, timeout)).unwrap(); - assert!(matches!(watcher_reward, None)); + assert!(watcher_reward.is_none()); } diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 9fe3858736..52788ab1df 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,12 +5,15 @@ 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_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; const TENDERMINT_TEST_SEED: &str = "tendermint test seed"; const TENDERMINT_CONSTANT_BALANCE_SEED: &str = "tendermint constant balance seed"; @@ -21,6 +24,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 +168,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 +226,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 +324,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 +371,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 +419,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 +665,131 @@ 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()); + + // 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")); + + // TODO: check currently delegated stakes and assert them + // This requires delegation listing feature + + let tx_details = block_on(tendermint_remove_delegation(&mm, coin_ticker, VALIDATOR_ADDRESS, "0.5")); + + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + assert!(tx_details.to.is_empty()); + assert_eq!(tx_details.transaction_type, TransactionType::RemoveDelegation); + + // TODO: check currently delegated stakes and assert them + // This requires delegation listing feature +} + mod swap { use super::*; @@ -674,6 +813,7 @@ mod swap { #[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 +892,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 +999,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/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index c8d3252ad4..fdb5dd9d74 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -6031,7 +6031,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 +6048,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, _ => { 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..cd5ba585b3 --- /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/lib.rs b/mm2src/mm2_net/src/lib.rs index 4ae26ca182..28293f9864 100644 --- a/mm2src/mm2_net/src/lib.rs +++ b/mm2src/mm2_net/src/lib.rs @@ -1,9 +1,7 @@ +pub mod event_streaming; pub mod grpc_web; -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; +pub mod transport; #[cfg(target_arch = "wasm32")] pub mod wasm; -#[cfg(target_arch = "wasm32")] pub mod wasm_event_stream; 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_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 6b7f43e7f4..812c1346de 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] @@ -38,14 +38,17 @@ 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.4", 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.4", 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..7e855487b3 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -7,6 +7,7 @@ use futures::{channel::oneshot, 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..36436efe4e 100644 --- a/mm2src/mm2_p2p/src/behaviours/mod.rs +++ b/mm2src/mm2_p2p/src/behaviours/mod.rs @@ -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(), @@ -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_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/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 115f120574..93e80a2dd1 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -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"; @@ -241,7 +247,11 @@ pub const ETH_MAINNET_NODE: &str = "https://mainnet.infura.io/v3/c01c1b4cf666425 pub const ETH_MAINNET_CHAIN_ID: u64 = 1; pub const ETH_MAINNET_SWAP_CONTRACT: &str = "0x24abe4c71fc658c91313b6552cd40cd808b3ea80"; -pub const ETH_SEPOLIA_NODES: &[&str] = &["https://ethereum-sepolia-rpc.publicnode.com","https://rpc2.sepolia.org","https://1rpc.io/sepolia"]; +pub const ETH_SEPOLIA_NODES: &[&str] = &[ + "https://ethereum-sepolia-rpc.publicnode.com", + "https://rpc2.sepolia.org", + "https://1rpc.io/sepolia", +]; pub const ETH_SEPOLIA_CHAIN_ID: u64 = 11155111; pub const ETH_SEPOLIA_SWAP_CONTRACT: &str = "0xeA6D65434A15377081495a9E7C5893543E7c32cB"; pub const ETH_SEPOLIA_TOKEN_CONTRACT: &str = "0x09d0d71FBC00D7CCF9CFf132f5E6825C88293F19"; @@ -517,9 +527,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'", }) } @@ -1129,10 +1141,16 @@ pub fn mm_ctx_with_custom_db_with_conf(conf: Option) -> MmArc { let ctx = ctx_builder.into_mm_arc(); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.shared_sqlite_conn.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .shared_sqlite_conn + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); ctx } @@ -1146,7 +1164,10 @@ pub async fn mm_ctx_with_custom_async_db() -> MmArc { let ctx = MmCtxBuilder::new().into_mm_arc(); let connection = AsyncConnection::open_in_memory().await.unwrap(); - let _ = ctx.async_sqlite_connection.pin(Arc::new(AsyncMutex::new(connection))); + let _ = ctx + .async_sqlite_connection + .set(Arc::new(AsyncMutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); ctx } @@ -1428,8 +1449,7 @@ impl MarketMakerIt { } let ctx = { - let builder = MmCtxBuilder::new() - .with_conf(conf.clone()); + let builder = MmCtxBuilder::new().with_conf(conf.clone()); let builder = if let Some(ns) = db_namespace_id { builder.with_test_db_namespace_with_id(ns) @@ -1522,7 +1542,7 @@ impl MarketMakerIt { let wasm_rpc = self .ctx .wasm_rpc - .as_option() + .get() .expect("'MmCtx::rpc' must be initialized already"); match wasm_rpc.request(payload.clone()).await { // Please note a new type of error will be introduced soon. @@ -3079,6 +3099,103 @@ 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 = "tendermint_validators"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "ticker": coin, + "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 = "add_delegation"; + 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 = "remove_delegation"; + 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 = "remove_delegation"; + 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, @@ -3259,18 +3376,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(); @@ -3340,12 +3458,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() } diff --git a/mm2src/proxy_signature/Cargo.toml b/mm2src/proxy_signature/Cargo.toml index 5392b9862a..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.4", 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..7e8e38fc24 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, warn}; use futures::channel::oneshot; use futures::future::{select, Either}; use mm2_err_handle::prelude::*; +use mm2_event_stream::{Event, StreamingManager}; 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,23 @@ 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) { + 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 +232,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 +274,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 +308,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 +318,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 +351,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 new file mode 100644 index 0000000000..4fd9514fb9 --- /dev/null +++ b/mm2src/trading_api/Cargo.toml @@ -0,0 +1,28 @@ +[package] +# integration with external trading api +name = "trading_api" +version = "0.1.0" +edition = "2018" + +[dependencies] +common = { path = "../common" } +enum_derives = { path = "../derives/enum_derives" } +mm2_core = { path = "../mm2_core" } +mm2_err_handle = { path = "../mm2_err_handle" } +mm2_net = { path = "../mm2_net" } +mm2_number = { path = "../mm2_number" } +mocktopus = { version = "0.8.0", optional = true } + +derive_more = "0.99" +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +lazy_static = "1.4" +serde = "1.0" +serde_derive = "1.0" +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +url = { version = "2.2.2", features = ["serde"] } + +[features] +test-ext-api = [] # use test config to connect to an external api + +[dev-dependencies] +mocktopus = { version = "0.8.0" } \ No newline at end of file diff --git a/mm2src/trading_api/src/lib.rs b/mm2src/trading_api/src/lib.rs new file mode 100644 index 0000000000..183e6d9bcd --- /dev/null +++ b/mm2src/trading_api/src/lib.rs @@ -0,0 +1,3 @@ +//! This module is for indirect connection to third-party trading APIs, processing their results and errors + +pub mod one_inch_api; diff --git a/mm2src/trading_api/src/one_inch_api.rs b/mm2src/trading_api/src/one_inch_api.rs new file mode 100644 index 0000000000..9b0af1625e --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api.rs @@ -0,0 +1,5 @@ +//! Wrapper for 1inch API. + +pub mod client; +pub mod errors; +pub mod types; diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs new file mode 100644 index 0000000000..ef3c61ef6b --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -0,0 +1,176 @@ +use super::errors::ApiClientError; +use crate::one_inch_api::errors::NativeError; +use common::StatusCode; +#[cfg(feature = "test-ext-api")] use lazy_static::lazy_static; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_mm_error::MapMmError, + map_to_mm::MapToMmResult, + mm_error::{MmError, MmResult}}; +use mm2_net::transport::slurp_url_with_headers; +use serde::de::DeserializeOwned; +use url::Url; + +#[cfg(any(test, feature = "mocktopus"))] +use mocktopus::macros::*; + +const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; +const SWAP_METHOD: &str = "swap"; +const QUOTE_METHOD: &str = "quote"; +const LIQUIDITY_SOURCES_METHOD: &str = "liquidity-sources"; +const TOKENS_METHOD: &str = "tokens"; + +const ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0: &str = "0x111111125421ca6dc452d289314280a0f8842a65"; +const ONE_INCH_ETH_SPECIAL_CONTRACT: &str = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + +#[cfg(test)] +const ONE_INCH_API_TEST_URL: &str = "https://api.1inch.dev"; + +#[cfg(feature = "test-ext-api")] +lazy_static! { + /// API key for testing + static ref ONE_INCH_API_TEST_AUTH: String = std::env::var("ONE_INCH_API_TEST_AUTH").unwrap_or_default(); +} + +pub(crate) type QueryParams<'life> = Vec<(&'life str, String)>; + +/// 1inch v6.0 supported eth-based chains +const ONE_INCH_V6_0_SUPPORTED_CHAINS: &[(&str, u64)] = &[ + ("Ethereum", 1), + ("Optimism", 10), + ("BSC", 56), + ("Gnosis", 100), + ("Polygon", 137), + ("Fantom", 250), + ("ZkSync", 324), + ("Klaytn", 8217), + ("Base", 8453), + ("Arbitrum", 42161), + ("Avalanche", 43114), + ("Aurora", 1313161554), +]; + +pub(crate) struct UrlBuilder<'a> { + base_url: Url, + endpoint: &'a str, + chain_id: u64, + method_name: String, + query_params: QueryParams<'a>, +} + +impl<'a> UrlBuilder<'a> { + pub(crate) fn new(api_client: &ApiClient, chain_id: u64, method_name: String) -> Self { + Self { + base_url: api_client.base_url.clone(), + endpoint: ApiClient::get_swap_endpoint(), + chain_id, + method_name, + query_params: vec![], + } + } + + pub(crate) fn with_query_params(&mut self, mut more_params: QueryParams<'a>) -> &mut Self { + self.query_params.append(&mut more_params); + self + } + + #[allow(clippy::result_large_err)] + pub(crate) fn build(&self) -> MmResult { + let url = self + .base_url + .join(self.endpoint)? + .join(&format!("{}/", self.chain_id))? + .join(self.method_name.as_str())?; + Ok(Url::parse_with_params( + url.as_str(), + self.query_params + .iter() + .map(|v| (v.0, v.1.as_str())) + .collect::>(), + )?) + } +} + +/// 1-inch API caller +pub struct ApiClient { + base_url: Url, +} + +#[allow(clippy::swap_ptr_to_ref)] // need for moctopus +#[cfg_attr(any(test, feature = "mocktopus"), mockable)] +impl ApiClient { + #[allow(unused_variables)] + #[allow(clippy::result_large_err)] + pub fn new(ctx: MmArc) -> MmResult { + #[cfg(not(test))] + let url_cfg = ctx.conf["1inch_api"] + .as_str() + .ok_or(ApiClientError::InvalidParam("No API config param".to_owned()))?; + + #[cfg(test)] + let url_cfg = ONE_INCH_API_TEST_URL; + + Ok(Self { + base_url: Url::parse(url_cfg)?, + }) + } + + pub const fn eth_special_contract() -> &'static str { ONE_INCH_ETH_SPECIAL_CONTRACT } + + pub const fn classic_swap_contract() -> &'static str { ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0 } + + pub fn is_chain_supported(chain_id: u64) -> bool { + ONE_INCH_V6_0_SUPPORTED_CHAINS.iter().any(|(_name, id)| *id == chain_id) + } + + fn get_headers() -> Vec<(&'static str, &'static str)> { + vec![ + #[cfg(feature = "test-ext-api")] + ("Authorization", ONE_INCH_API_TEST_AUTH.as_str()), + ("accept", "application/json"), + ] + } + + fn get_swap_endpoint() -> &'static str { ONE_INCH_API_ENDPOINT_V6_0 } + + pub const fn get_swap_method() -> &'static str { SWAP_METHOD } + + pub const fn get_quote_method() -> &'static str { QUOTE_METHOD } + + pub const fn get_liquidity_sources_method() -> &'static str { LIQUIDITY_SOURCES_METHOD } + + pub const fn get_tokens_method() -> &'static str { TOKENS_METHOD } + + pub(crate) async fn call_api(api_url: &Url) -> MmResult { + let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), ApiClient::get_headers()) + .await + .mm_err(ApiClientError::TransportError)?; + let body = serde_json::from_slice(&body).map_to_mm(|err| ApiClientError::ParseBodyError { + error_msg: err.to_string(), + })?; + if status_code != StatusCode::OK { + let error = NativeError::new(status_code, body); + return Err(MmError::new(ApiClientError::from_native_error(error))); + } + serde_json::from_value(body).map_err(|err| { + ApiClientError::ParseBodyError { + error_msg: err.to_string(), + } + .into() + }) + } + + pub async fn call_swap_api<'l, T: DeserializeOwned>( + &self, + chain_id: u64, + method: String, + params: Option>, + ) -> MmResult { + let mut builder = UrlBuilder::new(self, chain_id, method); + if let Some(params) = params { + builder.with_query_params(params); + } + let api_url = builder.build()?; + + ApiClient::call_api(&api_url).await + } +} diff --git a/mm2src/trading_api/src/one_inch_api/errors.rs b/mm2src/trading_api/src/one_inch_api/errors.rs new file mode 100644 index 0000000000..70264a9b89 --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/errors.rs @@ -0,0 +1,130 @@ +use common::StatusCode; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethereum_types::U256; +use mm2_net::transport::SlurpError; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Display, Serialize, EnumFromStringify)] +pub enum ApiClientError { + #[from_stringify("url::ParseError")] + InvalidParam(String), + #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] + OutOfBounds { + param: String, + value: String, + min: String, + max: String, + }, + TransportError(SlurpError), + ParseBodyError { + error_msg: String, + }, + #[display(fmt = "General API error: {error_msg} description: {description}")] + GeneralApiError { + error_msg: String, + description: String, + status_code: u16, + }, + #[display(fmt = "Allowance not enough, needed: {amount} allowance: {allowance}")] + AllowanceNotEnough { + error_msg: String, + description: String, + status_code: u16, + /// Amount to approve for the API contract + amount: U256, + /// Existing allowance for the API contract + allowance: U256, + }, +} + +// API error meta 'type' field known values +const META_TYPE_ALLOWANCE: &str = "allowance"; +const META_TYPE_AMOUNT: &str = "amount"; + +#[derive(Debug, Deserialize)] +pub(crate) struct Error400 { + pub error: String, + pub description: Option, + #[serde(rename = "statusCode")] + pub status_code: u16, + pub meta: Option>, + #[allow(dead_code)] + #[serde(rename = "requestId")] + pub request_id: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct Meta { + #[serde(rename = "type")] + pub meta_type: String, + #[serde(rename = "value")] + pub meta_value: String, +} + +#[derive(Debug)] +pub(crate) enum NativeError { + HttpError { error_msg: String, status_code: u16 }, + HttpError400(Error400), + ParseError { error_msg: String }, +} + +impl NativeError { + pub(crate) fn new(status_code: StatusCode, body: Value) -> Self { + if status_code == StatusCode::BAD_REQUEST { + match serde_json::from_value(body) { + Ok(err) => Self::HttpError400(err), + Err(err) => Self::ParseError { + error_msg: format!("could not parse error response: {}", err), + }, + } + } else { + Self::HttpError { + error_msg: body["error"].as_str().unwrap_or_default().to_owned(), + status_code: status_code.into(), + } + } + } +} + +impl ApiClientError { + /// Convert from native API errors to lib errors + /// Look for known API errors. If none found return as general API error + pub(crate) fn from_native_error(api_error: NativeError) -> ApiClientError { + match api_error { + NativeError::HttpError400(error_400) => { + if let Some(meta) = error_400.meta { + // Try if it's "Not enough allowance" error 'meta' data: + if let Some(meta_allowance) = meta.iter().find(|m| m.meta_type == META_TYPE_ALLOWANCE) { + // try find 'amount' value + let amount = if let Some(meta_amount) = meta.iter().find(|m| m.meta_type == META_TYPE_AMOUNT) { + U256::from_dec_str(&meta_amount.meta_value).unwrap_or_default() + } else { + Default::default() + }; + let allowance = U256::from_dec_str(&meta_allowance.meta_value).unwrap_or_default(); + return ApiClientError::AllowanceNotEnough { + error_msg: error_400.error, + status_code: error_400.status_code, + description: error_400.description.unwrap_or_default(), + amount, + allowance, + }; + } + } + ApiClientError::GeneralApiError { + error_msg: error_400.error, + status_code: error_400.status_code, + description: error_400.description.unwrap_or_default(), + } + }, + NativeError::HttpError { error_msg, status_code } => ApiClientError::GeneralApiError { + error_msg, + status_code, + description: Default::default(), + }, + NativeError::ParseError { error_msg } => ApiClientError::ParseBodyError { error_msg }, + } + } +} diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs new file mode 100644 index 0000000000..f13e943768 --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -0,0 +1,411 @@ +#![allow(clippy::result_large_err)] + +use super::client::QueryParams; +use super::errors::ApiClientError; +use common::{def_with_opt_param, push_if_some}; +use ethereum_types::Address; +use mm2_err_handle::mm_error::{MmError, MmResult}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use url::Url; + +const ONE_INCH_MAX_SLIPPAGE: f32 = 50.0; +const ONE_INCH_MAX_FEE_SHARE: f32 = 3.0; +const ONE_INCH_MAX_GAS: u128 = 11500000; +const ONE_INCH_MAX_PARTS: u32 = 100; +const ONE_INCH_MAX_MAIN_ROUTE_PARTS: u32 = 50; +const ONE_INCH_MAX_COMPLEXITY_LEVEL: u32 = 3; + +const BAD_URL_IN_RESPONSE_ERROR: &str = "unsupported url in response"; +const ONE_INCH_DOMAIN: &str = "1inch.io"; + +/// API params builder for swap quote +#[derive(Default)] +pub struct ClassicSwapQuoteParams { + /// Source token address + src: String, + /// Destination token address + dst: String, + amount: String, + // Optional fields + fee: Option, + protocols: Option, + gas_price: Option, + complexity_level: Option, + parts: Option, + main_route_parts: Option, + gas_limit: Option, + include_tokens_info: Option, + include_protocols: Option, + include_gas: Option, + connector_tokens: Option, +} + +impl ClassicSwapQuoteParams { + pub fn new(src: String, dst: String, amount: String) -> Self { + Self { + src, + dst, + amount, + ..Default::default() + } + } + + def_with_opt_param!(fee, f32); + def_with_opt_param!(protocols, String); + def_with_opt_param!(gas_price, String); + def_with_opt_param!(complexity_level, u32); + def_with_opt_param!(parts, u32); + def_with_opt_param!(main_route_parts, u32); + def_with_opt_param!(gas_limit, u128); + def_with_opt_param!(include_tokens_info, bool); + def_with_opt_param!(include_protocols, bool); + def_with_opt_param!(include_gas, bool); + def_with_opt_param!(connector_tokens, String); + + pub fn build_query_params(&self) -> MmResult, ApiClientError> { + self.validate_params()?; + + let mut params = vec![ + ("src", self.src.clone()), + ("dst", self.dst.clone()), + ("amount", self.amount.clone()), + ]; + + push_if_some!(params, "fee", self.fee); + push_if_some!(params, "protocols", &self.protocols); + push_if_some!(params, "gasPrice", &self.gas_price); + push_if_some!(params, "complexityLevel", self.complexity_level); + push_if_some!(params, "parts", self.parts); + push_if_some!(params, "mainRouteParts", self.main_route_parts); + push_if_some!(params, "gasLimit", self.gas_limit); + push_if_some!(params, "includeTokensInfo", self.include_tokens_info); + push_if_some!(params, "includeProtocols", self.include_protocols); + push_if_some!(params, "includeGas", self.include_gas); + push_if_some!(params, "connectorTokens", &self.connector_tokens); + Ok(params) + } + + /// Validate params by 1inch rules (to avoid extra requests) + fn validate_params(&self) -> MmResult<(), ApiClientError> { + validate_fee(&self.fee)?; + validate_complexity_level(&self.complexity_level)?; + validate_gas_limit(&self.gas_limit)?; + validate_parts(&self.parts)?; + validate_main_route_parts(&self.main_route_parts)?; + Ok(()) + } +} + +/// API params builder to create a tx for swap +#[derive(Default)] +pub struct ClassicSwapCreateParams { + src: String, + dst: String, + amount: String, + from: String, + slippage: f32, + // Optional fields + fee: Option, + protocols: Option, + gas_price: Option, + complexity_level: Option, + parts: Option, + main_route_parts: Option, + gas_limit: Option, + include_tokens_info: Option, + include_protocols: Option, + include_gas: Option, + connector_tokens: Option, + excluded_protocols: Option, + permit: Option, + compatibility: Option, + receiver: Option, + referrer: Option, + disable_estimate: Option, + allow_partial_fill: Option, + use_permit2: Option, +} + +impl ClassicSwapCreateParams { + pub fn new(src: String, dst: String, amount: String, from: String, slippage: f32) -> Self { + Self { + src, + dst, + amount, + from, + slippage, + ..Default::default() + } + } + + def_with_opt_param!(fee, f32); + def_with_opt_param!(protocols, String); + def_with_opt_param!(gas_price, String); + def_with_opt_param!(complexity_level, u32); + def_with_opt_param!(parts, u32); + def_with_opt_param!(main_route_parts, u32); + def_with_opt_param!(gas_limit, u128); + def_with_opt_param!(include_tokens_info, bool); + def_with_opt_param!(include_protocols, bool); + def_with_opt_param!(include_gas, bool); + def_with_opt_param!(connector_tokens, String); + def_with_opt_param!(excluded_protocols, String); + def_with_opt_param!(permit, String); + def_with_opt_param!(compatibility, bool); + def_with_opt_param!(receiver, String); + def_with_opt_param!(referrer, String); + def_with_opt_param!(disable_estimate, bool); + def_with_opt_param!(allow_partial_fill, bool); + def_with_opt_param!(use_permit2, bool); + + pub fn build_query_params(&self) -> MmResult, ApiClientError> { + self.validate_params()?; + + let mut params = vec![ + ("src", self.src.clone()), + ("dst", self.dst.clone()), + ("amount", self.amount.clone()), + ("from", self.from.clone()), + ("slippage", self.slippage.to_string()), + ]; + + push_if_some!(params, "fee", self.fee); + push_if_some!(params, "protocols", &self.protocols); + push_if_some!(params, "gasPrice", &self.gas_price); + push_if_some!(params, "complexityLevel", self.complexity_level); + push_if_some!(params, "parts", self.parts); + push_if_some!(params, "mainRouteParts", self.main_route_parts); + push_if_some!(params, "gasLimit", self.gas_limit); + push_if_some!(params, "includeTokensInfo", self.include_tokens_info); + push_if_some!(params, "includeProtocols", self.include_protocols); + push_if_some!(params, "includeGas", self.include_gas); + push_if_some!(params, "connectorTokens", &self.connector_tokens); + push_if_some!(params, "excludedProtocols", &self.excluded_protocols); + push_if_some!(params, "permit", &self.permit); + push_if_some!(params, "compatibility", &self.compatibility); + push_if_some!(params, "receiver", &self.receiver); + push_if_some!(params, "referrer", &self.referrer); + push_if_some!(params, "disableEstimate", self.disable_estimate); + push_if_some!(params, "allowPartialFill", self.allow_partial_fill); + push_if_some!(params, "usePermit2", self.use_permit2); + + Ok(params) + } + + /// Validate params by 1inch rules (to avoid extra requests) + fn validate_params(&self) -> MmResult<(), ApiClientError> { + validate_slippage(self.slippage)?; + validate_fee(&self.fee)?; + validate_complexity_level(&self.complexity_level)?; + validate_gas_limit(&self.gas_limit)?; + validate_parts(&self.parts)?; + validate_main_route_parts(&self.main_route_parts)?; + Ok(()) + } +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct TokenInfo { + pub address: Address, + pub symbol: String, + pub name: String, + pub decimals: u32, + pub eip2612: bool, + #[serde(rename = "isFoT", default)] + pub is_fot: bool, + #[serde(rename = "logoURI", with = "serde_one_inch_link")] + pub logo_uri: String, + pub tags: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ProtocolInfo { + pub name: String, + pub part: f64, + #[serde(rename = "fromTokenAddress")] + pub from_token_address: Address, + #[serde(rename = "toTokenAddress")] + pub to_token_address: Address, +} + +#[derive(Deserialize, Debug)] +pub struct ClassicSwapData { + /// dst token amount to receive, in api is a decimal number as string + #[serde(rename = "dstAmount")] + pub dst_amount: String, + #[serde(rename = "srcToken")] + pub src_token: Option, + #[serde(rename = "dstToken")] + pub dst_token: Option, + pub protocols: Option>>>, + pub tx: Option, + pub gas: Option, +} + +#[derive(Deserialize, Debug)] +pub struct TxFields { + pub from: Address, + pub to: Address, + pub data: String, + /// tx value, in api is a decimal number as string + pub value: String, + /// gas price, in api is a decimal number as string + #[serde(rename = "gasPrice")] + pub gas_price: String, + /// gas limit, in api is a decimal number + pub gas: u128, +} + +#[derive(Deserialize, Serialize)] +pub struct ProtocolImage { + pub id: String, + pub title: String, + #[serde(with = "serde_one_inch_link")] + pub img: String, + #[serde(with = "serde_one_inch_link")] + pub img_color: String, +} + +#[derive(Deserialize)] +pub struct ProtocolsResponse { + pub protocols: Vec, +} + +#[derive(Deserialize)] +pub struct TokensResponse { + pub tokens: HashMap, +} + +mod serde_one_inch_link { + use super::validate_one_inch_link; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + /// Just forward to the normal serializer + pub(super) fn serialize(s: &String, serializer: S) -> Result + where + S: Serializer, + { + s.serialize(serializer) + } + + /// Deserialise String with checking links + pub(super) fn deserialize<'a, D>(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + ::deserialize(deserializer) + .map(|value| validate_one_inch_link(&value).unwrap_or_default()) + } +} + +fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { + if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { + return Err(ApiClientError::OutOfBounds { + param: "slippage".to_owned(), + value: slippage.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_SLIPPAGE.to_string(), + } + .into()); + } + Ok(()) +} + +fn validate_fee(fee: &Option) -> MmResult<(), ApiClientError> { + if let Some(fee) = fee { + if !(0.0..=ONE_INCH_MAX_FEE_SHARE).contains(fee) { + return Err(ApiClientError::OutOfBounds { + param: "fee".to_owned(), + value: fee.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_FEE_SHARE.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_gas_limit(gas_limit: &Option) -> MmResult<(), ApiClientError> { + if let Some(gas_limit) = gas_limit { + if gas_limit > &ONE_INCH_MAX_GAS { + return Err(ApiClientError::OutOfBounds { + param: "gas_limit".to_owned(), + value: gas_limit.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_GAS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_parts(parts: &Option) -> MmResult<(), ApiClientError> { + if let Some(parts) = parts { + if parts > &ONE_INCH_MAX_PARTS { + return Err(ApiClientError::OutOfBounds { + param: "parts".to_owned(), + value: parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_PARTS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_main_route_parts(main_route_parts: &Option) -> MmResult<(), ApiClientError> { + if let Some(main_route_parts) = main_route_parts { + if main_route_parts > &ONE_INCH_MAX_MAIN_ROUTE_PARTS { + return Err(ApiClientError::OutOfBounds { + param: "main route parts".to_owned(), + value: main_route_parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_MAIN_ROUTE_PARTS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_complexity_level(complexity_level: &Option) -> MmResult<(), ApiClientError> { + if let Some(complexity_level) = complexity_level { + if complexity_level > &ONE_INCH_MAX_COMPLEXITY_LEVEL { + return Err(ApiClientError::OutOfBounds { + param: "complexity level".to_owned(), + value: complexity_level.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_COMPLEXITY_LEVEL.to_string(), + } + .into()); + } + } + Ok(()) +} + +/// Check if url is valid and is a subdomain of 1inch domain (simple anti-phishing check) +fn validate_one_inch_link(s: &str) -> MmResult { + let url = Url::parse(s).map_err(|_err| ApiClientError::ParseBodyError { + error_msg: BAD_URL_IN_RESPONSE_ERROR.to_owned(), + })?; + if let Some(host) = url.host() { + if host.to_string().ends_with(ONE_INCH_DOMAIN) { + return Ok(s.to_owned()); + } + } + MmError::err(ApiClientError::ParseBodyError { + error_msg: BAD_URL_IN_RESPONSE_ERROR.to_owned(), + }) +} + +#[test] +fn test_validate_one_inch_link() { + assert!(validate_one_inch_link("https://cdn.1inch.io/liquidity-sources-logo/wmatic_color.png").is_ok()); + assert!(validate_one_inch_link("https://example.org/somepath/somefile.png").is_err()); + assert!(validate_one_inch_link("https://inch.io/somepath/somefile.png").is_err()); + assert!(validate_one_inch_link("127.0.0.1").is_err()); +}