diff --git a/.cargo/config b/.cargo/config index 056307787e..c81c668c82 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,3 +1,27 @@ +[env] +JEMALLOC_SYS_WITH_MALLOC_CONF = "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" + +[target.'cfg(all())'] +rustflags = [ "-Zshare-generics=y" ] + +# # Install lld using package manager +# [target.x86_64-unknown-linux-gnu] +# linker = "clang" +# rustflags = [ "-Clink-arg=-fuse-ld=lld" ] +# +# # `brew install llvm` +# [target.x86_64-apple-darwin] +# rustflags = [ +# "-C", +# "link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld", +# ] +# +# [target.aarch64-apple-darwin] +# rustflags = [ +# "-C", +# "link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld", +# ] + [target.wasm32-unknown-unknown] runner = 'wasm-bindgen-test-runner' -rustflags = ["--cfg=web_sys_unstable_apis"] +rustflags = [ "--cfg=web_sys_unstable_apis" ] diff --git a/.dockerignore b/.dockerignore index 09656908a3..1cc2f87bf7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,7 @@ cmake-build-debug # Ignoring the Dockerfile allows us to change it without invalidating the "COPY ." step. /Dockerfile -/target +!/target/release/mm2 !/target/ci/mm2 /mm2src/*/target diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml new file mode 100644 index 0000000000..900048e6ae --- /dev/null +++ b/.github/workflows/dev-build.yml @@ -0,0 +1,416 @@ +name: Development builds +on: + push: + branches: + - dev + pull_request: + branches: + - dev + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +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: + linux-x86-64: + timeout-minutes: 30 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + cargo build --bin mm2 --profile ci + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-linux-x86-64.zip" + zip $NAME target/ci/mm2 -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + - name: Login to dockerhub + if: github.event_name != 'pull_request' + run: docker login --username ${{ secrets.DOCKER_HUB_USERNAME }} --password ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} docker.io + + - name: Build and push container image + if: github.event_name != 'pull_request' + run: | + CONTAINER_TAG="dev-$COMMIT_HASH" + docker build -t komodoofficial/atomicdexapi:"$CONTAINER_TAG" -t komodoofficial/atomicdexapi:dev-latest -f Dockerfile.dev-release . + docker push komodoofficial/atomicdexapi:"$CONTAINER_TAG" + docker push komodoofficial/atomicdexapi:dev-latest + + mac-x86-64: + timeout-minutes: 30 + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + cargo build --bin mm2 --profile ci --target x86_64-apple-darwin + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-mac-x86-64.zip" + zip $NAME target/x86_64-apple-darwin/ci/mm2 -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + win-x86-64: + timeout-minutes: 30 + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV + + - name: Build + run: | + if (test-path "./MM_VERSION") { + remove-item "./MM_VERSION" + } + echo $Env:COMMIT_HASH > ./MM_VERSION + cargo build --bin mm2 --profile ci + + - name: Compress build output + run: | + $NAME="mm2_$Env:COMMIT_HASH-win-x86-64.zip" + 7z a $NAME .\target\ci\mm2.exe .\target\ci\*.dll + mkdir $Env:BRANCH_NAME + mv $NAME ./$Env:BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + mac-dylib-x86-64: + timeout-minutes: 30 + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + cargo rustc --target x86_64-apple-darwin --lib --profile ci --package mm2_bin_lib --crate-type=staticlib + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-mac-dylib-x86-64.zip" + mv target/x86_64-apple-darwin/ci/libmm2lib.a target/x86_64-apple-darwin/ci/libmm2.a + zip $NAME target/x86_64-apple-darwin/ci/libmm2.a -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + wasm: + timeout-minutes: 30 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add wasm32-unknown-unknown + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + wasm-pack build mm2src/mm2_bin_lib --target web --out-dir ../../target/target-wasm-release + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-wasm.zip" + zip $NAME ./target/target-wasm-release/mm2lib_bg.wasm ./target/target-wasm-release/mm2lib.js ./target/target-wasm-release/snippets -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + ios-aarch64: + timeout-minutes: 30 + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add aarch64-apple-ios + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + cargo rustc --target aarch64-apple-ios --lib --profile ci --package mm2_bin_lib --crate-type=staticlib + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-ios-aarch64.zip" + mv target/aarch64-apple-ios/ci/libmm2lib.a target/aarch64-apple-ios/ci/libmm2.a + zip $NAME target/aarch64-apple-ios/ci/libmm2.a -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + android-aarch64: + timeout-minutes: 30 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add aarch64-linux-android + + - name: Setup NDK + run: ./android-ndk.sh x86 21 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - 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 --profile ci --crate-type=staticlib --package mm2_bin_lib + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-android-aarch64.zip" + mv target/aarch64-linux-android/ci/libmm2lib.a target/aarch64-linux-android/ci/libmm2.a + zip $NAME target/aarch64-linux-android/ci/libmm2.a -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + android-armv7: + timeout-minutes: 30 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add armv7-linux-androideabi + + - name: Setup NDK + run: ./android-ndk.sh x86 21 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - 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 --profile ci --crate-type=staticlib --package mm2_bin_lib + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-android-armv7.zip" + mv target/armv7-linux-androideabi/ci/libmm2lib.a target/armv7-linux-androideabi/ci/libmm2.a + zip $NAME target/armv7-linux-androideabi/ci/libmm2.a -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + + deployment-commitment: + if: github.event_name != 'pull_request' + needs: linux-x86-64 + timeout-minutes: 15 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Activate SSH + uses: webfactory/ssh-agent@v0.5.4 + with: + ssh-private-key: ${{ secrets.ATOMICDEX_DEPLOYMENTS_SSH }} + + - name: Commitment + run: | + 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 + 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 push + fi diff --git a/.github/workflows/fmt-and-lint.yml b/.github/workflows/fmt-and-lint.yml new file mode 100644 index 0000000000..b9c5c92701 --- /dev/null +++ b/.github/workflows/fmt-and-lint.yml @@ -0,0 +1,48 @@ +name: Format and Lint +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + fmt-and-lint: + timeout-minutes: 45 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal --component rustfmt clippy + rustup default nightly-2022-10-29 + + - name: fmt check + run: cargo fmt -- --check + + - name: x86-64 code lint + run: cargo clippy --all-targets --profile ci -- --D warnings + + wasm-lint: + timeout-minutes: 45 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal --component clippy + rustup default nightly-2022-10-29 + rustup target add wasm32-unknown-unknown + + - name: wasm code lint + run: cargo clippy --target wasm32-unknown-unknown --profile ci -- --D warnings diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml new file mode 100644 index 0000000000..9cf009f810 --- /dev/null +++ b/.github/workflows/release-build.yml @@ -0,0 +1,376 @@ +name: Release builds +on: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +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: + linux-x86-64: + timeout-minutes: 60 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + cargo build --bin mm2 --release + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-linux-x86-64.zip" + zip $NAME target/release/mm2 -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + - name: Login to dockerhub + run: docker login --username ${{ secrets.DOCKER_HUB_USERNAME }} --password ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} docker.io + + - name: Build and push container image + run: | + export CONTAINER_TAG=$(./target/release/mm2 --version | awk '{print $3}') + docker build -t komodoofficial/atomicdexapi:"$CONTAINER_TAG" -t komodoofficial/atomicdexapi:main-latest -f Dockerfile.release . + docker push komodoofficial/atomicdexapi:"$CONTAINER_TAG" + docker push komodoofficial/atomicdexapi:main-latest + + mac-x86-64: + timeout-minutes: 60 + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + cargo build --bin mm2 --release --target x86_64-apple-darwin + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-mac-x86-64.zip" + zip $NAME target/x86_64-apple-darwin/release/mm2 -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + win-x86-64: + timeout-minutes: 60 + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV + + - name: Build + run: | + if (test-path "./MM_VERSION") { + remove-item "./MM_VERSION" + } + echo $Env:COMMIT_HASH > ./MM_VERSION + cargo build --bin mm2 --release + + - name: Compress build output + run: | + $NAME="mm2_$Env:COMMIT_HASH-win-x86-64.zip" + 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll + mkdir $Env:BRANCH_NAME + mv $NAME ./$Env:BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + mac-dylib-x86-64: + timeout-minutes: 60 + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - 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 + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-mac-dylib-x86-64.zip" + mv target/x86_64-apple-darwin/release/libmm2lib.a target/x86_64-apple-darwin/release/libmm2.a + zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + wasm: + timeout-minutes: 60 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add wasm32-unknown-unknown + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - 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 + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-wasm.zip" + zip $NAME ./target/target-wasm-release/mm2lib_bg.wasm ./target/target-wasm-release/mm2lib.js ./target/target-wasm-release/snippets -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + ios-aarch64: + timeout-minutes: 60 + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add aarch64-apple-ios + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - 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 + + - name: Compress build output + run: | + NAME="mm2_$COMMIT_HASH-ios-aarch64.zip" + mv target/aarch64-apple-ios/release/libmm2lib.a target/aarch64-apple-ios/release/libmm2.a + zip $NAME target/aarch64-apple-ios/release/libmm2.a -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + android-aarch64: + timeout-minutes: 60 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add aarch64-linux-android + + - name: Setup NDK + run: ./android-ndk.sh x86 21 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - 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 build output + run: | + NAME="mm2_$COMMIT_HASH-android-aarch64.zip" + mv target/aarch64-linux-android/release/libmm2lib.a target/aarch64-linux-android/release/libmm2.a + zip $NAME target/aarch64-linux-android/release/libmm2.a -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" + + android-armv7: + timeout-minutes: 60 + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add armv7-linux-androideabi + + - name: Setup NDK + run: ./android-ndk.sh x86 21 + + - name: Calculate 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 + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - 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 build output + run: | + NAME="mm2_$COMMIT_HASH-android-armv7.zip" + mv target/armv7-linux-androideabi/release/libmm2lib.a target/armv7-linux-androideabi/release/libmm2.a + zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Upload output + uses: garygrossgarten/github-action-scp@release + with: + host: ${{ secrets.FILE_SERVER_HOST }} + username: ${{ secrets.FILE_SERVER_USERNAME }} + port: ${{ secrets.FILE_SERVER_PORT }} + privateKey: ${{ secrets.FILE_SERVER_KEY }} + local: ${{ env.BRANCH_NAME }} + remote: "/uploads/${{ env.BRANCH_NAME }}" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..5cd82b5311 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,210 @@ +name: Test +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + FROM_SHARED_RUNNER: true + +jobs: + linux-x86-64-unit: + timeout-minutes: 90 + runs-on: ubuntu-latest + env: + BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_LINUX }} + BOB_USERPASS: ${{ secrets.BOB_USERPASS_LINUX }} + ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_LINUX }} + ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_LINUX }} + TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Test + run: | + # wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash + cargo test --bins --lib --profile ci + + mac-x86-64-unit: + timeout-minutes: 90 + runs-on: macos-latest + env: + BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_MACOS }} + BOB_USERPASS: ${{ secrets.BOB_USERPASS_MACOS }} + ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_MACOS }} + ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_MACOS }} + TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add x86_64-apple-darwin + + - name: Test + run: | + # wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash + cargo test --bins --lib --target x86_64-apple-darwin --profile ci + + win-x86-64-unit: + timeout-minutes: 90 + runs-on: windows-latest + env: + BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_WIN }} + BOB_USERPASS: ${{ secrets.BOB_USERPASS_WIN }} + ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_WIN }} + ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_WIN }} + TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Test + run: | + # Invoke-WebRequest -Uri https://github.com/KomodoPlatform/komodo/raw/d456be35acd1f8584e1e4f971aea27bd0644d5c5/zcutil/wget64.exe -OutFile \wget64.exe + # Invoke-WebRequest -Uri https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.bat -OutFile \cmd.bat && \cmd.bat + + # Set Path in variable + # $config = "C:\ProgramData\Docker\config\daemon.json" + + # $json = Get-Content $config | Out-String | ConvertFrom-Json + # $json | Add-Member -Type NoteProperty -Name 'experimental' -Value $True + # $json | ConvertTo-Json | Set-Content $config + + # Check the file content + # type $config + + # Restart-Service docker + # Get-Service docker + + cargo test --bins --lib --profile ci + + linux-x86-64-mm2-integration: + timeout-minutes: 90 + runs-on: ubuntu-latest + env: + BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_LINUX }} + BOB_USERPASS: ${{ secrets.BOB_USERPASS_LINUX }} + ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_LINUX }} + ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_LINUX }} + TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Test + run: cargo test --test 'mm2_tests_main' --profile ci + + # https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits + # https://github.com/KomodoPlatform/atomicDEX-API/actions/runs/4419618128/jobs/7748266141#step:4:1790 + # mac-x86-64-mm2-integration: + # timeout-minutes: 90 + # runs-on: macos-latest + # env: + # BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_MACOS }} + # BOB_USERPASS: ${{ secrets.BOB_USERPASS_MACOS }} + # ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_MACOS }} + # ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_MACOS }} + # TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + # steps: + # - uses: actions/checkout@v3 + # - name: Install toolchain + # run: | + # rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + # rustup default nightly-2022-10-29 + # rustup target add x86_64-apple-darwin + + # - name: Test + # run: cargo test --test 'mm2_tests_main' --target x86_64-apple-darwin --profile ci + + win-x86-64-mm2-integration: + timeout-minutes: 90 + runs-on: windows-latest + env: + BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_WIN }} + BOB_USERPASS: ${{ secrets.BOB_USERPASS_WIN }} + ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_WIN }} + ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_WIN }} + TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Test + run: cargo test --test 'mm2_tests_main' --profile ci + + docker-tests: + timeout-minutes: 90 + runs-on: ubuntu-latest + env: + BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_LINUX }} + BOB_USERPASS: ${{ secrets.BOB_USERPASS_LINUX }} + ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_LINUX }} + ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_LINUX }} + TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + + - name: Test + run: | + wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash + cargo test --test 'docker_tests_main' --features run-docker-tests --profile ci + + wasm: + timeout-minutes: 90 + runs-on: ubuntu-latest + env: + BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_LINUX }} + BOB_USERPASS: ${{ secrets.BOB_USERPASS_LINUX }} + ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_LINUX }} + ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_LINUX }} + TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal + rustup default nightly-2022-10-29 + rustup target add wasm32-unknown-unknown + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Install firefox and geckodriver + run: | + sudo apt-get update -y + sudo apt-get install -y firefox + wget https://github.com/mozilla/geckodriver/releases/download/v0.32.2/geckodriver-v0.32.2-linux64.tar.gz + sudo tar -xzvf geckodriver-v0.32.2-linux64.tar.gz -C /bin + sudo chmod +x /bin/geckodriver + + - name: Test + run: WASM_BINDGEN_TEST_TIMEOUT=360 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main + diff --git a/.github/workflows/virustotal_scan.yml b/.github/workflows/virustotal_scan.yml index 551990ddc0..b292e47050 100644 --- a/.github/workflows/virustotal_scan.yml +++ b/.github/workflows/virustotal_scan.yml @@ -4,6 +4,10 @@ on: release: types: [created, edited, released, published] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: virustotal: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 1122bfa5e3..0ae1785eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +## v1.0.1-beta - 2023-03-17 + +**Features:** +- NFT integration `WIP` [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900) + - NFT integration PoC added. Includes ERC721 support for ETH and BSC [#1652](https://github.com/KomodoPlatform/atomicDEX-API/pull/1652) + - Withdraw ERC1155 and EVM based chains support added for NFT PoC [#1704](https://github.com/KomodoPlatform/atomicDEX-API/pull/1704) +- Swap watcher nodes [#1431](https://github.com/KomodoPlatform/atomicDEX-API/issues/1431) + - Watcher rewards for ETH swaps were added [#1658](https://github.com/KomodoPlatform/atomicDEX-API/pull/1658) +- Cosmos integration `WIP` [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) + - `ibc_withdraw`, `ibc_chains` and `ibc_transfer_channels` RPC methods were added [#1636](https://github.com/KomodoPlatform/atomicDEX-API/pull/1636) +- Lightning integration `WIP` [#1045](https://github.com/KomodoPlatform/atomicDEX-API/issues/1045) + - [rust-lightning](https://github.com/lightningdevkit/rust-lightning) was updated to [v0.0.113](https://github.com/lightningdevkit/rust-lightning/releases/tag/v0.0.113) in [#1655](https://github.com/KomodoPlatform/atomicDEX-API/pull/1655) + - Channel `current_confirmations` and `required_confirmations` were added to channel details RPCs in [#1655](https://github.com/KomodoPlatform/atomicDEX-API/pull/1655) + - `Uuid` is now used for internal channel id instead of `u64` [#1655](https://github.com/KomodoPlatform/atomicDEX-API/pull/1655) + - Some unit tests were added for multi path payments in [#1655](https://github.com/KomodoPlatform/atomicDEX-API/pull/1655) + - Some unit tests for claiming swaps on-chain for closed channels were added in [#1655](https://github.com/KomodoPlatform/atomicDEX-API/pull/1655), these unit tests are currently failing. + - `protocol_info` fields are now used to check if a lightning order can be matched or not in [#1655](https://github.com/KomodoPlatform/atomicDEX-API/pull/1655) + - 2 issues discovered while executing a KMD/LNBTC swap were fixed in [#1655](https://github.com/KomodoPlatform/atomicDEX-API/pull/1655), these issues were: + - When electrums were down, if a channel was closed, the channel closing transaction wasn't broadcasted. A check for a network error to rebroadcast the tx after a delay was added. + - If an invoice payment failed, retring paying the same invoice would cause the payment to not be updated to successful in the DB even if it were successful. A method to update the payment in the DB was added to fix this. + - `mm2_git` crate was added to provide an abstraction layer on Git for doing query/parse operations over the repositories [#1636](https://github.com/KomodoPlatform/atomicDEX-API/pull/1636) + +**Enhancements/Fixes:** +- Use `env_logger` to achieve flexible log filtering [#1725](https://github.com/KomodoPlatform/atomicDEX-API/pull/1725) +- IndexedDB Cursor can now iterate over the items step-by-step [#1678](https://github.com/KomodoPlatform/atomicDEX-API/pull/1678) +- Uuid lib was update from v0.7.4 to v1.2.2 in [#1655](https://github.com/KomodoPlatform/atomicDEX-API/pull/1655) +- A bug was fixed in [#1706](https://github.com/KomodoPlatform/atomicDEX-API/pull/1706) where EVM swap transactions were failing if sent before the approval transaction confirmation. +- Tendermint account sequence problem due to running multiple instances were fixed in [#1694](https://github.com/KomodoPlatform/atomicDEX-API/pull/1694) +- Maker/taker pubkeys were added to new columns in `stats_swaps` table in [#1665](https://github.com/KomodoPlatform/atomicDEX-API/pull/1665) and [#1717](https://github.com/KomodoPlatform/atomicDEX-API/pull/1717) +- Get rid of unnecessary / old dependencies: `crossterm`, `crossterm_winapi`, `mio 0.7.13`, `miow`, `ntapi`, `signal-hook`, `signal-hook-mio` in [#1710](https://github.com/KomodoPlatform/atomicDEX-API/pull/1710) +- A bug that caused EVM swap payments validation to fail because the tx was not available yet in the RPC node when calling `eth_getTransactionByHash` was fixed in [#1716](https://github.com/KomodoPlatform/atomicDEX-API/pull/1716). `eth_getTransactionByHash` in now retried in `wait_for_confirmations` until tx is found in the RPC node, this makes sure that the transaction is returned from `eth_getTransactionByHash` later when validating. +- CI/CD migrated from Azure to Github runners [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- CI tests are much stabilized [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- Integration and unit tests are seperated on CI stack [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- Codebase is updated in linting rules at wasm and test stack [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- `crossbeam` bumped to `0.8` from `0.7` [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- Un-used/Unstable parts of mm2 excluded from build outputs which avoids mm2 runtime from potential UB [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- Build time optimizations applied such as sharing generics instead of duplicating them in binary (which reduces output sizes) [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- `RUSTSEC-2020-0036`, `RUSTSEC-2021-0139` and `RUSTSEC-2023-0018` resolved [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- Enabled linting checks for wasm and test stack on CI [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- Release container base image updated to debian stable from ubuntu bionic [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- Fix dylib linking error of rusb [#1699](https://github.com/KomodoPlatform/atomicDEX-API/pull/1699) +- `OperationFailure::Other` error was expanded. New error variants were matched with `HwRpcError`, so error type will be `HwError`, not `InternalError` [#1719](https://github.com/KomodoPlatform/atomicDEX-API/pull/1719) +- RPC calls for evm chains was reduced in `wait_for_confirmations` function in [#1724](https://github.com/KomodoPlatform/atomicDEX-API/pull/1724) +- A possible endless loop in evm `wait_for_htlc_tx_spend` was fixed in [#1724](https://github.com/KomodoPlatform/atomicDEX-API/pull/1724) +- Wait time for taker fee validation was increased from 30 to 60 seconds in [#1730](https://github.com/KomodoPlatform/atomicDEX-API/pull/1730) to give the fee tx more time to appear in most nodes mempools. + ## v1.0.0-beta - 2023-03-08 **Features:** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 645223074e..2fa24a183e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Please note we have a code of conduct, please follow it in all your interactions Before uploading any changes, please make sure that the test suite passes locally before submitting a pull request with your changes. ``` -cargo test --all +cargo test --all --features run-docker-tests ``` We also use [Clippy](https://github.com/rust-lang/rust-clippy) to avoid common mistakes diff --git a/Cargo.lock b/Cargo.lock index e7fae16a35..de68861053 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,9 +445,9 @@ checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" [[package]] name = "bech32" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" [[package]] name = "bellman" @@ -458,7 +458,7 @@ dependencies = [ "bitvec 0.18.5", "blake2s_simd", "byteorder 1.4.3", - "crossbeam", + "crossbeam 0.7.3", "ff 0.8.0", "futures 0.1.29", "futures-cpupool", @@ -508,20 +508,20 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.28.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" +checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3" dependencies = [ "bech32", "bitcoin_hashes", - "secp256k1 0.22.1", + "secp256k1 0.24.3", ] [[package]] name = "bitcoin_hashes" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" +checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" [[package]] name = "bitcrypto" @@ -1043,7 +1043,7 @@ dependencies = [ "chain", "common", "cosmrs", - "crossbeam", + "crossbeam 0.8.2", "crypto", "db_common", "derive_more", @@ -1073,10 +1073,10 @@ dependencies = [ "lightning-background-processor", "lightning-invoice", "lightning-net-tokio", - "lightning-rapid-gossip-sync", "mm2_core", "mm2_db", "mm2_err_handle", + "mm2_git", "mm2_io", "mm2_metamask", "mm2_metrics", @@ -1099,7 +1099,7 @@ dependencies = [ "rustls 0.20.4", "script", "secp256k1 0.20.3", - "secp256k1 0.22.1", + "secp256k1 0.24.3", "ser_error", "ser_error_derive", "serde", @@ -1123,7 +1123,7 @@ dependencies = [ "tonic", "tonic-build", "utxo_signer", - "uuid 0.7.4", + "uuid 1.2.2", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -1180,8 +1180,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "chrono", - "crossbeam", - "crossterm", + "crossbeam 0.8.2", "findshlibs", "fnv", "futures 0.1.29", @@ -1215,7 +1214,7 @@ dependencies = [ "sha2 0.9.9", "shared_ref_counter", "tokio", - "uuid 0.7.4", + "uuid 1.2.2", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -1370,10 +1369,24 @@ dependencies = [ "crossbeam-channel 0.4.4", "crossbeam-deque 0.7.4", "crossbeam-epoch 0.8.2", - "crossbeam-queue", + "crossbeam-queue 0.2.3", "crossbeam-utils 0.7.2", ] +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-channel 0.5.1", + "crossbeam-deque 0.8.1", + "crossbeam-epoch 0.9.5", + "crossbeam-queue 0.3.8", + "crossbeam-utils 0.8.8", +] + [[package]] name = "crossbeam-channel" version = "0.4.4" @@ -1455,6 +1468,16 @@ dependencies = [ "maybe-uninit", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.8", +] + [[package]] name = "crossbeam-utils" version = "0.7.2" @@ -1476,31 +1499,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "crossterm" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d" -dependencies = [ - "bitflags", - "crossterm_winapi", - "libc", - "mio 0.7.13", - "parking_lot 0.11.1", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507" -dependencies = [ - "winapi", -] - [[package]] name = "crunchy" version = "0.2.2" @@ -1761,7 +1759,7 @@ dependencies = [ "log 0.4.14", "rusqlite", "sql-builder", - "uuid 0.7.4", + "uuid 1.2.2", ] [[package]] @@ -2130,7 +2128,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" dependencies = [ "blake2b_simd", "byteorder 1.4.3", @@ -2709,7 +2707,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e38a0ee5e8e3debd1336135939e6615d283b8375fab2f99d8b89dba718502212" dependencies = [ - "crossterm", "lazy_static", "libc", ] @@ -3183,6 +3180,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "io-lifetimes" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "iovec" version = "0.1.4" @@ -3375,9 +3382,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.126" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libloading" @@ -3815,18 +3822,18 @@ dependencies = [ [[package]] name = "lightning" -version = "0.0.110" +version = "0.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dce6da860338d5a9ddc3fd42432465310cfab93b342bbd23b41b7c1f7c509d3" +checksum = "087add70f81d2fdc6d4409bc0cef69e11ad366ef1d0068550159bd22b3ac8664" dependencies = [ "bitcoin", ] [[package]] name = "lightning-background-processor" -version = "0.0.110" +version = "0.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de9d0de42bb933ffb8d33c6b0a75302f08b35126bfc74398ba01ad0c201f8d" +checksum = "2288d211a2ab15e2c9fb492fb99c7998df1a37f228552f703824ee678b8980c9" dependencies = [ "bitcoin", "lightning", @@ -3835,23 +3842,23 @@ dependencies = [ [[package]] name = "lightning-invoice" -version = "0.18.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32aa02b7fd0bd95e40b6ca8d9d9232b162d5e23b41bd2bc42abe9e9c78d34d72" +checksum = "e9680857590c3529cf8c7d32b04501f215f2bf1e029fdfa22f4112f66c1741e4" dependencies = [ "bech32", "bitcoin_hashes", "lightning", "num-traits", - "secp256k1 0.22.1", + "secp256k1 0.24.3", "serde", ] [[package]] name = "lightning-net-tokio" -version = "0.0.110" +version = "0.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d093fbc643835bc64c0501b52a3531d2511dcb1237d0495d68fea3adc47d" +checksum = "2e94b019ffcbd423c67bc8e65093d46cf5c00ff696b4b633936fce6e4d0cb845" dependencies = [ "bitcoin", "lightning", @@ -3860,9 +3867,9 @@ dependencies = [ [[package]] name = "lightning-rapid-gossip-sync" -version = "0.0.110" +version = "0.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391732631b14f7a1d9dc84dc3f644484d9b73190a31087b3856505cf0525bea0" +checksum = "488b68c7d24093d35a83f37c560e427f1085f4c5d37918b81b11d95cd3675a0f" dependencies = [ "bitcoin", "lightning", @@ -3883,6 +3890,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.6" @@ -4120,19 +4133,6 @@ dependencies = [ "adler 1.0.2", ] -[[package]] -name = "mio" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" -dependencies = [ - "libc", - "log 0.4.14", - "miow", - "ntapi", - "winapi", -] - [[package]] name = "mio" version = "0.8.6" @@ -4145,15 +4145,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - [[package]] name = "mm2-libp2p" version = "0.1.0" @@ -4188,7 +4179,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "1.0.0-beta" +version = "1.0.1-beta" dependencies = [ "chrono", "common", @@ -4229,7 +4220,7 @@ dependencies = [ "serde", "serde_json", "shared_ref_counter", - "uuid 0.7.4", + "uuid 1.2.2", ] [[package]] @@ -4239,6 +4230,7 @@ dependencies = [ "async-trait", "common", "derive_more", + "enum_from", "futures 0.3.15", "hex 0.4.3", "itertools", @@ -4289,6 +4281,19 @@ dependencies = [ "web3", ] +[[package]] +name = "mm2_git" +version = "0.1.0" +dependencies = [ + "async-trait", + "common", + "http 0.2.7", + "mm2_err_handle", + "mm2_net", + "serde", + "serde_json", +] + [[package]] name = "mm2_gui_storage" version = "0.1.0" @@ -4342,7 +4347,7 @@ dependencies = [ "coins_activation", "common", "crc32fast", - "crossbeam", + "crossbeam 0.8.2", "crypto", "db_common", "derive_more", @@ -4350,6 +4355,7 @@ dependencies = [ "either", "enum-primitive-derive", "enum_from", + "env_logger 0.7.1", "ethereum-types", "futures 0.1.29", "futures 0.3.15", @@ -4405,7 +4411,7 @@ dependencies = [ "tokio", "trie-db", "trie-root 0.16.0", - "uuid 0.7.4", + "uuid 1.2.2", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -4523,7 +4529,6 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "common", - "crossterm", "crypto", "db_common", "futures 0.3.15", @@ -4541,7 +4546,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "uuid 0.7.4", + "uuid 1.2.2", ] [[package]] @@ -4669,15 +4674,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - [[package]] name = "num-bigint" version = "0.3.2" @@ -5320,9 +5316,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65a1118354442de7feb8a2a76f3d80ef01426bd45542c8c1fdffca41a758f846" +checksum = "8ae5a4388762d5815a9fc0dea33c56b021cdc8dde0c55e0c9ca57197254b0cab" dependencies = [ "bytes 1.1.0", "cfg-if 1.0.0", @@ -5830,15 +5826,6 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "reqwest" version = "0.11.9" @@ -6077,6 +6064,20 @@ dependencies = [ "semver 1.0.6", ] +[[package]] +name = "rustix" +version = "0.36.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + [[package]] name = "rustls" version = "0.19.1" @@ -6252,11 +6253,12 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.22.1" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" dependencies = [ - "secp256k1-sys 0.5.2", + "bitcoin_hashes", + "secp256k1-sys 0.6.1", ] [[package]] @@ -6270,9 +6272,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" dependencies = [ "cc", ] @@ -6520,27 +6522,6 @@ dependencies = [ "log 0.4.14", ] -[[package]] -name = "signal-hook" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" -dependencies = [ - "libc", - "mio 0.7.13", - "signal-hook", -] - [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -7772,16 +7753,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" dependencies = [ "cfg-if 1.0.0", "fastrand", - "libc", "redox_syscall 0.2.10", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys 0.42.0", ] [[package]] @@ -8082,7 +8062,7 @@ dependencies = [ "bytes 1.1.0", "libc", "memchr", - "mio 0.8.6", + "mio", "num_cpus", "parking_lot 0.12.0", "pin-project-lite 0.2.9", @@ -8595,19 +8575,20 @@ dependencies = [ [[package]] name = "uuid" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" -dependencies = [ - "rand 0.6.5", - "serde", -] +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "uuid" -version = "0.8.2" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +dependencies = [ + "getrandom 0.2.6", + "rand 0.8.4", + "serde", +] [[package]] name = "value-bag" @@ -9117,7 +9098,7 @@ checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" dependencies = [ "base64 0.13.0", "bech32", @@ -9141,7 +9122,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" dependencies = [ "bech32", "bs58", @@ -9159,7 +9140,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.0.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" dependencies = [ "blake2b_simd", "byteorder 1.4.3", @@ -9173,7 +9154,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" dependencies = [ "aes 0.6.0", "bitvec 0.18.5", @@ -9203,7 +9184,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git#95fa5110b4b3ec105ae5fed1aba3847f656ec9b7" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.0.0#cc467f04f7449ffbca49e4ba17cabc78e90ac7d1" dependencies = [ "bellman", "blake2b_simd", diff --git a/Cargo.toml b/Cargo.toml index 9c3c237abc..421bdca2c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "mm2src/mm2_db", "mm2src/mm2_err_handle", "mm2src/mm2_eth", + "mm2src/mm2_git", "mm2src/mm2_io", "mm2src/mm2_libp2p", "mm2src/mm2_metamask", diff --git a/Dockerfile b/Dockerfile index 1fdc5226c2..6a1ba87559 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN \ apt-get install -y git build-essential libssl-dev wget &&\ apt-get install -y cmake &&\ # https://github.com/rust-lang/rust-bindgen/blob/master/book/src/requirements.md#debian-based-linuxes - apt-get install -y llvm-3.9-dev libclang-3.9-dev clang-3.9 &&\ + apt-get install -y llvm-3.9-dev libclang-3.9-dev clang-3.9 lld &&\ # openssl-sys requirements, cf. https://crates.io/crates/openssl-sys apt-get install -y pkg-config libssl-dev &&\ apt-get clean @@ -48,9 +48,6 @@ RUN cd /mm2 && cargo fetch # Only needed when we're developing or changing something locally. #COPY . /mm2 -# Important for x86_64 builds -ENV JEMALLOC_SYS_WITH_MALLOC_CONF="background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" - # Build MM1 and MM2. # Increased verbosity here allows us to see the MM1 CMake logs. RUN cd /mm2 &&\ diff --git a/Dockerfile.release b/Dockerfile.release index 0ee77e1335..0b07d7f3a0 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -1,6 +1,5 @@ -FROM ubuntu:bionic +FROM debian:stable-slim WORKDIR /mm2 -COPY target-xenial/release/mm2 /app/mm2 +COPY target/release/mm2 /usr/local/bin/mm2 EXPOSE 7783 -ENV PATH="/app:${PATH}" -CMD mm2 \ No newline at end of file +CMD ["mm2"] \ No newline at end of file diff --git a/Dockerfile.x86_64-linux-android b/Dockerfile.x86_64-linux-android index e321d76eda..54b61089f4 100644 --- a/Dockerfile.x86_64-linux-android +++ b/Dockerfile.x86_64-linux-android @@ -4,6 +4,7 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends \ ca-certificates \ cmake \ + lld \ gcc \ libc6-dev \ make \ diff --git a/README.md b/README.md index da537c40f6..b5e9acaa39 100755 --- a/README.md +++ b/README.md @@ -89,12 +89,6 @@ If you want to build from source, the following prerequisites are required: rustup component add rustfmt-preview ``` -**Note for x86_64 UNIX systems**: -To have more efficient memory consumption please execute the following command before building mm2. (It's also good to have before launching mm2.) -```sh -export JEMALLOC_SYS_WITH_MALLOC_CONF="background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto" -``` - To build, run `cargo build` (or `cargo build -vv` to get verbose build output). For more detailed instructions, please refer to the [Installation Guide](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html). diff --git a/android-ndk.sh b/android-ndk.sh old mode 100644 new mode 100755 index f210ab9f9e..07ef7eb782 --- a/android-ndk.sh +++ b/android-ndk.sh @@ -4,20 +4,20 @@ NDK_URL=https://dl.google.com/android/repository/android-ndk-r21b-linux-x86_64.z main() { local arch=$1 \ - api=$2 + api=$2 local dependencies=( unzip - python - curl + python3 + curl ) - apt-get update + sudo apt-get update local purge_list=() for dep in ${dependencies[@]}; do if ! dpkg -L $dep; then - apt-get install --no-install-recommends -y $dep - purge_list+=( $dep ) + sudo apt-get install --no-install-recommends -y $dep + purge_list+=($dep) fi done @@ -27,13 +27,13 @@ main() { curl -O $NDK_URL unzip -q android-ndk-*.zip pushd android-ndk-*/ - ./build/tools/make_standalone_toolchain.py \ - --install-dir /android-ndk \ - --arch $arch \ - --api $api + sudo ./build/tools/make_standalone_toolchain.py \ + --install-dir /android-ndk \ + --arch $arch \ + --api $api # clean up - apt-get purge --auto-remove -y ${purge_list[@]} + sudo apt-get purge --auto-remove -y ${purge_list[@]} popd popd diff --git a/azure-pipelines-android-job.yml b/azure-pipelines-android-job.yml deleted file mode 100644 index 817cc9582a..0000000000 --- a/azure-pipelines-android-job.yml +++ /dev/null @@ -1,64 +0,0 @@ -parameters: - os: '' - -jobs: - - job: MM2_Android - timeoutInMinutes: 0 # 0 means infinite for self-hosted agent - pool: - name: Default - demands: agent.os -equals ${{ parameters.os }} - steps: - - checkout: self # self represents the repo where the initial Pipelines YAML file was found - clean: ${{ eq( variables['Build.Reason'], 'Schedule' ) }} # clean up only on Scheduled build - - bash: | - if [ $CLEANUP = "true" ] - then - git clean -ffdx - fi - displayName: Clean Up - failOnStderr: false - continueOnError: true - - bash: | - export TAG="$(git rev-parse --short=9 HEAD)" - echo "##vso[task.setvariable variable=COMMIT_HASH]${TAG}" - displayName: Setup ENV - - bash: | - VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_android_armv7_CI - if ! grep -q $VERSION MM_VERSION; then - echo $VERSION > MM_VERSION - fi - cat MM_VERSION - export PATH=$PATH:/home/azureagent/android-ndk/arch-ndk/x86_64/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 --profile ci --crate-type=staticlib --package mm2_bin_lib - displayName: 'Build armv7' - env: - MANUAL_MM_VERSION: true - - bash: | - VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_android_aarch64_CI - if ! grep -q $VERSION MM_VERSION; then - echo $VERSION > MM_VERSION - fi - cat MM_VERSION - export PATH=$PATH:/home/azureagent/android-ndk/arch-ndk/x86_64/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 --profile ci --crate-type=staticlib --package mm2_bin_lib - displayName: 'Build aarch64' - env: - MANUAL_MM_VERSION: true - - bash: | - rm -rf upload - mkdir upload - mv target/armv7-linux-androideabi/ci/libmm2lib.a upload/libmm2.a - zip upload/mm2-$(COMMIT_HASH)-android-armv7-CI upload/libmm2.a -j - rm upload/libmm2.a - mv target/aarch64-linux-android/ci/libmm2lib.a upload/libmm2.a - zip upload/mm2-$(COMMIT_HASH)-android-aarch64-CI upload/libmm2.a -j - rm upload/libmm2.a - displayName: 'Prepare upload' - - task: CopyFilesOverSSH@0 - inputs: - sshEndpoint: nightly_build_server - sourceFolder: 'upload' # Optional - contents: "**" - targetFolder: "uploads/$(Build.SourceBranchName)" # Optional - overwrite: true - displayName: 'Upload nightly' diff --git a/azure-pipelines-build-stage-job.yml b/azure-pipelines-build-stage-job.yml deleted file mode 100644 index ec9e5ee837..0000000000 --- a/azure-pipelines-build-stage-job.yml +++ /dev/null @@ -1,128 +0,0 @@ -# Job template for MM2 Build - -parameters: - name: '' # defaults for any parameters that aren't specified - os: '' - bob_passphrase: '' - bob_userpass: '' - alice_passphrase: '' - alice_userpass: '' - telegram_api_key: '' - -jobs: - - job: ${{ parameters.name }} - timeoutInMinutes: 0 # 0 means infinite for self-hosted agent - pool: - name: Default - demands: agent.os -equals ${{ parameters.os }} - steps: - - checkout: self # self represents the repo where the initial Pipelines YAML file was found - clean: ${{ eq( variables['Build.Reason'], 'Schedule' ) }} # clean up only on Scheduled build - - bash: | - if [ $CLEANUP = "true" ] - then - git clean -ffdx - fi - displayName: Clean Up - failOnStderr: false - continueOnError: true - # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-a-job-scoped-variable-from-a-script - - bash: | - export TAG="$(git rev-parse --short=9 HEAD)" - echo "##vso[task.setvariable variable=COMMIT_HASH]${TAG}" - displayName: Setup ENV - # On MacOS, cross-compile for x86_64-apple-darwin - - bash: | - rm -rf upload - mkdir upload - VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_CI - if ! grep -q $VERSION MM_VERSION; then - echo $VERSION > MM_VERSION - fi - cat MM_VERSION - if [ $AGENT_OS = "Darwin" ] - then - cargo build --bin mm2 --profile ci --target x86_64-apple-darwin - else - cargo build --bin mm2 --profile ci - fi - displayName: 'Build MM2' - condition: ne ( variables['Build.Reason'], 'PullRequest' ) - env: - 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" - - task: Docker@2 - displayName: Build & Push container of dev branch - condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) - inputs: - containerRegistry: dockerhub - repository: komodoofficial/atomicdexapi - command: buildAndPush - tags: | - dev-$(COMMIT_HASH) - dev-latest - Dockerfile: Dockerfile.dev-release - - bash: | - rm -rf atomicdex-deployments - 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 - git add .commit - git commit -m "[atomicDEX-API] $(COMMIT_HASH) is committed for git & container registry" - git push - fi - condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) - displayName: 'Update playground deployment' - # Explicit --test-threads=16 makes testing process slightly faster on agents that have <16 CPU cores. - # Always run tests on main branch and PRs - # On MacOS, run for x86_64-apple-darwin - - bash: | - if [ $AGENT_OS = "Darwin" ] - then - cargo test --all --target x86_64-apple-darwin --profile ci -- --test-threads=16 - else - cargo test --all --profile ci -- --test-threads=32 - fi - displayName: 'Test MM2' - timeoutInMinutes: 22 - env: - BOB_PASSPHRASE: $(${{ parameters.bob_passphrase }}) - BOB_USERPASS: $(${{ parameters.bob_userpass }}) - ALICE_PASSPHRASE: $(${{ parameters.alice_passphrase }}) - ALICE_USERPASS: $(${{ parameters.alice_userpass }}) - TELEGRAM_API_KEY: $(${{ parameters.telegram_api_key }}) - RUST_LOG: debug - MANUAL_MM_VERSION: true - condition: or( eq( variables['Build.Reason'], 'PullRequest' ), eq( variables['Build.SourceBranchName'], 'main' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) - - bash: | - containers=$(docker ps -q | wc -l) - echo $containers - if [ $containers -gt 0 ]; then - docker rm -f $(docker ps -q) - fi - displayName: 'Clean up Docker containers' - # Run unconditionally even if previous steps failed - condition: true - - bash: | - zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-CI target/ci/mm2 -j - displayName: 'Prepare CI build upload Linux' - condition: and ( eq( variables['Agent.OS'], 'Linux' ), ne ( variables['Build.Reason'], 'PullRequest' ) ) - - bash: | - zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-CI target/x86_64-apple-darwin/ci/mm2 -j - displayName: 'Prepare CI build upload MacOS' - condition: and ( eq( variables['Agent.OS'], 'Darwin' ), ne ( variables['Build.Reason'], 'PullRequest' ) ) - - powershell: | - 7z a .\upload\mm2-$(COMMIT_HASH)-$(Agent.OS)-CI.zip .\target\ci\mm2.exe .\target\ci\*.dll "$Env:windir\system32\msvcr100.dll" "$Env:windir\system32\msvcp140.dll" "$Env:windir\system32\vcruntime140.dll" - displayName: 'Prepare CI build upload Windows' - condition: and ( eq( variables['Agent.OS'], 'Windows_NT' ), ne ( variables['Build.Reason'], 'PullRequest' ) ) - # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/copy-files-over-ssh?view=vsts - - task: CopyFilesOverSSH@0 - inputs: - sshEndpoint: nightly_build_server - sourceFolder: 'upload' # Optional - contents: "**" - targetFolder: "uploads/$(Build.SourceBranchName)" # Optional - overwrite: true - displayName: 'Upload nightly' - condition: ne ( variables['Build.Reason'], 'PullRequest' ) diff --git a/azure-pipelines-ios-job.yml b/azure-pipelines-ios-job.yml deleted file mode 100644 index d513287742..0000000000 --- a/azure-pipelines-ios-job.yml +++ /dev/null @@ -1,49 +0,0 @@ -parameters: - os: '' - -jobs: - - job: MM2_iOS - timeoutInMinutes: 0 # 0 means infinite for self-hosted agent - pool: - name: Default - demands: agent.os -equals ${{ parameters.os }} - steps: - - checkout: self # self represents the repo where the initial Pipelines YAML file was found - clean: ${{ eq( variables['Build.Reason'], 'Schedule' ) }} # clean up only on Scheduled build - - bash: | - if [ $CLEANUP = "true" ] - then - git clean -ffdx - fi - displayName: Clean Up - failOnStderr: false - continueOnError: true - - bash: | - export TAG="$(git rev-parse --short=9 HEAD)" - echo "##vso[task.setvariable variable=COMMIT_HASH]${TAG}" - displayName: Setup ENV - - bash: | - VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_ios_aarch64_CI - if ! grep -q $VERSION MM_VERSION; then - echo $VERSION > MM_VERSION - fi - cat MM_VERSION - cargo rustc --target aarch64-apple-ios --lib --profile ci --package mm2_bin_lib --crate-type=staticlib - displayName: 'Build iOS lib' - env: - MANUAL_MM_VERSION: true - - bash: | - rm -rf upload - mkdir upload - mv target/aarch64-apple-ios/ci/libmm2lib.a upload/libmm2.a - zip upload/mm2-$(COMMIT_HASH)-ios-aarch64-CI upload/libmm2.a -j - rm upload/libmm2.a - displayName: 'Prepare upload' - - task: CopyFilesOverSSH@0 - inputs: - sshEndpoint: nightly_build_server - sourceFolder: 'upload' # Optional - contents: "**" - targetFolder: "uploads/$(Build.SourceBranchName)" # Optional - overwrite: true - displayName: 'Upload nightly' diff --git a/azure-pipelines-lint-stage-job.yml b/azure-pipelines-lint-stage-job.yml deleted file mode 100644 index 8f696eb8b2..0000000000 --- a/azure-pipelines-lint-stage-job.yml +++ /dev/null @@ -1,60 +0,0 @@ -# Job template for MM2 Build - -parameters: - name: '' # defaults for any parameters that aren't specified - os: '' - bob_passphrase: '' - bob_userpass: '' - alice_passphrase: '' - alice_userpass: '' - telegram_api_key: '' - -jobs: - - job: ${{ parameters.name }} - timeoutInMinutes: 0 # 0 means infinite for self-hosted agent - pool: - name: Default - demands: agent.os -equals ${{ parameters.os }} - steps: - - checkout: self # self represents the repo where the initial Pipelines YAML file was found - clean: ${{ eq( variables['Build.Reason'], 'Schedule' ) }} # clean up only on Scheduled build - - bash: | - if [ $CLEANUP = "true" ] - then - git clean -ffdx - fi - displayName: Clean Up - failOnStderr: false - continueOnError: true - - bash: | - cargo fmt -- --check - displayName: 'Check rustfmt warnings' - env: - MANUAL_MM_VERSION: true - - bash: | - if [ $AGENT_OS = "Darwin" ] - then - cargo clippy --profile ci --target x86_64-apple-darwin -- -D warnings - else - cargo clippy --profile ci -- -D warnings - fi - displayName: 'Check Clippy warnings' - env: - MANUAL_MM_VERSION: true - - bash: | - if [ $AGENT_OS = "Darwin" ] - then - cargo check --tests --profile ci --target x86_64-apple-darwin - else - cargo check --tests --profile ci - fi - displayName: 'Check Tests' - env: - MANUAL_MM_VERSION: true - - bash: | - cargo udeps - displayName: 'Check unused dependencies' - - bash: | - cargo deny check bans --hide-inclusion-graph - cargo deny check advisories --hide-inclusion-graph - displayName: 'Cargo deny checks' diff --git a/azure-pipelines-release-stage-job.yml b/azure-pipelines-release-stage-job.yml deleted file mode 100644 index 06ef60c34e..0000000000 --- a/azure-pipelines-release-stage-job.yml +++ /dev/null @@ -1,141 +0,0 @@ -# Job template for MM2 Release stage build - -parameters: - name: '' # defaults for any parameters that aren't specified - os: '' - bob_passphrase: '' - bob_userpass: '' - alice_passphrase: '' - alice_userpass: '' - -jobs: - - job: ${{ parameters.name }} - timeoutInMinutes: 0 # 0 means infinite for self-hosted agent - pool: - name: Default - demands: agent.os -equals ${{ parameters.os }} - steps: - # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-a-job-scoped-variable-from-a-script - - bash: | - export SHORT_HASH="$(git rev-parse --short=9 HEAD)" - echo "##vso[task.setvariable variable=COMMIT_HASH]${SHORT_HASH}" - export TAG="$(git tag -l --points-at HEAD)" - echo "##vso[task.setvariable variable=COMMIT_TAG]${TAG}" - if [ -z $TAG ]; then - echo "Commit tag is empty" - export RELEASE_TAG=beta-2.1.$(Build.BuildId) - else - export DEBUG_UPLOADED="$(curl -s https://api.github.com/repos/KomodoPlatform/atomicDEX-API/releases/tags/$TAG | grep $(Agent.OS)-Debug)" - export RELEASE_UPLOADED="$(curl -s https://api.github.com/repos/KomodoPlatform/atomicDEX-API/releases/tags/$TAG | grep $(Agent.OS)-Release)" - export RELEASE_TAG=$TAG - fi - echo DEBUG_UPLOADED:$DEBUG_UPLOADED - echo RELEASE_UPLOADED:$RELEASE_UPLOADED - echo RELEASE_TAG:$RELEASE_TAG - echo "##vso[task.setvariable variable=DEBUG_UPLOADED]${DEBUG_UPLOADED}" - echo "##vso[task.setvariable variable=RELEASE_UPLOADED]${RELEASE_UPLOADED}" - echo "##vso[task.setvariable variable=RELEASE_TAG]${RELEASE_TAG}" - displayName: Setup ENV - - bash: | - rm -rf upload - mkdir upload - displayName: 'Recreate upload dir' - - bash: | - VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Debug - if ! grep -q $VERSION MM_VERSION; then - echo $VERSION > MM_VERSION - fi - cat MM_VERSION - if [ $AGENT_OS = "Linux" ] - then - docker build -f Dockerfile.ubuntu.ci -t mm2_builder . - export UID=$(id -u) - export GID=$(id -g) - docker run \ - --user $UID:$GID \ - -v /home/azureagent/docker-cargo-cache/git:/root/.cargo/git \ - -v /home/azureagent/docker-cargo-cache/registry:/root/.cargo/registry \ - -v $PWD:$PWD \ - -w $PWD \ - -e HOME=/root \ - mm2_builder \ - /bin/bash -c "source /root/.cargo/env && cargo build -vv --target-dir target-xenial" - else - cargo build -vv - fi - displayName: 'Build MM2 Debug' - condition: eq( variables['DEBUG_UPLOADED'], '' ) - env: - 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" - - bash: | - zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Debug target-xenial/debug/mm2 target-xenial/debug/libmm2.a -j - displayName: 'Prepare debug build upload Linux' - condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['DEBUG_UPLOADED'], '' ) ) - - bash: | - cd target/debug - zip ../../upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Debug mm2.dSYM mm2 libmm2.a -r - displayName: 'Prepare debug build upload MacOS' - condition: and( eq( variables['Agent.OS'], 'Darwin' ), eq( variables['DEBUG_UPLOADED'], '' ) ) - - powershell: | - 7z a .\upload\mm2-$(COMMIT_HASH)-$(Agent.OS)-Debug.zip .\target\debug\mm2.exe .\target\debug\mm2.lib .\target\debug\*.dll "$Env:windir\system32\msvcr100.dll" "$Env:windir\system32\msvcp140.dll" "$Env:windir\system32\vcruntime140.dll" - displayName: 'Prepare debug build upload Windows' - condition: and( eq( variables['Agent.OS'], 'Windows_NT' ), eq( variables['DEBUG_UPLOADED'], '' ) ) - - bash: | - rm -f MM_VERSION - VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release - if ! grep -q $VERSION MM_VERSION; then - echo $VERSION > MM_VERSION - fi - cat MM_VERSION - touch mm2src/common/build.rs - if [ $AGENT_OS = "Linux" ] - then - docker build -f Dockerfile.ubuntu.ci -t mm2_builder . - export UID=$(id -u) - export GID=$(id -g) - docker run \ - --user $UID:$GID \ - -v /home/azureagent/docker-cargo-cache/git:/root/.cargo/git \ - -v /home/azureagent/docker-cargo-cache/registry:/root/.cargo/registry \ - -v $PWD:$PWD \ - -w $PWD \ - -e HOME=/root \ - mm2_builder \ - /bin/bash -c "source /root/.cargo/env && cargo build --release -vv --target-dir target-xenial" - else - cargo build --release -vv - fi - displayName: 'Build MM2 Release' - condition: eq( variables['RELEASE_UPLOADED'], '' ) - env: - MANUAL_MM_VERSION: true - - bash: | - objcopy --only-keep-debug target-xenial/release/mm2 target-xenial/release/mm2.debug - objcopy --only-keep-debug target-xenial/release/libmm2.a target-xenial/release/libmm2.debug.a - strip -g target-xenial/release/mm2 - strip -g target-xenial/release/libmm2.a - zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Release target-xenial/release/mm2 target-xenial/release/libmm2.a -j - zip upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Release-debuginfo.zip target-xenial/release/mm2.debug target-xenial/release/libmm2.debug.a -j - displayName: 'Prepare release build upload Linux' - condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['RELEASE_UPLOADED'], '' ) ) - - bash: | - cd target/release - zip ../../upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Release.zip mm2 libmm2.a - zip ../../upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Release.dSYM.zip mm2.dSYM -r - displayName: 'Prepare release build upload MacOS' - condition: and( eq( variables['Agent.OS'], 'Darwin' ), eq( variables['RELEASE_UPLOADED'], '' ) ) - - task: Docker@2 - displayName: Build and Push Docker Release image - condition: and( eq( variables['Agent.OS'], 'Linux' ), eq( variables['RELEASE_UPLOADED'], '' ) ) - inputs: - containerRegistry: dockerhub - repository: komodoofficial/atomicdexapi - command: buildAndPush - tags: | - $(RELEASE_TAG) - Dockerfile: Dockerfile.release - - powershell: | - 7z a .\upload\mm2-$(COMMIT_HASH)-$(Agent.OS)-Release.zip .\target\release\mm2.exe .\target\release\mm2.lib .\target\release\*.dll "$Env:windir\system32\msvcr100.dll" "$Env:windir\system32\msvcp140.dll" "$Env:windir\system32\vcruntime140.dll" - displayName: 'Prepare release build upload Windows' - condition: and( eq( variables['Agent.OS'], 'Windows_NT' ), eq( variables['RELEASE_UPLOADED'], '' ) ) diff --git a/azure-pipelines-wasm-stage-job.yml b/azure-pipelines-wasm-stage-job.yml deleted file mode 100644 index 5ea9024769..0000000000 --- a/azure-pipelines-wasm-stage-job.yml +++ /dev/null @@ -1,72 +0,0 @@ -# Job template for MM2 Build - -parameters: - name: '' # defaults for any parameters that aren't specified - os: '' - bob_passphrase: '' - bob_userpass: '' - alice_passphrase: '' - alice_userpass: '' - telegram_api_key: '' - -jobs: - - job: ${{ parameters.name }} - timeoutInMinutes: 0 # 0 means infinite for self-hosted agent - pool: - name: Default - demands: agent.os -equals ${{ parameters.os }} - steps: - - checkout: self # self represents the repo where the initial Pipelines YAML file was found - clean: ${{ eq( variables['Build.Reason'], 'Schedule' ) }} # clean up only on Scheduled build - - bash: | - if [ $CLEANUP = "true" ] - then - git clean -ffdx - fi - displayName: Clean Up - failOnStderr: false - continueOnError: true - # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-a-job-scoped-variable-from-a-script - - bash: | - export TAG="$(git rev-parse --short=9 HEAD)" - echo "##vso[task.setvariable variable=COMMIT_HASH]${TAG}" - displayName: Setup ENV - # Build WASM. - - bash: | - rm -rf upload - mkdir upload - VERSION=$(Build.BuildId)_$(Build.SourceBranchName)_$(COMMIT_HASH)_$(Agent.OS)_Release - if ! grep -q $VERSION MM_VERSION; then - echo $VERSION > MM_VERSION - fi - cat MM_VERSION - CC=clang-8 wasm-pack build mm2src/mm2_bin_lib --release --target web --out-dir ../../target/target-wasm-release - displayName: 'Build MM2 WASM Release' - condition: ne ( variables['Build.Reason'], 'PullRequest' ) - env: - MANUAL_MM_VERSION: true - - bash: | - CC=clang-8 cargo test --package mm2_main --target wasm32-unknown-unknown --release - displayName: 'Test MM2 WASM' - env: - WASM_BINDGEN_TEST_TIMEOUT: 180 - GECKODRIVER: '/home/azureagent/wasm/geckodriver' - BOB_PASSPHRASE: $(${{ parameters.bob_passphrase }}) - ALICE_PASSPHRASE: $(${{ parameters.alice_passphrase }}) - MANUAL_MM_VERSION: true - condition: or( eq( variables['Build.Reason'], 'PullRequest' ), eq( variables['Build.SourceBranchName'], 'main' ), eq( variables['Build.SourceBranchName'], 'dev' ) ) - - bash: | - cd target/target-wasm-release/ - zip ../../upload/mm2-$(COMMIT_HASH)-$(Agent.OS)-Wasm-Release mm2lib_bg.wasm mm2lib.js snippets -r - displayName: 'Prepare release WASM build upload Linux' - condition: ne ( variables['Build.Reason'], 'PullRequest' ) - # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/copy-files-over-ssh?view=vsts - - task: CopyFilesOverSSH@0 - inputs: - sshEndpoint: nightly_build_server - sourceFolder: 'upload' # Optional - contents: "**" - targetFolder: "uploads/$(Build.SourceBranchName)" # Optional - overwrite: true - displayName: 'Upload nightly' - condition: ne ( variables['Build.Reason'], 'PullRequest' ) \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 7fa370d87a..0000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,113 +0,0 @@ -# Starter pipeline -# Start with a minimal pipeline that you can customize to build and deploy your code. -# Add steps that build, run tests, deploy, and more: -# https://aka.ms/yaml - -variables: - - group: passphrases - -trigger: - branches: - include: - - refs/heads/* - paths: - exclude: - - docs/* - - README.md - - mm2src/README.md - - etomic_build/* - - iguana/Readme.md - - .gitignore - -pr: # https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema%2Cparameter-schema#pr-trigger - drafts: false - -# https://docs.microsoft.com/ru-ru/azure/devops/pipelines/process/scheduled-triggers?view=azure-devops&tabs=yaml#scheduled-triggers -# Triggers clean checkout to compile from scratch, remove old builds artifacts, and free disk space on CI agents. -# https://github.com/KomodoPlatform/atomicDEX-API/blob/957fd856fb74d462058a5ad47ec46d79e3bf1d83/azure-pipelines-build-stage-job.yml#L20 -schedules: - - cron: "0 0 * * *" - displayName: Scheduled clean midnight UTC build - branches: - include: - - dev - -stages: - - stage: Lint - displayName: Formatting, Clippy, and other checks - jobs: - - template: azure-pipelines-lint-stage-job.yml # Template reference - parameters: - name: 'MM2_Lint_Linux' - os: 'Linux' - - - template: azure-pipelines-lint-stage-job.yml # Template reference - parameters: - name: 'MM2_Lint_MacOS' - os: 'Darwin' - - - template: azure-pipelines-lint-stage-job.yml # Template reference - parameters: - name: 'MM2_Lint_Win' - os: 'Windows_NT' - - - stage: WASM - displayName: WASM build and test - condition: succeeded('Lint') - jobs: - - template: azure-pipelines-wasm-stage-job.yml # Template reference - parameters: - name: 'MM2_WASM_Linux' - os: 'Linux' - bob_passphrase: 'BOB_PASSPHRASE_LINUX' - bob_userpass: 'BOB_USERPASS_LINUX' - alice_passphrase: 'ALICE_PASSPHRASE_LINUX' - alice_userpass: 'ALICE_USERPASS_LINUX' - telegram_api_key: 'TELEGRAM_API_KEY' - - - stage: Mobile - displayName: Mobile libs build - condition: and(succeeded('WASM'), or(eq(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranchName'], 'main'), eq(variables['Build.SourceBranchName'], 'dev'))) - jobs: - - template: azure-pipelines-android-job.yml - parameters: - os: 'Linux' - - template: azure-pipelines-ios-job.yml - parameters: - os: 'Darwin' - - - stage: Desktop - displayName: Desktop Build and test - condition: or(eq(dependencies.Mobile.result, 'Succeeded'), and(eq(dependencies.Mobile.result, 'Skipped'), succeeded('WASM'))) - dependsOn: Mobile - jobs: - - template: azure-pipelines-build-stage-job.yml # Template reference - parameters: - name: 'MM2_Build_Linux' - os: 'Linux' - bob_passphrase: 'BOB_PASSPHRASE_LINUX' - bob_userpass: 'BOB_USERPASS_LINUX' - alice_passphrase: 'ALICE_PASSPHRASE_LINUX' - alice_userpass: 'ALICE_USERPASS_LINUX' - telegram_api_key: 'TELEGRAM_API_KEY' - - - template: azure-pipelines-build-stage-job.yml # Template reference - parameters: - name: 'MM2_Build_MacOS' - os: 'Darwin' - bob_passphrase: 'BOB_PASSPHRASE_MAC' - bob_userpass: 'BOB_USERPASS_MAC' - alice_passphrase: 'ALICE_PASSPHRASE_MAC' - alice_userpass: 'ALICE_USERPASS_MAC' - telegram_api_key: 'TELEGRAM_API_KEY' - - - template: azure-pipelines-build-stage-job.yml # Template reference - parameters: - name: 'MM2_Build_Windows' - os: 'Windows_NT' - bob_passphrase: 'BOB_PASSPHRASE_WIN' - bob_userpass: 'BOB_USERPASS_WIN' - alice_passphrase: 'ALICE_PASSPHRASE_WIN' - alice_userpass: 'ALICE_USERPASS_WIN' - telegram_api_key: 'TELEGRAM_API_KEY' - diff --git a/deny.toml b/deny.toml index 0120610616..a58cbd21e0 100644 --- a/deny.toml +++ b/deny.toml @@ -50,9 +50,6 @@ notice = "warn" # RUSTSEC-2020-0071 is related to time crate, which is used only by chrono in our deps tree, remove when https://github.com/chronotope/chrono/issues/700 is resolved # RUSTSEC-2022-0040 is related to owning-ref, which seems unmaintained. We need to find a way to get rid of it. https://github.com/KomodoPlatform/atomicDEX-API/issues/1429 -# RUSTSEC-2022-0055 is axum/axum-core vulnerability, which seems to be related only to server-side, which we don't utilize. -# RUSTSEC-2023-0001 is tokio Windows-specific bug in the code that we don't use -# Unignore RUSTSEC-2022-0084 after updating libp2p ignore = [ "RUSTSEC-2020-0071", "RUSTSEC-2022-0040", @@ -61,12 +58,9 @@ ignore = [ "RUSTSEC-2021-0145", "RUSTSEC-2020-0056", "RUSTSEC-2022-0080", - "RUSTSEC-2020-0036", - "RUSTSEC-2021-0139", "RUSTSEC-2021-0059", "RUSTSEC-2021-0060", - "RUSTSEC-2022-0090", - "RUSTSEC-2023-0018", + "RUSTSEC-2022-0090" ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories @@ -225,7 +219,6 @@ skip = [ { name = "getrandom", version = "*" }, { name = "group", version = "*" }, { name = "hashbrown", version = "*" }, - { name = "hdrhistogram", version = "*" }, { name = "hex", version = "*" }, { name = "hmac", version = "*" }, { name = "http", version = "*" }, @@ -288,7 +281,6 @@ skip = [ { name = "tiny-keccak", version = "*" }, { name = "tokio-util", version = "*" }, { name = "trie-root", version = "*" }, - { name = "uint", version = "*" }, { name = "unicode-xid", version = "*" }, { name = "unsigned-varint", version = "*" }, { name = "url", version = "*" }, diff --git a/docs/DEV_ENVIRONMENT.md b/docs/DEV_ENVIRONMENT.md index 3c827d6fdc..0253bdd25e 100644 --- a/docs/DEV_ENVIRONMENT.md +++ b/docs/DEV_ENVIRONMENT.md @@ -36,7 +36,7 @@ ``` sudo ln -s $(which podman) /usr/bin/docker ``` -9. Try `cargo test --features native --all -- --test-threads=16`. +9. Try `cargo test --features "native run-docker-tests" --all -- --test-threads=16`. ## Running WASM tests diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 1d77bb0001..cb05e52d33 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -6,9 +6,17 @@ edition = "2018" [features] zhtlc-native-tests = [] # TODO -# Remove this once the solana integration becomes stable/completed. -disable-solana-tests = [] -default = ["disable-solana-tests"] +enable-solana = [ + "dep:bincode", + "dep:ed25519-dalek-bip32", + "dep:solana-client", + "dep:solana-sdk", + "dep:solana-transaction-status", + "dep:spl-token", + "dep:spl-associated-token-account" +] +enable-nft-integration = [] +default = [] [lib] name = "coins" @@ -21,21 +29,19 @@ async-trait = "0.1.52" base64 = "0.10.0" base58 = "0.2.0" bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -bitcoin_hashes = "0.10.0" +bitcoin_hashes = "0.11" bitcrypto = { path = "../mm2_bitcoin/crypto" } -bincode = "1.3.3" byteorder = "1.3" bytes = "0.4" cfg-if = "1.0" chain = { path = "../mm2_bitcoin/chain" } common = { path = "../common" } cosmrs = { version = "0.7", default-features = false } -crossbeam = "0.7" +crossbeam = "0.8" crypto = { path = "../crypto" } db_common = { path = "../db_common" } derive_more = "0.99" ed25519-dalek = "1.0.1" -ed25519-dalek-bip32 = "0.2.0" enum_from = { path = "../derives/enum_from" } ethabi = { version = "17.0.0" } ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } @@ -57,6 +63,7 @@ lazy_static = "1.4" libc = "0.2" mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } +mm2_git = { path = "../mm2_git" } mm2_io = { path = "../mm2_io" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } @@ -88,18 +95,20 @@ utxo_signer = { path = "utxo_signer" } # using the same version as cosmrs tendermint-rpc = { version = "=0.23.7", default-features = false } tiny-bip39 = "0.8.0" -uuid = { version = "0.7", features = ["serde", "v4"] } +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } # One of web3 dependencies is the old `tokio-uds 0.1.7` which fails cross-compiling to ARM. # We don't need the default web3 features at all since we added our own web3 transport using shared HYPER instance. web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.19.0", default-features = false } zbase32 = "0.1.2" [target.'cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies] -solana-client = { version = "1", default-features = false } -solana-sdk = { version = "1", default-features = false } -solana-transaction-status = "1" -spl-token = { version = "3" } -spl-associated-token-account = "1" +bincode = { version = "1.3.3", default-features = false, optional = true } +ed25519-dalek-bip32 = { version = "0.2.0", default-features = false, optional = true } +solana-client = { version = "1", default-features = false, optional = true } +solana-sdk = { version = "1", default-features = false, optional = true } +solana-transaction-status = { version = "1", optional = true } +spl-token = { version = "3", optional = true } +spl-associated-token-account = { version = "1", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = { version = "0.3.27" } @@ -113,28 +122,27 @@ web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "Re [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs = { version = "1" } -bitcoin = "0.28.1" +bitcoin = "0.29" hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features hyper-rustls = { version = "0.23", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -lightning = "0.0.110" -lightning-background-processor = "0.0.110" -lightning-invoice = { version = "0.18.0", features = ["serde"] } -lightning-net-tokio = "0.0.110" -lightning-rapid-gossip-sync = "0.0.110" +lightning = "0.0.113" +lightning-background-processor = "0.0.113" +lightning-invoice = { version = "0.21.0", features = ["serde"] } +lightning-net-tokio = "0.0.113" rust-ini = { version = "0.13" } rustls = { version = "0.20", features = ["dangerous_configuration"] } -secp256k1v22 = { version = "0.22", package = "secp256k1" } +secp256k1v24 = { version = "0.24", package = "secp256k1" } tendermint-config = { version = "0.23.7", default-features = false } tokio = { version = "1.20" } tokio-rustls = { version = "0.23" } tonic = { version = "0.7", features = ["tls", "tls-webpki-roots", "compression"] } webpki-roots = { version = "0.22" } -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git" } -zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git" } -zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git" } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git" } +zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.0.0" } +zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.0.0" } +zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.0.0" } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.0.0" } [target.'cfg(windows)'.dependencies] winapi = "0.3" @@ -143,5 +151,5 @@ winapi = "0.3" mm2_test_helpers = { path = "../mm2_test_helpers" } [build-dependencies] -prost-build = { version = "0.10.3", default-features = false } +prost-build = { version = "0.10.4", default-features = false } tonic-build = { version = "0.7", features = ["prost", "compression"] } diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index a296f68adb..648e3428b4 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -60,7 +60,9 @@ impl From for ValidatePaymentError { match e { Web3RpcError::Transport(tr) => ValidatePaymentError::Transport(tr), Web3RpcError::InvalidResponse(resp) => ValidatePaymentError::InvalidRpcResponse(resp), - Web3RpcError::Internal(internal) => ValidatePaymentError::InternalError(internal), + Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { + ValidatePaymentError::InternalError(internal) + }, } } } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 6d7ecc942d..8c55c5c366 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -21,6 +21,9 @@ // Copyright © 2022 AtomicDEX. All rights reserved. // use super::eth::Action::{Call, Create}; +#[cfg(feature = "enable-nft-integration")] +use crate::nft::nft_structs::{ContractType, ConvertChain, NftListReq, TransactionNftDetails, WithdrawErc1155, + WithdrawErc721}; use async_trait::async_trait; use bitcrypto::{keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry}; @@ -32,6 +35,7 @@ use common::{get_utc_timestamp, now_ms, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::key_pair_from_secret; use crypto::{CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; use derive_more::Display; +use enum_from::EnumFromStringify; use ethabi::{Contract, Function, Token}; pub use ethcore_transaction::SignedTransaction as SignedEthTx; use ethcore_transaction::{Action, Transaction as UnSignedEthTx, UnverifiedTransaction}; @@ -73,23 +77,22 @@ cfg_wasm32! { } use super::{coin_conf, AsyncMutex, BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, - CoinProtocol, CoinTransportMetrics, CoinsContext, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, - IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, - NumConversError, NumConversResult, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, - PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, - RawTransactionResult, RefundError, RefundResult, RpcClientType, RpcTransportEventHandler, - RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, SendMakerPaymentArgs, - SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, - SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, - SendWatcherRefundsPaymentArgs, SignatureError, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, + CoinProtocol, CoinTransportMetrics, CoinsContext, ConfirmPaymentInput, EthValidateFeeArgs, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, MmCoin, + MyAddressError, MyWalletAddress, NegotiateSwapContractAddrErr, NumConversError, NumConversResult, + PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, + RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult, + RefundError, RefundPaymentArgs, RefundResult, RpcClientType, RpcTransportEventHandler, + RpcTransportEventHandlerShared, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult, - EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_RECEIVER_ERR_LOG, - INVALID_SENDER_ERR_LOG}; + EARLY_CONFIRMATION_ERR_LOG, INSUFFICIENT_WATCHER_REWARD_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(test)] mod eth_tests; @@ -97,7 +100,11 @@ pub use rlp; mod web3_transport; #[path = "eth/v2_activation.rs"] pub mod v2_activation; -use v2_activation::build_address_and_priv_key_policy; +#[cfg(feature = "enable-nft-integration")] +use crate::nft::{find_wallet_amount, WithdrawNftResult}; +#[cfg(feature = "enable-nft-integration")] +use crate::{lp_coinfind_or_err, MmCoinEnum, TransactionType}; +use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; mod nonce; use nonce::ParityNonce; @@ -109,6 +116,10 @@ use nonce::ParityNonce; const SWAP_CONTRACT_ABI: &str = include_str!("eth/swap_contract_abi.json"); /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md const ERC20_ABI: &str = include_str!("eth/erc20_abi.json"); +/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md +const ERC721_ABI: &str = include_str!("eth/erc721_abi.json"); +/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md +const ERC1155_ABI: &str = include_str!("eth/erc1155_abi.json"); /// Payment states from etomic swap smart contract: https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol#L5 pub const PAYMENT_STATE_UNINITIALIZED: u8 = 0; pub const PAYMENT_STATE_SENT: u8 = 1; @@ -147,11 +158,14 @@ const GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC: i64 = 90; lazy_static! { pub static ref SWAP_CONTRACT: Contract = Contract::load(SWAP_CONTRACT_ABI.as_bytes()).unwrap(); pub static ref ERC20_CONTRACT: Contract = Contract::load(ERC20_ABI.as_bytes()).unwrap(); + pub static ref ERC721_CONTRACT: Contract = Contract::load(ERC721_ABI.as_bytes()).unwrap(); + pub static ref ERC1155_CONTRACT: Contract = Contract::load(ERC1155_ABI.as_bytes()).unwrap(); } pub type Web3RpcFut = Box> + Send>; pub type Web3RpcResult = Result>; pub type GasStationResult = Result>; +type GasDetails = (U256, U256); #[derive(Debug, Display)] pub enum GasStationReqErr { @@ -188,6 +202,8 @@ pub enum Web3RpcError { Transport(String), #[display(fmt = "Invalid response: {}", _0)] InvalidResponse(String), + #[display(fmt = "Timeout: {}", _0)] + Timeout(String), #[display(fmt = "Internal: {}", _0)] Internal(String), } @@ -229,7 +245,9 @@ impl From for RawTransactionError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => RawTransactionError::Transport(tr), - Web3RpcError::Internal(internal) => RawTransactionError::InternalError(internal), + Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { + RawTransactionError::InternalError(internal) + }, } } } @@ -268,7 +286,9 @@ impl From for WithdrawError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(err) | Web3RpcError::InvalidResponse(err) => WithdrawError::Transport(err), - Web3RpcError::Internal(internal) => WithdrawError::InternalError(internal), + Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { + WithdrawError::InternalError(internal) + }, } } } @@ -281,7 +301,9 @@ impl From for TradePreimageError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(err) | Web3RpcError::InvalidResponse(err) => TradePreimageError::Transport(err), - Web3RpcError::Internal(internal) => TradePreimageError::InternalError(internal), + Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { + TradePreimageError::InternalError(internal) + }, } } } @@ -310,7 +332,7 @@ impl From for BalanceError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => BalanceError::Transport(tr), - Web3RpcError::Internal(internal) => BalanceError::Internal(internal), + Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => BalanceError::Internal(internal), } } } @@ -420,6 +442,7 @@ pub struct EthCoinImpl { sign_message_prefix: Option, swap_contract_address: Address, fallback_swap_contract: Option
, + contract_supports_watchers: bool, web3: Web3, /// The separate web3 instances kept to get nonce, will replace the web3 completely soon web3_instances: Vec, @@ -733,40 +756,8 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { }; let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; - let (gas, gas_price) = match req.fee { - Some(WithdrawFee::EthGas { gas_price, gas }) => { - let gas_price = wei_from_big_decimal(&gas_price, 9)?; - (gas.into(), gas_price) - }, - Some(fee_policy) => { - let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); - return MmError::err(WithdrawError::InvalidFeePolicy(error)); - }, - None => { - let gas_price = coin.get_gas_price().compat().await?; - // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH - let eth_value_for_estimate = if req.max && coin.coin_type == EthCoinType::Eth { - eth_value - gas_price * U256::from(21000) - } else { - eth_value - }; - let estimate_gas_req = CallRequest { - value: Some(eth_value_for_estimate), - data: Some(data.clone().into()), - from: Some(coin.my_address), - to: Some(call_addr), - gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), - ..CallRequest::default() - }; - // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. - // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. - let gas_limit = coin.estimate_gas(estimate_gas_req).compat().await?; - (gas_limit, gas_price) - }, - }; + let (gas, gas_price) = + get_eth_gas_details(&coin, req.fee, eth_value, data.clone().into(), call_addr, req.max).await?; let total_fee = gas * gas_price; let total_fee_dec = u256_to_big_decimal(total_fee, coin.decimals)?; @@ -876,6 +867,169 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { }) } +#[cfg(feature = "enable-nft-integration")] +/// `withdraw_erc1155` function returns details of `ERC-1155` transaction including tx hex, +/// which should be sent to`send_raw_transaction` RPC to broadcast the transaction. +pub async fn withdraw_erc1155(ctx: MmArc, req: WithdrawErc1155) -> WithdrawNftResult { + let coin = lp_coinfind_or_err(&ctx, &req.chain.to_ticker()).await?; + let (to_addr, token_addr, eth_coin) = get_valid_nft_add_to_withdraw(coin, &req.to, &req.token_address)?; + let my_address = eth_coin.my_address()?; + + // todo check amount in nft cache, instead of sending new moralis req + // dont use `get_nft_metadata` for erc1155, it can return info related to other owner. + let nft_req = NftListReq { + chains: vec![req.chain], + }; + let wallet_amount = find_wallet_amount(ctx, nft_req, req.token_address.clone(), req.token_id.clone()).await?; + + let amount_dec = if req.max { + wallet_amount.clone() + } else { + req.amount.unwrap_or_else(|| 1.into()) + }; + + if amount_dec > wallet_amount { + return MmError::err(WithdrawError::NotEnoughNftsAmount { + token_address: req.token_address, + token_id: req.token_id.to_string(), + available: wallet_amount, + required: amount_dec, + }); + } + + let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { + EthCoinType::Eth => { + let function = ERC1155_CONTRACT.function("safeTransferFrom")?; + let token_id_u256 = U256::from_dec_str(&req.token_id.to_string()) + .map_err(|e| format!("{:?}", e)) + .map_to_mm(NumConversError::new)?; + let amount_u256 = U256::from_dec_str(&amount_dec.to_string()) + .map_err(|e| format!("{:?}", e)) + .map_to_mm(NumConversError::new)?; + let data = function.encode_input(&[ + Token::Address(eth_coin.my_address), + Token::Address(to_addr), + Token::Uint(token_id_u256), + Token::Uint(amount_u256), + Token::Bytes("0x".into()), + ])?; + (0.into(), data, token_addr, eth_coin.ticker()) + }, + EthCoinType::Erc20 { .. } => { + return MmError::err(WithdrawError::InternalError( + "Erc20 coin type doesnt support withdraw nft".to_owned(), + )) + }, + }; + let (gas, gas_price) = + get_eth_gas_details(ð_coin, req.fee, eth_value, data.clone().into(), call_addr, false).await?; + let _nonce_lock = eth_coin.nonce_lock.lock().await; + let nonce = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + + let tx = UnSignedEthTx { + nonce, + value: eth_value, + action: Action::Call(call_addr), + data, + gas, + gas_price, + }; + + let secret = eth_coin.priv_key_policy.key_pair_or_err()?.secret(); + let signed = tx.sign(secret, eth_coin.chain_id); + let signed_bytes = rlp::encode(&signed); + let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + + Ok(TransactionNftDetails { + tx_hex: BytesJson::from(signed_bytes.to_vec()), + tx_hash: format!("{:02x}", signed.tx_hash()), + from: vec![my_address], + to: vec![req.to], + contract_type: ContractType::Erc1155, + token_address: req.token_address, + token_id: req.token_id, + amount: amount_dec, + fee_details: Some(fee_details.into()), + coin: eth_coin.ticker.clone(), + block_height: 0, + timestamp: now_ms() / 1000, + internal_id: 0, + transaction_type: TransactionType::NftTransfer, + }) +} + +#[cfg(feature = "enable-nft-integration")] +/// `withdraw_erc721` function returns details of `ERC-721` transaction including tx hex, +/// which should be sent to`send_raw_transaction` RPC to broadcast the transaction. +pub async fn withdraw_erc721(ctx: MmArc, req: WithdrawErc721) -> WithdrawNftResult { + let coin = lp_coinfind_or_err(&ctx, &req.chain.to_ticker()).await?; + let (to_addr, token_addr, eth_coin) = get_valid_nft_add_to_withdraw(coin, &req.to, &req.token_address)?; + let my_address = eth_coin.my_address()?; + + let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { + EthCoinType::Eth => { + let function = ERC721_CONTRACT.function("safeTransferFrom")?; + let token_id_u256 = U256::from_dec_str(&req.token_id.to_string()) + .map_err(|e| format!("{:?}", e)) + .map_to_mm(NumConversError::new)?; + let data = function.encode_input(&[ + Token::Address(eth_coin.my_address), + Token::Address(to_addr), + Token::Uint(token_id_u256), + ])?; + (0.into(), data, token_addr, eth_coin.ticker()) + }, + EthCoinType::Erc20 { .. } => { + return MmError::err(WithdrawError::InternalError( + "Erc20 coin type doesnt support withdraw nft".to_owned(), + )) + }, + }; + let (gas, gas_price) = + get_eth_gas_details(ð_coin, req.fee, eth_value, data.clone().into(), call_addr, false).await?; + let _nonce_lock = eth_coin.nonce_lock.lock().await; + let nonce = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + + let tx = UnSignedEthTx { + nonce, + value: eth_value, + action: Action::Call(call_addr), + data, + gas, + gas_price, + }; + + let secret = eth_coin.priv_key_policy.key_pair_or_err()?.secret(); + let signed = tx.sign(secret, eth_coin.chain_id); + let signed_bytes = rlp::encode(&signed); + let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + + Ok(TransactionNftDetails { + tx_hex: BytesJson::from(signed_bytes.to_vec()), + tx_hash: format!("{:02x}", signed.tx_hash()), + from: vec![my_address], + to: vec![req.to], + contract_type: ContractType::Erc721, + token_address: req.token_address, + token_id: req.token_id, + amount: 1.into(), + fee_details: Some(fee_details.into()), + coin: eth_coin.ticker.clone(), + block_height: 0, + timestamp: now_ms() / 1000, + internal_id: 0, + transaction_type: TransactionType::NftTransfer, + }) +} + #[derive(Clone)] pub struct EthCoin(Arc); impl Deref for EthCoin { @@ -894,224 +1048,71 @@ impl SwapOps for EthCoin { ) } - fn send_maker_payment(&self, maker_payment: SendMakerPaymentArgs) -> TransactionFut { - let taker_addr = try_tx_fus!(addr_from_raw_pubkey(maker_payment.other_pubkey)); - let swap_contract_address = try_tx_fus!(maker_payment.swap_contract_address.try_to_address()); - + fn send_maker_payment(&self, maker_payment: SendPaymentArgs) -> TransactionFut { Box::new( - self.send_hash_time_locked_payment( - self.etomic_swap_id(maker_payment.time_lock, maker_payment.secret_hash), - try_tx_fus!(wei_from_big_decimal(&maker_payment.amount, self.decimals)), - maker_payment.time_lock, - maker_payment.secret_hash, - taker_addr, - swap_contract_address, - ) - .map(TransactionEnum::from), + self.send_hash_time_locked_payment(maker_payment) + .map(TransactionEnum::from), ) } - fn send_taker_payment(&self, taker_payment: SendTakerPaymentArgs) -> TransactionFut { - let maker_addr = try_tx_fus!(addr_from_raw_pubkey(taker_payment.other_pubkey)); - let swap_contract_address = try_tx_fus!(taker_payment.swap_contract_address.try_to_address()); - + fn send_taker_payment(&self, taker_payment: SendPaymentArgs) -> TransactionFut { Box::new( - self.send_hash_time_locked_payment( - self.etomic_swap_id(taker_payment.time_lock, taker_payment.secret_hash), - try_tx_fus!(wei_from_big_decimal(&taker_payment.amount, self.decimals)), - taker_payment.time_lock, - taker_payment.secret_hash, - maker_addr, - swap_contract_address, - ) - .map(TransactionEnum::from), + self.send_hash_time_locked_payment(taker_payment) + .map(TransactionEnum::from), ) } - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_spends_payment_args.other_payment_tx)); - let signed = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = - try_tx_fus!(maker_spends_payment_args.swap_contract_address.try_to_address(), signed); - + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { Box::new( - self.spend_hash_time_locked_payment( - signed, - maker_spends_payment_args.secret_hash, - swap_contract_address, - maker_spends_payment_args.secret, - ) - .map(TransactionEnum::from), + self.spend_hash_time_locked_payment(maker_spends_payment_args) + .map(TransactionEnum::from), ) } - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_spends_payment_args.other_payment_tx)); - let signed = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(taker_spends_payment_args.swap_contract_address.try_to_address()); + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { Box::new( - self.spend_hash_time_locked_payment( - signed, - taker_spends_payment_args.secret_hash, - swap_contract_address, - taker_spends_payment_args.secret, - ) - .map(TransactionEnum::from), + self.spend_hash_time_locked_payment(taker_spends_payment_args) + .map(TransactionEnum::from), ) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_refunds_payment_args.payment_tx)); - let signed = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(taker_refunds_payment_args.swap_contract_address.try_to_address()); - + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { Box::new( - self.refund_hash_time_locked_payment(swap_contract_address, signed, taker_refunds_payment_args.secret_hash) + self.refund_hash_time_locked_payment(taker_refunds_payment_args) .map(TransactionEnum::from), ) } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_refunds_payment_args.payment_tx)); - let signed = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(maker_refunds_payment_args.swap_contract_address.try_to_address()); - + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { Box::new( - self.refund_hash_time_locked_payment(swap_contract_address, signed, maker_refunds_payment_args.secret_hash) + self.refund_hash_time_locked_payment(maker_refunds_payment_args) .map(TransactionEnum::from), ) } - fn validate_fee( - &self, - validate_fee_args: ValidateFeeArgs<'_>, - ) -> Box + Send> { - let selfi = self.clone(); + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { let tx = match validate_fee_args.fee_tx { TransactionEnum::SignedEthTx(t) => t.clone(), _ => panic!(), }; - let sender_addr = try_fus!(addr_from_raw_pubkey(validate_fee_args.expected_sender)); - let fee_addr = try_fus!(addr_from_raw_pubkey(validate_fee_args.fee_addr)); - let amount = validate_fee_args.amount.clone(); - let min_block_number = validate_fee_args.min_block_number; - - let fut = async move { - let expected_value = try_s!(wei_from_big_decimal(&amount, selfi.decimals)); - let tx_from_rpc = try_s!(selfi.web3.eth().transaction(TransactionId::Hash(tx.hash)).await); - let tx_from_rpc = match tx_from_rpc { - Some(t) => t, - None => return ERR!("Didn't find provided tx {:?} on ETH node", tx), - }; - - if tx_from_rpc.from != Some(sender_addr) { - return ERR!( - "Fee tx {:?} was sent from wrong address, expected {:?}", - tx_from_rpc, - sender_addr - ); - } - - if let Some(block_number) = tx_from_rpc.block_number { - if block_number <= min_block_number.into() { - return ERR!( - "Fee tx {:?} confirmed before min_block {}", - tx_from_rpc, - min_block_number, - ); - } - } - match &selfi.coin_type { - EthCoinType::Eth => { - if tx_from_rpc.to != Some(fee_addr) { - return ERR!( - "Fee tx {:?} was sent to wrong address, expected {:?}", - tx_from_rpc, - fee_addr - ); - } - - if tx_from_rpc.value < expected_value { - return ERR!( - "Fee tx {:?} value is less than expected {:?}", - tx_from_rpc, - expected_value - ); - } - }, - EthCoinType::Erc20 { - platform: _, - token_addr, - } => { - if tx_from_rpc.to != Some(*token_addr) { - return ERR!( - "ERC20 Fee tx {:?} called wrong smart contract, expected {:?}", - tx_from_rpc, - token_addr - ); - } - - let function = try_s!(ERC20_CONTRACT.function("transfer")); - let decoded_input = try_s!(decode_contract_call(function, &tx_from_rpc.input.0)); - - if decoded_input[0] != Token::Address(fee_addr) { - return ERR!( - "ERC20 Fee tx was sent to wrong address {:?}, expected {:?}", - decoded_input[0], - fee_addr - ); - } - - match decoded_input[1] { - Token::Uint(value) => { - if value < expected_value { - return ERR!("ERC20 Fee tx value {} is less than expected {}", value, expected_value); - } - }, - _ => return ERR!("Should have got uint token but got {:?}", decoded_input[1]), - } - }, - } - - Ok(()) - }; - Box::new(fut.boxed().compat()) + validate_fee_impl(self.clone(), EthValidateFeeArgs { + fee_tx_hash: &tx.hash, + expected_sender: validate_fee_args.expected_sender, + fee_addr: validate_fee_args.fee_addr, + amount: validate_fee_args.amount, + min_block_number: validate_fee_args.min_block_number, + uuid: validate_fee_args.uuid, + }) } + #[inline] fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - let swap_contract_address = try_f!(input - .swap_contract_address - .try_to_address() - .map_to_mm(ValidatePaymentError::InvalidParameter)); - self.validate_payment( - &input.payment_tx, - input.time_lock, - &input.other_pub, - &input.secret_hash, - input.amount, - swap_contract_address, - ) + self.validate_payment(input) } + #[inline] fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - let swap_contract_address = try_f!(input - .swap_contract_address - .try_to_address() - .map_to_mm(ValidatePaymentError::InvalidParameter)); - self.validate_payment( - &input.payment_tx, - input.time_lock, - &input.other_pub, - &input.secret_hash, - input.amount, - swap_contract_address, - ) + self.validate_payment(input) } fn check_if_my_payment_sent( @@ -1189,6 +1190,7 @@ impl SwapOps for EthCoin { swap_contract_address, input.secret_hash, input.search_from_block, + input.watcher_reward, ) .await } @@ -1203,6 +1205,7 @@ impl SwapOps for EthCoin { swap_contract_address, input.secret_hash, input.search_from_block, + input.watcher_reward, ) .await } @@ -1211,9 +1214,15 @@ impl SwapOps for EthCoin { unimplemented!(); } - async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + _secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, String> { let unverified: UnverifiedTransaction = try_s!(rlp::decode(spend_tx)); - let function = try_s!(SWAP_CONTRACT.function("receiverSpend")); + let function_name = get_function_name("receiverSpend", watcher_reward); + let function = try_s!(SWAP_CONTRACT.function(&function_name)); // Validate contract call; expected to be receiverSpend. // https://www.4byte.directory/signatures/?bytes4_signature=02ed292b. @@ -1343,7 +1352,10 @@ impl SwapOps for EthCoin { MmError::err(ValidateInstructionsErr::UnsupportedCoin(self.ticker().to_string())) } - fn is_supported_by_watchers(&self) -> bool { true } + fn is_supported_by_watchers(&self) -> bool { + false + //self.contract_supports_watchers + } } #[async_trait] @@ -1363,11 +1375,8 @@ impl MakerSwapTakerCoin for EthCoin { #[async_trait] impl WatcherOps for EthCoin { fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(input.preimage)); - let signed = try_tx_fus!(SignedEthTx::new(tx)); - Box::new( - self.watcher_spend_hash_time_locked_payment(signed, input.secret_hash, input.secret, input.taker_pub) + self.watcher_spends_hash_time_locked_payment(input) .map(TransactionEnum::from), ) } @@ -1403,107 +1412,25 @@ impl WatcherOps for EthCoin { Box::new(fut.boxed().compat()) } - fn send_taker_payment_refund_preimage( - &self, - watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(watcher_refunds_payment_args.payment_tx)); - let signed = try_tx_fus!(SignedEthTx::new(tx)); - + fn send_taker_payment_refund_preimage(&self, args: RefundPaymentArgs) -> TransactionFut { Box::new( - self.watcher_refunds_hash_time_locked_payment( - signed, - watcher_refunds_payment_args.secret_hash, - watcher_refunds_payment_args.other_pubkey, - ) - .map(TransactionEnum::from), + self.watcher_refunds_hash_time_locked_payment(args) + .map(TransactionEnum::from), ) } fn watcher_validate_taker_fee(&self, validate_fee_args: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { - let selfi = self.clone(); - let sender_addr = - try_f!(addr_from_raw_pubkey(&validate_fee_args.sender_pubkey) - .map_to_mm(ValidatePaymentError::InvalidParameter)); - let fee_addr = - try_f!(addr_from_raw_pubkey(&validate_fee_args.fee_addr).map_to_mm(ValidatePaymentError::InvalidParameter)); - let min_block_number = validate_fee_args.min_block_number; - let taker_fee_hash = validate_fee_args.taker_fee_hash; - - let fut = async move { - let tx_from_rpc = selfi - .web3 - .eth() - .transaction(TransactionId::Hash(H256::from_slice(taker_fee_hash.as_slice()))) - .await - .map_to_mm(|e| ValidatePaymentError::InvalidRpcResponse(e.to_string()))?; - - let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { - ValidatePaymentError::TxDoesNotExist(format!( - "Didn't find provided tx {:?} on ETH node", - H256::from_slice(taker_fee_hash.as_slice()) - )) - })?; - - if tx_from_rpc.from != Some(sender_addr) { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: Fee tx {:?} was sent from wrong address, expected {:?}", - INVALID_SENDER_ERR_LOG, tx_from_rpc, sender_addr - ))); - } - - if let Some(block_number) = tx_from_rpc.block_number { - if block_number <= min_block_number.into() { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: Fee tx {:?} confirmed before min_block {}", - EARLY_CONFIRMATION_ERR_LOG, tx_from_rpc, min_block_number - ))); - } - } - - //TODO: Validate if taker fee is old - - match &selfi.coin_type { - EthCoinType::Eth => { - if tx_from_rpc.to != Some(fee_addr) { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: Fee tx {:?} was sent to wrong address, expected {:?}", - INVALID_RECEIVER_ERR_LOG, tx_from_rpc, fee_addr - ))); - } - }, - EthCoinType::Erc20 { - platform: _, - token_addr, - } => { - if tx_from_rpc.to != Some(*token_addr) { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: ERC20 Fee tx {:?} called wrong smart contract, expected {:?}", - INVALID_CONTRACT_ADDRESS_ERR_LOG, tx_from_rpc, token_addr - ))); - } - - let function = ERC20_CONTRACT - .function("transfer") - .map_to_mm(|e| ValidatePaymentError::InternalError(e.to_string()))?; - let decoded_input = function - .decode_input(&tx_from_rpc.input.0) - .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; - let address_input = get_function_input_data(&decoded_input, function, 0) - .map_to_mm(ValidatePaymentError::TxDeserializationError)?; - if address_input != Token::Address(fee_addr) { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "{}: ERC20 Fee tx was sent to wrong address {:?}, expected {:?}", - INVALID_RECEIVER_ERR_LOG, address_input, fee_addr - ))); - } - }, - } - - Ok(()) - }; + validate_fee_impl(self.clone(), EthValidateFeeArgs { + fee_tx_hash: &H256::from_slice(validate_fee_args.taker_fee_hash.as_slice()), + expected_sender: &validate_fee_args.sender_pubkey, + fee_addr: &validate_fee_args.fee_addr, + amount: &BigDecimal::from(0), + min_block_number: validate_fee_args.min_block_number, + uuid: &[], + }) - Box::new(fut.boxed().compat()) + // TODO: Add validations specific for watchers + // 1.Validate if taker fee is old } fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { @@ -1532,8 +1459,7 @@ impl WatcherOps for EthCoin { if tx_from_rpc.from != Some(sender) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Payment tx {:?} was sent from wrong address, expected {:?}", - tx_from_rpc, sender + "{INVALID_SENDER_ERR_LOG}: Payment tx {tx_from_rpc:?} was sent from wrong address, expected {sender:?}" ))); } @@ -1547,7 +1473,7 @@ impl WatcherOps for EthCoin { && Some(swap_contract_address) != fallback_swap_contract { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Payment tx {tx_from_rpc:?} was sent to wrong address, expected either {expected_swap_contract_address:?} or the fallback {fallback_swap_contract:?}" + "{INVALID_CONTRACT_ADDRESS_ERR_LOG}: Payment tx {tx_from_rpc:?} was sent to wrong address, expected either {expected_swap_contract_address:?} or the fallback {fallback_swap_contract:?}" ))); } @@ -1558,26 +1484,28 @@ impl WatcherOps for EthCoin { .map_to_mm(ValidatePaymentError::Transport)?; if status != PAYMENT_STATE_SENT.into() { return MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( - "Payment state is not PAYMENT_STATE_SENT, got {}", - status + "{INVALID_PAYMENT_STATE_ERR_LOG}: Payment state is not PAYMENT_STATE_SENT, got {status}" ))); } + let min_watcher_reward = input.min_watcher_reward.ok_or_else(|| { + ValidatePaymentError::InvalidParameter("Minimum watcher reward argument is not provided".to_string()) + })?; + match &selfi.coin_type { EthCoinType::Eth => { + let function_name = get_function_name("ethPayment", input.min_watcher_reward.is_some()); let function = SWAP_CONTRACT - .function("ethPayment") + .function(&function_name) .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; - let decoded = function - .decode_input(&tx_from_rpc.input.0) + let decoded = decode_contract_call(function, &tx_from_rpc.input.0) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; let swap_id_input = get_function_input_data(&decoded, function, 0) .map_to_mm(ValidatePaymentError::TxDeserializationError)?; if swap_id_input != Token::FixedBytes(swap_id.clone()) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Invalid 'swap_id' {:?}, expected {:?}", - decoded, swap_id + "{INVALID_SWAP_ID_ERR_LOG}: Invalid 'swap_id' {decoded:?}, expected {swap_id:?}" ))); } @@ -1585,9 +1513,7 @@ impl WatcherOps for EthCoin { .map_to_mm(ValidatePaymentError::TxDeserializationError)?; if receiver_input != Token::Address(receiver) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Payment tx receiver arg {:?} is invalid, expected {:?}", - receiver_input, - Token::Address(receiver) + "{INVALID_RECEIVER_ERR_LOG}: Payment tx receiver arg {receiver_input:?} is invalid, expected {:?}", Token::Address(receiver) ))); } @@ -1610,24 +1536,46 @@ impl WatcherOps for EthCoin { Token::Uint(U256::from(input.time_lock)), ))); } + + let watcher_reward = get_function_input_data(&decoded, function, 4) + .map_to_mm(ValidatePaymentError::TxDeserializationError)? + .into_uint() + .ok_or_else(|| { + ValidatePaymentError::WrongPaymentTx("Invalid type for watcher reward argument".to_string()) + })? + .as_u64(); + + if watcher_reward < min_watcher_reward { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{INSUFFICIENT_WATCHER_REWARD_ERR_LOG}: Provided watcher reward {} is less than the minimum required amount {}", + watcher_reward, min_watcher_reward, + ))); + } }, EthCoinType::Erc20 { platform: _, token_addr, } => { + let function_name = get_function_name("erc20Payment", input.min_watcher_reward.is_some()); let function = SWAP_CONTRACT - .function("erc20Payment") + .function(&function_name) .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; - let decoded = function - .decode_input(&tx_from_rpc.input.0) + let decoded = decode_contract_call(function, &tx_from_rpc.input.0) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; + if tx_from_rpc.value.as_u64() < min_watcher_reward { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{INSUFFICIENT_WATCHER_REWARD_ERR_LOG}: Provided watcher reward {} is less than the minimum required amount {}", + tx_from_rpc.value.as_u64(), + min_watcher_reward, + ))); + } + let swap_id_input = get_function_input_data(&decoded, function, 0) .map_to_mm(ValidatePaymentError::TxDeserializationError)?; if swap_id_input != Token::FixedBytes(swap_id.clone()) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Invalid 'swap_id' {:?}, expected {:?}", - decoded, swap_id + "{INVALID_SWAP_ID_ERR_LOG}: Invalid 'swap_id' {decoded:?}, expected {swap_id:?}" ))); } @@ -1645,9 +1593,7 @@ impl WatcherOps for EthCoin { .map_to_mm(ValidatePaymentError::TxDeserializationError)?; if receiver_addr_input != Token::Address(receiver) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Payment tx receiver arg {:?} is invalid, expected {:?}", - receiver_addr_input, - Token::Address(receiver), + "{INVALID_RECEIVER_ERR_LOG}: Payment tx receiver arg {receiver_addr_input:?} is invalid, expected {:?}", Token::Address(receiver), ))); } @@ -1694,6 +1640,7 @@ impl WatcherOps for EthCoin { swap_contract_address, input.secret_hash, input.search_from_block, + input.watcher_reward, ) .await } @@ -1805,82 +1752,90 @@ impl MarketCoinOps for EthCoin { ) } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - _requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + macro_rules! update_status_with_error { + ($status: ident, $error: ident) => { + match $error.get_inner() { + Web3RpcError::Timeout(_) => $status.append(" Timed out."), + _ => $status.append(" Failed."), + } + }; + } + let ctx = try_fus!(MmArc::from_weak(&self.ctx).ok_or("No context")); let mut status = ctx.log.status_handle(); status.status(&[&self.ticker], "Waiting for confirmations…"); - status.deadline(wait_until * 1000); + status.deadline(input.wait_until * 1000); - let unsigned: UnverifiedTransaction = try_fus!(rlp::decode(tx)); + let unsigned: UnverifiedTransaction = try_fus!(rlp::decode(&input.payment_tx)); let tx = try_fus!(SignedEthTx::new(unsigned)); + let tx_hash = tx.hash(); - let required_confirms = U64::from(confirmations); + let required_confirms = U64::from(input.confirmations); + let check_every = input.check_every as f64; let selfi = self.clone(); let fut = async move { loop { - if status.ms2deadline().unwrap() < 0 { - status.append(" Timed out."); - return ERR!( - "Waited too long until {} for transaction {:?} confirmation ", - wait_until, - tx - ); - } - - let web3_receipt = match selfi.web3.eth().transaction_receipt(tx.hash()).await { - Ok(r) => r, + // Wait for one confirmation and return the transaction confirmation block number + let confirmed_at = match selfi + .transaction_confirmed_at(tx_hash, input.wait_until, check_every) + .compat() + .await + { + Ok(c) => c, Err(e) => { - error!( - "Error {:?} getting the {} transaction {:?}, retrying in 15 seconds", - e, - selfi.ticker(), - tx.tx_hash() - ); - Timer::sleep(check_every as f64).await; - continue; + update_status_with_error!(status, e); + return Err(e.to_string()); }, }; - if let Some(receipt) = web3_receipt { - if receipt.status != Some(1.into()) { - status.append(" Failed."); - return ERR!( - "Tx receipt {:?} status of {} tx {:?} is failed", - receipt, - selfi.ticker(), - tx.tx_hash() - ); - } - if let Some(confirmed_at) = receipt.block_number { - let current_block = match selfi.web3.eth().block_number().await { - Ok(b) => b, - Err(e) => { - error!( - "Error {:?} getting the {} block number retrying in 15 seconds", - e, - selfi.ticker() - ); - Timer::sleep(check_every as f64).await; - continue; - }, - }; - // checking if the current block is above the confirmed_at block prediction for pos chain to prevent overflow - if current_block >= confirmed_at && current_block - confirmed_at + 1 >= required_confirms { + // checking that confirmed_at is greater than zero to prevent overflow. + // untrusted RPC nodes might send a zero value to cause overflow if we didn't do this check. + // required_confirms should always be more than 0 anyways but we should keep this check nonetheless. + if confirmed_at <= U64::from(0) { + error!( + "confirmed_at: {}, for payment tx: {:02x}, for coin:{} should be greater than zero!", + confirmed_at, + tx_hash, + selfi.ticker() + ); + Timer::sleep(check_every).await; + continue; + } + + // Wait for a block that achieves the required confirmations + let confirmation_block_number = confirmed_at + required_confirms - 1; + if let Err(e) = selfi + .wait_for_block(confirmation_block_number, input.wait_until, check_every) + .compat() + .await + { + update_status_with_error!(status, e); + return Err(e.to_string()); + } + + // Make sure that there was no chain reorganization that led to transaction confirmation block to be changed + match selfi + .transaction_confirmed_at(tx_hash, input.wait_until, check_every) + .compat() + .await + { + Ok(conf) => { + if conf == confirmed_at { status.append(" Confirmed."); - return Ok(()); + break Ok(()); } - } + }, + Err(e) => { + update_status_with_error!(status, e); + return Err(e.to_string()); + }, } - Timer::sleep(check_every as f64).await; + + Timer::sleep(check_every).await; } }; + Box::new(fut.boxed().compat()) } @@ -1895,7 +1850,18 @@ impl MarketCoinOps for EthCoin { ) -> TransactionFut { let unverified: UnverifiedTransaction = try_tx_fus!(rlp::decode(tx_bytes)); let tx = try_tx_fus!(SignedEthTx::new(unverified)); - let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); + + let swap_contract_address = match swap_contract_address { + Some(addr) => try_tx_fus!(addr.try_to_address()), + None => match tx.action { + Call(address) => address, + Create => { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Invalid payment action: the payment action cannot be create" + )))) + }, + }, + }; let func_name = match self.coin_type { EthCoinType::Eth => "ethPayment", @@ -1917,6 +1883,14 @@ impl MarketCoinOps for EthCoin { let fut = async move { loop { + if now_ms() / 1000 > wait_until { + return TX_PLAIN_ERR!( + "Waited too long until {} for transaction {:?} to be spent ", + wait_until, + tx, + ); + } + let current_block = match selfi.current_block().compat().await { Ok(b) => b, Err(e) => { @@ -1961,15 +1935,7 @@ impl MarketCoinOps for EthCoin { } } - if now_ms() / 1000 > wait_until { - return TX_PLAIN_ERR!( - "Waited too long until {} for transaction {:?} to be spent ", - wait_until, - tx, - ); - } Timer::sleep(5.).await; - continue; } }; Box::new(fut.boxed().compat()) @@ -2890,31 +2856,49 @@ impl EthCoin { } } - fn send_hash_time_locked_payment( - &self, - id: Vec, - value: U256, - time_lock: u32, - secret_hash: &[u8], - receiver_addr: Address, - swap_contract_address: Address, - ) -> EthTxFut { - let secret_hash = if secret_hash.len() == 32 { - ripemd160(secret_hash).to_vec() + fn send_hash_time_locked_payment(&self, args: SendPaymentArgs<'_>) -> EthTxFut { + let receiver_addr = try_tx_fus!(addr_from_raw_pubkey(args.other_pubkey)); + let swap_contract_address = try_tx_fus!(args.swap_contract_address.try_to_address()); + let id = self.etomic_swap_id(args.time_lock, args.secret_hash); + let trade_amount = try_tx_fus!(wei_from_big_decimal(&args.amount, self.decimals)); + let watcher_reward = args.watcher_reward.map(U256::from); + let time_lock = U256::from(args.time_lock); + let gas = U256::from(ETH_GAS); + + let secret_hash = if args.secret_hash.len() == 32 { + ripemd160(args.secret_hash).to_vec() } else { - secret_hash.to_vec() + args.secret_hash.to_vec() }; match &self.coin_type { EthCoinType::Eth => { - let function = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); - let data = try_tx_fus!(function.encode_input(&[ - Token::FixedBytes(id), - Token::Address(receiver_addr), - Token::FixedBytes(secret_hash), - Token::Uint(U256::from(time_lock)) - ])); - self.sign_and_send_transaction(value, Action::Call(swap_contract_address), data, U256::from(ETH_GAS)) + let function_name = get_function_name("ethPayment", watcher_reward.is_some()); + let function = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + + let mut value = trade_amount; + let data = match watcher_reward { + Some(reward) => { + value += reward; + + try_tx_fus!(function.encode_input(&[ + Token::FixedBytes(id), + Token::Address(receiver_addr), + Token::FixedBytes(secret_hash), + Token::Uint(time_lock), + Token::Uint(reward), + ])) + }, + None => try_tx_fus!(function.encode_input(&[ + Token::FixedBytes(id), + Token::Address(receiver_addr), + Token::FixedBytes(secret_hash), + Token::Uint(time_lock), + ])), + }; + drop_mutability!(value); + + self.sign_and_send_transaction(value, Action::Call(swap_contract_address), data, gas) }, EthCoinType::Erc20 { platform: _, @@ -2924,37 +2908,55 @@ impl EthCoin { .allowance(swap_contract_address) .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))); - let function = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let function_name = get_function_name("erc20Payment", watcher_reward.is_some()); + let function = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); let data = try_tx_fus!(function.encode_input(&[ Token::FixedBytes(id), - Token::Uint(value), + Token::Uint(trade_amount), Token::Address(*token_addr), Token::Address(receiver_addr), Token::FixedBytes(secret_hash), - Token::Uint(U256::from(time_lock)) + Token::Uint(time_lock) ])); + let wait_for_required_allowance_until = args.wait_for_confirmation_until; let arc = self.clone(); Box::new(allowance_fut.and_then(move |allowed| -> EthTxFut { - if allowed < value { + if allowed < trade_amount { Box::new( arc.approve(swap_contract_address, U256::max_value()) - .and_then(move |_approved| { - arc.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), + .and_then(move |approved| { + // make sure the approve tx is confirmed by making sure that the allowed value has been updated + // this call is cheaper than waiting for confirmation calls + arc.wait_for_required_allowance( + swap_contract_address, + trade_amount, + wait_for_required_allowance_until, ) + .map_err(move |e| { + TransactionErr::Plain(ERRL!( + "Allowed value was not updated in time after sending approve transaction {:02x}: {}", + approved.tx_hash(), + e + )) + }) + .and_then(move |_| { + arc.sign_and_send_transaction( + watcher_reward.unwrap_or_else(|| 0.into()), + Action::Call(swap_contract_address), + data, + gas, + ) + }) }), ) } else { Box::new(arc.sign_and_send_transaction( - 0.into(), + watcher_reward.unwrap_or_else(|| 0.into()), Action::Call(swap_contract_address), data, - U256::from(ETH_GAS), + gas, )) } })) @@ -2962,17 +2964,15 @@ impl EthCoin { } } - fn watcher_spend_hash_time_locked_payment( - &self, - payment: SignedEthTx, - _secret_hash: &[u8], - secret: &[u8], - taker_pub: &[u8], - ) -> EthTxFut { - let spend_func = try_tx_fus!(SWAP_CONTRACT.function("watcherSpend")); + fn watcher_spends_hash_time_locked_payment(&self, input: SendMakerPaymentSpendPreimageInput) -> EthTxFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(input.preimage)); + let payment = try_tx_fus!(SignedEthTx::new(tx)); + + let function_name = get_function_name("receiverSpend", input.watcher_reward); + let spend_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); let clone = self.clone(); - let secret_vec = secret.to_vec(); - let taker_addr = addr_from_raw_pubkey(taker_pub).unwrap(); + let secret_vec = input.secret.to_vec(); + let taker_addr = addr_from_raw_pubkey(input.taker_pub).unwrap(); let swap_contract_address = match payment.action { Call(address) => address, Create => { @@ -2982,10 +2982,12 @@ impl EthCoin { }, }; + let watcher_reward = input.watcher_reward; match self.coin_type { EthCoinType::Eth => { - let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let function_name = get_function_name("ethPayment", watcher_reward); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); @@ -3002,13 +3004,15 @@ impl EthCoin { } let value = payment.value; + let watcher_reward_amount = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); let data = try_tx_fus!(spend_func.encode_input(&[ swap_id_input, Token::Uint(value), Token::FixedBytes(secret_vec.clone()), Token::Address(Address::default()), Token::Address(payment.sender()), - Token::Address(taker_addr) + Token::Address(taker_addr), + watcher_reward_amount, ])); clone.sign_and_send_transaction( @@ -3024,9 +3028,10 @@ impl EthCoin { platform: _, token_addr, } => { - let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let function_name = get_function_name("erc20Payment", watcher_reward); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); @@ -3048,9 +3053,9 @@ impl EthCoin { Token::FixedBytes(secret_vec.clone()), Token::Address(token_addr), Token::Address(payment.sender()), - Token::Address(taker_addr) + Token::Address(taker_addr), + Token::Uint(payment.value) ])); - clone.sign_and_send_transaction( 0.into(), Action::Call(swap_contract_address), @@ -3063,15 +3068,16 @@ impl EthCoin { } } - fn watcher_refunds_hash_time_locked_payment( - &self, - payment: SignedEthTx, - _secret_hash: &[u8], - taker_pub: &[u8], - ) -> EthTxFut { - let refund_func = try_tx_fus!(SWAP_CONTRACT.function("watcherRefund")); + fn watcher_refunds_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> EthTxFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.payment_tx)); + let payment = try_tx_fus!(SignedEthTx::new(tx)); + let watcher_reward = args.watcher_reward; + + let function_name = get_function_name("senderRefund", watcher_reward); + let refund_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let clone = self.clone(); - let taker_addr = addr_from_raw_pubkey(taker_pub).unwrap(); + let taker_addr = addr_from_raw_pubkey(args.other_pubkey).unwrap(); let swap_contract_address = match payment.action { Call(address) => address, Create => { @@ -3083,10 +3089,11 @@ impl EthCoin { match self.coin_type { EthCoinType::Eth => { - let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let function_name = get_function_name("ethPayment", watcher_reward); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); - let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); + let receiver_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); let hash_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 2)); let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); @@ -3103,13 +3110,16 @@ impl EthCoin { } let value = payment.value; + let watcher_reward_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); + let data = try_tx_fus!(refund_func.encode_input(&[ swap_id_input.clone(), Token::Uint(value), hash_input.clone(), Token::Address(Address::default()), Token::Address(taker_addr), - amount_input.clone(), + receiver_input.clone(), + watcher_reward_input, ])); clone.sign_and_send_transaction( @@ -3125,12 +3135,14 @@ impl EthCoin { platform: _, token_addr, } => { - let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); + let function_name = get_function_name("erc20Payment", watcher_reward); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); - let token_addr_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 3)); - let sender_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); + let receiver_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 3)); + let hash_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); Box::new( state_f @@ -3147,10 +3159,11 @@ impl EthCoin { let data = try_tx_fus!(refund_func.encode_input(&[ swap_id_input.clone(), amount_input.clone(), - sender_input.clone(), + hash_input.clone(), Token::Address(token_addr), Token::Address(taker_addr), - token_addr_input.clone(), + receiver_input.clone(), + Token::Uint(payment.value), ])); clone.sign_and_send_transaction( @@ -3165,20 +3178,22 @@ impl EthCoin { } } - fn spend_hash_time_locked_payment( - &self, - payment: SignedEthTx, - _secret_hash: &[u8], - swap_contract_address: Address, - secret: &[u8], - ) -> EthTxFut { - let spend_func = try_tx_fus!(SWAP_CONTRACT.function("receiverSpend")); + fn spend_hash_time_locked_payment(&self, args: SpendPaymentArgs) -> EthTxFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.other_payment_tx)); + let payment = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(args.swap_contract_address.try_to_address()); + let watcher_reward = args.watcher_reward; + + let function_name = get_function_name("receiverSpend", watcher_reward); + let spend_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let clone = self.clone(); - let secret_vec = secret.to_vec(); + let secret_vec = args.secret.to_vec(); match self.coin_type { EthCoinType::Eth => { - let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let function_name = get_function_name("ethPayment", watcher_reward); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); @@ -3194,14 +3209,25 @@ impl EthCoin { )))); } - let value = payment.value; - let data = try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - Token::FixedBytes(secret_vec), - Token::Address(Address::default()), - Token::Address(payment.sender()), - ])); + let data = if watcher_reward { + try_tx_fus!(spend_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(payment.value), + Token::FixedBytes(secret_vec), + Token::Address(Address::default()), + Token::Address(payment.sender()), + Token::Address(clone.my_address), + decoded[4].clone(), + ])) + } else { + try_tx_fus!(spend_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(payment.value), + Token::FixedBytes(secret_vec), + Token::Address(Address::default()), + Token::Address(payment.sender()), + ])) + }; clone.sign_and_send_transaction( 0.into(), @@ -3216,7 +3242,8 @@ impl EthCoin { platform: _, token_addr, } => { - let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let function_name = get_function_name("erc20Payment", watcher_reward); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); @@ -3232,13 +3259,25 @@ impl EthCoin { state )))); } - let data = try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - Token::FixedBytes(secret_vec), - Token::Address(token_addr), - Token::Address(payment.sender()), - ])); + let data = if watcher_reward { + try_tx_fus!(spend_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + Token::FixedBytes(secret_vec), + Token::Address(token_addr), + Token::Address(payment.sender()), + Token::Address(clone.my_address), + Token::Uint(payment.value), + ])) + } else { + try_tx_fus!(spend_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + Token::FixedBytes(secret_vec), + Token::Address(token_addr), + Token::Address(payment.sender()), + ])) + }; clone.sign_and_send_transaction( 0.into(), @@ -3252,18 +3291,22 @@ impl EthCoin { } } - fn refund_hash_time_locked_payment( - &self, - swap_contract_address: Address, - payment: SignedEthTx, - _secret_hash: &[u8], - ) -> EthTxFut { - let refund_func = try_tx_fus!(SWAP_CONTRACT.function("senderRefund")); + fn refund_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> EthTxFut { + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.payment_tx)); + let payment = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(args.swap_contract_address.try_to_address()); + + let watcher_reward = args.watcher_reward; + let function_name = get_function_name("senderRefund", watcher_reward); + let refund_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let clone = self.clone(); match self.coin_type { EthCoinType::Eth => { - let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let function_name = get_function_name("ethPayment", watcher_reward); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); @@ -3280,13 +3323,25 @@ impl EthCoin { } let value = payment.value; - let data = try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - decoded[2].clone(), - Token::Address(Address::default()), - decoded[1].clone(), - ])); + let data = if watcher_reward { + try_tx_fus!(refund_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + decoded[2].clone(), + Token::Address(Address::default()), + Token::Address(clone.my_address), + decoded[1].clone(), + decoded[4].clone(), + ])) + } else { + try_tx_fus!(refund_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + decoded[2].clone(), + Token::Address(Address::default()), + decoded[1].clone(), + ])) + }; clone.sign_and_send_transaction( 0.into(), @@ -3301,7 +3356,9 @@ impl EthCoin { platform: _, token_addr, } => { - let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let function_name = get_function_name("erc20Payment", watcher_reward); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); Box::new( @@ -3316,13 +3373,25 @@ impl EthCoin { )))); } - let data = try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - decoded[4].clone(), - Token::Address(token_addr), - decoded[3].clone(), - ])); + let data = if watcher_reward { + try_tx_fus!(refund_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + decoded[4].clone(), + Token::Address(token_addr), + Token::Address(clone.my_address), + decoded[3].clone(), + Token::Uint(payment.value), + ])) + } else { + try_tx_fus!(refund_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + decoded[4].clone(), + Token::Address(token_addr), + decoded[3].clone(), + ])) + }; clone.sign_and_send_transaction( 0.into(), @@ -3484,6 +3553,40 @@ impl EthCoin { Box::new(fut.boxed().compat()) } + fn wait_for_required_allowance( + &self, + spender: Address, + required_allowance: U256, + wait_until: u64, + ) -> Web3RpcFut<()> { + const CHECK_ALLOWANCE_EVERY: f64 = 5.; + + let selfi = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return MmError::err(Web3RpcError::Timeout(ERRL!( + "Waited too long until {} for allowance to be updated to at least {}", + wait_until, + required_allowance + ))); + } + + match selfi.allowance(spender).compat().await { + Ok(allowed) if allowed >= required_allowance => return Ok(()), + Ok(_allowed) => (), + Err(e) => match e.get_inner() { + Web3RpcError::Transport(e) => error!("Error {} on trying to get the allowed amount!", e), + _ => return Err(e), + }, + } + + Timer::sleep(CHECK_ALLOWANCE_EVERY).await; + } + }; + Box::new(fut.boxed().compat()) + } + fn approve(&self, spender: Address, amount: U256) -> EthTxFut { let coin = self.clone(); let fut = async move { @@ -3543,28 +3646,26 @@ impl EthCoin { Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) } - fn validate_payment( - &self, - payment_tx: &[u8], - time_lock: u32, - sender_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - expected_swap_contract_address: Address, - ) -> ValidatePaymentFut<()> { - let unsigned: UnverifiedTransaction = try_f!(rlp::decode(payment_tx)); + fn validate_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + let expected_swap_contract_address = try_f!(input + .swap_contract_address + .try_to_address() + .map_to_mm(ValidatePaymentError::InvalidParameter)); + + let unsigned: UnverifiedTransaction = try_f!(rlp::decode(&input.payment_tx)); let tx = try_f!(SignedEthTx::new(unsigned) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); - let sender = try_f!(addr_from_raw_pubkey(sender_pub).map_to_mm(ValidatePaymentError::InvalidParameter)); - let expected_value = try_f!(wei_from_big_decimal(&amount, self.decimals)); + let sender = try_f!(addr_from_raw_pubkey(&input.other_pub).map_to_mm(ValidatePaymentError::InvalidParameter)); + let selfi = self.clone(); - let swap_id = selfi.etomic_swap_id(time_lock, secret_hash); - let secret_hash = if secret_hash.len() == 32 { - ripemd160(secret_hash).to_vec() + let swap_id = selfi.etomic_swap_id(input.time_lock, &input.secret_hash); + let secret_hash = if input.secret_hash.len() == 32 { + ripemd160(&input.secret_hash).to_vec() } else { - secret_hash.to_vec() + input.secret_hash.to_vec() }; + let mut expected_value = try_f!(wei_from_big_decimal(&input.amount, self.decimals)); let fut = async move { let status = selfi .payment_status(expected_swap_contract_address, Token::FixedBytes(swap_id.clone())) @@ -3599,17 +3700,36 @@ impl EthCoin { ))); } + let function_name = get_function_name("ethPayment", input.min_watcher_reward.is_some()); + let function = SWAP_CONTRACT + .function(&function_name) + .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; + + let decoded = decode_contract_call(function, &tx_from_rpc.input.0) + .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; + + if let Some(min_reward) = input.min_watcher_reward { + let reward = decoded[4].clone().into_uint().ok_or_else(|| { + ValidatePaymentError::WrongPaymentTx("Invalid type for watcher reward argument".to_string()) + })?; + if reward.as_u64() < min_reward { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Insufficient watcher reward {}, must be at least {}", + reward, min_reward + ))); + } + + expected_value += reward; + } + drop_mutability!(expected_value); + if tx_from_rpc.value != expected_value { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx value arg {:?} is invalid, expected {:?}", tx_from_rpc, expected_value ))); } - let function = SWAP_CONTRACT - .function("ethPayment") - .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; - let decoded = decode_contract_call(function, &tx_from_rpc.input.0) - .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; + if decoded[0] != Token::FixedBytes(swap_id.clone()) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Invalid 'swap_id' {:?}, expected {:?}", @@ -3633,11 +3753,11 @@ impl EthCoin { ))); } - if decoded[3] != Token::Uint(U256::from(time_lock)) { + if decoded[3] != Token::Uint(U256::from(input.time_lock)) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx time_lock arg {:?} is invalid, expected {:?}", decoded[3], - Token::Uint(U256::from(time_lock)), + Token::Uint(U256::from(input.time_lock)), ))); } }, @@ -3651,11 +3771,21 @@ impl EthCoin { tx_from_rpc, expected_swap_contract_address, ))); } + if let Some(min_reward) = input.min_watcher_reward { + if tx_from_rpc.value.as_u64() < min_reward { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Payment tx value arg {} is less than the minimum expected watcher reward {}", + tx_from_rpc.value, min_reward + ))); + } + } + let function_name = get_function_name("erc20Payment", input.min_watcher_reward.is_some()); let function = SWAP_CONTRACT - .function("erc20Payment") + .function(&function_name) .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; let decoded = decode_contract_call(function, &tx_from_rpc.input.0) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; + if decoded[0] != Token::FixedBytes(swap_id.clone()) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Invalid 'swap_id' {:?}, expected {:?}", @@ -3695,11 +3825,11 @@ impl EthCoin { ))); } - if decoded[5] != Token::Uint(U256::from(time_lock)) { + if decoded[5] != Token::Uint(U256::from(input.time_lock)) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx time_lock arg {:?} is invalid, expected {:?}", decoded[5], - Token::Uint(U256::from(time_lock)), + Token::Uint(U256::from(input.time_lock)), ))); } }, @@ -3740,16 +3870,17 @@ impl EthCoin { swap_contract_address: Address, _secret_hash: &[u8], search_from_block: u64, + watcher_reward: bool, ) -> Result, String> { let unverified: UnverifiedTransaction = try_s!(rlp::decode(tx)); let tx = try_s!(SignedEthTx::new(unverified)); let func_name = match self.coin_type { - EthCoinType::Eth => "ethPayment", - EthCoinType::Erc20 { .. } => "erc20Payment", + EthCoinType::Eth => get_function_name("ethPayment", watcher_reward), + EthCoinType::Erc20 { .. } => get_function_name("erc20Payment", watcher_reward), }; - let payment_func = try_s!(SWAP_CONTRACT.function(func_name)); + let payment_func = try_s!(SWAP_CONTRACT.function(&func_name)); let decoded = try_s!(decode_contract_call(payment_func, &tx.data)); let id = match decoded.first() { Some(Token::FixedBytes(bytes)) => bytes.clone(), @@ -3828,7 +3959,7 @@ impl EthCoin { } /// Get gas price - fn get_gas_price(&self) -> Web3RpcFut { + pub fn get_gas_price(&self) -> Web3RpcFut { let coin = self.clone(); let fut = async move { // TODO refactor to error_log_passthrough once simple maker bot is merged @@ -3933,6 +4064,88 @@ impl EthCoin { ); Ok(None) } + + fn transaction_confirmed_at(&self, payment_hash: H256, wait_until: u64, check_every: f64) -> Web3RpcFut { + let selfi = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return MmError::err(Web3RpcError::Timeout(ERRL!( + "Waited too long until {} for payment tx: {:02x}, for coin:{}, to be confirmed!", + wait_until, + payment_hash, + selfi.ticker() + ))); + } + + let web3_receipt = match selfi.web3.eth().transaction_receipt(payment_hash).await { + Ok(r) => r, + Err(e) => { + error!( + "Error {:?} getting the {} transaction {:?}, retrying in 15 seconds", + e, + selfi.ticker(), + payment_hash + ); + Timer::sleep(check_every).await; + continue; + }, + }; + + if let Some(receipt) = web3_receipt { + if receipt.status != Some(1.into()) { + return MmError::err(Web3RpcError::Internal(ERRL!( + "Tx receipt {:?} status of {} tx {:?} is failed", + receipt, + selfi.ticker(), + payment_hash + ))); + } + + if let Some(confirmed_at) = receipt.block_number { + break Ok(confirmed_at); + } + } + + Timer::sleep(check_every).await; + } + }; + Box::new(fut.boxed().compat()) + } + + fn wait_for_block(&self, block_number: U64, wait_until: u64, check_every: f64) -> Web3RpcFut<()> { + let selfi = self.clone(); + let fut = async move { + loop { + if now_ms() / 1000 > wait_until { + return MmError::err(Web3RpcError::Timeout(ERRL!( + "Waited too long until {} for block number: {:02x} to appear on-chain, for coin:{}", + wait_until, + block_number, + selfi.ticker() + ))); + } + + match selfi.web3.eth().block_number().await { + Ok(current_block) => { + if current_block >= block_number { + break Ok(()); + } + }, + Err(e) => { + error!( + "Error {:?} getting the {} block number retrying in 15 seconds", + e, + selfi.ticker() + ); + }, + }; + + Timer::sleep(check_every).await; + } + }; + Box::new(fut.boxed().compat()) + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -4021,7 +4234,7 @@ impl MmCoin for EthCoin { &[&"tx_history", &self.ticker], &ERRL!("Transaction history is not supported for ETH/ERC20 coins"), ); - return Box::new(futures01::future::ok(())); + Box::new(futures01::future::ok(())) } cfg_native! { let coin = self.clone(); @@ -4199,9 +4412,17 @@ impl MmCoin for EthCoin { fn mature_confirmations(&self) -> Option { None } - fn coin_protocol_info(&self) -> Vec { Vec::new() } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } - fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.abortable_system) } @@ -4284,6 +4505,108 @@ impl GuiAuthMessages for EthCoin { } } +fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { + let fee_tx_hash = validate_fee_args.fee_tx_hash.to_owned(); + let sender_addr = try_f!( + addr_from_raw_pubkey(validate_fee_args.expected_sender).map_to_mm(ValidatePaymentError::InvalidParameter) + ); + let fee_addr = + try_f!(addr_from_raw_pubkey(validate_fee_args.fee_addr).map_to_mm(ValidatePaymentError::InvalidParameter)); + let amount = validate_fee_args.amount.clone(); + let min_block_number = validate_fee_args.min_block_number; + + let fut = async move { + let expected_value = wei_from_big_decimal(&amount, coin.decimals)?; + let tx_from_rpc = coin.web3.eth().transaction(TransactionId::Hash(fee_tx_hash)).await?; + + let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { + ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", fee_tx_hash)) + })?; + + if tx_from_rpc.from != Some(sender_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Fee tx {:?} was sent from wrong address, expected {:?}", + INVALID_SENDER_ERR_LOG, tx_from_rpc, sender_addr + ))); + } + + if let Some(block_number) = tx_from_rpc.block_number { + if block_number <= min_block_number.into() { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Fee tx {:?} confirmed before min_block {}", + EARLY_CONFIRMATION_ERR_LOG, tx_from_rpc, min_block_number + ))); + } + } + match &coin.coin_type { + EthCoinType::Eth => { + if tx_from_rpc.to != Some(fee_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Fee tx {:?} was sent to wrong address, expected {:?}", + INVALID_RECEIVER_ERR_LOG, tx_from_rpc, fee_addr + ))); + } + + if tx_from_rpc.value < expected_value { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Fee tx {:?} value is less than expected {:?}", + tx_from_rpc, expected_value + ))); + } + }, + EthCoinType::Erc20 { + platform: _, + token_addr, + } => { + if tx_from_rpc.to != Some(*token_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: ERC20 Fee tx {:?} called wrong smart contract, expected {:?}", + INVALID_CONTRACT_ADDRESS_ERR_LOG, tx_from_rpc, token_addr + ))); + } + + let function = ERC20_CONTRACT + .function("transfer") + .map_to_mm(|e| ValidatePaymentError::InternalError(e.to_string()))?; + let decoded_input = decode_contract_call(function, &tx_from_rpc.input.0) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + let address_input = get_function_input_data(&decoded_input, function, 0) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + + if address_input != Token::Address(fee_addr) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: ERC20 Fee tx was sent to wrong address {:?}, expected {:?}", + INVALID_RECEIVER_ERR_LOG, address_input, fee_addr + ))); + } + + let value_input = get_function_input_data(&decoded_input, function, 1) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + + match value_input { + Token::Uint(value) => { + if value < expected_value { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "ERC20 Fee tx value {} is less than expected {}", + value, expected_value + ))); + } + }, + _ => { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Should have got uint token but got {:?}", + value_input + ))) + }, + } + }, + } + + Ok(()) + }; + Box::new(fut.boxed().compat()) +} + fn get_function_input_data(decoded: &[Token], func: &Function, index: usize) -> Result { decoded.get(index).cloned().ok_or(format!( "Missing input in function {}: No input found at index {}", @@ -4292,6 +4615,14 @@ fn get_function_input_data(decoded: &[Token], func: &Function, index: usize) -> )) } +fn get_function_name(name: &str, watcher_reward: bool) -> String { + if watcher_reward { + format!("{}{}", name, "Reward") + } else { + name.to_owned() + } +} + pub fn addr_from_raw_pubkey(pubkey: &[u8]) -> Result { let pubkey = try_s!(PublicKey::from_slice(pubkey).map_err(|e| ERRL!("{:?}", e))); let eth_public = Public::from_slice(&pubkey.serialize_uncompressed()[1..65]); @@ -4532,13 +4863,13 @@ pub async fn eth_coin_from_conf_and_request( if swap_contract_address == Address::default() { return ERR!("swap_contract_address can't be zero address"); } - let fallback_swap_contract: Option
= try_s!(json::from_value(req["fallback_swap_contract"].clone())); if let Some(fallback) = fallback_swap_contract { if fallback == Address::default() { return ERR!("fallback_swap_contract can't be zero address"); } } + let contract_supports_watchers = req["contract_supports_watchers"].as_bool().unwrap_or_default(); let (my_address, key_pair) = try_s!(build_address_and_priv_key_policy(conf, priv_key_policy).await); @@ -4629,6 +4960,7 @@ pub async fn eth_coin_from_conf_and_request( sign_message_prefix, swap_contract_address, fallback_swap_contract, + contract_supports_watchers, decimals, ticker: ticker.into(), gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), @@ -4762,3 +5094,139 @@ fn increase_gas_price_by_stage(gas_price: U256, level: &FeeApproxStage) -> U256 }, } } + +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] +pub enum GetEthAddressError { + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + EthActivationV2Error(EthActivationV2Error), + Internal(String), +} + +impl From for GetEthAddressError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { GetEthAddressError::PrivKeyPolicyNotAllowed(e) } +} + +impl From for GetEthAddressError { + fn from(e: EthActivationV2Error) -> Self { GetEthAddressError::EthActivationV2Error(e) } +} + +impl From for GetEthAddressError { + fn from(e: CryptoCtxError) -> Self { GetEthAddressError::Internal(e.to_string()) } +} + +/// `get_eth_address` returns wallet address for coin with `ETH` protocol type. +pub async fn get_eth_address(ctx: &MmArc, ticker: &str) -> MmResult { + let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(ctx)?; + // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. + let priv_key_policy = EthPrivKeyBuildPolicy::try_from(priv_key_policy)?; + + let (my_address, ..) = build_address_and_priv_key_policy(&ctx.conf, priv_key_policy).await?; + let wallet_address = checksum_address(&format!("{:#02x}", my_address)); + + Ok(MyWalletAddress { + coin: ticker.to_owned(), + wallet_address, + }) +} + +#[cfg(feature = "enable-nft-integration")] +#[derive(Display)] +pub enum GetValidEthWithdrawAddError { + #[display(fmt = "My address {} and from address {} mismatch", my_address, from)] + AddressMismatchError { + my_address: String, + from: String, + }, + #[display(fmt = "{} coin doesn't support NFT withdrawing", coin)] + CoinDoesntSupportNftWithdraw { + coin: String, + }, + InvalidAddress(String), +} + +#[cfg(feature = "enable-nft-integration")] +fn get_valid_nft_add_to_withdraw( + coin_enum: MmCoinEnum, + to: &str, + token_add: &str, +) -> MmResult<(Address, Address, EthCoin), GetValidEthWithdrawAddError> { + let eth_coin = match coin_enum { + MmCoinEnum::EthCoin(eth_coin) => eth_coin, + _ => { + return MmError::err(GetValidEthWithdrawAddError::CoinDoesntSupportNftWithdraw { + coin: coin_enum.ticker().to_owned(), + }) + }, + }; + let to_addr = valid_addr_from_str(to).map_err(GetValidEthWithdrawAddError::InvalidAddress)?; + let token_addr = addr_from_str(token_add).map_err(GetValidEthWithdrawAddError::InvalidAddress)?; + Ok((to_addr, token_addr, eth_coin)) +} + +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize)] +pub enum EthGasDetailsErr { + #[display(fmt = "Invalid fee policy: {}", _0)] + InvalidFeePolicy(String), + #[from_stringify("NumConversError")] + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), +} + +impl From for EthGasDetailsErr { + fn from(e: web3::Error) -> Self { EthGasDetailsErr::from(Web3RpcError::from(e)) } +} + +impl From for EthGasDetailsErr { + fn from(e: Web3RpcError) -> Self { + match e { + Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => EthGasDetailsErr::Transport(tr), + Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => EthGasDetailsErr::Internal(internal), + } + } +} + +async fn get_eth_gas_details( + eth_coin: &EthCoin, + fee: Option, + eth_value: U256, + data: Bytes, + call_addr: Address, + fungible_max: bool, +) -> MmResult { + match fee { + Some(WithdrawFee::EthGas { gas_price, gas }) => { + let gas_price = wei_from_big_decimal(&gas_price, 9)?; + Ok((gas.into(), gas_price)) + }, + Some(fee_policy) => { + let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); + MmError::err(EthGasDetailsErr::InvalidFeePolicy(error)) + }, + None => { + let gas_price = eth_coin.get_gas_price().compat().await?; + // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH + let eth_value_for_estimate = if fungible_max && eth_coin.coin_type == EthCoinType::Eth { + eth_value - gas_price * U256::from(21000) + } else { + eth_value + }; + let estimate_gas_req = CallRequest { + value: Some(eth_value_for_estimate), + data: Some(data), + from: Some(eth_coin.my_address), + to: Some(call_addr), + gas: None, + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + gas_price: Some(gas_price), + ..CallRequest::default() + }; + // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. + // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. + let gas_limit = eth_coin.estimate_gas(estimate_gas_req).compat().await?; + Ok((gas_limit, gas_price)) + }, + } +} diff --git a/mm2src/coins/eth/erc1155_abi.json b/mm2src/coins/eth/erc1155_abi.json new file mode 100644 index 0000000000..211a562a85 --- /dev/null +++ b/mm2src/coins/eth/erc1155_abi.json @@ -0,0 +1,314 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/mm2src/coins/eth/erc721_abi.json b/mm2src/coins/eth/erc721_abi.json new file mode 100644 index 0000000000..20e0fca0b4 --- /dev/null +++ b/mm2src/coins/eth/erc721_abi.json @@ -0,0 +1,346 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "_approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 7a98b43c98..4536845ff4 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -48,7 +48,7 @@ fn eth_coin_for_test( {"coin":"JST","name":"jst","rpcport":80,"mm2":1,"protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":"0x2b294F029Fde858b2c62184e8390591755521d8E"}}} ] }); - let ctx = MmCtxBuilder::new().with_conf(conf.clone()).into_mm_arc(); + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); let ticker = match coin_type { EthCoinType::Eth => "ETH".to_string(), EthCoinType::Erc20 { .. } => "JST".to_string(), @@ -66,6 +66,7 @@ fn eth_coin_for_test( priv_key_policy: key_pair.into(), swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract, + contract_supports_watchers: false, ticker, web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -232,6 +233,7 @@ fn send_and_refund_erc20_payment() { priv_key_policy: key_pair.into(), swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, + contract_supports_watchers: false, web3_instances: vec![Web3Instance { web3: web3.clone(), is_parity: true, @@ -250,7 +252,7 @@ fn send_and_refund_erc20_payment() { erc20_tokens_infos: Default::default(), abortable_system: AbortableQueue::default(), })); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: (now_ms() / 1000) as u32 - 200, other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, @@ -259,19 +261,22 @@ fn send_and_refund_erc20_payment() { swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = coin.send_maker_payment(maker_payment_args).wait().unwrap(); log!("{:?}", payment); block_on(Timer::sleep(60.)); - let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + let maker_refunds_payment_args = RefundPaymentArgs { payment_tx: &payment.tx_hex(), time_lock: (now_ms() / 1000) as u32 - 200, other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, secret_hash: &[1; 20], swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let refund = coin .send_maker_refunds_payment(maker_refunds_payment_args) @@ -299,6 +304,7 @@ fn send_and_refund_eth_payment() { priv_key_policy: key_pair.into(), swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, + contract_supports_watchers: false, web3_instances: vec![Web3Instance { web3: web3.clone(), is_parity: true, @@ -317,7 +323,7 @@ fn send_and_refund_eth_payment() { erc20_tokens_infos: Default::default(), abortable_system: AbortableQueue::default(), })); - let send_maker_payment_args = SendMakerPaymentArgs { + let send_maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: (now_ms() / 1000) as u32 - 200, other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, @@ -326,19 +332,22 @@ fn send_and_refund_eth_payment() { swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = coin.send_maker_payment(send_maker_payment_args).wait().unwrap(); log!("{:?}", payment); block_on(Timer::sleep(60.)); - let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + let maker_refunds_payment_args = RefundPaymentArgs { payment_tx: &payment.tx_hex(), time_lock: (now_ms() / 1000) as u32 - 200, other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, secret_hash: &[1; 20], swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let refund = coin .send_maker_refunds_payment(maker_refunds_payment_args) @@ -374,6 +383,7 @@ fn test_nonce_several_urls() { priv_key_policy: key_pair.into(), swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, + contract_supports_watchers: false, web3_instances: vec![ Web3Instance { web3: web3_infura.clone(), @@ -439,6 +449,7 @@ fn test_wait_for_payment_spend_timeout() { priv_key_policy: key_pair.into(), swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, + contract_supports_watchers: false, ticker: "ETH".into(), web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -509,6 +520,7 @@ fn test_search_for_swap_tx_spend_was_spent() { priv_key_policy: key_pair.into(), swap_contract_address, fallback_swap_contract: None, + contract_supports_watchers: false, ticker: "ETH".into(), web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -553,9 +565,10 @@ fn test_search_for_swap_tx_spend_was_spent() { ]; let spend_tx = FoundSwapTxSpend::Spent(signed_eth_tx_from_bytes(&spend_tx).unwrap().into()); - let found_tx = block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, &[0; 20], 15643279)) - .unwrap() - .unwrap(); + let found_tx = + block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, &[0; 20], 15643279, false)) + .unwrap() + .unwrap(); assert_eq!(spend_tx, found_tx); } @@ -620,6 +633,7 @@ fn test_search_for_swap_tx_spend_was_refunded() { priv_key_policy: key_pair.into(), swap_contract_address, fallback_swap_contract: None, + contract_supports_watchers: false, ticker: "BAT".into(), web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -667,9 +681,10 @@ fn test_search_for_swap_tx_spend_was_refunded() { ]; let refund_tx = FoundSwapTxSpend::Refunded(signed_eth_tx_from_bytes(&refund_tx).unwrap().into()); - let found_tx = block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, &[0; 20], 13638713)) - .unwrap() - .unwrap(); + let found_tx = + block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, &[0; 20], 13638713, false)) + .unwrap() + .unwrap(); assert_eq!(refund_tx, found_tx); } @@ -697,7 +712,7 @@ fn test_withdraw_impl_manual_fee() { }; coin.my_balance().wait().unwrap(); - let tx_details = block_on(withdraw_impl(coin.clone(), withdraw_req)).unwrap(); + let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( EthTxFeeDetails { coin: "ETH".into(), @@ -741,7 +756,7 @@ fn test_withdraw_impl_fee_details() { }; coin.my_balance().wait().unwrap(); - let tx_details = block_on(withdraw_impl(coin.clone(), withdraw_req)).unwrap(); + let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( EthTxFeeDetails { coin: "ETH".into(), @@ -980,7 +995,7 @@ fn test_get_fee_to_send_taker_fee() { vec!["http://dummy.dummy".into()], None, ); - let actual = block_on(coin.get_fee_to_send_taker_fee(dex_fee_amount.clone(), FeeApproxStage::WithoutApprox)) + let actual = block_on(coin.get_fee_to_send_taker_fee(dex_fee_amount, FeeApproxStage::WithoutApprox)) .expect("!get_fee_to_send_taker_fee"); assert_eq!(actual, expected_fee); } @@ -1007,8 +1022,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { ); let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); - let error = - block_on(coin.get_fee_to_send_taker_fee(dex_fee_amount.clone(), FeeApproxStage::WithoutApprox)).unwrap_err(); + let error = block_on(coin.get_fee_to_send_taker_fee(dex_fee_amount, FeeApproxStage::WithoutApprox)).unwrap_err(); log!("{}", error); assert!( matches!(error.get_inner(), TradePreimageError::NotSufficientBalance { .. }), @@ -1030,14 +1044,17 @@ fn validate_dex_fee_invalid_sender_eth() { let amount: BigDecimal = "0.000526435076465".parse().unwrap(); let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, - expected_sender: &*DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, amount: &amount, min_block_number: 0, uuid: &[], }; - let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); - assert!(validate_err.contains("was sent from wrong address")); + let error = coin.validate_fee(validate_fee_args).wait().unwrap_err().into_inner(); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("was sent from wrong address")), + _ => panic!("Expected `WrongPaymentTx` wrong sender address, found {:?}", error), + } } #[test] @@ -1061,14 +1078,17 @@ fn validate_dex_fee_invalid_sender_erc() { let amount: BigDecimal = "5.548262548262548262".parse().unwrap(); let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, - expected_sender: &*DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, amount: &amount, min_block_number: 0, uuid: &[], }; - let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); - assert!(validate_err.contains("was sent from wrong address")); + let error = coin.validate_fee(validate_fee_args).wait().unwrap_err().into_inner(); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("was sent from wrong address")), + _ => panic!("Expected `WrongPaymentTx` wrong sender address, found {:?}", error), + } } fn sender_compressed_pub(tx: &SignedEthTx) -> [u8; 33] { @@ -1097,13 +1117,16 @@ fn validate_dex_fee_eth_confirmed_before_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &compressed_public, - fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, amount: &amount, min_block_number: 11784793, uuid: &[], }; - let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); - assert!(validate_err.contains("confirmed before min_block")); + let error = coin.validate_fee(validate_fee_args).wait().unwrap_err().into_inner(); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min_block")), + _ => panic!("Expected `WrongPaymentTx` early confirmation, found {:?}", error), + } } #[test] @@ -1131,13 +1154,16 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &compressed_public, - fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, amount: &amount, min_block_number: 11823975, uuid: &[], }; - let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); - assert!(validate_err.contains("confirmed before min_block")); + let error = coin.validate_fee(validate_fee_args).wait().unwrap_err().into_inner(); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min_block")), + _ => panic!("Expected `WrongPaymentTx` early confirmation, found {:?}", error), + } } #[test] @@ -1276,6 +1302,7 @@ fn test_message_hash() { priv_key_policy: key_pair.into(), swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, + contract_supports_watchers: false, web3_instances: vec![Web3Instance { web3: web3.clone(), is_parity: true, @@ -1320,6 +1347,7 @@ fn test_sign_verify_message() { priv_key_policy: key_pair.into(), swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, + contract_supports_watchers: false, web3_instances: vec![Web3Instance { web3: web3.clone(), is_parity: true, @@ -1375,6 +1403,7 @@ fn test_eth_extract_secret() { priv_key_policy: key_pair.into(), swap_contract_address, fallback_swap_contract: None, + contract_supports_watchers: false, ticker: "ETH".into(), web3_instances: vec![Web3Instance { web3: web3.clone(), @@ -1404,7 +1433,7 @@ fn test_eth_extract_secret() { 100, 189, 72, 74, 221, 144, 66, 170, 68, 121, 29, 105, 19, 194, 35, 245, 196, 131, 236, 29, 105, 101, 30, ]; - let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice())); + let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice(), false)); assert!(secret.is_ok()); let expect_secret = &[ 168, 151, 11, 232, 224, 253, 63, 180, 26, 114, 23, 184, 27, 10, 161, 80, 178, 251, 73, 204, 80, 174, 97, 118, @@ -1427,7 +1456,7 @@ fn test_eth_extract_secret() { 6, 108, 165, 181, 188, 40, 56, 47, 211, 229, 221, 73, 5, 15, 89, 81, 117, 225, 216, 108, 98, 226, 119, 232, 94, 184, 42, 106, ]; - let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice())) + let secret = block_on(coin.extract_secret(&[0u8; 20], tx_bytes.as_slice(), false)) .err() .unwrap(); assert!(secret.contains("Expected 'receiverSpend' contract call signature")); diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index f8e22335df..c8e0b0d905 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -11,7 +11,6 @@ wasm_bindgen_test_configure!(run_in_browser); fn pass() { let ctx = MmCtxBuilder::default().into_mm_arc(); let _coins_context = CoinsContext::from_ctx(&ctx).unwrap(); - assert_eq!(1, 1); } #[wasm_bindgen_test] @@ -31,6 +30,7 @@ async fn test_send() { priv_key_policy: key_pair.into(), swap_contract_address: Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(), fallback_swap_contract: None, + contract_supports_watchers: false, web3_instances: vec![Web3Instance { web3: web3.clone(), is_parity: true, @@ -49,7 +49,7 @@ async fn test_send() { erc20_tokens_infos: Default::default(), abortable_system: AbortableQueue::default(), })); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: 1000, other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, @@ -58,6 +58,8 @@ async fn test_send() { swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_maker_payment(maker_payment_args).compat().await; console::log_1(&format!("{:?}", tx).into()); diff --git a/mm2src/coins/eth/swap_contract_abi.json b/mm2src/coins/eth/swap_contract_abi.json index 7003d9a181..1ff37734b9 100644 --- a/mm2src/coins/eth/swap_contract_abi.json +++ b/mm2src/coins/eth/swap_contract_abi.json @@ -1,8 +1,29 @@ [ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "message", + "type": "string" + } + ], + "name": "Log", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "number", + "type": "uint256" + } + ], + "name": "LogNumber", + "type": "event" }, { "anonymous": false, @@ -84,7 +105,7 @@ ], "name": "erc20Payment", "outputs": [], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { @@ -110,9 +131,9 @@ "type": "address" }, { - "internalType": "bytes32", + "internalType": "bytes20", "name": "_secretHash", - "type": "bytes32" + "type": "bytes20" }, { "internalType": "uint64", @@ -120,7 +141,7 @@ "type": "uint64" } ], - "name": "erc20PaymentSha256", + "name": "erc20PaymentReward", "outputs": [], "stateMutability": "payable", "type": "function" @@ -165,49 +186,25 @@ "name": "_receiver", "type": "address" }, - { - "internalType": "bytes32", - "name": "_secretHash", - "type": "bytes32" - }, - { - "internalType": "uint64", - "name": "_lockTime", - "type": "uint64" - } - ], - "name": "ethPaymentSha256", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "payments", - "outputs": [ { "internalType": "bytes20", - "name": "paymentHash", + "name": "_secretHash", "type": "bytes20" }, { "internalType": "uint64", - "name": "lockTime", + "name": "_lockTime", "type": "uint64" }, { - "internalType": "enum EtomicSwap.PaymentState", - "name": "state", - "type": "uint8" + "internalType": "uint256", + "name": "_watcherReward", + "type": "uint256" } ], - "stateMutability": "view", + "name": "ethPaymentReward", + "outputs": [], + "stateMutability": "payable", "type": "function" }, { @@ -243,25 +240,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "secret_hash_algos", - "outputs": [ - { - "internalType": "enum EtomicSwap.SecretHashAlgo", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -275,22 +253,32 @@ "type": "uint256" }, { - "internalType": "bytes20", - "name": "_secretHash", - "type": "bytes20" + "internalType": "bytes32", + "name": "_secret", + "type": "bytes32" }, { "internalType": "address", "name": "_tokenAddress", "type": "address" }, + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, { "internalType": "address", "name": "_receiver", "type": "address" + }, + { + "internalType": "uint256", + "name": "_watcherReward", + "type": "uint256" } ], - "name": "senderRefund", + "name": "receiverSpendReward", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -308,9 +296,9 @@ "type": "uint256" }, { - "internalType": "bytes32", - "name": "_secretHash", - "type": "bytes32" + "internalType": "bytes20", + "name": "_paymentHash", + "type": "bytes20" }, { "internalType": "address", @@ -323,7 +311,7 @@ "type": "address" } ], - "name": "senderRefundSha256", + "name": "senderRefund", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -342,7 +330,7 @@ }, { "internalType": "bytes20", - "name": "_secretHash", + "name": "_paymentHash", "type": "bytes20" }, { @@ -359,49 +347,50 @@ "internalType": "address", "name": "_receiver", "type": "address" + }, + { + "internalType": "uint256", + "name": "_watcherReward", + "type": "uint256" } ], - "name": "watcherRefund", + "name": "senderRefundReward", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, { "inputs": [ { "internalType": "bytes32", - "name": "_id", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "_secret", + "name": "", "type": "bytes32" - }, + } + ], + "name": "payments", + "outputs": [ { - "internalType": "address", - "name": "_tokenAddress", - "type": "address" + "internalType": "bytes20", + "name": "paymentHash", + "type": "bytes20" }, { - "internalType": "address", - "name": "_sender", - "type": "address" + "internalType": "uint64", + "name": "lockTime", + "type": "uint64" }, { - "internalType": "address", - "name": "_receiver", - "type": "address" + "internalType": "enum EtomicSwap.PaymentState", + "name": "state", + "type": "uint8" } ], - "name": "watcherSpend", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" } ] \ No newline at end of file diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 3395cbed70..f3eda4d4ff 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -6,7 +6,7 @@ use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; -#[derive(Display, EnumFromTrait, Serialize, SerializeErrorType)] +#[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum EthActivationV2Error { InvalidPayload(String), @@ -90,6 +90,8 @@ pub struct EthActivationV2Request { pub rpc_mode: EthRpcMode, pub swap_contract_address: Address, pub fallback_swap_contract: Option
, + #[serde(default)] + pub contract_supports_watchers: bool, pub gas_station_url: Option, pub gas_station_decimals: Option, #[serde(default)] @@ -197,6 +199,7 @@ impl EthCoin { sign_message_prefix: self.sign_message_prefix.clone(), swap_contract_address: self.swap_contract_address, fallback_swap_contract: self.fallback_swap_contract, + contract_supports_watchers: self.contract_supports_watchers, decimals, ticker, gas_station_url: self.gas_station_url.clone(), @@ -292,6 +295,7 @@ pub async fn eth_coin_from_conf_and_request_v2( sign_message_prefix, swap_contract_address: req.swap_contract_address, fallback_swap_contract: req.fallback_swap_contract, + contract_supports_watchers: req.contract_supports_watchers, decimals: ETH_DECIMALS, ticker, gas_station_url: req.gas_station_url, diff --git a/mm2src/coins/hd_wallet_storage/wasm_storage.rs b/mm2src/coins/hd_wallet_storage/wasm_storage.rs index 721a4b09a7..a9e11143e2 100644 --- a/mm2src/coins/hd_wallet_storage/wasm_storage.rs +++ b/mm2src/coins/hd_wallet_storage/wasm_storage.rs @@ -98,18 +98,16 @@ impl TableSignature for HDAccountTable { fn table_name() -> &'static str { "hd_account" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_multi_index(WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; - table.create_multi_index( - WALLET_ACCOUNT_ID_INDEX, - &["coin", "hd_wallet_rmd160", "account_id"], - true, - )?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; + table.create_multi_index( + WALLET_ACCOUNT_ID_INDEX, + &["coin", "hd_wallet_rmd160", "account_id"], + true, + )?; } + Ok(()) } } @@ -318,7 +316,7 @@ impl HDWalletIndexedDbStorage { /// This function is used in `hd_wallet_storage::tests`. pub(super) async fn get_all_storage_items(ctx: &MmArc) -> Vec { - let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); + let coins_ctx = CoinsContext::from_ctx(ctx).unwrap(); let db = coins_ctx.hd_wallet_db.get_or_initialize().await.unwrap(); let transaction = db.inner.transaction().await.unwrap(); let table = transaction.table::().await.unwrap(); diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 8a69514384..f5ca99c625 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -15,19 +15,17 @@ use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_clt use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; use crate::utxo::{sat_from_big_decimal, utxo_common, BlockchainNetwork}; -use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, - HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, - PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, - RawTransactionRequest, RefundError, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, - SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, - SendSpendPaymentArgs, SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, - SendWatcherRefundsPaymentArgs, SignatureError, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionEnum, TransactionErr, - TransactionFut, TxMarshalingErr, UnexpectedDerivationMethod, UtxoStandardCoin, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFut, WithdrawRequest}; +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, RawTransactionError, + RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, + SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, + TradePreimageResult, TradePreimageValue, Transaction, TransactionEnum, TransactionErr, TransactionFut, + TxMarshalingErr, UnexpectedDerivationMethod, UtxoStandardCoin, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcoin::bech32::ToBase32; use bitcoin::hashes::Hash; @@ -36,7 +34,7 @@ use bitcrypto::ChecksumType; use bitcrypto::{dhash256, ripemd160}; use common::custom_futures::repeatable::{Ready, Retry}; use common::executor::{AbortableSystem, AbortedError, Timer}; -use common::log::{info, LogOnError, LogState}; +use common::log::{error, info, LogOnError, LogState}; use common::{async_blocking, get_local_duration_since_epoch, log, now_ms, PagingOptionsEnum}; use db_common::sqlite::rusqlite::Error as SqlError; use futures::{FutureExt, TryFutureExt}; @@ -45,8 +43,10 @@ use keys::{hash::H256, CompactSignature, KeyPair, Private, Public}; use lightning::chain::keysinterface::{KeysInterface, KeysManager, Recipient}; use lightning::ln::channelmanager::{ChannelDetails, MIN_FINAL_CLTV_EXPIRY}; use lightning::ln::{PaymentHash, PaymentPreimage}; +use lightning::routing::router::{DefaultRouter, PaymentParameters, RouteParameters, Router as RouterTrait}; +use lightning::util::ser::{Readable, Writeable}; use lightning_background_processor::BackgroundProcessor; -use lightning_invoice::utils::DefaultRouter; +use lightning_invoice::payment::Payer; use lightning_invoice::{payment, CreationError, InvoiceBuilder, SignOrCreationError}; use lightning_invoice::{Invoice, InvoiceDescription}; use ln_conf::{LightningCoinConf, PlatformCoinConfirmationTargets}; @@ -67,20 +67,22 @@ use mm2_number::{BigDecimal, MmNumber}; use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use script::TransactionInputSigner; -use secp256k1v22::PublicKey; +use secp256k1v24::PublicKey; use serde::Deserialize; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; use std::convert::TryInto; use std::fmt; +use std::io::Cursor; use std::net::SocketAddr; use std::str::FromStr; use std::sync::Arc; +use uuid::Uuid; const WAIT_FOR_REFUND_INTERVAL: f64 = 60.; pub const DEFAULT_INVOICE_EXPIRY: u32 = 3600; -pub type InvoicePayer = payment::InvoicePayer, Router, Arc, Arc, E>; +pub type InvoicePayer = payment::InvoicePayer, Router, Arc, E>; #[derive(Clone)] pub struct LightningCoin { @@ -112,10 +114,8 @@ pub struct LightningCoin { /// The lightning node router that takes care of finding routes for payments. // Todo: this should be removed once pay_invoice_with_max_total_cltv_expiry_delta similar functionality is implemented in rust-lightning pub router: Arc, - /// The lightning node scorer that takes care of scoring routes. Given the uncertainty of channel liquidity balances, - /// the scorer stores the probabilities that a route is successful based on knowledge learned from successful and unsuccessful attempts. - // Todo: this should be removed once pay_invoice_with_max_total_cltv_expiry_delta similar functionality is implemented in rust-lightning - pub scorer: Arc, + /// The lightning node logger, this is required to be passed to some function so that logs from these functions are displayed in mm2 logs. + pub logger: Arc, } impl fmt::Debug for LightningCoin { @@ -183,11 +183,11 @@ impl LightningCoin { }) } - pub(crate) async fn get_channel_by_rpc_id(&self, rpc_id: u64) -> Option { + pub(crate) async fn get_channel_by_uuid(&self, uuid: Uuid) -> Option { self.list_channels() .await .into_iter() - .find(|chan| chan.user_channel_id == rpc_id) + .find(|chan| chan.user_channel_id == uuid.as_u128()) } pub(crate) async fn pay_invoice( @@ -196,6 +196,16 @@ impl LightningCoin { max_total_cltv_expiry_delta: Option, ) -> Result> { let payment_hash = PaymentHash((invoice.payment_hash()).into_inner()); + // check if the invoice was already paid + if let Some(info) = self.db.get_payment_from_db(payment_hash).await? { + // If payment is still pending pay_invoice_with_max_total_cltv_expiry_delta/pay_invoice will return an error later + if info.status == HTLCStatus::Succeeded { + return MmError::err(PaymentError::Invoice(format!( + "Invoice with payment hash {} is already paid!", + hex::encode(payment_hash.0) + ))); + } + } let payment_type = PaymentType::OutboundPayment { destination: *invoice.payee_pub_key().unwrap_or(&invoice.recover_payee_pub_key()), }; @@ -212,7 +222,6 @@ impl LightningCoin { pay_invoice_with_max_total_cltv_expiry_delta( selfi.channel_manager, selfi.router, - selfi.scorer, &invoice, total_cltv, ) @@ -223,7 +232,8 @@ impl LightningCoin { }; let payment_info = PaymentInfo::new(payment_hash, payment_type, description, amt_msat); - self.db.add_payment_to_db(&payment_info).await?; + // So this only updates the payment in db if the user is retrying to pay an invoice payment that has failed + self.db.add_or_update_payment_in_db(&payment_info).await?; Ok(payment_info) } @@ -258,7 +268,7 @@ impl LightningCoin { pub(crate) async fn get_open_channels_by_filter( &self, filter: Option, - paging: PagingOptionsEnum, + paging: PagingOptionsEnum, limit: usize, ) -> GetOpenChannelsResult { fn apply_open_channel_filter(channel_details: &ChannelDetailsForRPC, filter: &OpenChannelsFilter) -> bool { @@ -358,11 +368,14 @@ impl LightningCoin { true } - let mut total_open_channels: Vec = - self.list_channels().await.into_iter().map(From::from).collect(); - - total_open_channels.sort_by(|a, b| a.rpc_channel_id.cmp(&b.rpc_channel_id)); + let mut total_open_channels = self.list_channels().await; + total_open_channels.sort_by(|a, b| { + b.short_channel_id + .unwrap_or(u64::MAX) + .cmp(&a.short_channel_id.unwrap_or(u64::MAX)) + }); drop_mutability!(total_open_channels); + let total_open_channels: Vec = total_open_channels.into_iter().map(From::from).collect(); let open_channels_filtered = if let Some(ref f) = filter { total_open_channels @@ -375,9 +388,9 @@ impl LightningCoin { let offset = match paging { PagingOptionsEnum::PageNumber(page) => (page.get() - 1) * limit, - PagingOptionsEnum::FromId(rpc_id) => open_channels_filtered + PagingOptionsEnum::FromId(uuid) => open_channels_filtered .iter() - .position(|x| x.rpc_channel_id == rpc_id) + .position(|x| x.uuid == uuid) .map(|pos| pos + 1) .unwrap_or_default(), }; @@ -397,6 +410,9 @@ impl LightningCoin { } } + // Todo: this can be removed after next rust-lightning release when min_final_cltv_expiry can be specified in + // Todo: create_invoice_from_channelmanager_and_duration_since_epoch_with_payment_hash https://github.com/lightningdevkit/rust-lightning/pull/1878 + // Todo: The above PR will also validate min_final_cltv_expiry. async fn create_invoice_for_hash( &self, payment_hash: PaymentHash, @@ -432,8 +448,6 @@ impl LightningCoin { .payment_hash(Hash::from_inner(payment_hash.0)) .payment_secret(payment_secret) .basic_mpp() - // Todo: This should be validated by the other side, right now this is not validated by rust-lightning and the PaymentReceived event doesn't include the final cltv of the payment for us to validate it - // Todo: This needs a PR opened to rust-lightning, I already contacted them about it and there is an issue opened for it https://github.com/lightningdevkit/rust-lightning/issues/1850 .min_final_cltv_expiry(min_final_cltv_expiry) .expiry_time(core::time::Duration::from_secs(invoice_expiry_delta_secs.into())); if let Some(amt) = amt_msat { @@ -523,7 +537,7 @@ impl LightningCoin { Ok(PaymentInstructions::Lightning(invoice)) } - fn spend_swap_payment(&self, spend_payment_args: SendSpendPaymentArgs<'_>) -> TransactionFut { + fn spend_swap_payment(&self, spend_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { let payment_hash = try_tx_fus!(payment_hash_from_slice(spend_payment_args.other_payment_tx)); let mut preimage = [b' '; 32]; preimage.copy_from_slice(spend_payment_args.secret); @@ -556,15 +570,15 @@ impl LightningCoin { let fut = async move { match coin.db.get_payment_from_db(payment_hash).await { Ok(Some(payment)) => { - let amount_received = payment.amt_msat; + let amount_claimable = payment.amt_msat; // Note: locktime doesn't need to be validated since min_final_cltv_expiry should be validated in rust-lightning after fixing the below issue // https://github.com/lightningdevkit/rust-lightning/issues/1850 - // Also, PaymentReceived won't be fired if amount_received < the amount requested in the invoice, this check is probably not needed. + // Also, PaymentClaimable won't be fired if amount_claimable < the amount requested in the invoice, this check is probably not needed. // But keeping it just in case any changes happen in rust-lightning - if amount_received != Some(amt_msat as i64) { + if amount_claimable != Some(amt_msat as i64) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Provided payment {} amount {:?} doesn't match required amount {}", - payment_hex, amount_received, amt_msat + payment_hex, amount_claimable, amt_msat ))); } Ok(()) @@ -601,7 +615,7 @@ impl SwapOps for LightningCoin { Box::new(fut.boxed().compat()) } - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs<'_>) -> TransactionFut { + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionFut { let PaymentInstructions::Lightning(invoice) = try_tx_fus!(maker_payment_args .payment_instructions .clone() @@ -615,7 +629,7 @@ impl SwapOps for LightningCoin { Box::new(fut.boxed().compat()) } - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs<'_>) -> TransactionFut { + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs<'_>) -> TransactionFut { let PaymentInstructions::Lightning(invoice) = try_tx_fus!(taker_payment_args .payment_instructions .clone() @@ -634,44 +648,29 @@ impl SwapOps for LightningCoin { } #[inline] - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs<'_>, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { self.spend_swap_payment(maker_spends_payment_args) } #[inline] - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs<'_>, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { self.spend_swap_payment(taker_spends_payment_args) } - fn send_taker_refunds_payment( - &self, - _taker_refunds_payment_args: SendTakerRefundsPaymentArgs<'_>, - ) -> TransactionFut { + fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut { Box::new(futures01::future::err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund lightning HTLC".into(), ))) } - fn send_maker_refunds_payment( - &self, - _maker_refunds_payment_args: SendMakerRefundsPaymentArgs<'_>, - ) -> TransactionFut { + fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut { Box::new(futures01::future::err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund lightning HTLC".into(), ))) } // Todo: This validates the dummy fee for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning - fn validate_fee( - &self, - _validate_fee_args: ValidateFeeArgs<'_>, - ) -> Box + Send> { + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { Box::new(futures01::future::ok(())) } @@ -726,7 +725,7 @@ impl SwapOps for LightningCoin { HTLCStatus::Succeeded => Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::LightningPayment( payment_hash, )))), - HTLCStatus::Received => { + HTLCStatus::Claimable => { ERR!( "Payment {} has an invalid status of {} in the db", payment_hex, @@ -760,7 +759,7 @@ impl SwapOps for LightningCoin { return ERR!("Payment {} should be an inbound payment!", payment_hex); } match payment.status { - HTLCStatus::Pending | HTLCStatus::Received => Ok(None), + HTLCStatus::Pending | HTLCStatus::Claimable => Ok(None), HTLCStatus::Succeeded => Ok(Some(FoundSwapTxSpend::Spent(TransactionEnum::LightningPayment( payment_hash, )))), @@ -782,7 +781,12 @@ impl SwapOps for LightningCoin { unimplemented!(); } - async fn extract_secret(&self, _secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + _secret_hash: &[u8], + spend_tx: &[u8], + _watcher_reward: bool, + ) -> Result, String> { let payment_hash = payment_hash_from_slice(spend_tx).map_err(|e| e.to_string())?; let payment_hex = hex::encode(payment_hash.0); @@ -967,10 +971,7 @@ impl WatcherOps for LightningCoin { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -1046,9 +1047,7 @@ impl MarketCoinOps for LightningCoin { Box::new(fut.boxed().compat()) } - fn base_coin_balance(&self) -> BalanceFut { - Box::new(self.platform_coin().my_balance().map(|res| res.spendable)) - } + fn base_coin_balance(&self) -> BalanceFut { Box::new(self.my_balance().map(|res| res.spendable)) } fn platform_ticker(&self) -> &str { self.platform_coin().ticker() } @@ -1071,24 +1070,18 @@ impl MarketCoinOps for LightningCoin { } // Todo: Add waiting for confirmations logic for the case of if the channel is closed and the htlc can be claimed on-chain - fn wait_for_confirmations( - &self, - tx: &[u8], - _confirmations: u64, - _requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { - let payment_hash = try_f!(payment_hash_from_slice(tx).map_err(|e| e.to_string())); + // Todo: The above is postponed and might not be needed after this issue is resolved https://github.com/lightningdevkit/rust-lightning/issues/2017 + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + let payment_hash = try_f!(payment_hash_from_slice(&input.payment_tx).map_err(|e| e.to_string())); let payment_hex = hex::encode(payment_hash.0); let coin = self.clone(); let fut = async move { loop { - if now_ms() / 1000 > wait_until { + if now_ms() / 1000 > input.wait_until { return ERR!( "Waited too long until {} for payment {} to be received", - wait_until, + input.wait_until, payment_hex ); } @@ -1098,7 +1091,7 @@ impl MarketCoinOps for LightningCoin { match payment.payment_type { PaymentType::OutboundPayment { .. } => match payment.status { HTLCStatus::Pending | HTLCStatus::Succeeded => return Ok(()), - HTLCStatus::Received => { + HTLCStatus::Claimable => { return ERR!( "Payment {} has an invalid status of {} in the db", payment_hex, @@ -1111,7 +1104,7 @@ impl MarketCoinOps for LightningCoin { HTLCStatus::Failed => return ERR!("Lightning swap payment {} failed", payment_hex), }, PaymentType::InboundPayment => match payment.status { - HTLCStatus::Received | HTLCStatus::Succeeded => return Ok(()), + HTLCStatus::Claimable | HTLCStatus::Succeeded => return Ok(()), HTLCStatus::Pending => info!("Payment {} not received yet!", payment_hex), HTLCStatus::Failed => return ERR!("Lightning swap payment {} failed", payment_hex), }, @@ -1124,7 +1117,7 @@ impl MarketCoinOps for LightningCoin { // note: When sleeping for only 1 second the test_send_payment_and_swaps unit test took 20 seconds to complete instead of 37 seconds when WAIT_CONFIRM_INTERVAL (15 seconds) is used // Todo: In next sprints, should add a mutex for lightning swap payments to avoid overloading the shared db connection with requests when the sleep time is reduced and multiple swaps are ran together // Todo: The aim is to make lightning swap payments as fast as possible. Running swap payments statuses should be loaded from db on restarts in this case. - Timer::sleep(check_every as f64).await; + Timer::sleep(input.check_every as f64).await; } }; Box::new(fut.boxed().compat()) @@ -1156,7 +1149,7 @@ impl MarketCoinOps for LightningCoin { match coin.db.get_payment_from_db(payment_hash).await { Ok(Some(payment)) => match payment.status { HTLCStatus::Pending => (), - HTLCStatus::Received => { + HTLCStatus::Claimable => { return Err(TransactionErr::Plain(ERRL!( "Payment {} has an invalid status of {} in the db", payment_hex, @@ -1208,13 +1201,31 @@ impl MarketCoinOps for LightningCoin { .to_string()) } - // Todo: min_tx_amount should depend on inbound_htlc_minimum_msat of the channel/s the payment will be sent through, 1 satoshi is used for for now (1000 of the base unit of lightning which is msat) - fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(1000, self.decimals()) } + // This will depend on the route/routes taken for the payment, since every channel's counterparty specifies the minimum amount they will allow to route. + // Since route is not specified at this stage yet, we can use the maximum of these minimum amounts as the min_tx_amount allowed. + // Default value: 1 msat if the counterparty is using LDK default value. + fn min_tx_amount(&self) -> BigDecimal { + let amount_in_msat = self + .channel_manager + .list_channels() + .iter() + .map(|c| c.counterparty.outbound_htlc_minimum_msat.unwrap_or(1)) + .max() + .unwrap_or(1) as i64; + big_decimal_from_sat(amount_in_msat, self.decimals()) + } // Todo: Equals to min_tx_amount for now (1 satoshi), should change this later + // Todo: doesn't take routing fees into account too, There is no way to know the route to the other side of the swap when placing the order, need to find a workaround for this fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } } +#[derive(Deserialize, Serialize)] +struct LightningProtocolInfo { + node_id: PublicKeyForRPC, + route_hints: Vec>, +} + #[async_trait] impl MmCoin for LightningCoin { fn is_asset_chain(&self) -> bool { false } @@ -1327,11 +1338,91 @@ impl MmCoin for LightningCoin { fn mature_confirmations(&self) -> Option { None } - // Todo: This uses default data for now for the sake of swap P.O.C., this should be implemented probably when implementing order matching if it's needed - fn coin_protocol_info(&self) -> Vec { Vec::new() } + // Channels for users/non-routing nodes should be private, so routing hints are sent as part of the protocol info + // alongside the receiver lightning node address/pubkey. + // Note: This is required only for the side that's getting paid in lightning. + // Todo: should take in consideration JIT routing and using LSPs in next PRs + fn coin_protocol_info(&self, amount_to_receive: Option) -> Vec { + let amt_msat = match amount_to_receive.map(|a| sat_from_big_decimal(&a.into(), self.decimals())) { + Some(Ok(amt)) => amt, + Some(Err(e)) => { + error!("{}", e); + return Vec::new(); + }, + None => return Vec::new(), + }; + let route_hints = filter_channels(self.channel_manager.list_usable_channels(), Some(amt_msat)) + .iter() + .map(|h| h.encode()) + .collect(); + let node_id = PublicKeyForRPC(self.channel_manager.get_our_node_id()); + let protocol_info = LightningProtocolInfo { node_id, route_hints }; + rmp_serde::to_vec(&protocol_info).expect("Serialization should not fail") + } - // Todo: This uses default data for now for the sake of swap P.O.C., this should be implemented probably when implementing order matching if it's needed - fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } + // Todo: should take in consideration JIT routing and using LSPs in next PRs + fn is_coin_protocol_supported( + &self, + info: &Option>, + amount_to_send: Option, + locktime: u64, + is_maker: bool, + ) -> bool { + macro_rules! log_err_and_return_false { + ($e:expr) => { + match $e { + Ok(res) => res, + Err(e) => { + error!("{}", e); + return false; + }, + } + }; + } + let final_value_msat = match amount_to_send.map(|amt| sat_from_big_decimal(&amt.into(), self.decimals())) { + Some(amt_or_err) => log_err_and_return_false!(amt_or_err), + None => return true, + }; + let protocol_info = match info.as_ref().map(rmp_serde::from_read_ref::<_, LightningProtocolInfo>) { + Some(info_or_err) => log_err_and_return_false!(info_or_err), + None => return false, + }; + let mut route_hints = Vec::new(); + for h in protocol_info.route_hints.iter() { + let hint = log_err_and_return_false!(Readable::read(&mut Cursor::new(h))); + route_hints.push(hint); + } + let mut payment_params = + PaymentParameters::from_node_id(protocol_info.node_id.into()).with_route_hints(route_hints); + let final_cltv_expiry_delta = if is_maker { + self.estimate_blocks_from_duration(locktime) + .try_into() + .expect("final_cltv_expiry_delta shouldn't exceed u32::MAX") + } else { + payment_params.max_total_cltv_expiry_delta = self + .estimate_blocks_from_duration(locktime) + .try_into() + .expect("max_total_cltv_expiry_delta shouldn't exceed u32::MAX"); + MIN_FINAL_CLTV_EXPIRY + }; + drop_mutability!(payment_params); + let route_params = RouteParameters { + payment_params, + final_value_msat, + final_cltv_expiry_delta, + }; + let payer = self.channel_manager.node_id(); + let first_hops = self.channel_manager.first_hops(); + let inflight_htlcs = self.channel_manager.compute_inflight_htlcs(); + self.router + .find_route( + &payer, + &route_params, + Some(&first_hops.iter().collect::>()), + inflight_htlcs, + ) + .is_ok() + } fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.platform.abortable_system) } diff --git a/mm2src/coins/lightning/ln_conf.rs b/mm2src/coins/lightning/ln_conf.rs index dd58946969..80c5047d62 100644 --- a/mm2src/coins/lightning/ln_conf.rs +++ b/mm2src/coins/lightning/ln_conf.rs @@ -110,6 +110,12 @@ pub struct OurChannelsConfigs { pub announced_channel: Option, /// When set, we commit to an upfront shutdown_pubkey at channel open. pub commit_upfront_shutdown_pubkey: Option, + /// The minimum balance that the other node has to maintain on their side, at all times. + /// This ensures that if our counterparty broadcasts a revoked state, we can punish them by claiming + /// at least this value on chain. + /// Default value: 1% of channel value. + /// Minimum value: 1000 sats + pub their_channel_reserve_sats: Option, } impl OurChannelsConfigs { @@ -141,6 +147,10 @@ impl OurChannelsConfigs { if let Some(commit) = config.commit_upfront_shutdown_pubkey { self.commit_upfront_shutdown_pubkey = Some(commit); } + + if let Some(reserve) = config.their_channel_reserve_sats { + self.their_channel_reserve_sats = Some(reserve); + } } } @@ -176,6 +186,10 @@ impl From for ChannelHandshakeConfig { channel_handshake_config.commit_upfront_shutdown_pubkey = commit; } + if let Some(reserve) = config.their_channel_reserve_sats { + channel_handshake_config.their_channel_reserve_proportional_millionths = reserve; + } + channel_handshake_config } } diff --git a/mm2src/coins/lightning/ln_db.rs b/mm2src/coins/lightning/ln_db.rs index 8ad4455a24..523e7daca3 100644 --- a/mm2src/coins/lightning/ln_db.rs +++ b/mm2src/coins/lightning/ln_db.rs @@ -3,13 +3,14 @@ use common::{now_ms, PagingOptionsEnum}; use db_common::sqlite::rusqlite::types::FromSqlError; use derive_more::Display; use lightning::ln::{PaymentHash, PaymentPreimage}; -use secp256k1v22::PublicKey; +use secp256k1v24::PublicKey; use serde::{Deserialize, Serialize}; use std::str::FromStr; +use uuid::Uuid; #[derive(Clone, Debug, PartialEq, Serialize)] pub struct DBChannelDetails { - pub rpc_id: i64, + pub uuid: Uuid, pub channel_id: String, pub counterparty_node_id: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -37,14 +38,14 @@ pub struct DBChannelDetails { impl DBChannelDetails { #[inline] pub fn new( - rpc_id: u64, + uuid: Uuid, channel_id: [u8; 32], counterparty_node_id: PublicKey, is_outbound: bool, is_public: bool, ) -> Self { DBChannelDetails { - rpc_id: rpc_id as i64, + uuid, channel_id: hex::encode(channel_id), counterparty_node_id: counterparty_node_id.to_string(), funding_tx: None, @@ -101,7 +102,7 @@ pub struct GetClosedChannelsResult { #[serde(rename_all = "lowercase")] pub enum HTLCStatus { Pending, - Received, + Claimable, Succeeded, Failed, } @@ -112,7 +113,7 @@ impl FromStr for HTLCStatus { fn from_str(s: &str) -> Result { match s { "Pending" => Ok(HTLCStatus::Pending), - "Received" => Ok(HTLCStatus::Received), + "Claimable" => Ok(HTLCStatus::Claimable), "Succeeded" => Ok(HTLCStatus::Succeeded), "Failed" => Ok(HTLCStatus::Failed), _ => Err(FromSqlError::InvalidType), @@ -205,9 +206,6 @@ pub trait LightningDB { /// Checks if tables have been initialized or not in DB. async fn is_db_initialized(&self) -> Result; - /// Gets the last added channel rpc_id. Can be used to deduce the rpc_id for a new channel to be added to DB. - async fn get_last_channel_rpc_id(&self) -> Result; - /// Inserts a new channel record in the DB. The record's data is completed using add_funding_tx_to_db, /// add_closing_tx_to_db, add_claiming_tx_to_db when this information is available. async fn add_channel_to_db(&self, details: &DBChannelDetails) -> Result<(), Self::Error>; @@ -215,7 +213,7 @@ pub trait LightningDB { /// Updates a channel's DB record with the channel's funding transaction information. async fn add_funding_tx_to_db( &self, - rpc_id: i64, + uuid: Uuid, funding_tx: String, funding_value: i64, funding_generated_in_block: i64, @@ -228,7 +226,7 @@ pub trait LightningDB { /// Updates the is_closed value for a channel in the DB to 1. async fn update_channel_to_closed( &self, - rpc_id: i64, + uuid: Uuid, closure_reason: String, close_at: i64, ) -> Result<(), Self::Error>; @@ -240,7 +238,7 @@ pub trait LightningDB { async fn get_closed_channels_with_no_closing_tx(&self) -> Result, Self::Error>; /// Updates a channel's DB record with the channel's closing transaction hash. - async fn add_closing_tx_to_db(&self, rpc_id: i64, closing_tx: String) -> Result<(), Self::Error>; + async fn add_closing_tx_to_db(&self, uuid: Uuid, closing_tx: String) -> Result<(), Self::Error>; /// Updates a channel's DB record with information about the transaction responsible for claiming the channel's /// closing balance back to the user's address. @@ -251,8 +249,8 @@ pub trait LightningDB { claimed_balance: f64, ) -> Result<(), Self::Error>; - /// Gets a channel record from DB by the channel's rpc_id. - async fn get_channel_from_db(&self, rpc_id: u64) -> Result, Self::Error>; + /// Gets a channel record from DB by the channel's uuid. + async fn get_channel_from_db(&self, uuid: Uuid) -> Result, Self::Error>; /// Gets the list of closed channels that match the provided filter criteria. The number of requested records is /// specified by the limit parameter, the starting record to list from is specified by the paging parameter. The @@ -260,13 +258,16 @@ pub trait LightningDB { async fn get_closed_channels_by_filter( &self, filter: Option, - paging: PagingOptionsEnum, + paging: PagingOptionsEnum, limit: usize, ) -> Result; /// Inserts a new payment record in the DB. async fn add_payment_to_db(&self, info: &PaymentInfo) -> Result<(), Self::Error>; + /// Inserts or updates a payment record in the DB. + async fn add_or_update_payment_in_db(&self, info: &PaymentInfo) -> Result<(), Self::Error>; + /// Updates a payment's preimage in DB by the payment's hash. async fn update_payment_preimage_in_db( &self, @@ -277,8 +278,8 @@ pub trait LightningDB { /// Updates a payment's status in DB by the payment's hash. async fn update_payment_status_in_db(&self, hash: PaymentHash, status: &HTLCStatus) -> Result<(), Self::Error>; - /// Updates a payment's status to received in DB by the payment's hash. Also, adds the payment preimage to the db. - async fn update_payment_to_received_in_db( + /// Updates a payment's status to claimable in DB by the payment's hash. Also, adds the payment preimage to the db. + async fn update_payment_to_claimable_in_db( &self, hash: PaymentHash, preimage: PaymentPreimage, diff --git a/mm2src/coins/lightning/ln_errors.rs b/mm2src/coins/lightning/ln_errors.rs index 5b349fd43a..5d479e40b6 100644 --- a/mm2src/coins/lightning/ln_errors.rs +++ b/mm2src/coins/lightning/ln_errors.rs @@ -8,6 +8,7 @@ use http::StatusCode; use mm2_err_handle::prelude::*; use rpc_task::RpcTaskError; use std::num::TryFromIntError; +use uuid::Uuid; pub type EnableLightningResult = Result>; pub type SaveChannelClosingResult = Result>; @@ -90,10 +91,8 @@ impl From for EnableLightningError { pub enum SaveChannelClosingError { #[display(fmt = "DB error: {}", _0)] DbError(String), - #[display(fmt = "Channel with rpc id {} not found in DB", _0)] - ChannelNotFound(u64), - #[display(fmt = "funding_generated_in_block is Null in DB")] - BlockHeightNull, + #[display(fmt = "Channel with uuid {} not found in DB", _0)] + ChannelNotFound(Uuid), #[display(fmt = "Funding transaction hash is Null in DB")] FundingTxNull, #[display(fmt = "Error parsing funding transaction hash: {}", _0)] diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index 351191e1c0..c819cb19ca 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -7,7 +7,7 @@ use bitcoin::blockdata::transaction::Transaction; use bitcoin::consensus::encode::serialize_hex; use common::executor::{AbortSettings, SpawnAbortable, SpawnFuture, Timer}; use common::log::{error, info}; -use common::now_ms; +use common::{new_uuid, now_ms}; use core::time::Duration; use futures::compat::Future01CompatExt; use lightning::chain::chaininterface::{ConfirmationTarget, FeeEstimator}; @@ -15,7 +15,7 @@ use lightning::chain::keysinterface::SpendableOutputDescriptor; use lightning::util::events::{Event, EventHandler, PaymentPurpose}; use rand::Rng; use script::{Builder, SignatureVersion}; -use secp256k1v22::Secp256k1; +use secp256k1v24::Secp256k1; use std::convert::{TryFrom, TryInto}; use std::sync::Arc; use utxo_signer::with_key_pair::sign_tx; @@ -23,6 +23,8 @@ use utxo_signer::with_key_pair::sign_tx; const TRY_LOOP_INTERVAL: f64 = 60.; /// 1 second. const CRITICAL_FUTURE_TIMEOUT: f64 = 1.0; +pub const CHANNEL_READY_LOG: &str = "Handling ChannelReady event for channel with uuid"; +pub const PAYMENT_CLAIMABLE_LOG: &str = "Handling PaymentClaimable event"; pub const SUCCESSFUL_CLAIM_LOG: &str = "Successfully claimed payment"; pub const SUCCESSFUL_SEND_LOG: &str = "Successfully sent payment"; @@ -35,7 +37,7 @@ pub struct LightningEventHandler { } impl EventHandler for LightningEventHandler { - fn handle_event(&self, event: &Event) { + fn handle_event(&self, event: Event) { match event { Event::FundingGenerationReady { temporary_channel_id, @@ -44,33 +46,34 @@ impl EventHandler for LightningEventHandler { user_channel_id, counterparty_node_id, } => self.handle_funding_generation_ready( - *temporary_channel_id, - *channel_value_satoshis, + temporary_channel_id, + channel_value_satoshis, output_script, - *user_channel_id, + user_channel_id, counterparty_node_id, ), - Event::PaymentReceived { + Event::PaymentClaimable { payment_hash, amount_msat, purpose, - } => self.handle_payment_received(*payment_hash, *amount_msat, purpose.clone()), + .. + } => self.handle_payment_claimable(payment_hash, amount_msat, purpose), Event::PaymentSent { payment_preimage, payment_hash, fee_paid_msat, .. - } => self.handle_payment_sent(*payment_preimage, *payment_hash, *fee_paid_msat), + } => self.handle_payment_sent(payment_preimage, payment_hash, fee_paid_msat), - Event::PaymentClaimed { payment_hash, amount_msat, .. } => self.handle_payment_claimed(*payment_hash, *amount_msat), + Event::PaymentClaimed { payment_hash, amount_msat, .. } => self.handle_payment_claimed(payment_hash, amount_msat), - Event::PaymentFailed { payment_hash, .. } => self.handle_payment_failed(*payment_hash), + Event::PaymentFailed { payment_hash, .. } => self.handle_payment_failed(payment_hash), - Event::PendingHTLCsForwardable { time_forwardable } => self.handle_pending_htlcs_forwards(*time_forwardable), + Event::PendingHTLCsForwardable { time_forwardable } => self.handle_pending_htlcs_forwards(time_forwardable), - Event::SpendableOutputs { outputs } => self.handle_spendable_outputs(outputs.clone()), + Event::SpendableOutputs { outputs } => self.handle_spendable_outputs(outputs), // Todo: an RPC for total amount earned Event::PaymentForwarded { fee_earned_msat, claim_from_onchain_tx, prev_channel_id, next_channel_id} => info!( @@ -86,7 +89,7 @@ impl EventHandler for LightningEventHandler { channel_id, user_channel_id, reason, - } => self.handle_channel_closed(*channel_id, *user_channel_id, reason.to_string()), + } => self.handle_channel_closed(channel_id, user_channel_id, reason.to_string()), // Todo: Add spent UTXOs to RecentlySpentOutPoints if it's not discarded Event::DiscardFunding { channel_id, transaction } => info!( @@ -113,15 +116,15 @@ impl EventHandler for LightningEventHandler { // Todo: Add information to db about why a payment failed using this event Event::PaymentPathFailed { payment_hash, - rejected_by_dest, + payment_failed_permanently, all_paths_failed, path, .. } => info!( - "Payment path: {:?}, failed for payment hash: {}, Was rejected by destination?: {}, All paths failed?: {}", + "Payment path: {:?}, failed for payment hash: {}, permanent failure?: {}, All paths failed?: {}", path.iter().map(|hop| hop.pubkey.to_string()).collect::>(), hex::encode(payment_hash.0), - rejected_by_dest, + payment_failed_permanently, all_paths_failed, ), @@ -131,7 +134,7 @@ impl EventHandler for LightningEventHandler { funding_satoshis, push_msat, channel_type: _, - } => self.handle_open_channel_request(*temporary_channel_id, *counterparty_node_id, *funding_satoshis, *push_msat), + } => self.handle_open_channel_request(temporary_channel_id, counterparty_node_id, funding_satoshis, push_msat), // Just log an error for now, but this event can be used along PaymentForwarded for a new RPC that shows stats about how a node // forward payments over it's outbound channels which can be useful for a user that wants to run a forwarding node for some profits. @@ -147,6 +150,8 @@ impl EventHandler for LightningEventHandler { // send_probe is not used for now but may be used in order matching in the future to check if a swap can happen or not. Event::ProbeSuccessful { .. } => (), Event::ProbeFailed { .. } => (), + Event::HTLCIntercepted { .. } => (), + Event::ChannelReady { user_channel_id, .. } => info!("{}: {}", CHANNEL_READY_LOG, Uuid::from_u128(user_channel_id)), } } } @@ -156,19 +161,15 @@ pub async fn init_abortable_events(platform: Arc, db: SqliteLightningD for channel_details in closed_channels_without_closing_tx { let platform_c = platform.clone(); let db = db.clone(); - let user_channel_id = channel_details.rpc_id; + let uuid = channel_details.uuid; platform.spawner().spawn(async move { if let Ok(closing_tx_hash) = platform_c .get_channel_closing_tx(channel_details) .await .error_log_passthrough() { - if let Err(e) = db.add_closing_tx_to_db(user_channel_id, closing_tx_hash).await { - log::error!( - "Unable to update channel {} closing details in DB: {}", - user_channel_id, - e - ); + if let Err(e) = db.add_closing_tx_to_db(uuid, closing_tx_hash).await { + log::error!("Unable to update channel {} closing details in DB: {}", uuid, e); } } }); @@ -188,7 +189,7 @@ pub enum SignFundingTransactionError { // Generates the raw funding transaction with one output equal to the channel value. fn sign_funding_transaction( - user_channel_id: u64, + uuid: Uuid, output_script: &Script, platform: Arc, ) -> Result { @@ -196,11 +197,11 @@ fn sign_funding_transaction( let mut unsigned = { let unsigned_funding_txs = platform.unsigned_funding_txs.lock(); unsigned_funding_txs - .get(&user_channel_id) + .get(&uuid) .ok_or_else(|| { SignFundingTransactionError::Internal(format!( - "Unsigned funding tx not found for internal channel id: {}", - user_channel_id + "Unsigned funding tx not found for channel with uuid: {}", + uuid )) })? .clone() @@ -234,20 +235,20 @@ fn sign_funding_transaction( async fn save_channel_closing_details( db: SqliteLightningDB, platform: Arc, - user_channel_id: u64, + uuid: Uuid, reason: String, ) -> SaveChannelClosingResult<()> { - db.update_channel_to_closed(user_channel_id as i64, reason, (now_ms() / 1000) as i64) + db.update_channel_to_closed(uuid, reason, (now_ms() / 1000) as i64) .await?; let channel_details = db - .get_channel_from_db(user_channel_id) + .get_channel_from_db(uuid) .await? - .ok_or_else(|| MmError::new(SaveChannelClosingError::ChannelNotFound(user_channel_id)))?; + .ok_or_else(|| MmError::new(SaveChannelClosingError::ChannelNotFound(uuid)))?; let closing_tx_hash = platform.get_channel_closing_tx(channel_details).await?; - db.add_closing_tx_to_db(user_channel_id as i64, closing_tx_hash).await?; + db.add_closing_tx_to_db(uuid, closing_tx_hash).await?; Ok(()) } @@ -288,20 +289,21 @@ impl LightningEventHandler { &self, temporary_channel_id: [u8; 32], channel_value_satoshis: u64, - output_script: &Script, - user_channel_id: u64, - counterparty_node_id: &PublicKey, + output_script: Script, + user_channel_id: u128, + counterparty_node_id: PublicKey, ) { + let uuid = Uuid::from_u128(user_channel_id); info!( - "Handling FundingGenerationReady event for internal channel id: {} with: {}", - user_channel_id, counterparty_node_id + "Handling FundingGenerationReady event for channel with uuid: {} with: {}", + uuid, counterparty_node_id ); - let funding_tx = match sign_funding_transaction(user_channel_id, output_script, self.platform.clone()) { + let funding_tx = match sign_funding_transaction(uuid, &output_script, self.platform.clone()) { Ok(tx) => tx, Err(e) => { error!( - "Error generating funding transaction for internal channel id {}: {}", - user_channel_id, + "Error generating funding transaction for channel with uuid {}: {}", + uuid, e.to_string() ); return; @@ -311,7 +313,7 @@ impl LightningEventHandler { // Give the funding transaction back to LDK for opening the channel. if let Err(e) = self.channel_manager - .funding_transaction_generated(&temporary_channel_id, counterparty_node_id, funding_tx) + .funding_transaction_generated(&temporary_channel_id, &counterparty_node_id, funding_tx) { error!("{:?}", e); return; @@ -322,7 +324,7 @@ impl LightningEventHandler { let fut = async move { let best_block_height = platform.best_block_height(); db.add_funding_tx_to_db( - user_channel_id as i64, + uuid, funding_txid.to_string(), channel_value_satoshis as i64, best_block_height as i64, @@ -335,20 +337,21 @@ impl LightningEventHandler { self.platform.spawner().spawn_with_settings(fut, settings); } - fn handle_payment_received(&self, payment_hash: PaymentHash, received_amount: u64, purpose: PaymentPurpose) { + fn handle_payment_claimable(&self, payment_hash: PaymentHash, claimable_amount: u64, purpose: PaymentPurpose) { info!( - "Handling PaymentReceived event for payment_hash: {} with amount {}", + "{} for payment_hash: {} with amount {}", + PAYMENT_CLAIMABLE_LOG, hex::encode(payment_hash.0), - received_amount + claimable_amount ); let db = self.db.clone(); let payment_preimage = match purpose { PaymentPurpose::InvoicePayment { payment_preimage, .. } => match payment_preimage { Some(preimage) => { let fut = async move { - db.update_payment_to_received_in_db(payment_hash, preimage) + db.update_payment_to_claimable_in_db(payment_hash, preimage) .await - .error_log_with_msg("Unable to update received payment info in DB!"); + .error_log_with_msg("Unable to update claimable payment info in DB!"); }; let settings = AbortSettings::default().critical_timout_s(CRITICAL_FUTURE_TIMEOUT); self.platform.spawner().spawn_with_settings(fut, settings); @@ -357,9 +360,9 @@ impl LightningEventHandler { // This is a swap related payment since we don't have the preimage yet None => { let amt_msat = Some( - received_amount + claimable_amount .try_into() - .expect("received_amount shouldn't exceed i64::MAX"), + .expect("claimable_amount shouldn't exceed i64::MAX"), ); let payment_info = PaymentInfo::new( payment_hash, @@ -367,7 +370,7 @@ impl LightningEventHandler { "Swap Payment".into(), amt_msat, ) - .with_status(HTLCStatus::Received); + .with_status(HTLCStatus::Claimable); let fut = async move { db.add_payment_to_db(&payment_info) .await @@ -382,14 +385,14 @@ impl LightningEventHandler { }, PaymentPurpose::SpontaneousPayment(preimage) => { let amt_msat = Some( - received_amount + claimable_amount .try_into() - .expect("received_amount shouldn't exceed i64::MAX"), + .expect("claimable_amount shouldn't exceed i64::MAX"), ); let payment_info = PaymentInfo::new(payment_hash, PaymentType::InboundPayment, "keysend".into(), amt_msat) .with_preimage(preimage) - .with_status(HTLCStatus::Received); + .with_status(HTLCStatus::Claimable); let fut = async move { db.add_payment_to_db(&payment_info) .await @@ -406,7 +409,7 @@ impl LightningEventHandler { fn handle_payment_claimed(&self, payment_hash: PaymentHash, amount_msat: u64) { info!( - "Received an amount of {} millisatoshis for payment hash {}", + "Claimed an amount of {} millisatoshis for payment hash {}", amount_msat, hex::encode(payment_hash.0) ); @@ -457,23 +460,21 @@ impl LightningEventHandler { self.platform.spawner().spawn_with_settings(fut, settings); } - fn handle_channel_closed(&self, channel_id: [u8; 32], user_channel_id: u64, reason: String) { + fn handle_channel_closed(&self, channel_id: [u8; 32], user_channel_id: u128, reason: String) { info!( "Channel: {} closed for the following reason: {}", hex::encode(channel_id), reason ); + let uuid = Uuid::from_u128(user_channel_id); let db = self.db.clone(); let platform = self.platform.clone(); let fut = async move { - if let Err(e) = save_channel_closing_details(db, platform, user_channel_id, reason).await { + if let Err(e) = save_channel_closing_details(db, platform, uuid, reason).await { // This is the case when a channel is closed before funding is broadcasted due to the counterparty disconnecting or other incompatibility issue. if e != SaveChannelClosingError::FundingTxNull.into() { - error!( - "Unable to update channel {} closing details in DB: {}", - user_channel_id, e - ); + error!("Unable to update channel {} closing details in DB: {}", uuid, e); } } }; @@ -620,70 +621,60 @@ impl LightningEventHandler { let channel_manager = self.channel_manager.clone(); let platform = self.platform.clone(); let fut = async move { - if let Ok(last_channel_rpc_id) = db.get_last_channel_rpc_id().await.error_log_passthrough() { - let user_channel_id = last_channel_rpc_id as u64 + 1; - - let trusted_nodes = trusted_nodes.lock().clone(); - let accepted_inbound_channel_with_0conf = trusted_nodes.contains(&counterparty_node_id) - && channel_manager - .accept_inbound_channel_from_trusted_peer_0conf( - &temporary_channel_id, - &counterparty_node_id, - user_channel_id, - ) - .is_ok(); + let uuid = new_uuid(); + let uuid_u128 = uuid.as_u128(); + let trusted_nodes = trusted_nodes.lock().clone(); + let accepted_inbound_channel_with_0conf = trusted_nodes.contains(&counterparty_node_id) + && channel_manager + .accept_inbound_channel_from_trusted_peer_0conf( + &temporary_channel_id, + &counterparty_node_id, + uuid_u128, + ) + .is_ok(); - if accepted_inbound_channel_with_0conf - || channel_manager - .accept_inbound_channel(&temporary_channel_id, &counterparty_node_id, user_channel_id) - .is_ok() + if accepted_inbound_channel_with_0conf + || channel_manager + .accept_inbound_channel(&temporary_channel_id, &counterparty_node_id, uuid_u128) + .is_ok() + { + let is_public = match channel_manager + .list_channels() + .into_iter() + .find(|chan| chan.user_channel_id == uuid_u128) { - let is_public = match channel_manager - .list_channels() - .into_iter() - .find(|chan| chan.user_channel_id == user_channel_id) - { - Some(details) => details.is_public, - None => { - error!( - "Inbound channel {} details should be found by list_channels!", - user_channel_id - ); - return; - }, - }; - - let pending_channel_details = DBChannelDetails::new( - user_channel_id, - temporary_channel_id, - counterparty_node_id, - false, - is_public, - ); - if let Err(e) = db.add_channel_to_db(&pending_channel_details).await { - error!("Unable to add new inbound channel {} to db: {}", user_channel_id, e); - } + Some(details) => details.is_public, + None => { + error!("Inbound channel {} details should be found by list_channels!", uuid); + return; + }, + }; - while let Some(details) = channel_manager - .list_channels() - .into_iter() - .find(|chan| chan.user_channel_id == user_channel_id) - { - if let Some(funding_tx) = details.funding_txo { - let best_block_height = platform.best_block_height(); - db.add_funding_tx_to_db( - user_channel_id as i64, - funding_tx.txid.to_string(), - funding_satoshis as i64, - best_block_height as i64, - ) - .await - .error_log(); - break; - } + let pending_channel_details = + DBChannelDetails::new(uuid, temporary_channel_id, counterparty_node_id, false, is_public); + if let Err(e) = db.add_channel_to_db(&pending_channel_details).await { + error!("Unable to add new inbound channel {} to db: {}", uuid, e); + } - Timer::sleep(TRY_LOOP_INTERVAL).await; + while let Some(details) = channel_manager + .list_channels() + .into_iter() + .find(|chan| chan.user_channel_id == uuid_u128) + { + if let Some(funding_tx) = details.funding_txo { + let best_block_height = platform.best_block_height(); + db.add_funding_tx_to_db( + uuid, + funding_tx.txid.to_string(), + funding_satoshis as i64, + best_block_height as i64, + ) + .await + .error_log(); + break; } + + Timer::sleep(TRY_LOOP_INTERVAL).await; } } }; diff --git a/mm2src/coins/lightning/ln_filesystem_persister.rs b/mm2src/coins/lightning/ln_filesystem_persister.rs index 49ecb111b3..d1444a62a6 100644 --- a/mm2src/coins/lightning/ln_filesystem_persister.rs +++ b/mm2src/coins/lightning/ln_filesystem_persister.rs @@ -12,7 +12,7 @@ use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringParam use lightning::util::persist::KVStorePersister; use lightning::util::ser::{ReadableArgs, Writeable}; use mm2_io::fs::{check_dir_operations, invalid_data_err, read_json, write_json}; -use secp256k1v22::PublicKey; +use secp256k1v24::PublicKey; use std::collections::{HashMap, HashSet}; use std::fs; use std::io::{BufReader, BufWriter, Cursor}; diff --git a/mm2src/coins/lightning/ln_p2p.rs b/mm2src/coins/lightning/ln_p2p.rs index c93c017f06..7db3a0b4e2 100644 --- a/mm2src/coins/lightning/ln_p2p.rs +++ b/mm2src/coins/lightning/ln_p2p.rs @@ -5,19 +5,21 @@ use derive_more::Display; use lightning::chain::Access; use lightning::ln::msgs::NetAddress; use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler, SimpleArcPeerManager}; +use lightning::onion_message::SimpleArcOnionMessenger; use lightning::routing::gossip; use lightning_net_tokio::SocketDescriptor; use mm2_net::ip_addr::fetch_external_ip; use rand::RngCore; -use secp256k1v22::{PublicKey, SecretKey}; +use secp256k1v24::PublicKey; use std::net::{IpAddr, Ipv4Addr}; +use std::num::TryFromIntError; use tokio::net::TcpListener; const TRY_RECONNECTING_TO_NODE_INTERVAL: f64 = 60.; const BROADCAST_NODE_ANNOUNCEMENT_INTERVAL: u64 = 600; pub type NetworkGossip = gossip::P2PGossipSync, Arc, Arc>; - +type OnionMessenger = SimpleArcOnionMessenger; pub type PeerManager = SimpleArcPeerManager; @@ -124,7 +126,7 @@ fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { } pub async fn ln_node_announcement_loop( - channel_manager: Arc, + peer_manager: Arc, node_name: [u8; 32], node_color: [u8; 3], port: u16, @@ -144,8 +146,8 @@ pub async fn ln_node_announcement_loop( continue; }, }; - let channel_manager = channel_manager.clone(); - async_blocking(move || channel_manager.broadcast_node_announcement(node_color, node_name, addresses)).await; + let peer_manager = peer_manager.clone(); + async_blocking(move || peer_manager.broadcast_node_announcement(node_color, node_name, addresses)).await; Timer::sleep(BROADCAST_NODE_ANNOUNCEMENT_INTERVAL as f64).await; } @@ -181,8 +183,8 @@ pub async fn init_peer_manager( platform: &Arc, listening_port: u16, channel_manager: Arc, + keys_manager: Arc, gossip_sync: Arc, - node_secret: SecretKey, logger: Arc, ) -> EnableLightningResult> { // The set (possibly empty) of socket addresses on which this node accepts incoming connections. @@ -196,18 +198,33 @@ pub async fn init_peer_manager( // ephemeral_random_data is used to derive per-connection ephemeral keys let mut ephemeral_bytes = [0; 32]; rand::thread_rng().fill_bytes(&mut ephemeral_bytes); + let onion_message_handler = Arc::new(OnionMessenger::new( + keys_manager.clone(), + logger.clone(), + IgnoringMessageHandler {}, + )); let lightning_msg_handler = MessageHandler { chan_handler: channel_manager, route_handler: gossip_sync, + onion_message_handler, }; + let node_secret = keys_manager + .get_node_secret(Recipient::Node) + .map_to_mm(|_| EnableLightningError::UnsupportedMode("'start_lightning'".into(), "local node".into()))?; + let current_time: u32 = get_local_duration_since_epoch() + .map_to_mm(|e| EnableLightningError::Internal(e.to_string()))? + .as_secs() + .try_into() + .map_to_mm(|e: TryFromIntError| EnableLightningError::Internal(e.to_string()))?; // IgnoringMessageHandler is used as custom message types (experimental and application-specific messages) is not needed let peer_manager: Arc = Arc::new(PeerManager::new( lightning_msg_handler, node_secret, + current_time, &ephemeral_bytes, logger, - Arc::new(IgnoringMessageHandler {}), + IgnoringMessageHandler {}, )); // Initialize p2p networking diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index 3444b4cb69..f6bea230ad 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -22,8 +22,9 @@ use lightning::chain::{chaininterface::{BroadcasterInterface, ConfirmationTarget Confirm, Filter, WatchedOutput}; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use spv_validation::spv_proof::TRY_SPV_PROOF_INTERVAL; -use std::convert::{TryFrom, TryInto}; +use std::convert::TryInto; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering, Ordering}; +use uuid::Uuid; const CHECK_FOR_NEW_BEST_BLOCK_INTERVAL: f64 = 60.; const TRY_LOOP_INTERVAL: f64 = 60.; @@ -168,7 +169,7 @@ pub struct Platform { /// This cache stores the outputs that the LN node has interest in. pub registered_outputs: PaMutex>, /// This cache stores transactions to be broadcasted once the other node accepts the channel - pub unsigned_funding_txs: PaMutex>, + pub unsigned_funding_txs: PaMutex>, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation. /// and on [`MmArc::stop`]. pub abortable_system: AbortableQueue, @@ -313,14 +314,14 @@ impl Platform { ) { // Retrieve channel manager transaction IDs to check the chain for un-confirmations let channel_manager_relevant_txids = channel_manager.get_relevant_txids(); - for txid in channel_manager_relevant_txids { + for (txid, _) in channel_manager_relevant_txids { self.process_tx_for_unconfirmation(txid, Arc::clone(&channel_manager)) .await; } // Retrieve chain monitor transaction IDs to check the chain for un-confirmations let chain_monitor_relevant_txids = chain_monitor.get_relevant_txids(); - for txid in chain_monitor_relevant_txids { + for (txid, _) in chain_monitor_relevant_txids { self.process_tx_for_unconfirmation(txid, Arc::clone(&chain_monitor)) .await; } @@ -514,7 +515,9 @@ impl Platform { pub async fn get_channel_closing_tx(&self, channel_details: DBChannelDetails) -> SaveChannelClosingResult { let from_block = channel_details .funding_generated_in_block - .ok_or_else(|| MmError::new(SaveChannelClosingError::BlockHeightNull))?; + .map(|b| b.try_into()) + .transpose()? + .unwrap_or_else(|| self.best_block_height()); let tx_id = channel_details .funding_tx @@ -532,7 +535,7 @@ impl Platform { &funding_tx_bytes.into_vec(), &[], (now_ms() / 1000) + 3600, - from_block.try_into()?, + from_block, &None, TAKER_PAYMENT_SPEND_SEARCH_INTERVAL, ) @@ -597,16 +600,41 @@ impl BroadcasterInterface for Platform { let txid = tx.txid(); let tx_hex = serialize_hex(tx); debug!("Trying to broadcast transaction: {}", tx_hex); + let tx_bytes = match hex::decode(&tx_hex) { + Ok(b) => b, + Err(e) => { + error!("Converting transaction to bytes error:{}", e); + return; + }, + }; - let fut = self.coin.send_raw_tx(&tx_hex); + let platform_coin = self.coin.clone(); let fut = async move { - match fut.compat().await { - Ok(id) => info!("Transaction broadcasted successfully: {:?} ", id), - // TODO: broadcast transaction through p2p network in case of error - Err(e) => error!("Broadcast transaction {} failed: {}", txid, e), + loop { + match platform_coin + .as_ref() + .rpc_client + .send_raw_transaction(tx_bytes.clone().into()) + .compat() + .await + { + Ok(id) => { + info!("Transaction broadcasted successfully: {:?} ", id); + break; + }, + // Todo: broadcast transaction through p2p network instead in case of error + // Todo: I don't want to rely on p2p broadcasting for now since there is no way to know if there are nodes running bitcoin in native mode or not + // Todo: Also we need to make sure that the transaction was broadcasted after relying on the p2p network + Err(e) => { + error!("Broadcast transaction {} failed: {}", txid, e); + if !e.get_inner().is_network_error() { + break; + } + Timer::sleep(TRY_LOOP_INTERVAL).await; + }, + } } }; - self.spawner().spawn(fut); } } @@ -617,33 +645,5 @@ impl Filter for Platform { fn register_tx(&self, txid: &Txid, _script_pubkey: &Script) { self.add_tx(*txid); } // Watches for any transactions that spend this output on-chain - fn register_output(&self, output: WatchedOutput) -> Option<(usize, Transaction)> { - self.add_output(output.clone()); - let block_hash = match output.block_hash { - Some(h) => H256Json::from(h.as_hash().into_inner()), - None => return None, - }; - - // Although this works for both native and electrum clients as the block hash is available, - // the filter interface which includes register_output and register_tx should be used for electrum clients only, - // this is the reason for initializing the filter as an option in the start_lightning function as it will be None - // when implementing lightning for native clients - let output_spend_fut = self.rpc_client().find_output_spend( - h256_from_txid(output.outpoint.txid), - output.script_pubkey.as_ref(), - output.outpoint.index.into(), - BlockHashOrHeight::Hash(block_hash), - ); - let maybe_output_spend_res = - tokio::task::block_in_place(move || output_spend_fut.wait()).error_log_passthrough(); - - if let Ok(Some(spent_output_info)) = maybe_output_spend_res { - match Transaction::try_from(spent_output_info.spending_tx) { - Ok(spending_tx) => return Some((spent_output_info.input_index, spending_tx)), - Err(e) => error!("Can't convert transaction error: {}", e.to_string()), - } - } - - None - } + fn register_output(&self, output: WatchedOutput) { self.add_output(output); } } diff --git a/mm2src/coins/lightning/ln_serialization.rs b/mm2src/coins/lightning/ln_serialization.rs index 511522c328..37ece14bee 100644 --- a/mm2src/coins/lightning/ln_serialization.rs +++ b/mm2src/coins/lightning/ln_serialization.rs @@ -3,11 +3,12 @@ use crate::lightning::ln_platform::h256_json_from_txid; use crate::H256Json; use lightning::chain::channelmonitor::Balance; use lightning::ln::channelmanager::ChannelDetails; -use secp256k1v22::PublicKey; +use secp256k1v24::PublicKey; use serde::{de, Serialize, Serializer}; use std::fmt; use std::net::{SocketAddr, ToSocketAddrs}; use std::str::FromStr; +use uuid::Uuid; // TODO: support connection to onion addresses #[derive(Debug, PartialEq)] @@ -101,7 +102,11 @@ impl<'de> de::Deserialize<'de> for PublicKeyForRPC { #[derive(Clone, Serialize)] pub struct ChannelDetailsForRPC { - pub rpc_channel_id: u64, + /// An internal identifier for the channel that doesn't change throughout the channels lifetime. + pub uuid: Uuid, + /// The channel's ID, prior to funding transaction generation, this is a random 32 bytes, + /// after funding transaction generation, this is the txid of the funding transaction xor the funding transaction output. + /// Note that this means this value is *not* persistent - it can change once during the lifetime of the channel. pub channel_id: H256Json, pub counterparty_node_id: PublicKeyForRPC, pub funding_tx: Option, @@ -112,6 +117,8 @@ pub struct ChannelDetailsForRPC { pub balance_msat: u64, pub outbound_capacity_msat: u64, pub inbound_capacity_msat: u64, + pub current_confirmations: Option, + pub required_confirmations: Option, // Channel is confirmed onchain, this means that funding_locked messages have been exchanged, // the channel is not currently being shut down, and the required confirmation count has been reached. pub is_ready: bool, @@ -125,7 +132,7 @@ pub struct ChannelDetailsForRPC { impl From for ChannelDetailsForRPC { fn from(details: ChannelDetails) -> ChannelDetailsForRPC { ChannelDetailsForRPC { - rpc_channel_id: details.user_channel_id, + uuid: Uuid::from_u128(details.user_channel_id), channel_id: details.channel_id.into(), counterparty_node_id: PublicKeyForRPC(details.counterparty.node_id), funding_tx: details.funding_txo.map(|tx| h256_json_from_txid(tx.txid)), @@ -135,6 +142,8 @@ impl From for ChannelDetailsForRPC { balance_msat: details.balance_msat, outbound_capacity_msat: details.outbound_capacity_msat, inbound_capacity_msat: details.inbound_capacity_msat, + current_confirmations: details.confirmations, + required_confirmations: details.confirmations_required, is_ready: details.is_channel_ready, is_usable: details.is_usable, is_public: details.is_public, @@ -279,7 +288,7 @@ pub enum ClaimableBalance { /// HTLCs which we sent to our counterparty which are claimable after a timeout (less on-chain /// fees) if the counterparty does not know the preimage for the HTLCs. These are somewhat /// likely to be claimed by our counterparty before we do. - MaybeClaimableHTLCAwaitingTimeout { + MaybeTimeoutClaimableHTLC { /// The amount available to claim, in satoshis, excluding the on-chain fees which will be /// required to do so. claimable_amount_satoshis: u64, @@ -287,6 +296,29 @@ pub enum ClaimableBalance { /// done so. claimable_height: u32, }, + /// HTLCs which we received from our counterparty which are claimable with a preimage which we + /// do not currently have. This will only be claimable if we receive the preimage from the node + /// to which we forwarded this HTLC before the timeout. + MaybePreimageClaimableHTLC { + /// The amount potentially available to claim, in satoshis, excluding the on-chain fees + /// which will be required to do so. + claimable_amount_satoshis: u64, + /// The height at which our counterparty will be able to claim the balance if we have not + /// yet received the preimage and claimed it ourselves. + expiry_height: u32, + }, + /// The channel has been closed, and our counterparty broadcasted a revoked commitment + /// transaction. + /// + /// Thus, we're able to claim all outputs in the commitment transaction, one of which has the + /// following amount. + CounterpartyRevokedOutputClaimable { + /// The amount, in satoshis, of the output which we can claim. + /// + /// Note that for outputs from HTLC balances this may be excluding some on-chain fees that + /// were already spent. + claimable_amount_satoshis: u64, + }, } impl From for ClaimableBalance { @@ -311,13 +343,25 @@ impl From for ClaimableBalance { claimable_amount_satoshis, timeout_height, }, - Balance::MaybeClaimableHTLCAwaitingTimeout { + Balance::MaybeTimeoutClaimableHTLC { claimable_amount_satoshis, claimable_height, - } => ClaimableBalance::MaybeClaimableHTLCAwaitingTimeout { + } => ClaimableBalance::MaybeTimeoutClaimableHTLC { claimable_amount_satoshis, claimable_height, }, + Balance::MaybePreimageClaimableHTLC { + claimable_amount_satoshis, + expiry_height, + } => ClaimableBalance::MaybePreimageClaimableHTLC { + claimable_amount_satoshis, + expiry_height, + }, + Balance::CounterpartyRevokedOutputClaimable { + claimable_amount_satoshis, + } => ClaimableBalance::CounterpartyRevokedOutputClaimable { + claimable_amount_satoshis, + }, } } } diff --git a/mm2src/coins/lightning/ln_sql.rs b/mm2src/coins/lightning/ln_sql.rs index b9ac1e2129..8f8f953fa1 100644 --- a/mm2src/coins/lightning/ln_sql.rs +++ b/mm2src/coins/lightning/ln_sql.rs @@ -4,6 +4,7 @@ use crate::lightning::ln_db::{ChannelType, ChannelVisibility, ClosedChannelsFilt use async_trait::async_trait; use common::{async_blocking, PagingOptionsEnum}; use db_common::owned_named_params; +use db_common::sqlite::rusqlite::types::Type; use db_common::sqlite::rusqlite::{params, Error as SqlError, Row, ToSql, NO_PARAMS}; use db_common::sqlite::sql_builder::SqlBuilder; use db_common::sqlite::{h256_option_slice_from_row, h256_slice_from_row, offset_by_id, query_single_row, @@ -11,9 +12,10 @@ use db_common::sqlite::{h256_option_slice_from_row, h256_slice_from_row, offset_ OwnedSqlNamedParams, SqlNamedParams, SqliteConnShared, CHECK_TABLE_EXISTS_SQL}; use gstuff::now_ms; use lightning::ln::{PaymentHash, PaymentPreimage}; -use secp256k1v22::PublicKey; +use secp256k1v24::PublicKey; use std::convert::TryInto; use std::str::FromStr; +use uuid::Uuid; fn channels_history_table(ticker: &str) -> String { ticker.to_owned() + "_channels_history" } @@ -26,7 +28,7 @@ fn create_channels_history_table_sql(for_coin: &str) -> Result let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( id INTEGER NOT NULL PRIMARY KEY, - rpc_id INTEGER NOT NULL UNIQUE, + uuid VARCHAR(255) NOT NULL UNIQUE, channel_id VARCHAR(255) NOT NULL, counterparty_node_id VARCHAR(255) NOT NULL, funding_tx VARCHAR(255), @@ -79,12 +81,9 @@ fn insert_channel_sql( let table_name = channels_history_table(for_coin); validate_table_name(&table_name)?; - let rpc_id = channel_detail.rpc_id; - let created_at = channel_detail.created_at; - let sql = format!( "INSERT INTO {} ( - rpc_id, + uuid, channel_id, counterparty_node_id, is_outbound, @@ -92,27 +91,24 @@ fn insert_channel_sql( is_closed, created_at ) VALUES ( - :rpc_id, :channel_id, :counterparty_node_id, :is_outbound, :is_public, :is_closed, :created_at + :uuid, :channel_id, :counterparty_node_id, :is_outbound, :is_public, :is_closed, :created_at )", table_name ); let params = owned_named_params! { - ":rpc_id": rpc_id, + ":uuid": channel_detail.uuid.to_string(), ":channel_id": channel_detail.channel_id.clone(), ":counterparty_node_id": channel_detail.counterparty_node_id.clone(), ":is_outbound": channel_detail.is_outbound, ":is_public": channel_detail.is_public, ":is_closed": channel_detail.is_closed, - ":created_at": created_at, + ":created_at": channel_detail.created_at, }; Ok((sql, params)) } -fn insert_payment_sql(for_coin: &str, payment_info: &PaymentInfo) -> Result<(String, OwnedSqlNamedParams), SqlError> { - let table_name = payments_history_table(for_coin); - validate_table_name(&table_name)?; - +fn payment_info_to_owned_named_params(payment_info: &PaymentInfo) -> OwnedSqlNamedParams { let payment_hash = hex::encode(payment_info.payment_hash.0); let (is_outbound, destination) = match payment_info.payment_type { PaymentType::OutboundPayment { destination } => (true, Some(destination.to_string())), @@ -121,6 +117,24 @@ fn insert_payment_sql(for_coin: &str, payment_info: &PaymentInfo) -> Result<(Str let preimage = payment_info.preimage.map(|p| hex::encode(p.0)); let status = payment_info.status.to_string(); + owned_named_params! { + ":payment_hash": payment_hash, + ":destination": destination, + ":description": payment_info.description.clone(), + ":preimage": preimage, + ":amount_msat": payment_info.amt_msat, + ":fee_paid_msat": payment_info.fee_paid_msat, + ":is_outbound": is_outbound, + ":status": status, + ":created_at": payment_info.created_at, + ":last_updated": payment_info.last_updated, + } +} + +fn insert_payment_sql(for_coin: &str, payment_info: &PaymentInfo) -> Result<(String, OwnedSqlNamedParams), SqlError> { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + let sql = format!( "INSERT INTO {} ( payment_hash, @@ -139,19 +153,32 @@ fn insert_payment_sql(for_coin: &str, payment_info: &PaymentInfo) -> Result<(Str table_name ); - let params = owned_named_params! { - ":payment_hash": payment_hash, - ":destination": destination, - ":description": payment_info.description.clone(), - ":preimage": preimage, - ":amount_msat": payment_info.amt_msat, - ":fee_paid_msat": payment_info.fee_paid_msat, - ":is_outbound": is_outbound, - ":status": status, - ":created_at": payment_info.created_at, - ":last_updated": payment_info.last_updated, - }; - Ok((sql, params)) + Ok((sql, payment_info_to_owned_named_params(payment_info))) +} + +fn upsert_payment_sql(for_coin: &str, payment_info: &PaymentInfo) -> Result<(String, OwnedSqlNamedParams), SqlError> { + let table_name = payments_history_table(for_coin); + validate_table_name(&table_name)?; + + let sql = format!( + "INSERT OR REPLACE INTO {} ( + payment_hash, + destination, + description, + preimage, + amount_msat, + fee_paid_msat, + is_outbound, + status, + created_at, + last_updated + ) VALUES ( + :payment_hash, :destination, :description, :preimage, :amount_msat, :fee_paid_msat, :is_outbound, :status, :created_at, :last_updated + )", + table_name + ); + + Ok((sql, payment_info_to_owned_named_params(payment_info))) } fn update_payment_preimage_sql(for_coin: &str) -> Result { @@ -186,7 +213,7 @@ fn update_payment_status_sql(for_coin: &str) -> Result { Ok(sql) } -fn update_received_payment_sql(for_coin: &str) -> Result { +fn update_claimable_payment_sql(for_coin: &str) -> Result { let table_name = payments_history_table(for_coin); validate_table_name(&table_name)?; @@ -221,13 +248,13 @@ fn update_sent_payment_sql(for_coin: &str) -> Result { Ok(sql) } -fn select_channel_by_rpc_id_sql(for_coin: &str) -> Result { +fn select_channel_by_uuid_sql(for_coin: &str) -> Result { let table_name = channels_history_table(for_coin); validate_table_name(&table_name)?; let sql = format!( "SELECT - rpc_id, + uuid, channel_id, counterparty_node_id, funding_tx, @@ -245,7 +272,7 @@ fn select_channel_by_rpc_id_sql(for_coin: &str) -> Result { FROM {} WHERE - rpc_id=?1", + uuid=?1", table_name ); @@ -280,7 +307,8 @@ fn select_payment_by_hash_sql(for_coin: &str) -> Result { fn channel_details_from_row(row: &Row<'_>) -> Result { let channel_details = DBChannelDetails { - rpc_id: row.get(0)?, + uuid: Uuid::parse_str(&row.get::<_, String>(0)?) + .map_err(|e| SqlError::FromSqlConversionFailure(0, Type::Text, Box::new(e)))?, channel_id: row.get(1)?, counterparty_node_id: row.get(2)?, funding_tx: row.get(3)?, @@ -323,15 +351,6 @@ fn payment_info_from_row(row: &Row<'_>) -> Result { Ok(payment_info) } -fn get_last_channel_rpc_id_sql(for_coin: &str) -> Result { - let table_name = channels_history_table(for_coin); - validate_table_name(&table_name)?; - - let sql = format!("SELECT IFNULL(MAX(rpc_id), 0) FROM {};", table_name); - - Ok(sql) -} - fn update_funding_tx_sql(for_coin: &str) -> Result { let table_name = channels_history_table(for_coin); validate_table_name(&table_name)?; @@ -342,7 +361,7 @@ fn update_funding_tx_sql(for_coin: &str) -> Result { funding_value = ?2, funding_generated_in_block = ?3 WHERE - rpc_id = ?4;", + uuid = ?4;", table_name ); @@ -366,7 +385,7 @@ fn update_channel_to_closed_sql(for_coin: &str) -> Result { validate_table_name(&table_name)?; let sql = format!( - "UPDATE {} SET closure_reason = ?1, is_closed = ?2, closed_at = ?3 WHERE rpc_id = ?4;", + "UPDATE {} SET closure_reason = ?1, is_closed = ?2, closed_at = ?3 WHERE uuid = ?4;", table_name ); @@ -377,7 +396,7 @@ fn update_closing_tx_sql(for_coin: &str) -> Result { let table_name = channels_history_table(for_coin); validate_table_name(&table_name)?; - let sql = format!("UPDATE {} SET closing_tx = ?1 WHERE rpc_id = ?2;", table_name); + let sql = format!("UPDATE {} SET closing_tx = ?1 WHERE uuid = ?2;", table_name); Ok(sql) } @@ -393,7 +412,7 @@ fn get_channels_builder_preimage(for_coin: &str) -> Result fn add_fields_to_get_channels_sql_builder(sql_builder: &mut SqlBuilder) { sql_builder - .field("rpc_id") + .field("uuid") .field("channel_id") .field("counterparty_node_id") .field("funding_tx") @@ -633,18 +652,6 @@ impl LightningDB for SqliteLightningDB { .await } - async fn get_last_channel_rpc_id(&self) -> Result { - let sql = get_last_channel_rpc_id_sql(self.db_ticker.as_str())?; - let sqlite_connection = self.sqlite_connection.clone(); - - async_blocking(move || { - let conn = sqlite_connection.lock().unwrap(); - let count: u32 = conn.query_row(&sql, NO_PARAMS, |r| r.get(0))?; - Ok(count) - }) - .await - } - async fn add_channel_to_db(&self, details: &DBChannelDetails) -> Result<(), Self::Error> { let for_coin = self.db_ticker.clone(); let (sql, params) = insert_channel_sql(&for_coin, details)?; @@ -660,7 +667,7 @@ impl LightningDB for SqliteLightningDB { async fn add_funding_tx_to_db( &self, - rpc_id: i64, + uuid: Uuid, funding_tx: String, funding_value: i64, funding_generated_in_block: i64, @@ -671,7 +678,7 @@ impl LightningDB for SqliteLightningDB { async_blocking(move || { let mut conn = sqlite_connection.lock().unwrap(); let sql_transaction = conn.transaction()?; - let params = params!(funding_tx, funding_value, funding_generated_in_block, rpc_id); + let params = params!(funding_tx, funding_value, funding_generated_in_block, uuid.to_string()); sql_transaction.execute(&update_funding_tx_sql(&for_coin)?, params)?; sql_transaction.commit()?; Ok(()) @@ -696,7 +703,7 @@ impl LightningDB for SqliteLightningDB { async fn update_channel_to_closed( &self, - rpc_id: i64, + uuid: Uuid, closure_reason: String, closed_at: i64, ) -> Result<(), Self::Error> { @@ -707,7 +714,7 @@ impl LightningDB for SqliteLightningDB { async_blocking(move || { let mut conn = sqlite_connection.lock().unwrap(); let sql_transaction = conn.transaction()?; - let params = params!(closure_reason, is_closed, closed_at, rpc_id); + let params = params!(closure_reason, is_closed, closed_at, uuid.to_string()); sql_transaction.execute(&update_channel_to_closed_sql(&for_coin)?, params)?; sql_transaction.commit()?; Ok(()) @@ -735,14 +742,14 @@ impl LightningDB for SqliteLightningDB { .await } - async fn add_closing_tx_to_db(&self, rpc_id: i64, closing_tx: String) -> Result<(), Self::Error> { + async fn add_closing_tx_to_db(&self, uuid: Uuid, closing_tx: String) -> Result<(), Self::Error> { let for_coin = self.db_ticker.clone(); let sqlite_connection = self.sqlite_connection.clone(); async_blocking(move || { let mut conn = sqlite_connection.lock().unwrap(); let sql_transaction = conn.transaction()?; - let params = params!(closing_tx, rpc_id); + let params = params!(closing_tx, uuid.to_string()); sql_transaction.execute(&update_closing_tx_sql(&for_coin)?, params)?; sql_transaction.commit()?; Ok(()) @@ -770,9 +777,9 @@ impl LightningDB for SqliteLightningDB { .await } - async fn get_channel_from_db(&self, rpc_id: u64) -> Result, Self::Error> { - let params = [rpc_id.to_string()]; - let sql = select_channel_by_rpc_id_sql(self.db_ticker.as_str())?; + async fn get_channel_from_db(&self, uuid: Uuid) -> Result, Self::Error> { + let params = [uuid.to_string()]; + let sql = select_channel_by_uuid_sql(self.db_ticker.as_str())?; let sqlite_connection = self.sqlite_connection.clone(); async_blocking(move || { @@ -785,7 +792,7 @@ impl LightningDB for SqliteLightningDB { async fn get_closed_channels_by_filter( &self, filter: Option, - paging: PagingOptionsEnum, + paging: PagingOptionsEnum, limit: usize, ) -> Result { let mut sql_builder = get_channels_builder_preimage(self.db_ticker.as_str())?; @@ -802,10 +809,10 @@ impl LightningDB for SqliteLightningDB { let offset = match paging { PagingOptionsEnum::PageNumber(page) => (page.get() - 1) * limit, - PagingOptionsEnum::FromId(rpc_id) => { - let params = [rpc_id as u32]; + PagingOptionsEnum::FromId(uuid) => { + let params = [uuid.to_string()]; let maybe_offset = - offset_by_id(&conn, &sql_builder, params, "rpc_id", "closed_at DESC", "rpc_id = ?1")?; + offset_by_id(&conn, &sql_builder, params, "uuid", "closed_at DESC", "uuid = ?1")?; match maybe_offset { Some(offset) => offset, None => { @@ -854,6 +861,19 @@ impl LightningDB for SqliteLightningDB { .await } + async fn add_or_update_payment_in_db(&self, info: &PaymentInfo) -> Result<(), Self::Error> { + let for_coin = self.db_ticker.clone(); + let (sql, params) = upsert_payment_sql(&for_coin, info)?; + + let sqlite_connection = self.sqlite_connection.clone(); + async_blocking(move || { + let conn = sqlite_connection.lock().unwrap(); + conn.execute_named(&sql, ¶ms.as_sql_named_params())?; + Ok(()) + }) + .await + } + async fn update_payment_preimage_in_db( &self, hash: PaymentHash, @@ -894,14 +914,14 @@ impl LightningDB for SqliteLightningDB { .await } - async fn update_payment_to_received_in_db( + async fn update_payment_to_claimable_in_db( &self, hash: PaymentHash, preimage: PaymentPreimage, ) -> Result<(), Self::Error> { let for_coin = self.db_ticker.clone(); let preimage = hex::encode(preimage.0); - let status = HTLCStatus::Received.to_string(); + let status = HTLCStatus::Claimable.to_string(); let last_updated = (now_ms() / 1000) as i64; let payment_hash = hex::encode(hash.0); @@ -910,7 +930,7 @@ impl LightningDB for SqliteLightningDB { let mut conn = sqlite_connection.lock().unwrap(); let sql_transaction = conn.transaction()?; let params = params!(preimage, status, last_updated, payment_hash); - sql_transaction.execute(&update_received_payment_sql(&for_coin)?, params)?; + sql_transaction.execute(&update_claimable_payment_sql(&for_coin)?, params)?; sql_transaction.commit()?; Ok(()) }) @@ -1025,11 +1045,11 @@ impl LightningDB for SqliteLightningDB { mod tests { use super::*; use crate::lightning::ln_db::DBChannelDetails; - use common::{block_on, now_ms}; + use common::{block_on, new_uuid, now_ms}; use db_common::sqlite::rusqlite::Connection; use rand::distributions::Alphanumeric; use rand::{Rng, RngCore}; - use secp256k1v22::{Secp256k1, SecretKey}; + use secp256k1v24::{Secp256k1, SecretKey}; use std::num::NonZeroUsize; use std::sync::{Arc, Mutex}; @@ -1038,9 +1058,9 @@ mod tests { let mut channels = vec![]; let s = Secp256k1::new(); let mut bytes = [0; 32]; - for i in 0..num { + for _i in 0..num { let details = DBChannelDetails { - rpc_id: (i + 1) as i64, + uuid: new_uuid(), channel_id: { rng.fill_bytes(&mut bytes); hex::encode(bytes) @@ -1157,37 +1177,30 @@ mod tests { block_on(db.init_db()).unwrap(); - let last_channel_rpc_id = block_on(db.get_last_channel_rpc_id()).unwrap(); - assert_eq!(last_channel_rpc_id, 0); - - let channel = block_on(db.get_channel_from_db(1)).unwrap(); + let uuid_1 = new_uuid(); + let channel = block_on(db.get_channel_from_db(uuid_1)).unwrap(); assert!(channel.is_none()); let mut expected_channel_details = DBChannelDetails::new( - 1, + uuid_1, [0; 32], PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9").unwrap(), true, true, ); block_on(db.add_channel_to_db(&expected_channel_details)).unwrap(); - let last_channel_rpc_id = block_on(db.get_last_channel_rpc_id()).unwrap(); - assert_eq!(last_channel_rpc_id, 1); - - let actual_channel_details = block_on(db.get_channel_from_db(1)).unwrap().unwrap(); + let actual_channel_details = block_on(db.get_channel_from_db(uuid_1)).unwrap().unwrap(); assert_eq!(expected_channel_details, actual_channel_details); - // must fail because we are adding channel with the same rpc_id + // must fail because we are adding channel with the same uuid block_on(db.add_channel_to_db(&expected_channel_details)).unwrap_err(); - assert_eq!(last_channel_rpc_id, 1); - expected_channel_details.rpc_id = 2; + let uuid_2 = new_uuid(); + expected_channel_details.uuid = uuid_2; block_on(db.add_channel_to_db(&expected_channel_details)).unwrap(); - let last_channel_rpc_id = block_on(db.get_last_channel_rpc_id()).unwrap(); - assert_eq!(last_channel_rpc_id, 2); block_on(db.add_funding_tx_to_db( - 2, + uuid_2, "9cdafd6d42dcbdc06b0b5bce1866deb82630581285bbfb56870577300c0a8c6e".into(), 3000, 50000, @@ -1198,7 +1211,7 @@ mod tests { expected_channel_details.funding_value = Some(3000); expected_channel_details.funding_generated_in_block = Some(50000); - let actual_channel_details = block_on(db.get_channel_from_db(2)).unwrap().unwrap(); + let actual_channel_details = block_on(db.get_channel_from_db(uuid_2)).unwrap().unwrap(); assert_eq!(expected_channel_details, actual_channel_details); block_on(db.update_funding_tx_block_height( @@ -1208,16 +1221,17 @@ mod tests { .unwrap(); expected_channel_details.funding_generated_in_block = Some(50001); - let actual_channel_details = block_on(db.get_channel_from_db(2)).unwrap().unwrap(); + let actual_channel_details = block_on(db.get_channel_from_db(uuid_2)).unwrap().unwrap(); assert_eq!(expected_channel_details, actual_channel_details); let current_time = (now_ms() / 1000) as i64; - block_on(db.update_channel_to_closed(2, "the channel was cooperatively closed".into(), current_time)).unwrap(); + block_on(db.update_channel_to_closed(uuid_2, "the channel was cooperatively closed".into(), current_time)) + .unwrap(); expected_channel_details.closure_reason = Some("the channel was cooperatively closed".into()); expected_channel_details.is_closed = true; expected_channel_details.closed_at = Some(current_time); - let actual_channel_details = block_on(db.get_channel_from_db(2)).unwrap().unwrap(); + let actual_channel_details = block_on(db.get_channel_from_db(uuid_2)).unwrap().unwrap(); assert_eq!(expected_channel_details, actual_channel_details); let closed_channels = @@ -1226,7 +1240,7 @@ mod tests { assert_eq!(expected_channel_details, closed_channels.channels[0]); block_on(db.update_channel_to_closed( - 1, + uuid_1, "the channel was cooperatively closed".into(), (now_ms() / 1000) as i64, )) @@ -1239,7 +1253,7 @@ mod tests { assert_eq!(actual_channels.len(), 1); block_on(db.add_closing_tx_to_db( - 2, + uuid_2, "5557df9ad2c9b3c57a4df8b4a7da0b7a6f4e923b4a01daa98bf9e5a3b33e9c8f".into(), )) .unwrap(); @@ -1249,7 +1263,7 @@ mod tests { let actual_channels = block_on(db.get_closed_channels_with_no_closing_tx()).unwrap(); assert!(actual_channels.is_empty()); - let actual_channel_details = block_on(db.get_channel_from_db(2)).unwrap().unwrap(); + let actual_channel_details = block_on(db.get_channel_from_db(uuid_2)).unwrap().unwrap(); assert_eq!(expected_channel_details, actual_channel_details); block_on(db.add_claiming_tx_to_db( @@ -1262,7 +1276,7 @@ mod tests { Some("97f061634a4a7b0b0c2b95648f86b1c39b95e0cf5073f07725b7143c095b612a".into()); expected_channel_details.claimed_balance = Some(2000.333333); - let actual_channel_details = block_on(db.get_channel_from_db(2)).unwrap().unwrap(); + let actual_channel_details = block_on(db.get_channel_from_db(uuid_2)).unwrap().unwrap(); assert_eq!(expected_channel_details, actual_channel_details); } @@ -1294,13 +1308,21 @@ mod tests { let actual_payment_info = block_on(db.get_payment_from_db(PaymentHash([0; 32]))).unwrap().unwrap(); assert_eq!(expected_payment_info, actual_payment_info); + // Test add_or_update_payment_in_db + expected_payment_info.status = HTLCStatus::Succeeded; + block_on(db.add_payment_to_db(&expected_payment_info)).unwrap_err(); + block_on(db.add_or_update_payment_in_db(&expected_payment_info)).unwrap(); + + let actual_payment_info = block_on(db.get_payment_from_db(PaymentHash([0; 32]))).unwrap().unwrap(); + assert_eq!(expected_payment_info, actual_payment_info); + + // Add another payment to DB expected_payment_info.payment_hash = PaymentHash([1; 32]); expected_payment_info.payment_type = PaymentType::OutboundPayment { destination: PublicKey::from_str("038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9") .unwrap(), }; expected_payment_info.amt_msat = None; - expected_payment_info.status = HTLCStatus::Succeeded; expected_payment_info.last_updated = (now_ms() / 1000) as i64; block_on(db.add_payment_to_db(&expected_payment_info)).unwrap(); @@ -1328,9 +1350,9 @@ mod tests { // Test update_payment_to_received_in_db let expected_preimage = PaymentPreimage([5; 32]); - block_on(db.update_payment_to_received_in_db(PaymentHash([1; 32]), expected_preimage)).unwrap(); + block_on(db.update_payment_to_claimable_in_db(PaymentHash([1; 32]), expected_preimage)).unwrap(); let payment_after_update = block_on(db.get_payment_from_db(PaymentHash([1; 32]))).unwrap().unwrap(); - assert_eq!(payment_after_update.status, HTLCStatus::Received); + assert_eq!(payment_after_update.status, HTLCStatus::Claimable); assert_eq!(payment_after_update.preimage.unwrap(), expected_preimage); // Test update_payment_to_sent_in_db @@ -1416,7 +1438,7 @@ mod tests { let result = block_on(db.get_payments_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); let expected_payments_vec: Vec = payments .iter() - .map(|p| p.clone()) + .cloned() .filter(|p| p.payment_type == PaymentType::InboundPayment) .collect(); let expected_payments = if expected_payments_vec.len() > 10 { @@ -1432,7 +1454,7 @@ mod tests { let result = block_on(db.get_payments_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); let expected_payments_vec: Vec = expected_payments_vec .iter() - .map(|p| p.clone()) + .cloned() .filter(|p| p.status == HTLCStatus::Succeeded) .collect(); let expected_payments = if expected_payments_vec.len() > 10 { @@ -1453,13 +1475,13 @@ mod tests { let result = block_on(db.get_payments_by_filter(Some(filter), paging, limit)).unwrap(); let expected_payments_vec: Vec = payments .iter() - .map(|p| p.clone()) - .filter(|p| p.description.contains(&substr)) + .cloned() + .filter(|p| p.description.contains(substr)) .collect(); let expected_payments = if expected_payments_vec.len() > 10 { expected_payments_vec[..10].to_vec() } else { - expected_payments_vec.clone() + expected_payments_vec }; let actual_payments = result.payments; @@ -1480,14 +1502,14 @@ mod tests { for channel in channels { block_on(db.add_channel_to_db(&channel)).unwrap(); block_on(db.add_funding_tx_to_db( - channel.rpc_id, + channel.uuid, channel.funding_tx.unwrap(), channel.funding_value.unwrap(), channel.funding_generated_in_block.unwrap(), )) .unwrap(); - block_on(db.update_channel_to_closed(channel.rpc_id, channel.closure_reason.unwrap(), 1655806080)).unwrap(); - block_on(db.add_closing_tx_to_db(channel.rpc_id, channel.closing_tx.clone().unwrap())).unwrap(); + block_on(db.update_channel_to_closed(channel.uuid, channel.closure_reason.unwrap(), 1655806080)).unwrap(); + block_on(db.add_closing_tx_to_db(channel.uuid, channel.closing_tx.clone().unwrap())).unwrap(); block_on(db.add_claiming_tx_to_db( channel.closing_tx.unwrap(), channel.claiming_tx.unwrap(), @@ -1526,17 +1548,6 @@ mod tests { assert_eq!(100, result.total); assert_eq!(expected_channels, actual_channels); - let from_rpc_id = 20; - let paging = PagingOptionsEnum::FromId(from_rpc_id); - let limit = 3; - - let result = block_on(db.get_closed_channels_by_filter(None, paging, limit)).unwrap(); - - let expected_channels = channels[20..23].to_vec(); - let actual_channels = result.channels; - - assert_eq!(expected_channels, actual_channels); - let mut filter = ClosedChannelsFilter { channel_id: None, counterparty_node_id: None, @@ -1555,11 +1566,8 @@ mod tests { let limit = 10; let result = block_on(db.get_closed_channels_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); - let expected_channels_vec: Vec = channels - .iter() - .map(|chan| chan.clone()) - .filter(|chan| chan.is_outbound) - .collect(); + let expected_channels_vec: Vec = + channels.iter().cloned().filter(|chan| chan.is_outbound).collect(); let expected_channels = if expected_channels_vec.len() > 10 { expected_channels_vec[..10].to_vec() } else { @@ -1573,7 +1581,7 @@ mod tests { let result = block_on(db.get_closed_channels_by_filter(Some(filter.clone()), paging.clone(), limit)).unwrap(); let expected_channels_vec: Vec = expected_channels_vec .iter() - .map(|chan| chan.clone()) + .cloned() .filter(|chan| chan.is_public) .collect(); let expected_channels = if expected_channels_vec.len() > 10 { @@ -1592,13 +1600,13 @@ mod tests { let result = block_on(db.get_closed_channels_by_filter(Some(filter), paging, limit)).unwrap(); let expected_channels_vec: Vec = channels .iter() - .map(|chan| chan.clone()) + .cloned() .filter(|chan| chan.channel_id == channel_id) .collect(); let expected_channels = if expected_channels_vec.len() > 10 { expected_channels_vec[..10].to_vec() } else { - expected_channels_vec.clone() + expected_channels_vec }; let actual_channels = result.channels; diff --git a/mm2src/coins/lightning/ln_storage.rs b/mm2src/coins/lightning/ln_storage.rs index 610720bc3f..9ece4d2e8f 100644 --- a/mm2src/coins/lightning/ln_storage.rs +++ b/mm2src/coins/lightning/ln_storage.rs @@ -4,7 +4,7 @@ use common::log::LogState; use lightning::routing::gossip; use lightning::routing::scoring::ProbabilisticScorer; use parking_lot::Mutex as PaMutex; -use secp256k1v22::PublicKey; +use secp256k1v24::PublicKey; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index eeb7e04203..8ab92629e7 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -9,15 +9,15 @@ use bitcoin_hashes::{sha256d, Hash}; use common::executor::SpawnFuture; use common::log::LogState; use lightning::chain::keysinterface::{InMemorySigner, KeysManager}; -use lightning::chain::{chainmonitor, BestBlock, Watch}; +use lightning::chain::{chainmonitor, BestBlock, ChannelMonitorUpdateStatus, Watch}; use lightning::ln::channelmanager::{ChainParameters, ChannelManagerReadArgs, PaymentId, PaymentSendFailure, SimpleArcChannelManager}; use lightning::routing::gossip::RoutingFees; -use lightning::routing::router::{PaymentParameters, RouteHint, RouteHintHop, RouteParameters}; +use lightning::routing::router::{PaymentParameters, RouteHint, RouteHintHop, RouteParameters, Router as RouterTrait}; use lightning::util::config::UserConfig; use lightning::util::errors::APIError; use lightning::util::ser::ReadableArgs; -use lightning_invoice::payment::{Payer, PaymentError as InvoicePaymentError, Router as RouterTrait}; +use lightning_invoice::payment::{Payer, PaymentError as InvoicePaymentError}; use mm2_core::mm_ctx::MmArc; use std::collections::hash_map::Entry; use std::fs::File; @@ -36,7 +36,7 @@ pub type ChainMonitor = chainmonitor::ChainMonitor< >; pub type ChannelManager = SimpleArcChannelManager; -pub type Router = DefaultRouter, Arc>; +pub type Router = DefaultRouter, Arc, Arc>; #[inline] fn ln_data_dir(ctx: &MmArc, ticker: &str) -> PathBuf { ctx.dbdir().join("LIGHTNING").join(ticker) } @@ -203,12 +203,15 @@ pub async fn init_channel_manager( for (_, channel_monitor) in channelmonitors.into_iter() { let funding_outpoint = channel_monitor.get_funding_txo().0; let chain_monitor = chain_monitor.clone(); - async_blocking(move || { - chain_monitor - .watch_channel(funding_outpoint, channel_monitor) - .map_to_mm(|e| EnableLightningError::IOError(format!("{:?}", e))) - }) - .await?; + if let ChannelMonitorUpdateStatus::PermanentFailure = + async_blocking(move || chain_monitor.watch_channel(funding_outpoint, channel_monitor)).await + { + let channel_id = hex::encode(funding_outpoint.to_channel_id()); + return MmError::err(EnableLightningError::IOError(format!( + "Failure to persist channel: {}!", + channel_id + ))); + } } channel_manager } else { @@ -347,7 +350,6 @@ impl From for PaymentError { pub(crate) fn pay_invoice_with_max_total_cltv_expiry_delta( channel_manager: Arc, router: Arc, - scorer: Arc, invoice: &Invoice, max_total_cltv_expiry_delta: u32, ) -> Result { @@ -370,21 +372,12 @@ pub(crate) fn pay_invoice_with_max_total_cltv_expiry_delta( final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32, }; - pay_internal( - channel_manager, - router, - scorer, - &route_params, - invoice, - &mut 0, - &mut Vec::new(), - ) + pay_internal(channel_manager, router, &route_params, invoice, &mut 0, &mut Vec::new()) } fn pay_internal( channel_manager: Arc, router: Arc, - scorer: Arc, params: &RouteParameters, invoice: &Invoice, attempts: &mut usize, @@ -392,34 +385,36 @@ fn pay_internal( ) -> Result { let payer = channel_manager.node_id(); let first_hops = channel_manager.first_hops(); - let payment_hash = PaymentHash((*invoice.payment_hash()).into_inner()); - // Todo: Routes should be checked before order matching also, this might require routing hints to be shared when matching orders. Just-in-time channels can solve this issue as well. + let payment_hash_inner = invoice.payment_hash().into_inner(); + let payment_hash = PaymentHash(payment_hash_inner); + let payment_id = PaymentId(payment_hash_inner); + // Todo: Would be better to implement pay_invoice_with_max_total_cltv_expiry_delta in rust-lightning + let inflight_htlcs = channel_manager.compute_inflight_htlcs(); let route = router .find_route( &payer, params, - &payment_hash, Some(&first_hops.iter().collect::>()), - &scorer.lock().unwrap(), + inflight_htlcs, ) .map_err(InvoicePaymentError::Routing)?; let payment_secret = Some(*invoice.payment_secret()); - match channel_manager.send_payment(&route, payment_hash, &payment_secret) { - Ok(payment_id) => Ok(payment_id), + match channel_manager.send_payment(&route, payment_hash, &payment_secret, payment_id) { + Ok(()) => Ok(payment_id), Err(e) => match e { PaymentSendFailure::ParameterError(_) => Err(e), PaymentSendFailure::PathParameterError(_) => Err(e), - PaymentSendFailure::AllFailedRetrySafe(err) => { + PaymentSendFailure::DuplicatePayment => Err(e), + PaymentSendFailure::AllFailedResendSafe(err) => { if *attempts > PAYMENT_RETRY_ATTEMPTS { - Err(PaymentSendFailure::AllFailedRetrySafe(errors.to_vec())) + Err(PaymentSendFailure::AllFailedResendSafe(errors.to_vec())) } else { *attempts += 1; errors.extend(err); Ok(pay_internal( channel_manager, router, - scorer, params, invoice, attempts, @@ -437,16 +432,7 @@ fn pay_internal( // recipient may misbehave and claim the funds, at which point we have to // consider the payment sent, so return `Ok()` here, ignoring any retry // errors. - let _ = retry_payment( - channel_manager, - router, - scorer, - payment_id, - payment_hash, - &retry_data, - &mut 0, - errors, - ); + let _ = retry_payment(channel_manager, router, payment_id, &retry_data, &mut 0, errors); Ok(payment_id) } else { // This may happen if we send a payment and some paths fail, but @@ -465,59 +451,39 @@ fn pay_internal( fn retry_payment( channel_manager: Arc, router: Arc, - scorer: Arc, payment_id: PaymentId, - payment_hash: PaymentHash, params: &RouteParameters, attempts: &mut usize, errors: &mut Vec, ) -> Result<(), PaymentError> { let payer = channel_manager.node_id(); let first_hops = channel_manager.first_hops(); + let inflight_htlcs = channel_manager.compute_inflight_htlcs(); let route = router .find_route( &payer, params, - &payment_hash, Some(&first_hops.iter().collect::>()), - &scorer.lock().unwrap(), + inflight_htlcs, ) .map_err(InvoicePaymentError::Routing)?; match channel_manager.retry_payment(&route, payment_id) { Ok(()) => Ok(()), - Err(PaymentSendFailure::AllFailedRetrySafe(err)) => { + Err(PaymentSendFailure::AllFailedResendSafe(err)) => { if *attempts > PAYMENT_RETRY_ATTEMPTS { - let e = PaymentSendFailure::AllFailedRetrySafe(errors.to_vec()); + let e = PaymentSendFailure::AllFailedResendSafe(errors.to_vec()); Err(InvoicePaymentError::Sending(e).into()) } else { *attempts += 1; errors.extend(err); - retry_payment( - channel_manager, - router, - scorer, - payment_id, - payment_hash, - params, - attempts, - errors, - ) + retry_payment(channel_manager, router, payment_id, params, attempts, errors) } }, Err(PaymentSendFailure::PartialFailure { failed_paths_retry, .. }) => { if let Some(retry) = failed_paths_retry { // Always return Ok for the same reason as noted in pay_internal. - let _ = retry_payment( - channel_manager, - router, - scorer, - payment_id, - payment_hash, - &retry, - attempts, - errors, - ); + let _ = retry_payment(channel_manager, router, payment_id, &retry, attempts, errors); } Ok(()) }, diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 265b3e69ea..78118f54b7 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -19,6 +19,13 @@ // marketmaker // +// `mockable` implementation uses these +#![allow( + clippy::forget_ref, + clippy::forget_copy, + clippy::swap_ptr_to_ref, + clippy::forget_non_drop +)] #![allow(uncommon_codepoints)] #![feature(integer_atomics)] #![feature(async_closure)] @@ -45,6 +52,7 @@ use crypto::{Bip32Error, CryptoCtx, CryptoCtxError, DerivationPath, GlobalHDAcco Secp256k1Secret, WithHwRpcError}; use derive_more::Display; use enum_from::{EnumFromStringify, EnumFromTrait}; +use ethereum_types::H256; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; @@ -200,7 +208,10 @@ use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut}; pub mod coins_tests; pub mod eth; -use eth::{eth_coin_from_conf_and_request, EthCoin, EthTxFeeDetails, SignedEthTx}; +#[cfg(feature = "enable-nft-integration")] +use eth::GetValidEthWithdrawAddError; +use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetailsErr, EthTxFeeDetails, + GetEthAddressError, SignedEthTx}; pub mod hd_confirm_address; pub mod hd_pubkey; @@ -236,11 +247,26 @@ pub mod tx_history_storage; #[doc(hidden)] #[allow(unused_variables)] -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] pub mod solana; -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] pub use solana::spl::SplToken; -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] pub use solana::{SolanaActivationParams, SolanaCoin, SolanaFeeDetails}; pub mod utxo; @@ -255,6 +281,10 @@ use utxo::utxo_standard::{utxo_standard_coin_with_policy, UtxoStandardCoin}; use utxo::UtxoActivationParams; use utxo::{BlockchainNetwork, GenerateTxError, UtxoFeeDetails, UtxoTx}; +#[cfg(feature = "enable-nft-integration")] pub mod nft; +#[cfg(feature = "enable-nft-integration")] +use nft::nft_errors::GetNftInfoError; + #[cfg(not(target_arch = "wasm32"))] pub mod z_coin; #[cfg(not(target_arch = "wasm32"))] use z_coin::ZCoin; @@ -278,13 +308,6 @@ pub type RawTransactionResult = Result = Box> + Send + 'a>; pub type RefundResult = Result>; -pub type SendMakerPaymentArgs<'a> = SendSwapPaymentArgs<'a>; -pub type SendTakerPaymentArgs<'a> = SendSwapPaymentArgs<'a>; -pub type SendMakerSpendsTakerPaymentArgs<'a> = SendSpendPaymentArgs<'a>; -pub type SendTakerSpendsMakerPaymentArgs<'a> = SendSpendPaymentArgs<'a>; -pub type SendTakerRefundsPaymentArgs<'a> = SendRefundPaymentArgs<'a>; -pub type SendMakerRefundsPaymentArgs<'a> = SendRefundPaymentArgs<'a>; -pub type SendWatcherRefundsPaymentArgs<'a> = SendRefundPaymentArgs<'a>; pub type IguanaPrivKey = Secp256k1Secret; @@ -294,6 +317,11 @@ pub const EARLY_CONFIRMATION_ERR_LOG: &str = "Early confirmation"; pub const OLD_TRANSACTION_ERR_LOG: &str = "Old transaction"; pub const INVALID_RECEIVER_ERR_LOG: &str = "Invalid receiver"; pub const INVALID_CONTRACT_ADDRESS_ERR_LOG: &str = "Invalid contract address"; +pub const INVALID_PAYMENT_STATE_ERR_LOG: &str = "Invalid payment state"; +pub const INVALID_SWAP_ID_ERR_LOG: &str = "Invalid swap id"; +pub const INVALID_SCRIPT_ERR_LOG: &str = "Invalid script"; +pub const INVALID_REFUND_TX_ERR_LOG: &str = "Invalid refund transaction"; +pub const INSUFFICIENT_WATCHER_REWARD_ERR_LOG: &str = "Insufficient watcher reward"; #[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -331,6 +359,38 @@ impl From for RawTransactionError { } } +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetMyAddressError { + CoinsConfCheckError(String), + CoinIsNotSupported(String), + #[from_stringify("CryptoCtxError")] + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[from_stringify("serde_json::Error")] + #[display(fmt = "Invalid request error error: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Get Eth address error: {}", _0)] + GetEthAddressError(GetEthAddressError), +} + +impl From for GetMyAddressError { + fn from(e: GetEthAddressError) -> Self { GetMyAddressError::GetEthAddressError(e) } +} + +impl HttpStatusCode for GetMyAddressError { + fn status_code(&self) -> StatusCode { + match self { + GetMyAddressError::CoinsConfCheckError(_) + | GetMyAddressError::CoinIsNotSupported(_) + | GetMyAddressError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + GetMyAddressError::Internal(_) | GetMyAddressError::GetEthAddressError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} + #[derive(Deserialize)] pub struct RawTransactionRequest { pub coin: String, @@ -343,6 +403,17 @@ pub struct RawTransactionRes { pub tx_hex: BytesJson, } +#[derive(Debug, Deserialize)] +pub struct MyAddressReq { + coin: String, +} + +#[derive(Debug, Serialize)] +pub struct MyWalletAddress { + coin: String, + wallet_address: String, +} + pub type SignatureResult = Result>; pub type VerificationResult = Result>; @@ -361,7 +432,7 @@ pub enum TxHistoryError { InternalError(String), } -#[derive(Clone, Debug, Display)] +#[derive(Clone, Debug, Deserialize, Display, PartialEq)] pub enum PrivKeyPolicyNotAllowed { #[display(fmt = "Hardware Wallet is not supported")] HardwareWalletNotSupported, @@ -499,6 +570,15 @@ pub enum ValidateOtherPubKeyErr { InvalidPubKey(String), } +#[derive(Clone, Debug)] +pub struct ConfirmPaymentInput { + pub payment_tx: Vec, + pub confirmations: u64, + pub requires_nota: bool, + pub wait_until: u64, + pub check_every: u64, +} + #[derive(Clone, Debug)] pub struct WatcherValidateTakerFeeInput { pub taker_fee_hash: Vec, @@ -518,6 +598,7 @@ pub struct WatcherValidatePaymentInput { pub secret_hash: Vec, pub try_spv_proof_until: u64, pub confirmations: u64, + pub min_watcher_reward: Option, } #[derive(Clone, Debug)] @@ -532,6 +613,7 @@ pub struct ValidatePaymentInput { pub try_spv_proof_until: u64, pub confirmations: u64, pub unique_swap_data: Vec, + pub min_watcher_reward: Option, } #[derive(Clone, Debug)] @@ -542,6 +624,7 @@ pub struct WatcherSearchForSwapTxSpendInput<'a> { pub secret_hash: &'a [u8], pub tx: &'a [u8], pub search_from_block: u64, + pub watcher_reward: bool, } #[derive(Clone, Debug)] @@ -550,6 +633,7 @@ pub struct SendMakerPaymentSpendPreimageInput<'a> { pub secret_hash: &'a [u8], pub secret: &'a [u8], pub taker_pub: &'a [u8], + pub watcher_reward: bool, } pub struct SearchForSwapTxSpendInput<'a> { @@ -560,10 +644,11 @@ pub struct SearchForSwapTxSpendInput<'a> { pub search_from_block: u64, pub swap_contract_address: &'a Option, pub swap_unique_data: &'a [u8], + pub watcher_reward: bool, } #[derive(Clone, Debug)] -pub struct SendSwapPaymentArgs<'a> { +pub struct SendPaymentArgs<'a> { pub time_lock_duration: u64, pub time_lock: u32, /// This is either: @@ -575,10 +660,12 @@ pub struct SendSwapPaymentArgs<'a> { pub swap_contract_address: &'a Option, pub swap_unique_data: &'a [u8], pub payment_instructions: &'a Option, + pub watcher_reward: Option, + pub wait_for_confirmation_until: u64, } #[derive(Clone, Debug)] -pub struct SendSpendPaymentArgs<'a> { +pub struct SpendPaymentArgs<'a> { /// This is either: /// * Taker's payment tx if this structure is used in [`SwapOps::send_maker_spends_taker_payment`]. /// * Maker's payment tx if this structure is used in [`SwapOps::send_taker_spends_maker_payment`]. @@ -592,10 +679,11 @@ pub struct SendSpendPaymentArgs<'a> { pub secret_hash: &'a [u8], pub swap_contract_address: &'a Option, pub swap_unique_data: &'a [u8], + pub watcher_reward: bool, } #[derive(Clone, Debug)] -pub struct SendRefundPaymentArgs<'a> { +pub struct RefundPaymentArgs<'a> { pub payment_tx: &'a [u8], pub time_lock: u32, /// This is either: @@ -605,6 +693,7 @@ pub struct SendRefundPaymentArgs<'a> { pub secret_hash: &'a [u8], pub swap_contract_address: &'a Option, pub swap_unique_data: &'a [u8], + pub watcher_reward: bool, } #[derive(Clone, Debug)] @@ -629,6 +718,15 @@ pub struct ValidateFeeArgs<'a> { pub uuid: &'a [u8], } +pub struct EthValidateFeeArgs<'a> { + pub fee_tx_hash: &'a H256, + pub expected_sender: &'a [u8], + pub fee_addr: &'a [u8], + pub amount: &'a BigDecimal, + pub min_block_number: u64, + pub uuid: &'a [u8], +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum PaymentInstructions { #[cfg(not(target_arch = "wasm32"))] @@ -669,28 +767,19 @@ pub enum RefundError { pub trait SwapOps { fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, uuid: &[u8]) -> TransactionFut; - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs<'_>) -> TransactionFut; + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionFut; - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs<'_>) -> TransactionFut; + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs<'_>) -> TransactionFut; - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs<'_>, - ) -> TransactionFut; + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut; - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs<'_>, - ) -> TransactionFut; + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut; - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs<'_>) - -> TransactionFut; + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut; - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs<'_>) - -> TransactionFut; + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut; - fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) - -> Box + Send>; + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()>; fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()>; @@ -711,7 +800,12 @@ pub trait SwapOps { input: SearchForSwapTxSpendInput<'_>, ) -> Result, String>; - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String>; + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, String>; fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result>; @@ -782,6 +876,9 @@ pub trait SwapOps { fn is_supported_by_watchers(&self) -> bool { false } + // Do we also need a method for the fallback contract? + fn contract_supports_watchers(&self) -> bool { true } + fn maker_locktime_multiplier(&self) -> f64 { 2.0 } } @@ -807,10 +904,7 @@ pub trait MakerSwapTakerCoin { pub trait WatcherOps { fn send_maker_payment_spend_preimage(&self, input: SendMakerPaymentSpendPreimageInput) -> TransactionFut; - fn send_taker_payment_refund_preimage( - &self, - watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut; + fn send_taker_payment_refund_preimage(&self, watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut; fn create_taker_payment_refund_preimage( &self, @@ -883,14 +977,7 @@ pub trait MarketCoinOps { /// Receives raw transaction bytes as input and returns tx hash in hexadecimal format fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send>; - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send>; + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send>; fn wait_for_htlc_tx_spend( &self, @@ -1085,7 +1172,12 @@ pub enum TxFeeDetails { Qrc20(Qrc20FeeDetails), Slp(SlpFeeDetails), Tendermint(TendermintFeeDetails), - #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] + #[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") + ))] Solana(SolanaFeeDetails), } @@ -1101,7 +1193,12 @@ impl<'de> Deserialize<'de> for TxFeeDetails { Utxo(UtxoFeeDetails), Eth(EthTxFeeDetails), Qrc20(Qrc20FeeDetails), - #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] + #[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") + ))] Solana(SolanaFeeDetails), Tendermint(TendermintFeeDetails), } @@ -1110,7 +1207,12 @@ impl<'de> Deserialize<'de> for TxFeeDetails { TxFeeDetailsUnTagged::Utxo(f) => Ok(TxFeeDetails::Utxo(f)), TxFeeDetailsUnTagged::Eth(f) => Ok(TxFeeDetails::Eth(f)), TxFeeDetailsUnTagged::Qrc20(f) => Ok(TxFeeDetails::Qrc20(f)), - #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] + #[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") + ))] TxFeeDetailsUnTagged::Solana(f) => Ok(TxFeeDetails::Solana(f)), TxFeeDetailsUnTagged::Tendermint(f) => Ok(TxFeeDetails::Tendermint(f)), } @@ -1129,7 +1231,12 @@ impl From for TxFeeDetails { fn from(qrc20_details: Qrc20FeeDetails) -> Self { TxFeeDetails::Qrc20(qrc20_details) } } -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] impl From for TxFeeDetails { fn from(solana_details: SolanaFeeDetails) -> Self { TxFeeDetails::Solana(solana_details) } } @@ -1165,6 +1272,7 @@ pub enum TransactionType { msg_type: CustomTendermintMsgType, token_id: Option, }, + NftTransfer, } /// Transaction details @@ -1667,7 +1775,7 @@ impl DelegationError { } } -#[derive(Clone, Debug, Display, EnumFromStringify, EnumFromTrait, Serialize, SerializeErrorType, PartialEq)] +#[derive(Clone, Debug, Display, EnumFromStringify, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum WithdrawError { #[display( @@ -1730,6 +1838,38 @@ pub enum WithdrawError { #[from_stringify("NumConversError", "UnexpectedDerivationMethod", "PrivKeyPolicyNotAllowed")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), + #[cfg(feature = "enable-nft-integration")] + #[display(fmt = "{} coin doesn't support NFT withdrawing", coin)] + CoinDoesntSupportNftWithdraw { coin: String }, + #[cfg(feature = "enable-nft-integration")] + #[display(fmt = "My address {} and from address {} mismatch", my_address, from)] + AddressMismatchError { my_address: String, from: String }, + #[cfg(feature = "enable-nft-integration")] + #[display(fmt = "Contract type {} doesnt support 'withdraw_nft' yet", _0)] + ContractTypeDoesntSupportNftWithdrawing(String), + #[display(fmt = "Action not allowed for coin: {}", _0)] + ActionNotAllowed(String), + #[cfg(feature = "enable-nft-integration")] + GetNftInfoError(GetNftInfoError), + #[cfg(feature = "enable-nft-integration")] + #[display( + fmt = "Not enough NFTs amount with token_address: {} and token_id {}. Available {}, required {}", + token_address, + token_id, + available, + required + )] + NotEnoughNftsAmount { + token_address: String, + token_id: String, + available: BigDecimal, + required: BigDecimal, + }, +} + +#[cfg(feature = "enable-nft-integration")] +impl From for WithdrawError { + fn from(e: GetNftInfoError) -> Self { WithdrawError::GetNftInfoError(e) } } impl HttpStatusCode for WithdrawError { @@ -1748,11 +1888,18 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::FromAddressNotFound | WithdrawError::UnexpectedFromAddress(_) | WithdrawError::UnknownAccount { .. } - | WithdrawError::UnexpectedUserAction { .. } => StatusCode::BAD_REQUEST, + | WithdrawError::UnexpectedUserAction { .. } + | WithdrawError::ActionNotAllowed(_) => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, WithdrawError::Transport(_) | WithdrawError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + #[cfg(feature = "enable-nft-integration")] + WithdrawError::GetNftInfoError(_) + | WithdrawError::AddressMismatchError { .. } + | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) + | WithdrawError::CoinDoesntSupportNftWithdraw { .. } + | WithdrawError::NotEnoughNftsAmount { .. } => StatusCode::BAD_REQUEST, } } } @@ -1787,6 +1934,31 @@ impl From for WithdrawError { fn from(e: TimeoutError) -> Self { WithdrawError::Timeout(e.duration) } } +#[cfg(feature = "enable-nft-integration")] +impl From for WithdrawError { + fn from(e: GetValidEthWithdrawAddError) -> Self { + match e { + GetValidEthWithdrawAddError::AddressMismatchError { my_address, from } => { + WithdrawError::AddressMismatchError { my_address, from } + }, + GetValidEthWithdrawAddError::CoinDoesntSupportNftWithdraw { coin } => { + WithdrawError::CoinDoesntSupportNftWithdraw { coin } + }, + GetValidEthWithdrawAddError::InvalidAddress(e) => WithdrawError::InvalidAddress(e), + } + } +} + +impl From for WithdrawError { + fn from(e: EthGasDetailsErr) -> Self { + match e { + EthGasDetailsErr::InvalidFeePolicy(e) => WithdrawError::InvalidFeePolicy(e), + EthGasDetailsErr::Internal(e) => WithdrawError::InternalError(e), + EthGasDetailsErr::Transport(e) => WithdrawError::Transport(e), + } + } +} + impl WithdrawError { /// Construct [`WithdrawError`] from [`GenerateTxError`] using additional `coin` and `decimals`. pub fn from_generate_tx_error(gen_tx_err: GenerateTxError, coin: String, decimals: u8) -> WithdrawError { @@ -1831,7 +2003,7 @@ impl WithdrawError { } } -#[derive(Serialize, Display, Debug, EnumFromStringify, SerializeErrorType)] +#[derive(Debug, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum SignatureError { #[display(fmt = "Invalid request: {}", _0)] @@ -1856,7 +2028,7 @@ impl HttpStatusCode for SignatureError { } } -#[derive(Serialize, Display, Debug, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum VerificationError { #[display(fmt = "Invalid request: {}", _0)] @@ -2048,11 +2220,18 @@ pub trait MmCoin: /// The minimum number of confirmations at which a transaction is considered mature. fn mature_confirmations(&self) -> Option; - /// Get some of the coin config info in serialized format for p2p messaging. - fn coin_protocol_info(&self) -> Vec; + /// Get some of the coin protocol related info in serialized format for p2p messaging. + fn coin_protocol_info(&self, amount_to_receive: Option) -> Vec; /// Check if serialized coin protocol info is supported by current version. - fn is_coin_protocol_supported(&self, info: &Option>) -> bool; + /// Can also be used to check if orders can be matched or not. + fn is_coin_protocol_supported( + &self, + info: &Option>, + amount_to_send: Option, + locktime: u64, + is_maker: bool, + ) -> bool; /// Abort all coin related futures on coin deactivation. fn on_disabled(&self) -> Result<(), AbortedError>; @@ -2111,9 +2290,19 @@ pub enum MmCoinEnum { SlpToken(SlpToken), Tendermint(TendermintCoin), TendermintToken(TendermintToken), - #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] + #[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") + ))] SolanaCoin(SolanaCoin), - #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] + #[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") + ))] SplToken(SplToken), #[cfg(not(target_arch = "wasm32"))] LightningCoin(LightningCoin), @@ -2132,12 +2321,22 @@ impl From for MmCoinEnum { fn from(c: TestCoin) -> MmCoinEnum { MmCoinEnum::Test(c) } } -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] impl From for MmCoinEnum { fn from(c: SolanaCoin) -> MmCoinEnum { MmCoinEnum::SolanaCoin(c) } } -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] impl From for MmCoinEnum { fn from(c: SplToken) -> MmCoinEnum { MmCoinEnum::SplToken(c) } } @@ -2194,9 +2393,19 @@ impl Deref for MmCoinEnum { #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::ZCoin(ref c) => c, MmCoinEnum::Test(ref c) => c, - #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] + #[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") + ))] MmCoinEnum::SolanaCoin(ref c) => c, - #[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] + #[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") + ))] MmCoinEnum::SplToken(ref c) => c, } } @@ -2215,6 +2424,8 @@ impl MmCoinEnum { _ => false, } } + + pub fn is_eth(&self) -> bool { matches!(self, MmCoinEnum::EthCoin(_)) } } #[async_trait] @@ -2478,7 +2689,7 @@ pub trait CoinWithDerivationMethod { } #[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "protocol_data")] pub enum CoinProtocol { UTXO, @@ -2509,9 +2720,9 @@ pub enum CoinProtocol { network: BlockchainNetwork, confirmation_targets: PlatformCoinConfirmationTargets, }, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] SOLANA, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] SPLTOKEN { platform: String, token_contract_address: String, @@ -2698,34 +2909,11 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result Result return ERR!("ZHTLC protocol is not supported by lp_coininit"), #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { .. } => return ERR!("Lightning protocol is not supported by lp_coininit"), - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] CoinProtocol::SOLANA => { return ERR!("Solana protocol is not supported by lp_coininit - use enable_solana_with_tokens instead") }, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] CoinProtocol::SPLTOKEN { .. } => { return ERR!("SplToken protocol is not supported by lp_coininit - use enable_spl instead") }, @@ -3367,7 +3555,7 @@ pub fn address_by_coin_conf_and_pubkey_str( CoinProtocol::LIGHTNING { .. } => { ERR!("address_by_coin_conf_and_pubkey_str is not implemented for lightning protocol yet!") }, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] CoinProtocol::SOLANA | CoinProtocol::SPLTOKEN { .. } => { ERR!("Solana pubkey is the public address - you do not need to use this rpc call.") }, @@ -3596,3 +3784,59 @@ pub trait RpcCommonOps { /// Returns an alive RPC client or returns an error if no RPC endpoint is currently available. async fn get_live_client(&self) -> Result; } + +/// `get_my_address` function returns wallet address for necessary coin without its activation. +/// Currently supports only coins with `ETH` protocol type. +pub async fn get_my_address(ctx: MmArc, req: MyAddressReq) -> MmResult { + let ticker = req.coin.as_str(); + let coins_en = coin_conf(&ctx, ticker); + coins_conf_check(&ctx, &coins_en, ticker, None).map_to_mm(GetMyAddressError::CoinsConfCheckError)?; + + let protocol: CoinProtocol = json::from_value(coins_en["protocol"].clone())?; + + let my_address = match protocol { + CoinProtocol::ETH => get_eth_address(&ctx, ticker).await?, + _ => { + return MmError::err(GetMyAddressError::CoinIsNotSupported(format!( + "{} doesn't support get_my_address", + req.coin + ))); + }, + }; + + Ok(my_address) +} + +fn coins_conf_check(ctx: &MmArc, coins_en: &Json, ticker: &str, req: Option<&Json>) -> Result<(), String> { + if coins_en.is_null() { + let warning = format!( + "Warning, coin {} is used without a corresponding configuration.", + ticker + ); + ctx.log.log( + "😅", + #[allow(clippy::unnecessary_cast)] + &[&("coin" as &str), &ticker, &("no-conf" as &str)], + &warning, + ); + } + + if let Some(req) = req { + if coins_en["mm2"].is_null() && req["mm2"].is_null() { + return ERR!(concat!( + "mm2 param is not set neither in coins config nor enable request, assuming that coin is not supported" + )); + } + } else if coins_en["mm2"].is_null() { + return ERR!(concat!( + "mm2 param is not set in coins config, assuming that coin is not supported" + )); + } + + if coins_en["protocol"].is_null() { + return ERR!( + r#""protocol" field is missing in coins file. The file format is deprecated, please execute ./mm2 update_config command to convert it or download a new one"# + ); + } + Ok(()) +} diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index 971ab7b19e..1635be9e80 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -233,7 +233,8 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T TransactionType::StakingDelegation | TransactionType::RemoveDelegation | TransactionType::FeeForTokenTx - | TransactionType::StandardTransfer => tx_hash.clone(), + | TransactionType::StandardTransfer + | TransactionType::NftTransfer => tx_hash.clone(), }; TransactionDetails { diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs new file mode 100644 index 0000000000..2b768738bd --- /dev/null +++ b/mm2src/coins/nft.rs @@ -0,0 +1,287 @@ +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::{MmError, MmResult}; + +pub(crate) mod nft_errors; +pub(crate) mod nft_structs; + +use crate::WithdrawError; +use nft_errors::GetNftInfoError; +use nft_structs::{Chain, ConvertChain, Nft, NftList, NftListReq, NftMetadataReq, NftTransferHistory, + NftTransferHistoryWrapper, NftTransfersReq, NftWrapper, NftsTransferHistoryList, + TransactionNftDetails, WithdrawNftReq}; + +use crate::eth::{get_eth_address, withdraw_erc1155, withdraw_erc721}; +use common::{APPLICATION_JSON, X_API_KEY}; +use http::header::ACCEPT; +use mm2_number::BigDecimal; +use serde_json::Value as Json; + +/// url for moralis requests +const URL_MORALIS: &str = "https://deep-index.moralis.io/api/v2/"; +/// query parameter for moralis request: The format of the token ID +const FORMAT_DECIMAL_MORALIS: &str = "format=decimal"; +/// query parameter for moralis request: The transfer direction +const DIRECTION_BOTH_MORALIS: &str = "direction=both"; + +pub type WithdrawNftResult = Result>; + +/// `get_nft_list` function returns list of NFTs on requested chains owned by user. +pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult { + let api_key = ctx.conf["api_key"] + .as_str() + .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; + + let mut res_list = Vec::new(); + + for chain in req.chains { + let (coin_str, chain_str) = chain.to_ticker_chain(); + let my_address = get_eth_address(&ctx, &coin_str).await?; + let uri_without_cursor = format!( + "{}{}/nft?chain={}&{}", + URL_MORALIS, my_address.wallet_address, chain_str, FORMAT_DECIMAL_MORALIS + ); + + // The cursor returned in the previous response (used for getting the next page). + let mut cursor = String::new(); + loop { + let uri = format!("{}{}", uri_without_cursor, cursor); + let response = send_moralis_request(uri.as_str(), api_key).await?; + if let Some(nfts_list) = response["result"].as_array() { + for nft_json in nfts_list { + let nft_wrapper: NftWrapper = serde_json::from_str(&nft_json.to_string())?; + let nft = Nft { + chain, + token_address: nft_wrapper.token_address, + token_id: nft_wrapper.token_id.0, + amount: nft_wrapper.amount.0, + owner_of: nft_wrapper.owner_of, + token_hash: nft_wrapper.token_hash, + block_number_minted: *nft_wrapper.block_number_minted, + block_number: *nft_wrapper.block_number, + contract_type: nft_wrapper.contract_type.map(|v| v.0), + name: nft_wrapper.name, + symbol: nft_wrapper.symbol, + token_uri: nft_wrapper.token_uri, + metadata: nft_wrapper.metadata, + last_token_uri_sync: nft_wrapper.last_token_uri_sync, + last_metadata_sync: nft_wrapper.last_metadata_sync, + minter_address: nft_wrapper.minter_address, + }; + // collect NFTs from the page + res_list.push(nft); + } + // if cursor is not null, there are other NFTs on next page, + // and we need to send new request with cursor to get info from the next page. + if let Some(cursor_res) = response["cursor"].as_str() { + cursor = format!("{}{}", "&cursor=", cursor_res); + continue; + } else { + break; + } + } + } + } + drop_mutability!(res_list); + let nft_list = NftList { nfts: res_list }; + Ok(nft_list) +} + +/// `get_nft_metadata` function returns info of one specific NFT. +/// Current implementation sends request to Moralis. +/// Later, after adding caching, metadata lookup can be performed using previously obtained NFTs info without +/// sending new moralis request. The moralis request can be sent as a fallback, if the data was not found in the cache. +/// +/// **Caution:** ERC-1155 token can have a total supply more than 1, which means there could be several owners +/// of the same token. `get_nft_metadata` returns NFTs info with the most recent owner. +/// **Dont** use this function to get specific info about owner address, amount etc, you will get info not related to my_address. +pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult { + let api_key = ctx.conf["api_key"] + .as_str() + .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; + let chain_str = match req.chain { + Chain::Avalanche => "avalanche", + Chain::Bsc => "bsc", + Chain::Eth => "eth", + Chain::Fantom => "fantom", + Chain::Polygon => "polygon", + }; + let uri = format!( + "{}nft/{}/{}?chain={}&{}", + URL_MORALIS, req.token_address, req.token_id, chain_str, FORMAT_DECIMAL_MORALIS + ); + let response = send_moralis_request(uri.as_str(), api_key).await?; + let nft_wrapper: NftWrapper = serde_json::from_str(&response.to_string())?; + let nft_metadata = Nft { + chain: req.chain, + token_address: nft_wrapper.token_address, + token_id: nft_wrapper.token_id.0, + amount: nft_wrapper.amount.0, + owner_of: nft_wrapper.owner_of, + token_hash: nft_wrapper.token_hash, + block_number_minted: *nft_wrapper.block_number_minted, + block_number: *nft_wrapper.block_number, + contract_type: nft_wrapper.contract_type.map(|v| v.0), + name: nft_wrapper.name, + symbol: nft_wrapper.symbol, + token_uri: nft_wrapper.token_uri, + metadata: nft_wrapper.metadata, + last_token_uri_sync: nft_wrapper.last_token_uri_sync, + last_metadata_sync: nft_wrapper.last_metadata_sync, + minter_address: nft_wrapper.minter_address, + }; + Ok(nft_metadata) +} + +/// `get_nft_transfers` function returns a transfer history of NFTs on requested chains owned by user. +/// Currently doesnt support filters. +pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult { + let api_key = ctx.conf["api_key"] + .as_str() + .ok_or_else(|| MmError::new(GetNftInfoError::ApiKeyError))?; + + let mut res_list = Vec::new(); + + for chain in req.chains { + let (coin_str, chain_str) = match chain { + Chain::Avalanche => ("AVAX", "avalanche"), + Chain::Bsc => ("BNB", "bsc"), + Chain::Eth => ("ETH", "eth"), + Chain::Fantom => ("FTM", "fantom"), + Chain::Polygon => ("MATIC", "polygon"), + }; + let my_address = get_eth_address(&ctx, coin_str).await?; + let uri_without_cursor = format!( + "{}{}/nft/transfers?chain={}&{}&{}", + URL_MORALIS, my_address.wallet_address, chain_str, FORMAT_DECIMAL_MORALIS, DIRECTION_BOTH_MORALIS + ); + + // The cursor returned in the previous response (used for getting the next page). + let mut cursor = String::new(); + loop { + let uri = format!("{}{}", uri_without_cursor, cursor); + let response = send_moralis_request(uri.as_str(), api_key).await?; + if let Some(transfer_list) = response["result"].as_array() { + for transfer in transfer_list { + let transfer_wrapper: NftTransferHistoryWrapper = serde_json::from_str(&transfer.to_string())?; + let transfer_history = NftTransferHistory { + chain, + block_number: *transfer_wrapper.block_number, + block_timestamp: transfer_wrapper.block_timestamp, + block_hash: transfer_wrapper.block_hash, + transaction_hash: transfer_wrapper.transaction_hash, + transaction_index: transfer_wrapper.transaction_index, + log_index: transfer_wrapper.log_index, + value: transfer_wrapper.value.0, + contract_type: transfer_wrapper.contract_type.0, + transaction_type: transfer_wrapper.transaction_type, + token_address: transfer_wrapper.token_address, + token_id: transfer_wrapper.token_id.0, + from_address: transfer_wrapper.from_address, + to_address: transfer_wrapper.to_address, + amount: transfer_wrapper.amount.0, + verified: transfer_wrapper.verified, + operator: transfer_wrapper.operator, + }; + // collect NFTs transfers from the page + res_list.push(transfer_history); + } + // if the cursor is not null, there are other NFTs transfers on next page, + // and we need to send new request with cursor to get info from the next page. + if let Some(cursor_res) = response["cursor"].as_str() { + cursor = format!("{}{}", "&cursor=", cursor_res); + continue; + } else { + break; + } + } + } + } + drop_mutability!(res_list); + let transfer_history_list = NftsTransferHistoryList { + transfer_history: res_list, + }; + Ok(transfer_history_list) +} + +/// `withdraw_nft` function generates, signs and returns a transaction that transfers NFT +/// from my address to recipient's address. +/// This method generates a raw transaction which should then be broadcast using `send_raw_transaction`. +pub async fn withdraw_nft(ctx: MmArc, req_type: WithdrawNftReq) -> WithdrawNftResult { + match req_type { + WithdrawNftReq::WithdrawErc1155(erc1155_req) => withdraw_erc1155(ctx, erc1155_req).await, + WithdrawNftReq::WithdrawErc721(erc721_req) => withdraw_erc721(ctx, erc721_req).await, + } +} + +#[cfg(not(target_arch = "wasm32"))] +async fn send_moralis_request(uri: &str, api_key: &str) -> MmResult { + use http::header::HeaderValue; + use mm2_net::transport::slurp_req_body; + + let request = http::Request::builder() + .method("GET") + .uri(uri) + .header(X_API_KEY, api_key) + .header(ACCEPT, HeaderValue::from_static(APPLICATION_JSON)) + .body(hyper::Body::from(""))?; + + let (status, _header, body) = slurp_req_body(request).await?; + if !status.is_success() { + return Err(MmError::new(GetNftInfoError::Transport(format!( + "Response !200 from {}: {}, {}", + uri, status, body + )))); + } + Ok(body) +} + +#[cfg(target_arch = "wasm32")] +async fn send_moralis_request(uri: &str, api_key: &str) -> MmResult { + use mm2_net::wasm_http::FetchRequest; + + macro_rules! try_or { + ($exp:expr, $errtype:ident) => { + match $exp { + Ok(x) => x, + Err(e) => return Err(MmError::new(GetNftInfoError::$errtype(ERRL!("{:?}", e)))), + } + }; + } + + let result = FetchRequest::get(uri) + .cors() + .body_utf8("".to_owned()) + .header(X_API_KEY, api_key) + .header(ACCEPT.as_str(), APPLICATION_JSON) + .request_str() + .await; + let (status_code, response_str) = try_or!(result, Transport); + if !status_code.is_success() { + return Err(MmError::new(GetNftInfoError::Transport(ERRL!( + "!200: {}, {}", + status_code, + response_str + )))); + } + + let response: Json = try_or!(serde_json::from_str(&response_str), InvalidResponse); + Ok(response) +} + +/// This function uses `get_nft_list` method to get the correct info about amount of specific NFT owned by my_address. +pub(crate) async fn find_wallet_amount( + ctx: MmArc, + nft_list: NftListReq, + token_address_req: String, + token_id_req: BigDecimal, +) -> MmResult { + let nft_list = get_nft_list(ctx, nft_list).await?.nfts; + let nft = nft_list + .into_iter() + .find(|nft| nft.token_address == token_address_req && nft.token_id == token_id_req) + .ok_or_else(|| GetNftInfoError::TokenNotFoundInWallet { + token_address: token_address_req, + token_id: token_id_req.to_string(), + })?; + Ok(nft.amount) +} diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs new file mode 100644 index 0000000000..bd510108ab --- /dev/null +++ b/mm2src/coins/nft/nft_errors.rs @@ -0,0 +1,78 @@ +use crate::eth::GetEthAddressError; +use common::HttpStatusCode; +use derive_more::Display; +use enum_from::EnumFromStringify; +use http::StatusCode; +use mm2_net::transport::SlurpError; +use serde::{Deserialize, Serialize}; +use web3::Error; + +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetNftInfoError { + /// `http::Error` can appear on an HTTP request [`http::Builder::build`] building. + #[from_stringify("http::Error")] + #[display(fmt = "Invalid request: {}", _0)] + InvalidRequest(String), + #[display(fmt = "Transport: {}", _0)] + Transport(String), + #[from_stringify("serde_json::Error")] + #[display(fmt = "Invalid response: {}", _0)] + InvalidResponse(String), + #[display(fmt = "Internal: {}", _0)] + Internal(String), + GetEthAddressError(GetEthAddressError), + #[display(fmt = "X-API-Key is missing")] + ApiKeyError, + #[display( + fmt = "Token: token_address {}, token_id {} was not find in wallet", + token_address, + token_id + )] + TokenNotFoundInWallet { + token_address: String, + token_id: String, + }, +} + +impl From for GetNftInfoError { + fn from(e: SlurpError) -> Self { + let error_str = e.to_string(); + match e { + SlurpError::ErrorDeserializing { .. } => GetNftInfoError::InvalidResponse(error_str), + SlurpError::Transport { .. } | SlurpError::Timeout { .. } => GetNftInfoError::Transport(error_str), + SlurpError::Internal(_) | SlurpError::InvalidRequest(_) => GetNftInfoError::Internal(error_str), + } + } +} + +impl From for GetNftInfoError { + fn from(e: Error) -> Self { + let error_str = e.to_string(); + match e { + web3::Error::InvalidResponse(_) | web3::Error::Decoder(_) | web3::Error::Rpc(_) => { + GetNftInfoError::InvalidResponse(error_str) + }, + web3::Error::Transport(_) | web3::Error::Io(_) => GetNftInfoError::Transport(error_str), + _ => GetNftInfoError::Internal(error_str), + } + } +} + +impl From for GetNftInfoError { + fn from(e: GetEthAddressError) -> Self { GetNftInfoError::GetEthAddressError(e) } +} + +impl HttpStatusCode for GetNftInfoError { + fn status_code(&self) -> StatusCode { + match self { + GetNftInfoError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + GetNftInfoError::InvalidResponse(_) => StatusCode::FAILED_DEPENDENCY, + GetNftInfoError::ApiKeyError => StatusCode::FORBIDDEN, + GetNftInfoError::Transport(_) + | GetNftInfoError::Internal(_) + | GetNftInfoError::GetEthAddressError(_) + | GetNftInfoError::TokenNotFoundInWallet { .. } => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs new file mode 100644 index 0000000000..09e247967c --- /dev/null +++ b/mm2src/coins/nft/nft_structs.rs @@ -0,0 +1,257 @@ +use crate::{TransactionType, TxFeeDetails, WithdrawFee}; +use mm2_number::BigDecimal; +use rpc::v1::types::Bytes as BytesJson; +use serde::Deserialize; +use std::str::FromStr; + +#[derive(Debug, Deserialize)] +pub struct NftListReq { + pub(crate) chains: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct NftMetadataReq { + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) chain: Chain, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub(crate) enum Chain { + Avalanche, + Bsc, + Eth, + Fantom, + Polygon, +} + +pub(crate) trait ConvertChain { + fn to_ticker(&self) -> String; + + fn to_ticker_chain(&self) -> (String, String); +} + +impl ConvertChain for Chain { + fn to_ticker(&self) -> String { + match self { + Chain::Avalanche => "AVAX".to_owned(), + Chain::Bsc => "BNB".to_owned(), + Chain::Eth => "ETH".to_owned(), + Chain::Fantom => "FTM".to_owned(), + Chain::Polygon => "MATIC".to_owned(), + } + } + + fn to_ticker_chain(&self) -> (String, String) { + match self { + Chain::Avalanche => ("AVAX".to_owned(), "avalanche".to_owned()), + Chain::Bsc => ("BNB".to_owned(), "bsc".to_owned()), + Chain::Eth => ("ETH".to_owned(), "eth".to_owned()), + Chain::Fantom => ("FTM".to_owned(), "fantom".to_owned()), + Chain::Polygon => ("MATIC".to_owned(), "polygon".to_owned()), + } + } +} + +#[derive(Debug, Display)] +pub(crate) enum ParseContractTypeError { + UnsupportedContractType, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub(crate) enum ContractType { + Erc1155, + Erc721, +} + +impl FromStr for ContractType { + type Err = ParseContractTypeError; + + #[inline] + fn from_str(s: &str) -> Result { + match s { + "ERC1155" => Ok(ContractType::Erc1155), + "ERC721" => Ok(ContractType::Erc721), + _ => Err(ParseContractTypeError::UnsupportedContractType), + } + } +} + +#[derive(Debug, Serialize)] +pub struct Nft { + pub(crate) chain: Chain, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) amount: BigDecimal, + pub(crate) owner_of: String, + pub(crate) token_hash: String, + pub(crate) block_number_minted: u64, + pub(crate) block_number: u64, + pub(crate) contract_type: Option, + pub(crate) name: Option, + pub(crate) symbol: Option, + pub(crate) token_uri: Option, + pub(crate) metadata: Option, + pub(crate) last_token_uri_sync: Option, + pub(crate) last_metadata_sync: Option, + pub(crate) minter_address: Option, +} + +/// This structure is for deserializing NFT json to struct. +/// Its needed to convert fields properly, because all fields in json have string type. +#[derive(Debug, Deserialize)] +pub(crate) struct NftWrapper { + pub(crate) token_address: String, + pub(crate) token_id: SerdeStringWrap, + pub(crate) amount: SerdeStringWrap, + pub(crate) owner_of: String, + pub(crate) token_hash: String, + pub(crate) block_number_minted: SerdeStringWrap, + pub(crate) block_number: SerdeStringWrap, + pub(crate) contract_type: Option>, + pub(crate) name: Option, + pub(crate) symbol: Option, + pub(crate) token_uri: Option, + pub(crate) metadata: Option, + pub(crate) last_token_uri_sync: Option, + pub(crate) last_metadata_sync: Option, + pub(crate) minter_address: Option, +} + +#[derive(Debug)] +pub(crate) struct SerdeStringWrap(pub(crate) T); + +impl<'de, T> Deserialize<'de> for SerdeStringWrap +where + T: std::str::FromStr, + T::Err: std::fmt::Debug + std::fmt::Display, +{ + fn deserialize>(deserializer: D) -> Result { + let value: &str = Deserialize::deserialize(deserializer)?; + let value: T = match value.parse() { + Ok(v) => v, + Err(e) => return Err(::custom(e)), + }; + Ok(SerdeStringWrap(value)) + } +} + +impl std::ops::Deref for SerdeStringWrap { + type Target = T; + fn deref(&self) -> &T { &self.0 } +} + +#[derive(Debug, Serialize)] +pub struct NftList { + pub(crate) nfts: Vec, +} + +#[derive(Clone, Deserialize)] +pub struct WithdrawErc1155 { + pub(crate) chain: Chain, + pub(crate) to: String, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) amount: Option, + #[serde(default)] + pub(crate) max: bool, + pub(crate) fee: Option, +} + +#[derive(Clone, Deserialize)] +pub struct WithdrawErc721 { + pub(crate) chain: Chain, + pub(crate) to: String, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) fee: Option, +} + +#[derive(Clone, Deserialize)] +#[serde(tag = "type", content = "withdraw_data")] +pub enum WithdrawNftReq { + WithdrawErc1155(WithdrawErc1155), + WithdrawErc721(WithdrawErc721), +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TransactionNftDetails { + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction` RPC to broadcast the transaction + pub(crate) tx_hex: BytesJson, + pub(crate) tx_hash: String, + /// NFTs are sent from these addresses + pub(crate) from: Vec, + /// NFTs are sent to these addresses + pub(crate) to: Vec, + pub(crate) contract_type: ContractType, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) amount: BigDecimal, + pub(crate) fee_details: Option, + /// The coin transaction belongs to + pub(crate) coin: String, + /// Block height + pub(crate) block_height: u64, + /// Transaction timestamp + pub(crate) timestamp: u64, + /// Internal MM2 id used for internal transaction identification, for some coins it might be equal to transaction hash + pub(crate) internal_id: i64, + /// Type of transactions, default is StandardTransfer + #[serde(default)] + pub(crate) transaction_type: TransactionType, +} + +#[derive(Debug, Deserialize)] +pub struct NftTransfersReq { + pub(crate) chains: Vec, +} + +#[derive(Debug, Serialize)] +pub(crate) struct NftTransferHistory { + pub(crate) chain: Chain, + pub(crate) block_number: u64, + pub(crate) block_timestamp: String, + pub(crate) block_hash: String, + /// Transaction hash in hexadecimal format + pub(crate) transaction_hash: String, + pub(crate) transaction_index: u64, + pub(crate) log_index: u64, + pub(crate) value: BigDecimal, + pub(crate) contract_type: ContractType, + pub(crate) transaction_type: String, + pub(crate) token_address: String, + pub(crate) token_id: BigDecimal, + pub(crate) from_address: String, + pub(crate) to_address: String, + pub(crate) amount: BigDecimal, + pub(crate) verified: u64, + pub(crate) operator: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct NftTransferHistoryWrapper { + pub(crate) block_number: SerdeStringWrap, + pub(crate) block_timestamp: String, + pub(crate) block_hash: String, + /// Transaction hash in hexadecimal format + pub(crate) transaction_hash: String, + pub(crate) transaction_index: u64, + pub(crate) log_index: u64, + pub(crate) value: SerdeStringWrap, + pub(crate) contract_type: SerdeStringWrap, + pub(crate) transaction_type: String, + pub(crate) token_address: String, + pub(crate) token_id: SerdeStringWrap, + pub(crate) from_address: String, + pub(crate) to_address: String, + pub(crate) amount: SerdeStringWrap, + pub(crate) verified: u64, + pub(crate) operator: Option, +} + +#[derive(Debug, Serialize)] +pub struct NftsTransferHistoryList { + pub(crate) transfer_history: Vec, +} diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index cddbfaf6d5..54219400cf 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -15,20 +15,18 @@ use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, Broadca GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; -use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, - FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, MmCoin, - NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, - PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RefundError, RefundResult, - SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, - SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, - SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, - SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, - TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, - ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, - WithdrawRequest, WithdrawResult}; +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, + MmCoin, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, + TransactionErr, TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, + ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -765,7 +763,7 @@ impl SwapOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs) -> TransactionFut { let time_lock = maker_payment_args.time_lock; let taker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(maker_payment_args.other_pubkey)); let id = qrc20_swap_id(time_lock, maker_payment_args.secret_hash); @@ -783,7 +781,7 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs) -> TransactionFut { let time_lock = taker_payment_args.time_lock; let maker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(taker_payment_args.other_pubkey)); let id = qrc20_swap_id(time_lock, taker_payment_args.secret_hash); @@ -801,10 +799,7 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { let payment_tx: UtxoTx = try_tx_fus!(deserialize(maker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); let swap_contract_address = try_tx_fus!(maker_spends_payment_args.swap_contract_address.try_to_address()); @@ -820,10 +815,7 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { let payment_tx: UtxoTx = try_tx_fus!(deserialize(taker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); let secret = taker_spends_payment_args.secret.to_vec(); @@ -839,7 +831,7 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { let payment_tx: UtxoTx = try_tx_fus!(deserialize(taker_refunds_payment_args.payment_tx).map_err(|e| ERRL!("{:?}", e))); let swap_contract_address = try_tx_fus!(taker_refunds_payment_args.swap_contract_address.try_to_address()); @@ -854,7 +846,7 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { let payment_tx: UtxoTx = try_tx_fus!(deserialize(maker_refunds_payment_args.payment_tx).map_err(|e| ERRL!("{:?}", e))); let swap_contract_address = try_tx_fus!(maker_refunds_payment_args.swap_contract_address.try_to_address()); @@ -869,10 +861,7 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn validate_fee( - &self, - validate_fee_args: ValidateFeeArgs<'_>, - ) -> Box + Send> { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { let fee_tx = validate_fee_args.fee_tx; let min_block_number = validate_fee_args.min_block_number; let fee_tx = match fee_tx { @@ -880,20 +869,26 @@ impl SwapOps for Qrc20Coin { _ => panic!("Unexpected TransactionEnum"), }; let fee_tx_hash = fee_tx.hash().reversed().into(); - if !try_fus!(check_all_utxo_inputs_signed_by_pub( + let inputs_signed_by_pub = try_f!(check_all_utxo_inputs_signed_by_pub( fee_tx, validate_fee_args.expected_sender - )) { - return Box::new(futures01::future::err(ERRL!("The dex fee was sent from wrong address"))); + )); + if !inputs_signed_by_pub { + return Box::new(futures01::future::err( + ValidatePaymentError::WrongPaymentTx("The dex fee was sent from wrong address".to_string()).into(), + )); } - let fee_addr = try_fus!(self.contract_address_from_raw_pubkey(validate_fee_args.fee_addr)); - let expected_value = try_fus!(wei_from_big_decimal(validate_fee_args.amount, self.utxo.decimals)); + let fee_addr = try_f!(self + .contract_address_from_raw_pubkey(validate_fee_args.fee_addr) + .map_to_mm(ValidatePaymentError::WrongPaymentTx)); + let expected_value = try_f!(wei_from_big_decimal(validate_fee_args.amount, self.utxo.decimals)); let selfi = self.clone(); let fut = async move { selfi .validate_fee_impl(fee_tx_hash, fee_addr, expected_value, min_block_number) .await + .map_to_mm(ValidatePaymentError::WrongPaymentTx) }; Box::new(fut.boxed().compat()) } @@ -997,7 +992,12 @@ impl SwapOps for Qrc20Coin { } #[inline] - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + _watcher_reward: bool, + ) -> Result, String> { self.extract_secret_impl(secret_hash, spend_tx) } @@ -1130,10 +1130,7 @@ impl WatcherOps for Qrc20Coin { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -1223,19 +1220,18 @@ impl MarketCoinOps for Qrc20Coin { utxo_common::send_raw_tx_bytes(&self.utxo, tx) } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { - let tx: UtxoTx = try_fus!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + let tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); let selfi = self.clone(); let fut = async move { selfi - .wait_for_confirmations_and_check_result(tx, confirmations, requires_nota, wait_until, check_every) + .wait_for_confirmations_and_check_result( + tx, + input.confirmations, + input.requires_nota, + input.wait_until, + input.check_every, + ) .await }; Box::new(fut.boxed().compat()) @@ -1450,9 +1446,17 @@ impl MmCoin for Qrc20Coin { fn mature_confirmations(&self) -> Option { Some(self.utxo.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { + utxo_common::coin_protocol_info(self) + } - fn is_coin_protocol_supported(&self, info: &Option>) -> bool { + fn is_coin_protocol_supported( + &self, + info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { utxo_common::is_coin_protocol_supported(self, info) } diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index 0e61ce0a79..b3ae9e7655 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -824,7 +824,7 @@ mod tests { let expected_id = TxInternalId::new(tx_hash.as_slice().into(), 13, 257); let actual_bytes: BytesJson = expected_id.clone().into(); - let mut expected_bytes = tx_hash.clone(); + let mut expected_bytes = 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()); @@ -848,7 +848,7 @@ mod tests { .as_slice() .into(); let tx_height = 699545; - let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash.clone())).unwrap(); + let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut transfer_map = transfer_map_expected.clone(); assert_eq!( @@ -880,7 +880,7 @@ mod tests { .as_slice() .into(); let tx_height = 699545; - let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash.clone())).unwrap(); + let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut transfer_map_zero_timestamp = transfer_map_expected .clone() @@ -922,10 +922,9 @@ mod tests { .as_slice() .into(); let tx_height = 699545; - let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash.clone())).unwrap(); + let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut transfer_map_unexpected_tx_id = transfer_map_expected - .clone() .into_iter() .map(|(mut id, tx)| { // just another tx_hash @@ -963,9 +962,9 @@ mod tests { .as_slice() .into(); let tx_height = 681443; - let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash.clone())).unwrap(); + let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut history_map_expected = HistoryMapByHash::new(); - history_map_expected.insert(tx_hash.clone(), transfer_map_expected); + history_map_expected.insert(tx_hash, transfer_map_expected); let tx_ids = vec![(tx_hash, tx_height)]; let mut history_map = HistoryMapByHash::new(); @@ -988,9 +987,9 @@ mod tests { .as_slice() .into(); let tx_height = 699545; - let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash.clone())).unwrap(); + let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut history_map_expected = HistoryMapByHash::new(); - history_map_expected.insert(tx_hash.clone(), transfer_map_expected); + history_map_expected.insert(tx_hash, transfer_map_expected); let tx_ids = vec![(tx_hash, tx_height)]; let mut history_map = history_map_expected.clone(); @@ -1020,10 +1019,10 @@ mod tests { .as_slice() .into(); let tx_height = 699545; - let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash.clone())).unwrap(); + let transfer_map_expected = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); let mut history_map_expected = HistoryMapByHash::new(); // should contain only valid tx - history_map_expected.insert(tx_hash.clone(), transfer_map_expected); + history_map_expected.insert(tx_hash, transfer_map_expected); let tx_ids = vec![(tx_hash, tx_height), (tx_hash_invalid, tx_height)]; let mut history_map = HistoryMapByHash::default(); diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 12f606cb26..2835752f0c 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -170,6 +170,7 @@ fn test_validate_maker_payment() { try_spv_proof_until: now_ms() / 1000 + 30, confirmations: 1, unique_swap_data: Vec::new(), + min_watcher_reward: None, }; coin.validate_maker_payment(input.clone()).wait().unwrap(); @@ -259,27 +260,40 @@ fn test_wait_for_confirmations_excepted() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 1; // the transaction is mined already let check_every = 1; - coin.wait_for_confirmations(&payment_tx, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx, + confirmations, + requires_nota, + wait_until, + check_every, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); // tx_hash: ed53b97deb2ad76974c972cb084f6ba63bd9f16c91c4a39106a20c6d14599b2a // `erc20Payment` contract call excepted let payment_tx = hex::decode("01000000014c1411bac38ca25a2816342b019df81f503e1db75b25c6da618b08484dc2ff49010000006b483045022100da3e90fbcc45a94573c28213b36dc616630e3adfa42a7f16bdf917e8a76b954502206ad0830bb16e5c25466903ae7f749e291586726f1497ae9fc2e709c1b6cd1857012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff040000000000000000625403a08601012844095ea7b3000000000000000000000000ba8b71f3544b93e2f681f996da519a98ace0107a000000000000000000000000000000000000000000000000000000000000000014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000625403a08601012844095ea7b3000000000000000000000000ba8b71f3544b93e2f681f996da519a98ace0107a000000000000000000000000000000000000000000000000000000000000000a14d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000e35403a0860101284cc49b415b2a0a1a8b4af2762154115ced87e2424b3cb940c0181cc3c850523702f1ec298fef0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8324b6b2e5444c2639cc0fb7bcea5afba3f3cdce239000000000000000000000000000000000000000000000000000000000000000000000000000000005fa0fffb14ba8b71f3544b93e2f681f996da519a98ace0107ac2493d4a03000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acae2ea15f").unwrap(); - let error = coin - .wait_for_confirmations(&payment_tx, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap_err(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx, + confirmations, + requires_nota, + wait_until, + check_every, + }; + let error = coin.wait_for_confirmations(confirm_payment_input).wait().unwrap_err(); log!("error: {:?}", error); assert!(error.contains("Contract call failed with an error: Revert")); // tx_hash: aa992c028c07e239dbd2ff32bf67251f026929c644b4d02a469e351cb44abab7 // `receiverSpend` contract call excepted let payment_tx = hex::decode("0100000007077ccb377a68fd6079503f856df4e553e337015f8419cd0f2a949c31db175df7050000006a473044022058097f54be31ae5af197f72e4410b33b22f29fad5b1a1cefb30ee45b3b3477dc02205c1098850fa2f2c1929c27af6261f83abce7682eb769f909dd09e9be5e0bd469012102aa32922f4b05cbc7384dd85b86021c98e4102f5da3df48bc516aa76f8119559affffffffc191895a431db3dccbf4f9d4b8cd8301124343e66275194ad734a77ffe56b95e030000006a4730440220491fed7954c6f43acc7226c337bb16ac71b38df50f55a819441d9b2b9e4a04b502201f95be6941b6619c0ca246e15adb090b82cd908f7c85108a1dcc02eafb7cc725012102aa32922f4b05cbc7384dd85b86021c98e4102f5da3df48bc516aa76f8119559afffffffff678de174fb81d3820df43a2c29945b08df4fb080deb8088ef11b3711c0fe8df020000006a473044022071d9c0ec57ab23360a4f73d0edfc2f67614b56f6d2e54387b39c3de1fa894c7d022030ea65d157784ff68cae9c9acb0dd626205073f478003b1cb1d0d581dcb27b1c012102aa32922f4b05cbc7384dd85b86021c98e4102f5da3df48bc516aa76f8119559affffffffe1ef8740ce51ed3172efea91a5e559b5fe63dc6fede8a9037ad47fbc38560b51040000006a47304402203f056dff0be1f24ed96c72904c9aac3ac964913d0c3228bfab3fa4bef7f22c060220658a121bf8f29d86c18ec1aee4460f363c0704d2f05cc9d7923e978e917f48ca012102aa32922f4b05cbc7384dd85b86021c98e4102f5da3df48bc516aa76f8119559affffffffe825dea61113bbd67dd35cbc9d88890ac222f55bf0201a7f9fb96592e0614d4d080000006b483045022100bb10f195c57c1eed9de3d9d9726484f839e25d83deb54cf2142df37099df6a8d02202a025182caaa5348350b410ee783180e9ce3ccac5e361eb50b162311e9d803f1012102aa32922f4b05cbc7384dd85b86021c98e4102f5da3df48bc516aa76f8119559affffffffe1ef8740ce51ed3172efea91a5e559b5fe63dc6fede8a9037ad47fbc38560b51060000006a47304402205550e0b4e1425f2f7a8645c6fd408ba0603cca5ca408202729041f5eab0b0cd202205c98fc8e91a37960d38f0104e81d3d48f737c4000ef45e2372c84d857455da34012102aa32922f4b05cbc7384dd85b86021c98e4102f5da3df48bc516aa76f8119559affffffffe825dea61113bbd67dd35cbc9d88890ac222f55bf0201a7f9fb96592e0614d4d060000006b483045022100b0d21cbb5d94b4995d9cb81e7440849dbe645416bca6d51bb5450e10753523220220299f105d573cdb785233699b5a9be8f907d9821a74cfd91fb72911a4a6e1bdb8012102aa32922f4b05cbc7384dd85b86021c98e4102f5da3df48bc516aa76f8119559affffffff020000000000000000c35403a0860101284ca402ed292be8b1d4904e8f1924bd7a2eb4d8085214c17af3d8d7574b2740a86b6296d343c00000000000000000000000000000000000000000000000000000000005f5e10028fcc0c5f6d9619d3c1f90af51e891d62333eb748c568f7da2a7734240d37d38000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000d020b63f5a989776516bdc04d426ba118130c00214ba8b71f3544b93e2f681f996da519a98ace0107ac270630800000000001976a914fb7dad7ce97deecf50a4573a2bd7639c79bdc08588aca64aaa5f").unwrap(); - let error = coin - .wait_for_confirmations(&payment_tx, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap_err(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx, + confirmations, + requires_nota, + wait_until, + check_every, + }; + let error = coin.wait_for_confirmations(confirm_payment_input).wait().unwrap_err(); log!("error: {:?}", error); assert!(error.contains("Contract call failed with an error: Revert")); } @@ -314,7 +328,7 @@ fn test_send_taker_fee() { uuid: &[], }) .wait(); - assert_eq!(result, Ok(())); + assert!(result.is_ok()); } #[test] @@ -342,7 +356,7 @@ fn test_validate_fee() { uuid: &[], }) .wait(); - assert_eq!(result, Ok(())); + assert!(result.is_ok()); let fee_addr_dif = hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(); let err = coin @@ -355,10 +369,13 @@ fn test_validate_fee() { uuid: &[], }) .wait() - .err() - .expect("Expected an error"); + .expect_err("Expected an error") + .into_inner(); log!("error: {:?}", err); - assert!(err.contains("QRC20 Fee tx was sent to wrong address")); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("QRC20 Fee tx was sent to wrong address")), + _ => panic!("Expected `WrongPaymentTx` wrong receiver address, found {:?}", err), + } let err = coin .validate_fee(ValidateFeeArgs { @@ -370,10 +387,13 @@ fn test_validate_fee() { uuid: &[], }) .wait() - .err() - .expect("Expected an error"); + .expect_err("Expected an error") + .into_inner(); log!("error: {:?}", err); - assert!(err.contains("was sent from wrong address")); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("was sent from wrong address")), + _ => panic!("Expected `WrongPaymentTx` wrong sender address, found {:?}", err), + } let err = coin .validate_fee(ValidateFeeArgs { @@ -385,10 +405,13 @@ fn test_validate_fee() { uuid: &[], }) .wait() - .err() - .expect("Expected an error"); + .expect_err("Expected an error") + .into_inner(); log!("error: {:?}", err); - assert!(err.contains("confirmed before min_block")); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min_block")), + _ => panic!("Expected `WrongPaymentTx` early confirmation, found {:?}", err), + } let amount_dif = BigDecimal::from_str("0.02").unwrap(); let err = coin @@ -401,10 +424,15 @@ fn test_validate_fee() { uuid: &[], }) .wait() - .err() - .expect("Expected an error"); + .expect_err("Expected an error") + .into_inner(); log!("error: {:?}", err); - assert!(err.contains("QRC20 Fee tx value 1000000 is less than expected 2000000")); + match err { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains("QRC20 Fee tx value 1000000 is less than expected 2000000")) + }, + _ => panic!("Expected `WrongPaymentTx` invalid fee value, found {:?}", err), + } // QTUM tx "8a51f0ffd45f34974de50f07c5bf2f0949da4e88433f8f75191953a442cf9310" let tx = TransactionEnum::UtxoTx("020000000113640281c9332caeddd02a8dd0d784809e1ad87bda3c972d89d5ae41f5494b85010000006a47304402207c5c904a93310b8672f4ecdbab356b65dd869a426e92f1064a567be7ccfc61ff02203e4173b9467127f7de4682513a21efb5980e66dbed4da91dff46534b8e77c7ef012102baefe72b3591de2070c0da3853226b00f082d72daa417688b61cb18c1d543d1afeffffff020001b2c4000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acbc4dd20c2f0000001976a9144208fa7be80dcf972f767194ad365950495064a488ac76e70800".into()); @@ -419,10 +447,13 @@ fn test_validate_fee() { uuid: &[], }) .wait() - .err() - .expect("Expected an error"); + .expect_err("Expected an error") + .into_inner(); log!("error: {:?}", err); - assert!(err.contains("Expected 'transfer' contract call")); + match err { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Expected 'transfer' contract call")), + _ => panic!("Expected `WrongPaymentTx` invalid contract call, found {:?}", err), + } } #[test] @@ -478,7 +509,7 @@ fn test_extract_secret() { // taker spent maker payment - d3f5dab4d54c14b3d7ed8c7f5c8cc7f47ccf45ce589fdc7cd5140a3c1c3df6e1 let tx_hex = hex::decode("01000000033f56ecafafc8602fde083ba868d1192d6649b8433e42e1a2d79ba007ea4f7abb010000006b48304502210093404e90e40d22730013035d31c404c875646dcf2fad9aa298348558b6d65ba60220297d045eac5617c1a3eddb71d4bca9772841afa3c4c9d6c68d8d2d42ee6de3950121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff9cac7fe90d597922a1d92e05306c2215628e7ea6d5b855bfb4289c2944f4c73a030000006b483045022100b987da58c2c0c40ce5b6ef2a59e8124ed4ef7a8b3e60c7fb631139280019bc93022069649bcde6fe4dd5df9462a1fcae40598488d6af8c324cd083f5c08afd9568be0121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff70b9870f2b0c65d220a839acecebf80f5b44c3ca4c982fa2fdc5552c037f5610010000006a473044022071b34dd3ebb72d29ca24f3fa0fc96571c815668d3b185dd45cc46a7222b6843f02206c39c030e618d411d4124f7b3e7ca1dd5436775bd8083a85712d123d933a51300121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff020000000000000000c35403a0860101284ca402ed292b806a1835a1b514ad643f2acdb5c8db6b6a9714accff3275ea0d79a3f23be8fd00000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2c02288d4010000001976a914783cf0be521101942da509846ea476e683aad83288ac0f047f5f").unwrap(); - let secret = block_on(coin.extract_secret(secret_hash, &tx_hex)).unwrap(); + let secret = block_on(coin.extract_secret(secret_hash, &tx_hex, false)).unwrap(); assert_eq!(secret, expected_secret); } @@ -499,7 +530,7 @@ fn test_extract_secret_malicious() { let spend_tx = hex::decode("01000000022bc8299981ec0cea664cdf9df4f8306396a02e2067d6ac2d3770b34646d2bc2a010000006b483045022100eb13ef2d99ac1cd9984045c2365654b115dd8a7815b7fbf8e2a257f0b93d1592022060d648e73118c843e97f75fafc94e5ff6da70ec8ba36ae255f8c96e2626af6260121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffffd92a0a10ac6d144b36033916f67ae79889f40f35096629a5cd87be1a08f40ee7010000006b48304502210080cdad5c4770dfbeb760e215494c63cc30da843b8505e75e7bf9e8dad18568000220234c0b11c41bfbcdd50046c69059976aedabe17657fe43d809af71e9635678e20121022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1affffffff030000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000202020202020202020202020202020202020202020202020202020202020202000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac20000000000000000c35403a0860101284ca402ed292b8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d000101010101010101010101010101010101010101010101010101010101010101000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000009e032d4b0090a11dc40fe6c47601499a35d55fbb14ba8b71f3544b93e2f681f996da519a98ace0107ac2b8ea82d3010000001976a914783cf0be521101942da509846ea476e683aad83288ac735d855f").unwrap(); let expected_secret = &[1; 32]; let secret_hash = &*dhash160(expected_secret); - let actual = block_on(coin.extract_secret(secret_hash, &spend_tx)); + let actual = block_on(coin.extract_secret(secret_hash, &spend_tx, false)); assert_eq!(actual, Ok(expected_secret.to_vec())); } @@ -528,23 +559,19 @@ fn test_generate_token_transfer_script_pubkey() { let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); let to_addr = qtum::contract_addr_from_utxo_addr(to_addr).unwrap(); let amount: U256 = 1000000000.into(); - let actual = coin - .transfer_output(to_addr.clone(), amount, gas_limit, gas_price) - .unwrap(); + let actual = coin.transfer_output(to_addr, amount, gas_limit, gas_price).unwrap(); assert_eq!(expected, actual); assert!(coin .transfer_output( - to_addr.clone(), - amount, - 0, // gas_limit cannot be zero + to_addr, amount, 0, // gas_limit cannot be zero gas_price, ) .is_err()); assert!(coin .transfer_output( - to_addr.clone(), + to_addr, amount, gas_limit, 0, // gas_price cannot be zero @@ -565,7 +592,7 @@ fn test_transfer_details_by_hash() { let tx_hex:BytesJson = hex::decode("0100000001426d27fde82e12e1ce84e73ca41e2a30420f4c94aaa37b30d4c5b8b4f762c042040000006a473044022032665891693ee732571cefaa6d322ec5114c78259f2adbe03a0d7e6b65fbf40d022035c9319ca41e5423e09a8a613ac749a20b8f5ad6ba4ad6bb60e4a020b085d009012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff050000000000000000625403a08601012844095ea7b30000000000000000000000001549128bbfb33b997949b4105b6a6371c998e212000000000000000000000000000000000000000000000000000000000000000014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000625403a08601012844095ea7b30000000000000000000000001549128bbfb33b997949b4105b6a6371c998e21200000000000000000000000000000000000000000000000000000000000927c014d362e096e873eb7907e205fadc6175c6fec7bc44c20000000000000000835403a0860101284c640c565ae300000000000000000000000000000000000000000000000000000000000493e0000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000000000000000000000000000000000000000000000141549128bbfb33b997949b4105b6a6371c998e212c20000000000000000835403a0860101284c640c565ae300000000000000000000000000000000000000000000000000000000000493e0000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc440000000000000000000000000000000000000000000000000000000000000001141549128bbfb33b997949b4105b6a6371c998e212c231754b04000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acf7cd8b5f").unwrap().into(); let details = block_on(coin.transfer_details_by_hash(tx_hash)).unwrap(); - let mut it = details.into_iter().sorted_by(|(id_x, _), (id_y, _)| id_x.cmp(&id_y)); + 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 { let fee = Qrc20FeeDetails { @@ -681,7 +708,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), + tx_hex, tx_hash: tx_hash_bytes.to_tx_hash(), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], @@ -719,7 +746,7 @@ fn test_get_trade_fee() { let actual_trade_fee = coin.get_trade_fee().wait().unwrap(); let expected_trade_fee_amount = big_decimal_from_sat( - (2 * CONTRACT_CALL_GAS_FEE + SWAP_PAYMENT_GAS_FEE + EXPECTED_TX_FEE) as i64, + 2 * CONTRACT_CALL_GAS_FEE + SWAP_PAYMENT_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals, ); let expected = TradeFee { @@ -1043,12 +1070,12 @@ fn test_validate_maker_payment_malicious() { confirmations: 1, other_pub: maker_pub, unique_swap_data: Vec::new(), + min_watcher_reward: None, }; let error = coin .validate_maker_payment(input) .wait() - .err() - .expect("'erc20Payment' was called from another swap contract, expected an error") + .expect_err("'erc20Payment' was called from another swap contract, expected an error") .into_inner(); log!("error: {}", error); match error { @@ -1148,6 +1175,6 @@ fn test_send_contract_calls_recoverable_tx() { // The error variant should equal to `TxRecoverable` assert_eq!( discriminant(&tx_err), - discriminant(&TransactionErr::TxRecoverable(TransactionEnum::from(tx), String::new())) + discriminant(&TransactionErr::TxRecoverable(tx, String::new())) ); } diff --git a/mm2src/coins/rpc_command/lightning/close_channel.rs b/mm2src/coins/rpc_command/lightning/close_channel.rs index a3fa05644c..716b7db969 100644 --- a/mm2src/coins/rpc_command/lightning/close_channel.rs +++ b/mm2src/coins/rpc_command/lightning/close_channel.rs @@ -3,6 +3,7 @@ use common::{async_blocking, HttpStatusCode}; use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use uuid::Uuid; type CloseChannelResult = Result>; @@ -13,8 +14,8 @@ pub enum CloseChannelError { UnsupportedCoin(String), #[display(fmt = "No such coin {}", _0)] NoSuchCoin(String), - #[display(fmt = "No such channel with rpc_channel_id {}", _0)] - NoSuchChannel(u64), + #[display(fmt = "No such channel with uuid {}", _0)] + NoSuchChannel(Uuid), #[display(fmt = "Closing channel error: {}", _0)] CloseChannelError(String), } @@ -40,7 +41,7 @@ impl From for CloseChannelError { #[derive(Deserialize)] pub struct CloseChannelReq { pub coin: String, - pub rpc_channel_id: u64, + pub uuid: Uuid, #[serde(default)] pub force_close: bool, } @@ -52,9 +53,9 @@ pub async fn close_channel(ctx: MmArc, req: CloseChannelReq) -> CloseChannelResu }; let channel_details = ln_coin - .get_channel_by_rpc_id(req.rpc_channel_id) + .get_channel_by_uuid(req.uuid) .await - .ok_or(CloseChannelError::NoSuchChannel(req.rpc_channel_id))?; + .ok_or(CloseChannelError::NoSuchChannel(req.uuid))?; let channel_id = channel_details.channel_id; let counterparty_node_id = channel_details.counterparty.node_id; @@ -76,8 +77,5 @@ pub async fn close_channel(ctx: MmArc, req: CloseChannelReq) -> CloseChannelResu .await?; } - Ok(format!( - "Initiated closing of channel with rpc_channel_id: {}", - req.rpc_channel_id - )) + Ok(format!("Initiated closing of channel with uuid: {}", req.uuid)) } diff --git a/mm2src/coins/rpc_command/lightning/generate_invoice.rs b/mm2src/coins/rpc_command/lightning/generate_invoice.rs index 94740793eb..83d0fc41f6 100644 --- a/mm2src/coins/rpc_command/lightning/generate_invoice.rs +++ b/mm2src/coins/rpc_command/lightning/generate_invoice.rs @@ -92,6 +92,7 @@ pub async fn generate_invoice( let network = ln_coin.platform.network.clone().into(); let channel_manager = ln_coin.channel_manager.clone(); let keys_manager = ln_coin.keys_manager.clone(); + let logger = ln_coin.logger.clone(); let amount_in_msat = req.amount_in_msat; let description = req.description.clone(); let expiry = req.expiry.unwrap_or(DEFAULT_INVOICE_EXPIRY); @@ -99,6 +100,7 @@ pub async fn generate_invoice( create_invoice_from_channelmanager( &channel_manager, keys_manager, + logger, network, amount_in_msat, description, diff --git a/mm2src/coins/rpc_command/lightning/get_channel_details.rs b/mm2src/coins/rpc_command/lightning/get_channel_details.rs index f6c18ae633..c96cf06e57 100644 --- a/mm2src/coins/rpc_command/lightning/get_channel_details.rs +++ b/mm2src/coins/rpc_command/lightning/get_channel_details.rs @@ -6,6 +6,7 @@ use db_common::sqlite::rusqlite::Error as SqlError; use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use uuid::Uuid; type GetChannelDetailsResult = Result>; @@ -16,8 +17,8 @@ pub enum GetChannelDetailsError { UnsupportedCoin(String), #[display(fmt = "No such coin {}", _0)] NoSuchCoin(String), - #[display(fmt = "Channel with rpc id: {} is not found", _0)] - NoSuchChannel(u64), + #[display(fmt = "Channel with uuid: {} is not found", _0)] + NoSuchChannel(Uuid), #[display(fmt = "DB error {}", _0)] DbError(String), } @@ -47,7 +48,7 @@ impl From for GetChannelDetailsError { #[derive(Deserialize)] pub struct GetChannelDetailsRequest { pub coin: String, - pub rpc_channel_id: u64, + pub uuid: Uuid, } #[derive(Serialize)] @@ -66,14 +67,14 @@ pub async fn get_channel_details( e => return MmError::err(GetChannelDetailsError::UnsupportedCoin(e.ticker().to_string())), }; - let channel_details = match ln_coin.get_channel_by_rpc_id(req.rpc_channel_id).await { + let channel_details = match ln_coin.get_channel_by_uuid(req.uuid).await { Some(details) => GetChannelDetailsResponse::Open(details.into()), None => GetChannelDetailsResponse::Closed( ln_coin .db - .get_channel_from_db(req.rpc_channel_id) + .get_channel_from_db(req.uuid) .await? - .ok_or(GetChannelDetailsError::NoSuchChannel(req.rpc_channel_id))?, + .ok_or(GetChannelDetailsError::NoSuchChannel(req.uuid))?, ), }; diff --git a/mm2src/coins/rpc_command/lightning/list_channels.rs b/mm2src/coins/rpc_command/lightning/list_channels.rs index a96106a99f..d3eb582633 100644 --- a/mm2src/coins/rpc_command/lightning/list_channels.rs +++ b/mm2src/coins/rpc_command/lightning/list_channels.rs @@ -7,6 +7,7 @@ use db_common::sqlite::rusqlite::Error as SqlError; use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use uuid::Uuid; type ListChannelsResult = Result>; @@ -50,7 +51,7 @@ pub struct ListOpenChannelsRequest { #[serde(default = "ten")] limit: usize, #[serde(default)] - paging_options: PagingOptionsEnum, + paging_options: PagingOptionsEnum, } #[derive(Serialize)] @@ -60,7 +61,7 @@ pub struct ListOpenChannelsResponse { skipped: usize, total: usize, total_pages: usize, - paging_options: PagingOptionsEnum, + paging_options: PagingOptionsEnum, } pub async fn list_open_channels_by_filter( @@ -93,7 +94,7 @@ pub struct ListClosedChannelsRequest { #[serde(default = "ten")] limit: usize, #[serde(default)] - paging_options: PagingOptionsEnum, + paging_options: PagingOptionsEnum, } #[derive(Serialize)] @@ -103,7 +104,7 @@ pub struct ListClosedChannelsResponse { skipped: usize, total: usize, total_pages: usize, - paging_options: PagingOptionsEnum, + paging_options: PagingOptionsEnum, } pub async fn list_closed_channels_by_filter( diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index 9d131b1b48..f79ec0b433 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -9,7 +9,7 @@ use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, GenerateTxError, Mm UnexpectedDerivationMethod, UtxoRpcError}; use chain::TransactionOutput; use common::log::error; -use common::{async_blocking, HttpStatusCode}; +use common::{async_blocking, new_uuid, HttpStatusCode}; use db_common::sqlite::rusqlite::Error as SqlError; use http::StatusCode; use keys::AddressHashEnum; @@ -18,6 +18,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; use script::Builder; +use uuid::Uuid; type OpenChannelResult = Result>; @@ -127,7 +128,7 @@ pub struct OpenChannelRequest { #[derive(Serialize)] pub struct OpenChannelResponse { - rpc_channel_id: u64, + uuid: Uuid, node_address: NodeAddress, } @@ -197,22 +198,21 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes drop_mutability!(conf); let user_config: UserConfig = conf.into(); - let rpc_channel_id = ln_coin.db.get_last_channel_rpc_id().await? as u64 + 1; - + let uuid = new_uuid(); let temp_channel_id = async_blocking(move || { channel_manager - .create_channel(node_pubkey, amount_in_sat, push_msat, rpc_channel_id, Some(user_config)) + .create_channel(node_pubkey, amount_in_sat, push_msat, uuid.as_u128(), Some(user_config)) .map_to_mm(|e| OpenChannelError::FailureToOpenChannel(node_pubkey.to_string(), format!("{:?}", e))) }) .await?; { let mut unsigned_funding_txs = ln_coin.platform.unsigned_funding_txs.lock(); - unsigned_funding_txs.insert(rpc_channel_id, unsigned); + unsigned_funding_txs.insert(uuid, unsigned); } let pending_channel_details = DBChannelDetails::new( - rpc_channel_id, + uuid, temp_channel_id, node_pubkey, true, @@ -227,11 +227,11 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes .await?; if let Err(e) = ln_coin.db.add_channel_to_db(&pending_channel_details).await { - error!("Unable to add new outbound channel {} to db: {}", rpc_channel_id, e); + error!("Unable to add new outbound channel {} to db: {}", uuid, e); } Ok(OpenChannelResponse { - rpc_channel_id, + uuid, node_address: req.node_address, }) } diff --git a/mm2src/coins/rpc_command/lightning/update_channel.rs b/mm2src/coins/rpc_command/lightning/update_channel.rs index a108659ea0..ad7768831f 100644 --- a/mm2src/coins/rpc_command/lightning/update_channel.rs +++ b/mm2src/coins/rpc_command/lightning/update_channel.rs @@ -4,6 +4,7 @@ use common::{async_blocking, HttpStatusCode}; use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use uuid::Uuid; type UpdateChannelResult = Result>; @@ -14,10 +15,10 @@ pub enum UpdateChannelError { UnsupportedCoin(String), #[display(fmt = "No such coin {}", _0)] NoSuchCoin(String), - #[display(fmt = "No such channel with rpc_channel_id {}", _0)] - NoSuchChannel(u64), + #[display(fmt = "No such channel with uuid {}", _0)] + NoSuchChannel(Uuid), #[display(fmt = "Failure to channel {}: {}", _0, _1)] - FailureToUpdateChannel(u64, String), + FailureToUpdateChannel(Uuid, String), } impl HttpStatusCode for UpdateChannelError { @@ -41,7 +42,7 @@ impl From for UpdateChannelError { #[derive(Deserialize)] pub struct UpdateChannelReq { pub coin: String, - pub rpc_channel_id: u64, + pub uuid: Uuid, pub channel_options: ChannelOptions, } @@ -58,9 +59,9 @@ pub async fn update_channel(ctx: MmArc, req: UpdateChannelReq) -> UpdateChannelR }; let channel_details = ln_coin - .get_channel_by_rpc_id(req.rpc_channel_id) + .get_channel_by_uuid(req.uuid) .await - .ok_or(UpdateChannelError::NoSuchChannel(req.rpc_channel_id))?; + .ok_or(UpdateChannelError::NoSuchChannel(req.uuid))?; async_blocking(move || { let mut channel_options = ln_coin @@ -76,7 +77,7 @@ pub async fn update_channel(ctx: MmArc, req: UpdateChannelReq) -> UpdateChannelR ln_coin .channel_manager .update_channel_config(&counterparty_node_id, channel_ids, &channel_options.clone().into()) - .map_to_mm(|e| UpdateChannelError::FailureToUpdateChannel(req.rpc_channel_id, format!("{:?}", e)))?; + .map_to_mm(|e| UpdateChannelError::FailureToUpdateChannel(req.uuid, format!("{:?}", e)))?; Ok(UpdateChannelResponse { channel_options }) }) .await diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index 945cea77fe..c401853b2d 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -8,3 +8,4 @@ pub mod init_create_account; pub mod init_scan_for_new_addresses; pub mod init_withdraw; #[cfg(not(target_arch = "wasm32"))] pub mod lightning; +pub mod tendermint; diff --git a/mm2src/coins/rpc_command/tendermint/ibc_chains.rs b/mm2src/coins/rpc_command/tendermint/ibc_chains.rs new file mode 100644 index 0000000000..67ed93e9fa --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/ibc_chains.rs @@ -0,0 +1,35 @@ +use common::HttpStatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; + +use crate::tendermint; + +pub type IBCChainRegistriesResult = Result>; + +#[derive(Clone, Serialize)] +pub struct IBCChainRegistriesResponse { + pub(crate) chain_registry_list: Vec, +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] +pub enum IBCChainsRequestError { + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for IBCChainsRequestError { + fn status_code(&self) -> common::StatusCode { + match self { + IBCChainsRequestError::Transport(_) => common::StatusCode::SERVICE_UNAVAILABLE, + IBCChainsRequestError::InternalError(_) => common::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[inline(always)] +pub async fn ibc_chains(_ctx: MmArc, _req: serde_json::Value) -> IBCChainRegistriesResult { + tendermint::get_ibc_chain_list().await +} diff --git a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs new file mode 100644 index 0000000000..fce69042c6 --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs @@ -0,0 +1,76 @@ +use common::HttpStatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; + +use crate::{lp_coinfind_or_err, MmCoinEnum}; + +pub type IBCTransferChannelsResult = Result>; + +#[derive(Clone, Deserialize)] +pub struct IBCTransferChannelsRequest { + pub(crate) coin: String, + pub(crate) destination_chain_registry_name: String, +} + +#[derive(Clone, Serialize)] +pub struct IBCTransferChannelsResponse { + pub(crate) ibc_transfer_channels: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub(crate) struct IBCTransferChannel { + pub(crate) channel_id: String, + pub(crate) ordering: String, + pub(crate) version: String, + pub(crate) tags: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct IBCTransferChannelTag { + pub(crate) status: String, + pub(crate) preferred: bool, + pub(crate) dex: Option, +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] +pub enum IBCTransferChannelsRequestError { + #[display(fmt = "No such coin {}", _0)] + NoSuchCoin(String), + #[display( + fmt = "Only tendermint based coins are allowed for `ibc_transfer_channels` operation. Current coin: {}", + _0 + )] + UnsupportedCoin(String), + #[display(fmt = "Could not find '{}' registry source.", _0)] + RegistrySourceCouldNotFound(String), + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for IBCTransferChannelsRequestError { + fn status_code(&self) -> common::StatusCode { + match self { + IBCTransferChannelsRequestError::UnsupportedCoin(_) | IBCTransferChannelsRequestError::NoSuchCoin(_) => { + common::StatusCode::BAD_REQUEST + }, + IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(_) => common::StatusCode::NOT_FOUND, + IBCTransferChannelsRequestError::Transport(_) => common::StatusCode::SERVICE_UNAVAILABLE, + IBCTransferChannelsRequestError::InternalError(_) => common::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn ibc_transfer_channels(ctx: MmArc, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin) + .await + .map_err(|_| IBCTransferChannelsRequestError::NoSuchCoin(req.coin.clone()))?; + + match coin { + MmCoinEnum::Tendermint(coin) => coin.get_ibc_transfer_channels(req).await, + MmCoinEnum::TendermintToken(token) => token.platform_coin.get_ibc_transfer_channels(req).await, + _ => MmError::err(IBCTransferChannelsRequestError::UnsupportedCoin(req.coin)), + } +} diff --git a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs new file mode 100644 index 0000000000..a490ef5cec --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs @@ -0,0 +1,27 @@ +use common::Future01CompatExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; +use mm2_number::BigDecimal; + +use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawResult}; + +#[derive(Clone, Deserialize)] +pub struct IBCWithdrawRequest { + pub(crate) ibc_source_channel: String, + pub(crate) coin: String, + pub(crate) to: String, + #[serde(default)] + pub(crate) amount: BigDecimal, + #[serde(default)] + pub(crate) max: bool, + pub(crate) memo: Option, +} + +pub async fn ibc_withdraw(ctx: MmArc, req: IBCWithdrawRequest) -> WithdrawResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::Tendermint(coin) => coin.ibc_withdraw(req).compat().await, + MmCoinEnum::TendermintToken(token) => token.ibc_withdraw(req).compat().await, + _ => MmError::err(WithdrawError::ActionNotAllowed(req.coin)), + } +} diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs new file mode 100644 index 0000000000..d8211abeac --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -0,0 +1,14 @@ +mod ibc_chains; +mod ibc_transfer_channels; +mod ibc_withdraw; + +pub use ibc_chains::*; +pub use ibc_transfer_channels::*; +pub use ibc_withdraw::*; + +// Global constants for interacting with https://github.com/KomodoPlatform/chain-registry repository +// using `mm2_git` crate. +pub(crate) const CHAIN_REGISTRY_REPO_OWNER: &str = "KomodoPlatform"; +pub(crate) const CHAIN_REGISTRY_REPO_NAME: &str = "chain-registry"; +pub(crate) const CHAIN_REGISTRY_BRANCH: &str = "master"; +pub(crate) const CHAIN_REGISTRY_IBC_DIR_NAME: &str = "_IBC"; diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index cd92a1c30f..ef3cc38c49 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -2,18 +2,17 @@ use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, Trade use crate::coin_errors::MyAddressError; use crate::solana::solana_common::{lamports_to_sol, PrepareTransferData, SufficientBalanceError}; use crate::solana::spl::SplTokenInfo; -use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, - MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RefundError, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, - SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, - SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, - SignatureResult, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, + FoundSwapTxSpend, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, + PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, + RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, + TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, + TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest, + WithdrawResult}; use async_trait::async_trait; use base58::ToBase58; use bincode::{deserialize, serialize}; @@ -47,11 +46,8 @@ pub mod solana_common; mod solana_decode_tx_helpers; pub mod spl; -#[cfg(all(test, not(feature = "disable-solana-tests")))] mod solana_common_tests; -#[cfg(all(test, not(feature = "disable-solana-tests")))] mod solana_tests; -#[cfg(all(test, not(feature = "disable-solana-tests")))] mod spl_tests; pub const SOLANA_DEFAULT_DECIMALS: u64 = 9; @@ -443,14 +439,7 @@ impl MarketCoinOps for SolanaCoin { Box::new(fut.boxed().compat()) } - fn wait_for_confirmations( - &self, - _tx: &[u8], - _confirmations: u64, - _requires_nota: bool, - _wait_until: u64, - _check_every: u64, - ) -> Box + Send> { + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { unimplemented!() } @@ -489,35 +478,27 @@ impl MarketCoinOps for SolanaCoin { impl SwapOps for SolanaCoin { fn send_taker_fee(&self, _fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { unimplemented!() } - fn send_maker_payment(&self, _maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { unimplemented!() } + fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_payment(&self, _taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { unimplemented!() } + fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_spends_taker_payment( - &self, - _maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_spends_maker_payment( - &self, - _taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!() } - fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> Box + Send> { - unimplemented!() - } + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } @@ -548,7 +529,14 @@ impl SwapOps for SolanaCoin { unimplemented!(); } - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, String> { + unimplemented!() + } fn is_auto_refundable(&self) -> bool { false } @@ -657,10 +645,7 @@ impl WatcherOps for SolanaCoin { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -768,9 +753,17 @@ impl MmCoin for SolanaCoin { fn mature_confirmations(&self) -> Option { None } - fn coin_protocol_info(&self) -> Vec { Vec::new() } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } - fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.abortable_system) } diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index 05b4e76566..8aa917aed0 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -2,18 +2,16 @@ use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, Trade use crate::coin_errors::MyAddressError; use crate::solana::solana_common::{ui_amount_to_amount, PrepareTransferData, SufficientBalanceError}; use crate::solana::{solana_common, AccountError, SolanaCommonOps, SolanaFeeDetails}; -use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, FeeApproxStage, FoundSwapTxSpend, - MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, - RawTransactionFut, RawTransactionRequest, RefundError, RefundResult, SearchForSwapTxSpendInput, - SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, - SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, - SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, SignatureResult, SolanaCoin, - TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, - TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest, - WithdrawResult}; +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, + FoundSwapTxSpend, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, + PaymentInstructionsErr, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignatureResult, SolanaCoin, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionDetails, TransactionFut, TransactionType, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + VerificationResult, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bincode::serialize; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; @@ -267,14 +265,7 @@ impl MarketCoinOps for SplToken { self.platform_coin.send_raw_tx_bytes(tx) } - fn wait_for_confirmations( - &self, - _tx: &[u8], - _confirmations: u64, - _requires_nota: bool, - _wait_until: u64, - _check_every: u64, - ) -> Box + Send> { + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { unimplemented!() } @@ -309,35 +300,25 @@ impl MarketCoinOps for SplToken { impl SwapOps for SplToken { fn send_taker_fee(&self, _fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { unimplemented!() } - fn send_maker_payment(&self, _maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { unimplemented!() } + fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_payment(&self, _taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { unimplemented!() } + fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_spends_taker_payment( - &self, - _maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_spends_maker_payment( - &self, - _taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { - todo!() - } + fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { todo!() } - fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> Box + Send> { - unimplemented!() - } + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } @@ -368,7 +349,14 @@ impl SwapOps for SplToken { unimplemented!(); } - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, String> { + unimplemented!() + } fn is_auto_refundable(&self) -> bool { false } @@ -477,10 +465,7 @@ impl WatcherOps for SplToken { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -561,9 +546,17 @@ impl MmCoin for SplToken { fn mature_confirmations(&self) -> Option { Some(1) } - fn coin_protocol_info(&self) -> Vec { Vec::new() } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } - fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } fn on_disabled(&self) -> Result<(), AbortedError> { self.conf.abortable_system.abort_all() } diff --git a/mm2src/coins/tendermint/ibc/ibc_proto.rs b/mm2src/coins/tendermint/ibc/ibc_proto.rs new file mode 100644 index 0000000000..ceccb128f8 --- /dev/null +++ b/mm2src/coins/tendermint/ibc/ibc_proto.rs @@ -0,0 +1,20 @@ +#[derive(prost::Message)] +pub(crate) struct IBCTransferV1Proto { + #[prost(string, tag = "1")] + pub(crate) source_port: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) source_channel: prost::alloc::string::String, + #[prost(message, optional, tag = "3")] + pub(crate) token: Option, + #[prost(string, tag = "4")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "5")] + pub(crate) receiver: prost::alloc::string::String, + #[prost(message, optional, tag = "6")] + pub(crate) timeout_height: Option, + #[prost(uint64, tag = "7")] + pub(crate) timeout_timestamp: u64, + // Not supported by some of the cosmos chains like IRIS + // #[prost(string, optional, tag = "8")] + // pub(crate) memo: Option, +} diff --git a/mm2src/coins/tendermint/ibc/mod.rs b/mm2src/coins/tendermint/ibc/mod.rs new file mode 100644 index 0000000000..9e1c905398 --- /dev/null +++ b/mm2src/coins/tendermint/ibc/mod.rs @@ -0,0 +1,6 @@ +mod ibc_proto; +pub(crate) mod transfer_v1; + +pub(crate) const IBC_OUT_SOURCE_PORT: &str = "transfer"; +pub(crate) const IBC_OUT_TIMEOUT_IN_NANOS: u64 = 60000000000 * 15; // 15 minutes +pub(crate) const IBC_GAS_LIMIT_DEFAULT: u64 = 150_000; diff --git a/mm2src/coins/tendermint/ibc/transfer_v1.rs b/mm2src/coins/tendermint/ibc/transfer_v1.rs new file mode 100644 index 0000000000..34f693aaaa --- /dev/null +++ b/mm2src/coins/tendermint/ibc/transfer_v1.rs @@ -0,0 +1,108 @@ +use super::{ibc_proto::IBCTransferV1Proto, IBC_OUT_SOURCE_PORT, IBC_OUT_TIMEOUT_IN_NANOS}; +use crate::tendermint::type_urls::IBC_TRANSFER_TYPE_URL; +use common::number_type_casting::SafeTypeCastingNumbers; +use cosmrs::{tx::{Msg, MsgProto}, + AccountId, Coin, ErrorReport}; +use std::convert::TryFrom; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct MsgTransfer { + /// the port on which the packet will be sent + pub(crate) source_port: String, + /// the channel by which the packet will be sent + pub(crate) source_channel: String, + /// the tokens to be transferred + pub(crate) token: Coin, + /// the sender address + pub(crate) sender: AccountId, + /// the recipient address on the destination chain + pub(crate) receiver: AccountId, + /// Timeout height relative to the current block height. + /// The timeout is disabled when set to 0. + pub(crate) timeout_height: Option, + /// Timeout timestamp in absolute nanoseconds since unix epoch. + /// The timeout is disabled when set to 0. + pub(crate) timeout_timestamp: u64, + // Not supported by some of the cosmos chains like IRIS + // pub(crate) memo: Option, +} + +impl MsgTransfer { + pub(crate) fn new_with_default_timeout( + source_channel: String, + sender: AccountId, + receiver: AccountId, + token: Coin, + ) -> Self { + let timestamp_as_nanos: u64 = common::get_local_duration_since_epoch() + .expect("get_local_duration_since_epoch shouldn't fail") + .as_nanos() + .into_or_max(); + + Self { + source_port: IBC_OUT_SOURCE_PORT.to_owned(), + source_channel, + sender, + receiver, + token, + timeout_height: None, + timeout_timestamp: timestamp_as_nanos + IBC_OUT_TIMEOUT_IN_NANOS, + // memo: Some(memo.clone()), + } + } +} + +impl Msg for MsgTransfer { + type Proto = IBCTransferV1Proto; +} + +impl TryFrom for MsgTransfer { + type Error = ErrorReport; + + #[inline(always)] + fn try_from(proto: IBCTransferV1Proto) -> Result { MsgTransfer::try_from(&proto) } +} + +impl TryFrom<&IBCTransferV1Proto> for MsgTransfer { + type Error = ErrorReport; + + fn try_from(proto: &IBCTransferV1Proto) -> Result { + Ok(MsgTransfer { + source_port: proto.source_port.to_owned(), + source_channel: proto.source_channel.to_owned(), + token: proto + .token + .to_owned() + .map(TryFrom::try_from) + .ok_or_else(|| ErrorReport::msg("token can't be empty"))??, + sender: proto.sender.parse()?, + receiver: proto.receiver.parse()?, + timeout_height: None, + timeout_timestamp: proto.timeout_timestamp, + // memo: proto.memo.to_owned(), + }) + } +} + +impl From for IBCTransferV1Proto { + fn from(coin: MsgTransfer) -> IBCTransferV1Proto { IBCTransferV1Proto::from(&coin) } +} + +impl From<&MsgTransfer> for IBCTransferV1Proto { + fn from(msg: &MsgTransfer) -> IBCTransferV1Proto { + IBCTransferV1Proto { + source_port: msg.source_port.to_owned(), + source_channel: msg.source_channel.to_owned(), + token: Some(msg.token.to_owned().into()), + sender: msg.sender.to_string(), + receiver: msg.receiver.to_string(), + timeout_height: None, + timeout_timestamp: msg.timeout_timestamp, + // memo: msg.memo.to_owned(), + } + } +} + +impl MsgProto for IBCTransferV1Proto { + const TYPE_URL: &'static str = IBC_TRANSFER_TYPE_URL; +} diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index 6e904c4567..d480a4964e 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -2,6 +2,7 @@ // Useful resources // https://docs.cosmos.network/ +mod ibc; mod iris; mod rpc; mod tendermint_coin; @@ -25,6 +26,8 @@ pub(crate) const TENDERMINT_COIN_PROTOCOL_TYPE: &str = "TENDERMINT"; pub(crate) const TENDERMINT_ASSET_PROTOCOL_TYPE: &str = "TENDERMINTTOKEN"; pub(crate) mod type_urls { + pub(crate) const IBC_TRANSFER_TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; + pub(crate) const CREATE_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgCreateHTLC"; pub(crate) const CLAIM_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgClaimHTLC"; } diff --git a/mm2src/coins/tendermint/rpc/mod.rs b/mm2src/coins/tendermint/rpc/mod.rs index bd34834ce9..26ccdf6553 100644 --- a/mm2src/coins/tendermint/rpc/mod.rs +++ b/mm2src/coins/tendermint/rpc/mod.rs @@ -1,9 +1,10 @@ #[cfg(not(target_arch = "wasm32"))] mod tendermint_native_rpc; #[cfg(not(target_arch = "wasm32"))] -pub use tendermint_native_rpc::*; +pub(crate) use tendermint_native_rpc::*; #[cfg(target_arch = "wasm32")] mod tendermint_wasm_rpc; -#[cfg(target_arch = "wasm32")] pub use tendermint_wasm_rpc::*; +#[cfg(target_arch = "wasm32")] +pub(crate) use tendermint_wasm_rpc::*; pub(crate) const TX_SUCCESS_CODE: u32 = 0; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index cb70b0b8fb..594508a3af 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1,21 +1,27 @@ +use super::ibc::transfer_v1::MsgTransfer; +use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::iris::htlc::{IrisHtlc, MsgClaimHtlc, MsgCreateHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, HTLC_STATE_REFUNDED}; use super::iris::htlc_proto::{CreateHtlcProtoRep, QueryHtlcRequestProto, QueryHtlcResponseProto}; use super::rpc::*; use crate::coin_errors::{MyAddressError, ValidatePaymentError}; +use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, + IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequest, + IBCTransferChannelsRequestError, IBCTransferChannelsResponse, + IBCTransferChannelsResult, IBCWithdrawRequest, CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, CHAIN_REGISTRY_REPO_NAME, CHAIN_REGISTRY_REPO_OWNER}; +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, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, - MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, - RawTransactionRequest, RawTransactionRes, RefundError, RefundResult, RpcCommonOps, - SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, - SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, - SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, - SignatureError, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionErr, TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, + CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, + MakerSwapTakerCoin, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PaymentInstructions, + PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, + RawTransactionFut, RawTransactionRequest, RawTransactionRes, RefundError, RefundPaymentArgs, RefundResult, + RpcCommonOps, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, + TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, + TransactionEnum, TransactionErr, TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, @@ -51,6 +57,7 @@ use itertools::Itertools; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; use mm2_number::MmNumber; use parking_lot::Mutex as PaMutex; use primitives::hash::H256; @@ -92,9 +99,11 @@ pub(crate) const TX_DEFAULT_MEMO: &str = ""; const MAX_TIME_LOCK: i64 = 34560; const MIN_TIME_LOCK: i64 = 50; +const ACCOUNT_SEQUENCE_ERR: &str = "incorrect account sequence"; + #[async_trait] pub trait TendermintCommons { - fn platform_denom(&self) -> String; + fn platform_denom(&self) -> &Denom; fn set_history_sync_state(&self, new_state: HistorySyncState); @@ -121,6 +130,7 @@ pub struct TendermintProtocolInfo { pub account_prefix: String, chain_id: String, gas_price: Option, + chain_registry_name: Option, } #[derive(Clone)] @@ -214,13 +224,13 @@ pub struct TendermintCoinImpl { pub(super) denom: Denom, chain_id: ChainId, gas_price: Option, - pub(super) sequence_lock: AsyncMutex<()>, pub(crate) tokens_info: PaMutex>, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation /// or on [`MmArc::stop`]. pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, + chain_registry_name: Option, } #[derive(Clone)] @@ -265,6 +275,7 @@ pub enum TendermintCoinRpcError { InvalidResponse(String), PerformError(String), RpcClientError(String), + InternalError(String), } impl From for TendermintCoinRpcError { @@ -282,6 +293,7 @@ impl From for BalanceError { TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e.to_string()), TendermintCoinRpcError::PerformError(e) => BalanceError::Transport(e), TendermintCoinRpcError::RpcClientError(e) => BalanceError::Transport(e), + TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), } } } @@ -293,6 +305,7 @@ impl From for ValidatePaymentError { TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e.to_string()), TendermintCoinRpcError::PerformError(e) => ValidatePaymentError::Transport(e), TendermintCoinRpcError::RpcClientError(e) => ValidatePaymentError::Transport(e), + TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), } } } @@ -390,7 +403,7 @@ impl From for SearchForSwapTxSpendErr { #[async_trait] impl TendermintCommons for TendermintCoin { - fn platform_denom(&self) -> String { self.denom.to_string() } + fn platform_denom(&self) -> &Denom { &self.denom } fn set_history_sync_state(&self, new_state: HistorySyncState) { *self.history_sync_state.lock().unwrap() = new_state; @@ -498,14 +511,234 @@ impl TendermintCoin { chain_id, gas_price: protocol_info.gas_price, avg_blocktime: conf.avg_blocktime, - sequence_lock: AsyncMutex::new(()), tokens_info: PaMutex::new(HashMap::new()), abortable_system, history_sync_state: Mutex::new(history_sync_state), client: TendermintRpcClient(AsyncMutex::new(client_impl)), + chain_registry_name: protocol_info.chain_registry_name, }))) } + pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { + let coin = self.clone(); + let fut = async move { + let to_address = + AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + + let (balance_denom, balance_dec) = coin + .get_balance_as_unsigned_and_decimal(&coin.denom, coin.decimals()) + .await?; + + // << BEGIN TX SIMULATION FOR FEE CALCULATION + let (amount_denom, amount_dec) = if req.max { + let amount_denom = balance_denom; + (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) + } else { + (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) + }; + + if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { + return MmError::err(WithdrawError::AmountTooLow { + amount: amount_dec, + threshold: coin.min_tx_amount(), + }); + } + + let received_by_me = if to_address == coin.account_id { + amount_dec + } else { + BigDecimal::default() + }; + + let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + + let msg_transfer = MsgTransfer::new_with_default_timeout( + req.ibc_source_channel.clone(), + coin.account_id.clone(), + to_address.clone(), + Coin { + denom: coin.denom.clone(), + amount: amount_denom.into(), + }, + ) + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let current_block = coin + .current_block() + .compat() + .await + .map_to_mm(WithdrawError::Transport)?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + // >> END TX SIMULATION FOR FEE CALCULATION + + let fee_amount_u64 = coin + .calculate_fee_amount_as_u64(msg_transfer.clone(), timeout_height, memo.clone()) + .await?; + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); + + let fee_amount = Coin { + denom: coin.denom.clone(), + amount: fee_amount_u64.into(), + }; + + let fee = Fee::from_amount_and_gas(fee_amount, IBC_GAS_LIMIT_DEFAULT); + + let (amount_denom, total_amount) = if req.max { + if balance_denom < fee_amount_u64 { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_dec, + required: fee_amount_dec, + }); + } + let amount_denom = balance_denom - fee_amount_u64; + (amount_denom, balance_dec) + } else { + let total = &req.amount + &fee_amount_dec; + if balance_dec < total { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: balance_dec, + required: total, + }); + } + + (sat_from_big_decimal(&req.amount, coin.decimals)?, total) + }; + + let msg_transfer = MsgTransfer::new_with_default_timeout( + req.ibc_source_channel.clone(), + coin.account_id.clone(), + to_address.clone(), + Coin { + denom: coin.denom.clone(), + amount: amount_denom.into(), + }, + ) + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let account_info = coin.my_account_info().await?; + let tx_raw = coin + .any_to_signed_raw_tx(account_info, msg_transfer, fee, timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let tx_bytes = tx_raw + .to_bytes() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let hash = sha256(&tx_bytes); + Ok(TransactionDetails { + tx_hash: hex::encode_upper(hash.as_slice()), + tx_hex: tx_bytes.into(), + from: vec![coin.account_id.to_string()], + to: vec![req.to], + my_balance_change: &received_by_me - &total_amount, + spent_by_me: total_amount.clone(), + total_amount, + received_by_me, + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: coin.ticker.clone(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit: IBC_GAS_LIMIT_DEFAULT, + })), + coin: coin.ticker.to_string(), + internal_id: hash.to_vec().into(), + kmd_rewards: None, + transaction_type: TransactionType::default(), + memo: Some(memo), + }) + }; + Box::new(fut.boxed().compat()) + } + + pub async fn get_ibc_transfer_channels(&self, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { + #[derive(Deserialize)] + struct ChainRegistry { + channels: Vec, + } + + #[derive(Deserialize)] + struct ChannelInfo { + channel_id: String, + port_id: String, + } + + #[derive(Deserialize)] + struct IbcChannel { + chain_1: ChannelInfo, + #[allow(dead_code)] + chain_2: ChannelInfo, + ordering: String, + version: String, + tags: Option, + } + + let src_chain_registry_name = self.chain_registry_name.as_ref().or_mm_err(|| { + IBCTransferChannelsRequestError::InternalError(format!( + "`chain_registry_name` is not set for '{}'", + self.platform_ticker() + )) + })?; + + let source_filename = format!( + "{}-{}.json", + src_chain_registry_name, req.destination_chain_registry_name + ); + + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = git_controller + .client + .get_file_metadata_list( + CHAIN_REGISTRY_REPO_OWNER, + CHAIN_REGISTRY_REPO_NAME, + CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, + ) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + let source_channel_file = metadata_list + .iter() + .find(|metadata| metadata.name == source_filename) + .or_mm_err(|| IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(source_filename))?; + + let mut registry_object = git_controller + .client + .deserialize_json_source::(source_channel_file.to_owned()) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + registry_object + .channels + .retain(|ch| ch.chain_1.port_id == *IBC_OUT_SOURCE_PORT); + + let result: Vec = registry_object + .channels + .iter() + .map(|ch| IBCTransferChannel { + channel_id: ch.chain_1.channel_id.clone(), + ordering: ch.ordering.clone(), + version: ch.version.clone(), + tags: ch.tags.clone().map(|t| IBCTransferChannelTag { + status: t.status, + preferred: t.preferred, + dex: t.dex, + }), + }) + .collect(); + + Ok(IBCTransferChannelsResponse { + ibc_transfer_channels: result, + }) + } + #[inline(always)] fn gas_price(&self) -> f64 { self.gas_price.unwrap_or(DEFAULT_GAS_PRICE) } @@ -578,7 +811,7 @@ impl TendermintCoin { amount: Vec, secret_hash: &[u8], ) -> String { - // Needs to be sorted if cointains multiple coins + // Needs to be sorted if contains multiple coins // let mut amount = amount; // amount.sort(); @@ -596,23 +829,82 @@ impl TendermintCoin { sha256(&htlc_id).to_string().to_uppercase() } + pub(super) async fn seq_safe_send_raw_tx_bytes( + &self, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> Result<(String, Raw), TransactionErr> { + let (tx_id, tx_raw) = loop { + let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( + try_tx_s!(self.my_account_info().await), + tx_payload.clone(), + fee.clone(), + timeout_height, + memo.clone(), + )); + + match self.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await { + Ok(tx_id) => break (tx_id, tx_raw), + Err(e) => { + if e.contains(ACCOUNT_SEQUENCE_ERR) { + debug!("Got wrong account sequence, trying again."); + continue; + } + + return Err(crate::TransactionErr::Plain(ERRL!("{}", e))); + }, + }; + }; + + Ok((tx_id, tx_raw)) + } + #[allow(deprecated)] pub(super) async fn calculate_fee( &self, - base_denom: Denom, - tx_bytes: Vec, + msg: Any, + timeout_height: u64, + memo: String, ) -> MmResult { let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); - let request = SimulateRequest { tx_bytes, tx: None }; - let request = AbciRequest::new( - Some(path), - request.encode_to_vec(), - ABCI_REQUEST_HEIGHT, - ABCI_REQUEST_PROVE, - ); - let raw_response = self.rpc_client().await?.perform(request).await?; - let response = SimulateResponse::decode(raw_response.response.value.as_slice())?; + let (response, raw_response) = loop { + let account_info = self.my_account_info().await?; + let tx_bytes = self + .gen_simulated_tx(account_info, msg.clone(), timeout_height, memo.clone()) + .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; + + let request = AbciRequest::new( + Some(path.clone()), + SimulateRequest { tx_bytes, tx: None }.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let raw_response = self.rpc_client().await?.perform(request).await?; + + if raw_response.response.log.to_string().contains(ACCOUNT_SEQUENCE_ERR) { + debug!("Got wrong account sequence, trying again."); + continue; + } + + match raw_response.response.code { + cosmrs::tendermint::abci::Code::Ok => {}, + cosmrs::tendermint::abci::Code::Err(ecode) => { + return MmError::err(TendermintCoinRpcError::InvalidResponse(format!( + "Could not read gas_info. Error code: {} Message: {}", + ecode, raw_response.response.log + ))); + }, + }; + + break ( + SimulateResponse::decode(raw_response.response.value.as_slice())?, + raw_response, + ); + }; let gas = response.gas_info.as_ref().ok_or_else(|| { TendermintCoinRpcError::InvalidResponse(format!( @@ -624,7 +916,7 @@ impl TendermintCoin { let amount = ((gas.gas_used as f64 * 1.5) * self.gas_price()).ceil(); let fee_amount = Coin { - denom: base_denom, + denom: self.platform_denom().clone(), amount: (amount as u64).into(), }; @@ -632,18 +924,49 @@ impl TendermintCoin { } #[allow(deprecated)] - pub(super) async fn calculate_fee_amount_as_u64(&self, tx_bytes: Vec) -> MmResult { + pub(super) async fn calculate_fee_amount_as_u64( + &self, + msg: Any, + timeout_height: u64, + memo: String, + ) -> MmResult { let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); - let request = SimulateRequest { tx_bytes, tx: None }; - let request = AbciRequest::new( - Some(path), - request.encode_to_vec(), - ABCI_REQUEST_HEIGHT, - ABCI_REQUEST_PROVE, - ); - let raw_response = self.rpc_client().await?.perform(request).await?; - let response = SimulateResponse::decode(raw_response.response.value.as_slice())?; + let (response, raw_response) = loop { + let account_info = self.my_account_info().await?; + let tx_bytes = self + .gen_simulated_tx(account_info, msg.clone(), timeout_height, memo.clone()) + .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; + + let request = AbciRequest::new( + Some(path.clone()), + SimulateRequest { tx_bytes, tx: None }.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); + + let raw_response = self.rpc_client().await?.perform(request).await?; + + if raw_response.response.log.to_string().contains(ACCOUNT_SEQUENCE_ERR) { + debug!("Got wrong account sequence, trying again."); + continue; + } + + match raw_response.response.code { + cosmrs::tendermint::abci::Code::Ok => {}, + cosmrs::tendermint::abci::Code::Err(ecode) => { + return MmError::err(TendermintCoinRpcError::InvalidResponse(format!( + "Could not read gas_info. Error code: {} Message: {}", + ecode, raw_response.response.log + ))); + }, + }; + + break ( + SimulateResponse::decode(raw_response.response.value.as_slice())?, + raw_response, + ); + }; let gas = response.gas_info.as_ref().ok_or_else(|| { TendermintCoinRpcError::InvalidResponse(format!( @@ -868,29 +1191,27 @@ impl TendermintCoin { let create_htlc_tx = try_tx_s!(coin.gen_create_htlc_tx(denom, &to, amount, &secret_hash, time_lock as u64)); - let _sequence_lock = coin.sequence_lock.lock().await; let current_block = try_tx_s!(coin.current_block().compat().await); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let account_info = try_tx_s!(coin.my_account_info().await); - let simulated_tx = try_tx_s!(coin.gen_simulated_tx( - account_info.clone(), - create_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - )); - - let fee = try_tx_s!(coin.calculate_fee(coin.denom.clone(), simulated_tx).await); - - let tx_raw = try_tx_s!(coin.any_to_signed_raw_tx( - account_info.clone(), - create_htlc_tx.msg_payload.clone(), - fee, - timeout_height, - TX_DEFAULT_MEMO.into(), - )); + let fee = try_tx_s!( + coin.calculate_fee( + create_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.to_owned() + ) + .await + ); - let _tx_id = try_tx_s!(coin.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await); + let (_tx_id, tx_raw) = try_tx_s!( + coin.seq_safe_send_raw_tx_bytes( + create_htlc_tx.msg_payload.clone(), + fee.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .await + ); Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { data: tx_raw.into(), @@ -927,30 +1248,18 @@ impl TendermintCoin { let coin = self.clone(); let fut = async move { - let _sequence_lock = coin.sequence_lock.lock().await; - let account_info = try_tx_s!(coin.my_account_info().await); - let current_block = try_tx_s!(coin.current_block().compat().await.map_to_mm(WithdrawError::Transport)); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let simulated_tx = try_tx_s!(coin.gen_simulated_tx( - account_info.clone(), - tx_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - )); - - let fee = try_tx_s!(coin.calculate_fee(coin.denom.clone(), simulated_tx).await); - - let tx_raw = try_tx_s!(coin - .any_to_signed_raw_tx(account_info, tx_payload, fee, timeout_height, memo) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))); - - let tx_bytes = try_tx_s!(tx_raw - .to_bytes() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))); + let fee = try_tx_s!( + coin.calculate_fee(tx_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned()) + .await + ); - let _tx_id = try_tx_s!(coin.send_raw_tx_bytes(&tx_bytes).compat().await); + let (_tx_id, tx_raw) = try_tx_s!( + coin.seq_safe_send_raw_tx_bytes(tx_payload.clone(), fee.clone(), timeout_height, memo.clone()) + .await + ); Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { data: tx_raw.into(), @@ -970,30 +1279,33 @@ impl TendermintCoin { decimals: u8, uuid: &[u8], denom: String, - ) -> Box + Send> { + ) -> ValidatePaymentFut<()> { let tx = match fee_tx { TransactionEnum::CosmosTransaction(tx) => tx.clone(), invalid_variant => { - return Box::new(futures01::future::err(ERRL!( - "Unexpected tx variant {:?}", - invalid_variant - ))) + return Box::new(futures01::future::err( + ValidatePaymentError::WrongPaymentTx(format!("Unexpected tx variant {:?}", invalid_variant)).into(), + )) }, }; - let uuid = try_fus!(Uuid::from_slice(uuid)).to_string(); + let uuid = try_f!(Uuid::from_slice(uuid).map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))) + .to_string(); + let sender_pubkey_hash = dhash160(expected_sender); - let expected_sender_address = - try_fus!(AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice())).to_string(); + let expected_sender_address = try_f!(AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice()) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))) + .to_string(); let dex_fee_addr_pubkey_hash = dhash160(fee_addr); - let expected_dex_fee_address = try_fus!(AccountId::new( + let expected_dex_fee_address = try_f!(AccountId::new( &self.account_prefix, dex_fee_addr_pubkey_hash.as_slice() - )) + ) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))) .to_string(); - let expected_amount = try_fus!(sat_from_big_decimal(amount, decimals)); + let expected_amount = try_f!(sat_from_big_decimal(amount, decimals)); let expected_amount = CoinProto { denom, amount: expected_amount.to_string(), @@ -1001,45 +1313,61 @@ impl TendermintCoin { let coin = self.clone(); let fut = async move { - let tx_body = try_s!(TxBody::decode(tx.data.body_bytes.as_slice())); + let tx_body = TxBody::decode(tx.data.body_bytes.as_slice()) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; if tx_body.messages.len() != 1 { - return ERR!("Tx body must have exactly one message"); + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Tx body must have exactly one message".to_string(), + )); } - let msg = try_s!(MsgSendProto::decode(tx_body.messages[0].value.as_slice())); + let msg = MsgSendProto::decode(tx_body.messages[0].value.as_slice()) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; if msg.to_address != expected_dex_fee_address { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Dex fee is sent to wrong address: {}, expected {}", - msg.to_address, - expected_dex_fee_address - ); + msg.to_address, expected_dex_fee_address + ))); } if msg.amount.len() != 1 { - return ERR!("Msg must have exactly one Coin"); + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Msg must have exactly one Coin".to_string(), + )); } if msg.amount[0] != expected_amount { - return ERR!("Invalid amount {:?}, expected {:?}", msg.amount[0], expected_amount); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid amount {:?}, expected {:?}", + msg.amount[0], expected_amount + ))); } if msg.from_address != expected_sender_address { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Invalid sender: {}, expected {}", - msg.from_address, - expected_sender_address - ); + msg.from_address, expected_sender_address + ))); } if tx_body.memo != uuid { - return ERR!("Invalid memo: {}, expected {}", msg.from_address, uuid); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Invalid memo: {}, expected {}", + msg.from_address, uuid + ))); } let encoded_tx = tx.data.encode_to_vec(); let hash = hex::encode_upper(sha256(&encoded_tx).as_slice()); - let encoded_from_rpc = try_s!(coin.request_tx(hash).await).encode_to_vec(); + let encoded_from_rpc = coin + .request_tx(hash) + .await + .map_err(|e| MmError::new(ValidatePaymentError::TxDeserializationError(e.into_inner().to_string())))? + .encode_to_vec(); if encoded_tx != encoded_from_rpc { - return ERR!("Transaction from RPC doesn't match the input"); + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Transaction from RPC doesn't match the input".to_string(), + )); } Ok(()) }; @@ -1156,23 +1484,15 @@ impl TendermintCoin { })?; let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let account_info = self.my_account_info().await?; - let simulated_tx = self - .gen_simulated_tx( - account_info.clone(), + let fee_uamount = self + .calculate_fee_amount_as_u64( create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO.to_owned(), ) - .map_err(|e| { - MmError::new(TradePreimageError::InternalError(format!( - "Tx simulation failed. {:?}", - e - ))) - })?; + .await?; - let fee_uamount = self.calculate_fee_amount_as_u64(simulated_tx).await?; let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, self.decimals); Ok(TradeFee { @@ -1201,7 +1521,6 @@ impl TendermintCoin { })?; let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let account_info = self.my_account_info().await?; let msg_send = MsgSend { from_address: self.account_id.clone(), @@ -1214,16 +1533,9 @@ impl TendermintCoin { .to_any() .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; - let simulated_tx = self - .gen_simulated_tx(account_info.clone(), msg_send, timeout_height, TX_DEFAULT_MEMO.into()) - .map_err(|e| { - MmError::new(TradePreimageError::InternalError(format!( - "Tx simulation failed. {:?}", - e - ))) - })?; - - let fee_uamount = self.calculate_fee_amount_as_u64(simulated_tx).await?; + let fee_uamount = self + .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned()) + .await?; let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, decimals); Ok(TradeFee { @@ -1233,6 +1545,17 @@ impl TendermintCoin { }) } + pub(super) async fn get_balance_as_unsigned_and_decimal( + &self, + denom: &Denom, + decimals: u8, + ) -> MmResult<(u64, BigDecimal), TendermintCoinRpcError> { + let denom_ubalance = self.balance_for_denom(denom.to_string()).await?; + let denom_balance_dec = big_decimal_from_sat_unsigned(denom_ubalance, decimals); + + Ok((denom_ubalance, denom_balance_dec)) + } + async fn request_tx(&self, hash: String) -> MmResult { let path = AbciPath::from_str(ABCI_GET_TX_PATH).expect("valid path"); let request = GetTxRequest { hash }; @@ -1400,6 +1723,46 @@ fn clients_from_urls(rpc_urls: &[String]) -> MmResult, Tendermin Ok(clients) } +pub async fn get_ibc_chain_list() -> IBCChainRegistriesResult { + fn map_metadata_to_chain_registry_name(metadata: &FileMetadata) -> Result> { + let split_filename_by_dash: Vec<&str> = metadata.name.split('-').collect(); + let chain_registry_name = split_filename_by_dash + .first() + .or_mm_err(|| { + IBCChainsRequestError::InternalError(format!( + "Could not read chain registry name from '{}'", + metadata.name + )) + })? + .to_string(); + + Ok(chain_registry_name) + } + + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = git_controller + .client + .get_file_metadata_list( + CHAIN_REGISTRY_REPO_OWNER, + CHAIN_REGISTRY_REPO_NAME, + CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, + ) + .await + .map_err(|e| IBCChainsRequestError::Transport(format!("{:?}", e)))?; + + let chain_list: Result, MmError> = + metadata_list.iter().map(map_metadata_to_chain_registry_name).collect(); + + let mut distinct_chain_list = chain_list?; + distinct_chain_list.dedup(); + + Ok(IBCChainRegistriesResponse { + chain_registry_list: distinct_chain_list, + }) +} + #[async_trait] #[allow(unused_variables)] impl MmCoin for TendermintCoin { @@ -1418,25 +1781,19 @@ impl MmCoin for TendermintCoin { coin.account_prefix ))); } - let balance_denom = coin.balance_for_denom(coin.denom.to_string()).await?; - let balance_dec = big_decimal_from_sat_unsigned(balance_denom, coin.decimals); + + let (balance_denom, balance_dec) = coin + .get_balance_as_unsigned_and_decimal(&coin.denom, coin.decimals()) + .await?; // << BEGIN TX SIMULATION FOR FEE CALCULATION - let (amount_denom, amount_dec, total_amount) = if req.max { + let (amount_denom, amount_dec) = if req.max { let amount_denom = balance_denom; - ( - amount_denom, - big_decimal_from_sat_unsigned(amount_denom, coin.decimals), - balance_dec.clone(), - ) + (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) } else { let total = req.amount.clone(); - ( - sat_from_big_decimal(&req.amount, coin.decimals)?, - req.amount.clone(), - total, - ) + (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) }; if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { @@ -1470,17 +1827,12 @@ impl MmCoin for TendermintCoin { .await .map_to_mm(WithdrawError::Transport)?; - let _sequence_lock = coin.sequence_lock.lock().await; - let account_info = coin.my_account_info().await?; - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - - let simulated_tx = coin - .gen_simulated_tx(account_info.clone(), msg_send.clone(), timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; // >> END TX SIMULATION FOR FEE CALCULATION - let fee_amount_u64 = coin.calculate_fee_amount_as_u64(simulated_tx).await?; + let fee_amount_u64 = coin + .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, memo.clone()) + .await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); let fee_amount = Coin { @@ -1490,7 +1842,7 @@ impl MmCoin for TendermintCoin { let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); - let (amount_denom, amount_dec, total_amount) = if req.max { + let (amount_denom, total_amount) = if req.max { if balance_denom < fee_amount_u64 { return MmError::err(WithdrawError::NotSufficientBalance { coin: coin.ticker.clone(), @@ -1499,11 +1851,7 @@ impl MmCoin for TendermintCoin { }); } let amount_denom = balance_denom - fee_amount_u64; - ( - amount_denom, - big_decimal_from_sat_unsigned(amount_denom, coin.decimals), - balance_dec, - ) + (amount_denom, balance_dec) } else { let total = &req.amount + &fee_amount_dec; if balance_dec < total { @@ -1514,11 +1862,7 @@ impl MmCoin for TendermintCoin { }); } - ( - sat_from_big_decimal(&req.amount, coin.decimals)?, - req.amount.clone(), - total, - ) + (sat_from_big_decimal(&req.amount, coin.decimals)?, total) }; let msg_send = MsgSend { @@ -1532,6 +1876,7 @@ impl MmCoin for TendermintCoin { .to_any() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let account_info = coin.my_account_info().await?; let tx_raw = coin .any_to_signed_raw_tx(account_info, msg_send, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; @@ -1679,9 +2024,17 @@ impl MmCoin for TendermintCoin { fn mature_confirmations(&self) -> Option { None } - fn coin_protocol_info(&self) -> Vec { Vec::new() } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } - fn is_coin_protocol_supported(&self, info: &Option>) -> bool { true } + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.abortable_system) } @@ -1736,6 +2089,8 @@ impl MarketCoinOps for TendermintCoin { self.send_raw_tx_bytes(&tx_bytes) } + /// Consider using `seq_safe_raw_tx_bytes` instead. + /// This is considered as unsafe due to sequence mismatches. fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { // as sanity check try_fus!(Raw::from_bytes(tx)); @@ -1748,6 +2103,18 @@ impl MarketCoinOps for TendermintCoin { .broadcast_tx_commit(tx_bytes.into()) .await ); + + if broadcast_res.check_tx.log.to_string().contains(ACCOUNT_SEQUENCE_ERR) + || broadcast_res.deliver_tx.log.to_string().contains(ACCOUNT_SEQUENCE_ERR) + { + return ERR!( + "{}. check_tx log: {}, deliver_tx log: {}", + ACCOUNT_SEQUENCE_ERR, + broadcast_res.check_tx.log, + broadcast_res.deliver_tx.log + ); + } + if !broadcast_res.check_tx.code.is_ok() { return ERR!("Tx check failed {:?}", broadcast_res.check_tx); } @@ -1760,26 +2127,19 @@ impl MarketCoinOps for TendermintCoin { Box::new(fut.boxed().compat()) } - fn wait_for_confirmations( - &self, - tx_bytes: &[u8], - _confirmations: u64, - _requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { // Sanity check - let _: TxRaw = try_fus!(Message::decode(tx_bytes)); + let _: TxRaw = try_fus!(Message::decode(input.payment_tx.as_slice())); - let tx_hash = hex::encode_upper(sha256(tx_bytes)); + let tx_hash = hex::encode_upper(sha256(&input.payment_tx)); let coin = self.clone(); let fut = async move { loop { - if now_ms() / 1000 > wait_until { + if now_ms() / 1000 > input.wait_until { return ERR!( "Waited too long until {} for payment {} to be received", - wait_until, + input.wait_until, tx_hash.clone() ); } @@ -1796,7 +2156,7 @@ impl MarketCoinOps for TendermintCoin { }; }; - Timer::sleep(check_every as f64).await; + Timer::sleep(input.check_every as f64).await; } }; @@ -1888,7 +2248,7 @@ impl SwapOps for TendermintCoin { self.send_taker_fee_for_denom(fee_addr, amount, self.denom.clone(), self.decimals, uuid) } - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs) -> TransactionFut { self.send_htlc_for_denom( maker_payment_args.time_lock_duration, maker_payment_args.other_pubkey, @@ -1899,7 +2259,7 @@ impl SwapOps for TendermintCoin { ) } - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs) -> TransactionFut { self.send_htlc_for_denom( taker_payment_args.time_lock_duration, taker_payment_args.other_pubkey, @@ -1910,10 +2270,7 @@ impl SwapOps for TendermintCoin { ) } - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { let tx = try_tx_fus!(cosmrs::Tx::from_bytes(maker_spends_payment_args.other_payment_tx)); let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(prost::Message::decode(msg.value.as_slice())); @@ -1935,30 +2292,27 @@ impl SwapOps for TendermintCoin { let coin = self.clone(); let fut = async move { - let _sequence_lock = coin.sequence_lock.lock().await; let current_block = try_tx_s!(coin.current_block().compat().await); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let account_info = try_tx_s!(coin.my_account_info().await); - - let simulated_tx = try_tx_s!(coin.gen_simulated_tx( - account_info.clone(), - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - )); - - let fee = try_tx_s!(coin.calculate_fee(coin.denom.clone(), simulated_tx).await); - - let tx_raw = try_tx_s!(coin.any_to_signed_raw_tx( - account_info, - claim_htlc_tx.msg_payload, - fee, - timeout_height, - TX_DEFAULT_MEMO.into(), - )); + let fee = try_tx_s!( + coin.calculate_fee( + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.to_owned() + ) + .await + ); - let tx_id = try_tx_s!(coin.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await); + let (_tx_id, tx_raw) = try_tx_s!( + coin.seq_safe_send_raw_tx_bytes( + claim_htlc_tx.msg_payload.clone(), + fee.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .await + ); Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { data: tx_raw.into(), @@ -1968,10 +2322,7 @@ impl SwapOps for TendermintCoin { Box::new(fut.boxed().compat()) } - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { let tx = try_tx_fus!(cosmrs::Tx::from_bytes(taker_spends_payment_args.other_payment_tx)); let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(prost::Message::decode(msg.value.as_slice())); @@ -1993,30 +2344,27 @@ impl SwapOps for TendermintCoin { let coin = self.clone(); let fut = async move { - let _sequence_lock = coin.sequence_lock.lock().await; let current_block = try_tx_s!(coin.current_block().compat().await); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let account_info = try_tx_s!(coin.my_account_info().await); - - let simulated_tx = try_tx_s!(coin.gen_simulated_tx( - account_info.clone(), - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - )); - - let fee = try_tx_s!(coin.calculate_fee(coin.denom.clone(), simulated_tx).await); - - let tx_raw = try_tx_s!(coin.any_to_signed_raw_tx( - account_info, - claim_htlc_tx.msg_payload, - fee, - timeout_height, - TX_DEFAULT_MEMO.into(), - )); + let fee = try_tx_s!( + coin.calculate_fee( + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .await + ); - let tx_id = try_tx_s!(coin.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await); + let (tx_id, tx_raw) = try_tx_s!( + coin.seq_safe_send_raw_tx_bytes( + claim_htlc_tx.msg_payload.clone(), + fee.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + ) + .await + ); Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { data: tx_raw.into(), @@ -2026,19 +2374,19 @@ impl SwapOps for TendermintCoin { Box::new(fut.boxed().compat()) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { Box::new(futures01::future::err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund IRIS HTLC".into(), ))) } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { Box::new(futures01::future::err(TransactionErr::Plain( "Doesn't need transaction broadcast to refund IRIS HTLC".into(), ))) } - fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { self.validate_fee_for_denom( validate_fee_args.fee_tx, validate_fee_args.expected_sender, @@ -2085,7 +2433,12 @@ impl SwapOps for TendermintCoin { self.search_for_swap_tx_spend(input).await.map_err(|e| e.to_string()) } - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, 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.")); let htlc_proto: super::iris::htlc_proto::ClaimHtlcProtoRep = @@ -2214,10 +2567,7 @@ impl WatcherOps for TendermintCoin { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -2324,6 +2674,7 @@ pub mod tendermint_coin_tests { account_prefix: String::from("iaa"), chain_id: String::from("nyancat-9"), gas_price: None, + chain_registry_name: None, } } @@ -2334,6 +2685,7 @@ pub mod tendermint_coin_tests { account_prefix: String::from("iaa"), chain_id: String::from("nyancat-9"), gas_price: None, + chain_registry_name: None, } } @@ -2351,7 +2703,7 @@ pub mod tendermint_coin_tests { fn test_htlc_create_and_claim() { let rpc_urls = vec![IRIS_TESTNET_RPC_URL.to_string()]; - let protocol_conf = get_iris_usdc_ibc_protocol(); + let protocol_conf = get_iris_protocol(); let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); @@ -2365,7 +2717,7 @@ pub mod tendermint_coin_tests { let coin = block_on(TendermintCoin::init( &ctx, - "USDC-IBC".to_string(), + "IRIS".to_string(), conf, protocol_conf, rpc_urls, @@ -2375,7 +2727,6 @@ pub mod tendermint_coin_tests { .unwrap(); // << BEGIN HTLC CREATION - let base_denom: Denom = "unyan".parse().unwrap(); let to: AccountId = IRIS_TESTNET_HTLC_PAIR2_ADDRESS.parse().unwrap(); const UAMOUNT: u64 = 1; let amount: cosmrs::Decimal = UAMOUNT.into(); @@ -2391,33 +2742,22 @@ pub mod tendermint_coin_tests { let current_block = block_on(async { current_block_fut.await.unwrap() }); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let account_info_fut = coin.my_account_info(); - let account_info = block_on(async { account_info_fut.await.unwrap() }); - - let simulated_tx = coin - .gen_simulated_tx( - account_info.clone(), + let fee = block_on(async { + coin.calculate_fee( create_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), - ) - .unwrap(); - - let fee = block_on(async { coin.calculate_fee(base_denom.clone(), simulated_tx).await.unwrap() }); - - let raw_tx = block_on(async { - coin.any_to_signed_raw_tx( - account_info.clone(), - create_htlc_tx.msg_payload.clone(), - fee, - timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO.to_owned(), ) + .await .unwrap() }); - let tx_bytes = raw_tx.to_bytes().unwrap(); - let send_tx_fut = coin.send_raw_tx_bytes(&tx_bytes).compat(); + let send_tx_fut = coin.seq_safe_send_raw_tx_bytes( + create_htlc_tx.msg_payload.clone(), + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + ); block_on(async { send_tx_fut.await.unwrap(); }); @@ -2446,36 +2786,22 @@ pub mod tendermint_coin_tests { let current_block = common::block_on(async { current_block_fut.await.unwrap() }); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let account_info_fut = coin.my_account_info(); - let account_info = block_on(async { account_info_fut.await.unwrap() }); - - let simulated_tx = coin - .gen_simulated_tx( - account_info.clone(), + let fee = block_on(async { + coin.calculate_fee( claim_htlc_tx.msg_payload.clone(), timeout_height, - TX_DEFAULT_MEMO.into(), + TX_DEFAULT_MEMO.to_owned(), ) - .unwrap(); + .await + .unwrap() + }); - let fee = block_on(async { coin.calculate_fee(base_denom.clone(), simulated_tx).await.unwrap() }); + let send_tx_fut = + coin.seq_safe_send_raw_tx_bytes(claim_htlc_tx.msg_payload, fee, timeout_height, TX_DEFAULT_MEMO.into()); - let raw_tx = coin - .any_to_signed_raw_tx( - account_info, - claim_htlc_tx.msg_payload, - fee, - timeout_height, - TX_DEFAULT_MEMO.into(), - ) - .unwrap(); + let (tx_id, _tx_raw) = block_on(async { send_tx_fut.await.unwrap() }); - let tx_bytes = raw_tx.to_bytes().unwrap(); - let send_tx_fut = coin.send_raw_tx_bytes(&tx_bytes).compat(); - block_on(async { - send_tx_fut.await.unwrap(); - }); - println!("Claim HTLC tx hash {}", hex::encode_upper(sha256(&tx_bytes).as_slice())); + println!("Claim HTLC tx hash {}", tx_id); // >> END HTLC CLAIMING } @@ -2605,7 +2931,7 @@ pub mod tendermint_coin_tests { // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 let expected_spend_hash = "565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137"; let hash = spend_tx.tx_hash(); - assert_eq!(hex::encode_upper(&hash.0), expected_spend_hash); + assert_eq!(hex::encode_upper(hash.0), expected_spend_hash); } #[test] @@ -2646,7 +2972,7 @@ pub mod tendermint_coin_tests { }); let invalid_amount = 1.into(); - let validate_err = coin + let error = coin .validate_fee(ValidateFeeArgs { fee_tx: &create_htlc_tx, expected_sender: &[], @@ -2656,9 +2982,18 @@ pub mod tendermint_coin_tests { uuid: &[1; 16], }) .wait() - .unwrap_err(); - println!("{}", validate_err); - assert!(validate_err.contains("failed to decode Protobuf message: MsgSend.amount")); + .unwrap_err() + .into_inner(); + println!("{}", error); + match error { + ValidatePaymentError::TxDeserializationError(err) => { + assert!(err.contains("failed to decode Protobuf message: MsgSend.amount")) + }, + _ => panic!( + "Expected `WrongPaymentTx` MsgSend.amount decode failure, found {:?}", + error + ), + } // just a random transfer tx not related to AtomicDEX, should fail on recipient address check // https://nyancat.iobscan.io/#/tx?txHash=65815814E7D74832D87956144C1E84801DC94FE9A509D207A0ABC3F17775E5DF @@ -2671,7 +3006,7 @@ pub mod tendermint_coin_tests { data: TxRaw::decode(random_transfer_tx_bytes.as_slice()).unwrap(), }); - let validate_err = coin + let error = coin .validate_fee(ValidateFeeArgs { fee_tx: &random_transfer_tx, expected_sender: &[], @@ -2681,9 +3016,13 @@ pub mod tendermint_coin_tests { uuid: &[1; 16], }) .wait() - .unwrap_err(); - println!("{}", validate_err); - assert!(validate_err.contains("sent to wrong address")); + .unwrap_err() + .into_inner(); + println!("{}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("sent to wrong address")), + _ => panic!("Expected `WrongPaymentTx` wrong address, found {:?}", error), + } // dex fee tx sent during real swap // https://nyancat.iobscan.io/#/tx?txHash=8AA6B9591FE1EE93C8B89DE4F2C59B2F5D3473BD9FB5F3CFF6A5442BEDC881D7 @@ -2700,7 +3039,7 @@ pub mod tendermint_coin_tests { data: TxRaw::decode(dex_fee_tx.encode_to_vec().as_slice()).unwrap(), }); - let validate_err = coin + let error = coin .validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &[], @@ -2710,13 +3049,17 @@ pub mod tendermint_coin_tests { uuid: &[1; 16], }) .wait() - .unwrap_err(); - println!("{}", validate_err); - assert!(validate_err.contains("Invalid amount")); + .unwrap_err() + .into_inner(); + println!("{}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Invalid amount")), + _ => panic!("Expected `WrongPaymentTx` invalid amount, found {:?}", error), + } let valid_amount: BigDecimal = "0.0001".parse().unwrap(); // valid amount but invalid sender - let validate_err = coin + let error = coin .validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, @@ -2726,12 +3069,16 @@ pub mod tendermint_coin_tests { uuid: &[1; 16], }) .wait() - .unwrap_err(); - println!("{}", validate_err); - assert!(validate_err.contains("Invalid sender")); + .unwrap_err() + .into_inner(); + println!("{}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Invalid sender")), + _ => panic!("Expected `WrongPaymentTx` invalid sender, found {:?}", error), + } // invalid memo - let validate_err = coin + let error = coin .validate_fee(ValidateFeeArgs { fee_tx: &dex_fee_tx, expected_sender: &pubkey, @@ -2741,9 +3088,13 @@ pub mod tendermint_coin_tests { uuid: &[1; 16], }) .wait() - .unwrap_err(); - println!("{}", validate_err); - assert!(validate_err.contains("Invalid memo")); + .unwrap_err() + .into_inner(); + println!("{}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Invalid memo")), + _ => panic!("Expected `WrongPaymentTx` invalid memo, found {:?}", error), + } // https://nyancat.iobscan.io/#/tx?txHash=5939A9D1AF57BB828714E0C4C4D7F2AEE349BB719B0A1F25F8FBCC3BB227C5F9 let fee_with_memo_hash = "5939A9D1AF57BB828714E0C4C4D7F2AEE349BB719B0A1F25F8FBCC3BB227C5F9"; @@ -2822,6 +3173,7 @@ pub mod tendermint_coin_tests { try_spv_proof_until: 0, confirmations: 0, unique_swap_data: Vec::new(), + min_watcher_reward: None, }; let validate_err = coin.validate_taker_payment(input).wait().unwrap_err(); match validate_err.into_inner() { @@ -2847,6 +3199,7 @@ pub mod tendermint_coin_tests { try_spv_proof_until: 0, confirmations: 0, unique_swap_data: Vec::new(), + min_watcher_reward: None, }; let validate_err = block_on( coin.validate_payment_for_denom(input, "nim".parse().unwrap(), 6) @@ -2919,6 +3272,7 @@ pub mod tendermint_coin_tests { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let spend_tx = match block_on(coin.search_for_swap_tx_spend_my(input)).unwrap().unwrap() { @@ -2929,7 +3283,7 @@ pub mod tendermint_coin_tests { // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 let expected_spend_hash = "565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137"; let hash = spend_tx.tx_hash(); - assert_eq!(hex::encode_upper(&hash.0), expected_spend_hash); + assert_eq!(hex::encode_upper(hash.0), expected_spend_hash); } #[test] @@ -2992,6 +3346,7 @@ pub mod tendermint_coin_tests { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; match block_on(coin.search_for_swap_tx_spend_my(input)).unwrap().unwrap() { @@ -3087,11 +3442,14 @@ pub mod tendermint_coin_tests { .unwrap() .encode_to_vec(); - block_on( - coin.wait_for_confirmations(&tx_bytes, 0, false, wait_until(), CHECK_INTERVAL) - .compat(), - ) - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx_bytes, + confirmations: 0, + requires_nota: false, + wait_until: wait_until(), + check_every: CHECK_INTERVAL, + }; + block_on(coin.wait_for_confirmations(confirm_payment_input).compat()).unwrap(); } for failed_tx_hash in FAILED_TX_HASH_SAMPLES { @@ -3099,11 +3457,14 @@ pub mod tendermint_coin_tests { .unwrap() .encode_to_vec(); - block_on( - coin.wait_for_confirmations(&tx_bytes, 0, false, wait_until(), CHECK_INTERVAL) - .compat(), - ) - .unwrap_err(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx_bytes, + confirmations: 0, + requires_nota: false, + wait_until: wait_until(), + check_every: CHECK_INTERVAL, + }; + block_on(coin.wait_for_confirmations(confirm_payment_input).compat()).unwrap_err(); } } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 3a0b6af9f1..54dc2dbc28 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -1,17 +1,20 @@ -/// Module containing implementation for Tendermint Tokens. They include native assets + IBC +//! Module containing implementation for Tendermint Tokens. They include native assets + IBC + +use super::ibc::transfer_v1::MsgTransfer; +use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; +use crate::rpc_command::tendermint::IBCWithdrawRequest; 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, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, - MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, NegotiateSwapContractAddrErr, - PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, RawTransactionRequest, RefundError, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, - SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, - SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, - SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, - TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, + RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, + TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, TxFeeDetails, + TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; @@ -97,6 +100,136 @@ impl TendermintToken { }; Ok(TendermintToken(Arc::new(token_impl))) } + + pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { + let platform = self.platform_coin.clone(); + let token = self.clone(); + let fut = async move { + let to_address = + AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; + + let (base_denom_balance, base_denom_balance_dec) = platform + .get_balance_as_unsigned_and_decimal(&platform.denom, token.decimals()) + .await?; + + let (balance_denom, balance_dec) = platform + .get_balance_as_unsigned_and_decimal(&token.denom, token.decimals()) + .await?; + + let (amount_denom, amount_dec, total_amount) = if req.max { + ( + balance_denom, + big_decimal_from_sat_unsigned(balance_denom, token.decimals), + balance_dec, + ) + } else { + if balance_dec < req.amount { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: token.ticker.clone(), + available: balance_dec, + required: req.amount, + }); + } + + ( + sat_from_big_decimal(&req.amount, token.decimals())?, + req.amount.clone(), + req.amount, + ) + }; + + if !platform.is_tx_amount_enough(token.decimals, &amount_dec) { + return MmError::err(WithdrawError::AmountTooLow { + amount: amount_dec, + threshold: token.min_tx_amount(), + }); + } + + let received_by_me = if to_address == platform.account_id { + amount_dec + } else { + BigDecimal::default() + }; + + let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + + let msg_transfer = MsgTransfer::new_with_default_timeout( + req.ibc_source_channel.clone(), + platform.account_id.clone(), + to_address.clone(), + Coin { + denom: token.denom.clone(), + amount: amount_denom.into(), + }, + ) + .to_any() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let current_block = token + .current_block() + .compat() + .await + .map_to_mm(WithdrawError::Transport)?; + + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; + + let fee_amount_u64 = platform + .calculate_fee_amount_as_u64(msg_transfer.clone(), timeout_height, memo.clone()) + .await?; + + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); + + if base_denom_balance < fee_amount_u64 { + return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { + coin: platform.ticker().to_string(), + available: base_denom_balance_dec, + required: fee_amount_dec, + }); + } + + let fee_amount = Coin { + denom: platform.denom.clone(), + amount: fee_amount_u64.into(), + }; + + let fee = Fee::from_amount_and_gas(fee_amount, IBC_GAS_LIMIT_DEFAULT); + + let account_info = platform.my_account_info().await?; + let tx_raw = platform + .any_to_signed_raw_tx(account_info, msg_transfer, fee, timeout_height, memo.clone()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let tx_bytes = tx_raw + .to_bytes() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + + let hash = sha256(&tx_bytes); + Ok(TransactionDetails { + tx_hash: hex::encode_upper(hash.as_slice()), + tx_hex: tx_bytes.into(), + from: vec![platform.account_id.to_string()], + to: vec![req.to], + my_balance_change: &received_by_me - &total_amount, + spent_by_me: total_amount.clone(), + total_amount, + received_by_me, + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { + coin: platform.ticker().to_string(), + amount: fee_amount_dec, + uamount: fee_amount_u64, + gas_limit: IBC_GAS_LIMIT_DEFAULT, + })), + coin: token.ticker.clone(), + internal_id: hash.to_vec().into(), + kmd_rewards: None, + transaction_type: TransactionType::default(), + memo: Some(memo), + }) + }; + Box::new(fut.boxed().compat()) + } } #[async_trait] @@ -107,7 +240,7 @@ impl SwapOps for TendermintToken { .send_taker_fee_for_denom(fee_addr, amount, self.denom.clone(), self.decimals, uuid) } - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs) -> TransactionFut { self.platform_coin.send_htlc_for_denom( maker_payment_args.time_lock_duration, maker_payment_args.other_pubkey, @@ -118,7 +251,7 @@ impl SwapOps for TendermintToken { ) } - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs) -> TransactionFut { self.platform_coin.send_htlc_for_denom( taker_payment_args.time_lock_duration, taker_payment_args.other_pubkey, @@ -129,35 +262,29 @@ impl SwapOps for TendermintToken { ) } - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { self.platform_coin .send_maker_spends_taker_payment(maker_spends_payment_args) } - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { self.platform_coin .send_taker_spends_maker_payment(taker_spends_payment_args) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { Box::new(futures01::future::err(TransactionErr::Plain( "Doesn't need transaction broadcast to be refunded".into(), ))) } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { Box::new(futures01::future::err(TransactionErr::Plain( "Doesn't need transaction broadcast to be refunded".into(), ))) } - fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { self.platform_coin.validate_fee_for_denom( validate_fee_args.fee_tx, validate_fee_args.expected_sender, @@ -206,8 +333,15 @@ impl SwapOps for TendermintToken { self.platform_coin.search_for_swap_tx_spend_other(input).await } - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { - self.platform_coin.extract_secret(secret_hash, spend_tx).await + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, String> { + self.platform_coin + .extract_secret(secret_hash, spend_tx, watcher_reward) + .await } fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { @@ -327,10 +461,7 @@ impl WatcherOps for TendermintToken { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -391,16 +522,8 @@ impl MarketCoinOps for TendermintToken { self.platform_coin.send_raw_tx_bytes(tx) } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { - self.platform_coin - .wait_for_confirmations(tx, confirmations, requires_nota, wait_until, check_every) + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + self.platform_coin.wait_for_confirmations(input) } fn wait_for_htlc_tx_spend( @@ -456,11 +579,13 @@ impl MmCoin for TendermintToken { ))); } - let base_denom_balance = platform.balance_for_denom(platform.denom.to_string()).await?; - let base_denom_balance_dec = big_decimal_from_sat_unsigned(base_denom_balance, token.decimals()); + let (base_denom_balance, base_denom_balance_dec) = platform + .get_balance_as_unsigned_and_decimal(&platform.denom, token.decimals()) + .await?; - let balance_denom = platform.balance_for_denom(token.denom.to_string()).await?; - let balance_dec = big_decimal_from_sat_unsigned(balance_denom, token.decimals()); + let (balance_denom, balance_dec) = platform + .get_balance_as_unsigned_and_decimal(&token.denom, token.decimals()) + .await?; let (amount_denom, amount_dec, total_amount) = if req.max { ( @@ -515,16 +640,12 @@ impl MmCoin for TendermintToken { .await .map_to_mm(WithdrawError::Transport)?; - let _sequence_lock = platform.sequence_lock.lock().await; - let account_info = platform.my_account_info().await?; - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let simulated_tx = platform - .gen_simulated_tx(account_info.clone(), msg_send.clone(), timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let fee_amount_u64 = platform + .calculate_fee_amount_as_u64(msg_send.clone(), timeout_height, memo.clone()) + .await?; - let fee_amount_u64 = platform.calculate_fee_amount_as_u64(simulated_tx).await?; let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); if base_denom_balance < fee_amount_u64 { @@ -542,6 +663,7 @@ impl MmCoin for TendermintToken { let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); + let account_info = platform.my_account_info().await?; let tx_raw = platform .any_to_signed_raw_tx(account_info, msg_send, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; @@ -658,10 +780,19 @@ impl MmCoin for TendermintToken { fn mature_confirmations(&self) -> Option { None } - fn coin_protocol_info(&self) -> Vec { self.platform_coin.coin_protocol_info() } + fn coin_protocol_info(&self, amount_to_receive: Option) -> Vec { + self.platform_coin.coin_protocol_info(amount_to_receive) + } - fn is_coin_protocol_supported(&self, info: &Option>) -> bool { - self.platform_coin.is_coin_protocol_supported(info) + fn is_coin_protocol_supported( + &self, + info: &Option>, + amount_to_send: Option, + locktime: u64, + is_maker: bool, + ) -> bool { + self.platform_coin + .is_coin_protocol_supported(info, amount_to_send, locktime, is_maker) } fn on_disabled(&self) -> Result<(), AbortedError> { self.abortable_system.abort_all() } diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index f833594401..fa637e858a 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -652,7 +652,7 @@ where } let tx_sent_by_me = address == transfer_details.from; - let is_platform_coin_tx = transfer_details.denom == coin.platform_denom(); + let is_platform_coin_tx = transfer_details.denom == coin.platform_denom().to_string(); let is_self_tx = transfer_details.to == transfer_details.from && tx_sent_by_me; let is_sign_claim_htlc = tx_sent_by_me && matches!(transfer_details.transfer_event_type, TransferEventType::ClaimHtlc); diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 8b59b92cae..01e4dda0cc 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -3,15 +3,14 @@ use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TransactionEnum, TransactionFut}; use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, - FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, PaymentInstructions, - PaymentInstructionsErr, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, - SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, - SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, - SendWatcherRefundsPaymentArgs, SignatureResult, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; + ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, + PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, + TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use common::executor::AbortedError; use futures01::Future; @@ -65,14 +64,7 @@ impl MarketCoinOps for TestCoin { fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { unimplemented!() } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { unimplemented!() } @@ -108,35 +100,27 @@ impl MarketCoinOps for TestCoin { impl SwapOps for TestCoin { fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, uuid: &[u8]) -> TransactionFut { unimplemented!() } - fn send_maker_payment(&self, _maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { unimplemented!() } + fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_payment(&self, _taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { unimplemented!() } + fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_spends_taker_payment( - &self, - _maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_spends_maker_payment( - &self, - _taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + fn send_taker_refunds_payment(&self, _taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + fn send_maker_refunds_payment(&self, _maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!() } - fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> Box + Send> { - unimplemented!() - } + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } @@ -167,7 +151,14 @@ impl SwapOps for TestCoin { unimplemented!(); } - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { unimplemented!() } + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + watcher_reward: bool, + ) -> Result, String> { + unimplemented!() + } fn is_auto_refundable(&self) -> bool { false } @@ -273,10 +264,7 @@ impl WatcherOps for TestCoin { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -356,9 +344,17 @@ impl MmCoin for TestCoin { fn mature_confirmations(&self) -> Option { unimplemented!() } - fn coin_protocol_info(&self) -> Vec { Vec::new() } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } - fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } fn on_disabled(&self) -> Result<(), AbortedError> { Ok(()) } diff --git a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs index 69e9bb4eee..d160b68fa3 100644 --- a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs +++ b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs @@ -393,7 +393,7 @@ async fn test_get_raw_tx_bytes_on_add_transactions_impl() { let tx_hash = "6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"; - let maybe_tx_hex = storage.tx_bytes_from_cache(&wallet_id, &tx_hash).await.unwrap(); + let maybe_tx_hex = storage.tx_bytes_from_cache(&wallet_id, tx_hash).await.unwrap(); assert!(maybe_tx_hex.is_none()); let tx1 = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); @@ -409,11 +409,7 @@ async fn test_get_raw_tx_bytes_on_add_transactions_impl() { .await .unwrap(); - let tx_hex = storage - .tx_bytes_from_cache(&wallet_id, &tx_hash) - .await - .unwrap() - .unwrap(); + let tx_hex = storage.tx_bytes_from_cache(&wallet_id, tx_hash).await.unwrap().unwrap(); assert_eq!(tx_hex, expected_tx_hex); } diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs index 80d573de23..90ef077cde 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs @@ -60,8 +60,10 @@ impl HistoryId { fn new(ticker: &str, wallet_address: &str) -> HistoryId { HistoryId(format!("{}_{}", ticker, wallet_address)) } fn as_str(&self) -> &str { &self.0 } +} - fn to_string(&self) -> String { self.0.clone() } +impl std::fmt::Display for HistoryId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", &self.0) } } #[derive(Debug, Deserialize, Serialize)] @@ -74,13 +76,11 @@ impl TableSignature for TxHistoryTableV1 { fn table_name() -> &'static str { "tx_history" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_index("history_id", true)?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_index("history_id", true)?; } + Ok(()) } } @@ -94,7 +94,7 @@ mod tests { #[wasm_bindgen_test] async fn test_tx_history() { - const DB_NAME: &'static str = "TEST_TX_HISTORY"; + const DB_NAME: &str = "TEST_TX_HISTORY"; let db = TxHistoryDb::init(DbIdentifier::for_test(DB_NAME)) .await .expect("!TxHistoryDb::init_with_fs_path"); diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs index 12255a6a10..1b19565bf7 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs @@ -417,32 +417,29 @@ impl TableSignature for TxHistoryTableV2 { fn table_name() -> &'static str { "tx_history_v2" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_multi_index(TxHistoryTableV2::WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; - table.create_multi_index( - TxHistoryTableV2::WALLET_ID_INTERNAL_ID_INDEX, - &["coin", "hd_wallet_rmd160", "internal_id"], - true, - )?; - table.create_multi_index( - TxHistoryTableV2::WALLET_ID_TX_HASH_INDEX, - &["coin", "hd_wallet_rmd160", "tx_hash"], - false, - )?; - table.create_multi_index( - TxHistoryTableV2::WALLET_ID_CONFIRMATION_STATUS_INDEX, - &["coin", "hd_wallet_rmd160", "confirmation_status"], - false, - )?; - table.create_multi_index( - TxHistoryTableV2::WALLET_ID_TOKEN_ID_INDEX, - &["coin", "hd_wallet_rmd160", "token_id"], - false, - )?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(TxHistoryTableV2::WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; + table.create_multi_index( + TxHistoryTableV2::WALLET_ID_INTERNAL_ID_INDEX, + &["coin", "hd_wallet_rmd160", "internal_id"], + true, + )?; + table.create_multi_index( + TxHistoryTableV2::WALLET_ID_TX_HASH_INDEX, + &["coin", "hd_wallet_rmd160", "tx_hash"], + false, + )?; + table.create_multi_index( + TxHistoryTableV2::WALLET_ID_CONFIRMATION_STATUS_INDEX, + &["coin", "hd_wallet_rmd160", "confirmation_status"], + false, + )?; + table.create_multi_index( + TxHistoryTableV2::WALLET_ID_TOKEN_ID_INDEX, + &["coin", "hd_wallet_rmd160", "token_id"], + false, + )?; } Ok(()) } @@ -474,12 +471,9 @@ impl TableSignature for TxCacheTableV2 { fn table_name() -> &'static str { "tx_cache_v2" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_multi_index(TxCacheTableV2::COIN_TX_HASH_INDEX, &["coin", "tx_hash"], true)?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(TxCacheTableV2::COIN_TX_HASH_INDEX, &["coin", "tx_hash"], true)?; } Ok(()) } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index e04d345877..896600d0a3 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -10,16 +10,15 @@ use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; use crate::{BlockHeightAndTime, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinProtocol, - CoinWithDerivationMethod, IguanaPrivKey, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, - PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, - RefundError, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, - SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, - SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, - SendWatcherRefundsPaymentArgs, SignatureResult, SwapOps, TakerSwapMakerCoin, TradePreimageValue, - TransactionFut, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationResult, WatcherOps, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; + CoinWithDerivationMethod, ConfirmPaymentInput, IguanaPrivKey, MakerSwapTakerCoin, + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, + SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TransactionType, + TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; use common::log::warn; use derive_more::Display; @@ -835,86 +834,36 @@ impl SwapOps for BchCoin { } #[inline] - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { - utxo_common::send_maker_payment( - self.clone(), - maker_payment_args.time_lock, - maker_payment_args.other_pubkey, - maker_payment_args.secret_hash, - maker_payment_args.amount, - maker_payment_args.secret_hash, - ) + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs) -> TransactionFut { + utxo_common::send_maker_payment(self.clone(), maker_payment_args) } #[inline] - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { - utxo_common::send_taker_payment( - self.clone(), - taker_payment_args.time_lock, - taker_payment_args.other_pubkey, - taker_payment_args.secret_hash, - taker_payment_args.amount, - taker_payment_args.swap_unique_data, - ) + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs) -> TransactionFut { + utxo_common::send_taker_payment(self.clone(), taker_payment_args) } #[inline] - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment( - self.clone(), - maker_spends_payment_args.other_payment_tx, - maker_spends_payment_args.time_lock, - maker_spends_payment_args.other_pubkey, - maker_spends_payment_args.secret, - maker_spends_payment_args.secret_hash, - maker_spends_payment_args.swap_unique_data, - ) + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) } #[inline] - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment( - self.clone(), - taker_spends_payment_args.other_payment_tx, - taker_spends_payment_args.time_lock, - taker_spends_payment_args.other_pubkey, - taker_spends_payment_args.secret, - taker_spends_payment_args.secret_hash, - taker_spends_payment_args.swap_unique_data, - ) + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) } #[inline] - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { - utxo_common::send_taker_refunds_payment( - self.clone(), - taker_refunds_payment_args.payment_tx, - taker_refunds_payment_args.time_lock, - taker_refunds_payment_args.other_pubkey, - taker_refunds_payment_args.secret_hash, - taker_refunds_payment_args.swap_unique_data, - ) + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args) } #[inline] - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { - utxo_common::send_maker_refunds_payment( - self.clone(), - maker_refunds_payment_args.payment_tx, - maker_refunds_payment_args.time_lock, - maker_refunds_payment_args.other_pubkey, - maker_refunds_payment_args.secret_hash, - maker_refunds_payment_args.swap_unique_data, - ) + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args) } - fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { let tx = match validate_fee_args.fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), _ => panic!(), @@ -976,7 +925,12 @@ impl SwapOps for BchCoin { } #[inline] - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + _watcher_reward: bool, + ) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -1125,10 +1079,7 @@ impl WatcherOps for BchCoin { } #[inline] - fn send_taker_payment_refund_preimage( - &self, - watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { utxo_common::send_taker_payment_refund_preimage(self, watcher_refunds_payment_args) } @@ -1196,22 +1147,8 @@ impl MarketCoinOps for BchCoin { utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { - utxo_common::wait_for_confirmations( - &self.utxo_arc, - tx, - confirmations, - requires_nota, - wait_until, - check_every, - ) + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + utxo_common::wait_for_confirmations(&self.utxo_arc, input) } fn wait_for_htlc_tx_spend( @@ -1327,9 +1264,17 @@ impl MmCoin for BchCoin { fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { + utxo_common::coin_protocol_info(self) + } - fn is_coin_protocol_supported(&self, info: &Option>) -> bool { + fn is_coin_protocol_supported( + &self, + info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { utxo_common::is_coin_protocol_supported(self, info) } diff --git a/mm2src/coins/utxo/bchd_grpc.rs b/mm2src/coins/utxo/bchd_grpc.rs index 70e3594bb9..d07abd09ad 100644 --- a/mm2src/coins/utxo/bchd_grpc.rs +++ b/mm2src/coins/utxo/bchd_grpc.rs @@ -323,7 +323,7 @@ mod bchd_grpc_tests { let err = block_on(validate_slp_utxos(BCHD_TESTNET_URLS, &slp_utxos, &token_id)).unwrap_err(); match err.into_inner().kind { ValidateSlpUtxosErrKind::InvalidSlpTxData(_) => (), - err @ _ => panic!("Unexpected error {:?}", err), + err => panic!("Unexpected error {:?}", err), } } @@ -365,7 +365,7 @@ mod bchd_grpc_tests { assert_eq!(invalid_utxo, for_unspent); assert_eq!(expected_validity, validity_result); }, - err @ _ => panic!("Unexpected error {:?}", err), + err => panic!("Unexpected error {:?}", err), } } @@ -406,7 +406,7 @@ mod bchd_grpc_tests { assert_eq!(invalid_token_id, expected); assert_eq!(valid_token_id, actual); }, - err @ _ => panic!("Unexpected error {:?}", err), + err => panic!("Unexpected error {:?}", err), } } @@ -426,7 +426,7 @@ mod bchd_grpc_tests { CheckSlpTransactionErrKind::InvalidTransaction { reason, .. } => { println!("{}", reason); }, - err @ _ => panic!("Unexpected error {:?}", err), + err => panic!("Unexpected error {:?}", err), } } } diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index ab3f336ceb..8fa047cfa1 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -24,17 +24,16 @@ use crate::utxo::utxo_builder::{BlockHeaderUtxoArcOps, MergeUtxoArcOps, UtxoCoin UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; -use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, DelegationError, - DelegationFut, GetWithdrawSenderAddress, IguanaPrivKey, MakerSwapTakerCoin, NegotiateSwapContractAddrErr, - PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundResult, - SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, - SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, - SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, SendWatcherRefundsPaymentArgs, - SignatureResult, StakingInfosFut, SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawSenderAddress}; +use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, + DelegationError, DelegationFut, GetWithdrawSenderAddress, IguanaPrivKey, MakerSwapTakerCoin, + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, StakingInfosFut, + SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFut, WithdrawSenderAddress}; use common::executor::{AbortableSystem, AbortedError}; use crypto::Bip44Chain; use ethereum_types::H160; @@ -532,86 +531,36 @@ impl SwapOps for QtumCoin { } #[inline] - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { - utxo_common::send_maker_payment( - self.clone(), - maker_payment_args.time_lock, - maker_payment_args.other_pubkey, - maker_payment_args.secret_hash, - maker_payment_args.amount, - maker_payment_args.swap_unique_data, - ) + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs) -> TransactionFut { + utxo_common::send_maker_payment(self.clone(), maker_payment_args) } #[inline] - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { - utxo_common::send_taker_payment( - self.clone(), - taker_payment_args.time_lock, - taker_payment_args.other_pubkey, - taker_payment_args.secret_hash, - taker_payment_args.amount, - taker_payment_args.swap_unique_data, - ) + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs) -> TransactionFut { + utxo_common::send_taker_payment(self.clone(), taker_payment_args) } #[inline] - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment( - self.clone(), - maker_spends_payment_args.other_payment_tx, - maker_spends_payment_args.time_lock, - maker_spends_payment_args.other_pubkey, - maker_spends_payment_args.secret, - maker_spends_payment_args.secret_hash, - maker_spends_payment_args.swap_unique_data, - ) + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) } #[inline] - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment( - self.clone(), - taker_spends_payment_args.other_payment_tx, - taker_spends_payment_args.time_lock, - taker_spends_payment_args.other_pubkey, - taker_spends_payment_args.secret, - taker_spends_payment_args.secret_hash, - taker_spends_payment_args.swap_unique_data, - ) + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) } #[inline] - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { - utxo_common::send_taker_refunds_payment( - self.clone(), - taker_refunds_payment_args.payment_tx, - taker_refunds_payment_args.time_lock, - taker_refunds_payment_args.other_pubkey, - taker_refunds_payment_args.secret_hash, - taker_refunds_payment_args.swap_unique_data, - ) + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args) } #[inline] - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { - utxo_common::send_maker_refunds_payment( - self.clone(), - maker_refunds_payment_args.payment_tx, - maker_refunds_payment_args.time_lock, - maker_refunds_payment_args.other_pubkey, - maker_refunds_payment_args.secret_hash, - maker_refunds_payment_args.swap_unique_data, - ) + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args) } - fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { let tx = match validate_fee_args.fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), _ => panic!(), @@ -673,7 +622,12 @@ impl SwapOps for QtumCoin { } #[inline] - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + _watcher_reward: bool, + ) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -818,10 +772,7 @@ impl WatcherOps for QtumCoin { } #[inline] - fn send_taker_payment_refund_preimage( - &self, - watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { utxo_common::send_taker_payment_refund_preimage(self, watcher_refunds_payment_args) } @@ -881,22 +832,8 @@ impl MarketCoinOps for QtumCoin { utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { - utxo_common::wait_for_confirmations( - &self.utxo_arc, - tx, - confirmations, - requires_nota, - wait_until, - check_every, - ) + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + utxo_common::wait_for_confirmations(&self.utxo_arc, input) } fn wait_for_htlc_tx_spend( @@ -1017,9 +954,17 @@ impl MmCoin for QtumCoin { fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { + utxo_common::coin_protocol_info(self) + } - fn is_coin_protocol_supported(&self, info: &Option>) -> bool { + fn is_coin_protocol_supported( + &self, + info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { utxo_common::is_coin_protocol_supported(self, info) } diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 2029702ee5..e06715c99d 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -1436,6 +1436,7 @@ enum ElectrumConfig { } /// Electrum client configuration +#[allow(clippy::upper_case_acronyms)] #[cfg(target_arch = "wasm32")] #[derive(Clone, Debug, Serialize)] enum ElectrumConfig { diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 1f7cf01839..b5d1731169 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -13,13 +13,12 @@ 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, FeeApproxStage, FoundSwapTxSpend, - HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, - NumConversError, PaymentInstructions, PaymentInstructionsErr, PrivKeyPolicyNotAllowed, RawTransactionFut, - RawTransactionRequest, RefundError, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, - SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, - SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, - SendWatcherRefundsPaymentArgs, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, + FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, + NegotiateSwapContractAddrErr, NumConversError, PaymentInstructions, PaymentInstructionsErr, + PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, @@ -752,7 +751,7 @@ impl SlpToken { validate_fut .compat() .await - .map_to_mm(ValidateDexFeeError::ValidatePaymentError)?; + .map_err(|e| MmError::new(ValidateDexFeeError::ValidatePaymentError(e.into_inner().to_string())))?; Ok(()) } @@ -1156,16 +1155,8 @@ impl MarketCoinOps for SlpToken { Box::new(fut.boxed().compat()) } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { - self.platform_coin - .wait_for_confirmations(tx, confirmations, requires_nota, wait_until, check_every) + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + self.platform_coin.wait_for_confirmations(input) } fn wait_for_htlc_tx_spend( @@ -1224,7 +1215,7 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat().map(|tx| tx.into())) } - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs) -> TransactionFut { + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs) -> TransactionFut { let taker_pub = try_tx_fus!(Public::from_slice(maker_payment_args.other_pubkey)); let amount = try_tx_fus!(sat_from_big_decimal(&maker_payment_args.amount, self.decimals())); let secret_hash = maker_payment_args.secret_hash.to_owned(); @@ -1242,7 +1233,7 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs) -> TransactionFut { let maker_pub = try_tx_fus!(Public::from_slice(taker_payment_args.other_pubkey)); let amount = try_tx_fus!(sat_from_big_decimal(&taker_payment_args.amount, self.decimals())); let secret_hash = taker_payment_args.secret_hash.to_owned(); @@ -1261,10 +1252,7 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { let tx = maker_spends_payment_args.other_payment_tx.to_owned(); let taker_pub = try_tx_fus!(Public::from_slice(maker_spends_payment_args.other_pubkey)); let secret = maker_spends_payment_args.secret.to_owned(); @@ -1283,10 +1271,7 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { let tx = taker_spends_payment_args.other_payment_tx.to_owned(); let maker_pub = try_tx_fus!(Public::from_slice(taker_spends_payment_args.other_pubkey)); let secret = taker_spends_payment_args.secret.to_owned(); @@ -1305,7 +1290,7 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { let tx = taker_refunds_payment_args.payment_tx.to_owned(); let maker_pub = try_tx_fus!(Public::from_slice(taker_refunds_payment_args.other_pubkey)); let secret_hash = taker_refunds_payment_args.secret_hash.to_owned(); @@ -1323,7 +1308,7 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat().map_err(TransactionErr::Plain)) } - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { let tx = maker_refunds_payment_args.payment_tx.to_owned(); let taker_pub = try_tx_fus!(Public::from_slice(maker_refunds_payment_args.other_pubkey)); let secret_hash = maker_refunds_payment_args.secret_hash.to_owned(); @@ -1341,7 +1326,7 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { let tx = match validate_fee_args.fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), _ => panic!(), @@ -1353,10 +1338,9 @@ impl SwapOps for SlpToken { let min_block_number = validate_fee_args.min_block_number; let fut = async move { - try_s!( - coin.validate_dex_fee(tx, &expected_sender, &fee_addr, amount, min_block_number) - .await - ); + coin.validate_dex_fee(tx, &expected_sender, &fee_addr, amount, min_block_number) + .await + .map_err(|e| MmError::new(ValidatePaymentError::WrongPaymentTx(e.into_inner().to_string())))?; Ok(()) }; Box::new(fut.boxed().compat()) @@ -1415,7 +1399,12 @@ impl SwapOps for SlpToken { } #[inline] - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + _watcher_reward: bool, + ) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -1530,10 +1519,7 @@ impl WatcherOps for SlpToken { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -1871,9 +1857,17 @@ impl MmCoin for SlpToken { fn mature_confirmations(&self) -> Option { self.platform_coin.mature_confirmations() } - fn coin_protocol_info(&self) -> Vec { Vec::new() } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } - fn is_coin_protocol_supported(&self, _info: &Option>) -> bool { true } + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } fn on_disabled(&self) -> Result<(), AbortedError> { self.conf.abortable_system.abort_all() } @@ -2119,6 +2113,7 @@ mod slp_tests { try_spv_proof_until: now_ms() / 1000 + 60, unique_swap_data: Vec::new(), swap_contract_address: None, + min_watcher_reward: None, }; block_on(fusd.validate_htlc(input)).unwrap(); } @@ -2195,7 +2190,7 @@ mod slp_tests { let err = block_on(fusd.broadcast_tx(&utxo_tx)).unwrap_err(); match err.into_inner() { BroadcastTxErr::Other(err) => assert!(err.contains("is not valid with reason outputs greater than inputs")), - e @ _ => panic!("Unexpected err {:?}", e), + e => panic!("Unexpected err {:?}", e), }; // The error variant should equal to `TxRecoverable` @@ -2253,11 +2248,12 @@ mod slp_tests { try_spv_proof_until: now_ms() / 1000 + 60, confirmations: 1, unique_swap_data: Vec::new(), + min_watcher_reward: None, }; let validity_err = block_on(fusd.validate_htlc(input)).unwrap_err(); match validity_err.into_inner() { ValidatePaymentError::WrongPaymentTx(e) => println!("{:#?}", e), - err @ _ => panic!("Unexpected err {:#?}", err), + err => panic!("Unexpected err {:#?}", err), }; } diff --git a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs index 1932d9afda..85a5954e81 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs @@ -58,7 +58,7 @@ impl BlockHeaderStorage { }) } - #[cfg(test)] + #[allow(dead_code)] pub(crate) fn into_inner(self) -> Box { self.inner } } @@ -108,7 +108,7 @@ impl BlockHeaderStorageOps for BlockHeaderStorage { async fn is_table_empty(&self) -> Result<(), BlockHeaderStorageError> { self.inner.is_table_empty().await } } -#[cfg(test)] +#[cfg(any(test, target_arch = "wasm32"))] mod block_headers_storage_tests { use super::*; use chain::BlockHeaderBits; @@ -137,7 +137,7 @@ mod block_headers_storage_tests { let block_header: BlockHeader = "0000002076d41d3e4b0bfd4c0d3b30aa69fdff3ed35d85829efd04000000000000000000b386498b583390959d9bac72346986e3015e83ac0b54bc7747a11a494ac35c94bb3ce65a53fb45177f7e311c".into(); headers.insert(520481, block_header); storage.add_block_headers_to_storage(headers).await.unwrap(); - assert!(!storage.is_table_empty().await.is_ok()); + assert!(storage.is_table_empty().await.is_err()); } pub(crate) async fn test_get_block_header_impl(for_coin: &str) { @@ -152,7 +152,7 @@ mod block_headers_storage_tests { headers.insert(520481, block_header); storage.add_block_headers_to_storage(headers).await.unwrap(); - assert!(!storage.is_table_empty().await.is_ok()); + assert!(storage.is_table_empty().await.is_err()); let hex = storage.get_block_header_raw(520481).await.unwrap().unwrap(); assert_eq!(hex, "0000002076d41d3e4b0bfd4c0d3b30aa69fdff3ed35d85829efd04000000000000000000b386498b583390959d9bac72346986e3015e83ac0b54bc7747a11a494ac35c94bb3ce65a53fb45177f7e311c".to_string()); @@ -189,7 +189,7 @@ mod block_headers_storage_tests { headers.insert(201593, block_header); storage.add_block_headers_to_storage(headers).await.unwrap(); - assert!(!storage.is_table_empty().await.is_ok()); + assert!(storage.is_table_empty().await.is_err()); let actual_block_header = storage .get_last_block_header_with_non_max_bits(MAX_BITS_BTC) @@ -222,7 +222,7 @@ mod block_headers_storage_tests { headers.insert(201593, block_header); storage.add_block_headers_to_storage(headers).await.unwrap(); - assert!(!storage.is_table_empty().await.is_ok()); + assert!(storage.is_table_empty().await.is_err()); let last_block_height = storage.get_last_block_height().await.unwrap(); assert_eq!(last_block_height.unwrap(), 201595); @@ -250,7 +250,7 @@ mod block_headers_storage_tests { headers.insert(201593, block_header); storage.add_block_headers_to_storage(headers).await.unwrap(); - assert!(!storage.is_table_empty().await.is_ok()); + assert!(storage.is_table_empty().await.is_err()); // Remove 2 headers from storage. storage.remove_headers_up_to_height(201594).await.unwrap(); @@ -313,7 +313,7 @@ mod native_tests { fn test_remove_headers_up_to_height() { block_on(test_remove_headers_up_to_height_impl(FOR_COIN_GET)) } } -#[cfg(all(test, target_arch = "wasm32"))] +#[cfg(target_arch = "wasm32")] mod wasm_test { use super::*; use crate::utxo::utxo_block_header_storage::block_headers_storage_tests::*; @@ -323,7 +323,7 @@ mod wasm_test { wasm_bindgen_test_configure!(run_in_browser); - const FOR_COIN: &str = "RICK"; + const FOR_COIN: &str = "tBTC"; #[wasm_bindgen_test] async fn test_storage_init() { diff --git a/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs index 79121043d6..315a94cdd8 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs @@ -1,8 +1,8 @@ -use mm2_db::indexed_db::{DbUpgrader, OnUpgradeResult, TableSignature}; +use mm2_db::indexed_db::{BeBigUint, DbUpgrader, OnUpgradeResult, TableSignature}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BlockHeaderStorageTable { - pub height: u64, + pub height: BeBigUint, pub bits: u32, pub hash: String, pub raw_header: String, @@ -10,7 +10,7 @@ pub struct BlockHeaderStorageTable { } impl BlockHeaderStorageTable { - pub const HEIGHT_TICKER_INDEX: &str = "block_height_ticker_index"; + pub const TICKER_HEIGHT_INDEX: &str = "block_height_ticker_index"; pub const HASH_TICKER_INDEX: &str = "block_hash_ticker_index"; } @@ -18,14 +18,11 @@ impl TableSignature for BlockHeaderStorageTable { fn table_name() -> &'static str { "block_header_storage_cache_table" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_multi_index(Self::HEIGHT_TICKER_INDEX, &["height", "ticker"], true)?; - table.create_multi_index(Self::HASH_TICKER_INDEX, &["hash", "ticker"], true)?; - table.create_index("ticker", false)?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index(Self::TICKER_HEIGHT_INDEX, &["ticker", "height"], true)?; + table.create_multi_index(Self::HASH_TICKER_INDEX, &["hash", "ticker"], true)?; + table.create_index("ticker", false)?; } Ok(()) } diff --git a/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs index c5a02dd151..3f5e9aae8e 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs @@ -3,10 +3,10 @@ use super::BlockHeaderStorageTable; use async_trait::async_trait; use chain::BlockHeader; use mm2_core::mm_ctx::MmArc; -use mm2_db::indexed_db::cursor_prelude::{CollectCursor, WithOnly}; -use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder, +use mm2_db::indexed_db::{BeBigUint, ConstructibleDb, DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder, InitDbResult, MultiIndex, SharedDb}; use mm2_err_handle::prelude::*; +use num_traits::ToPrimitive; use primitives::hash::H256; use serialization::Reader; use spv_validation::storage::{BlockHeaderStorageError, BlockHeaderStorageOps}; @@ -93,15 +93,15 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { let bits: u32 = header.bits.into(); let headers_to_store = BlockHeaderStorageTable { ticker: ticker.clone(), - height, + height: BeBigUint::from(height), bits, hash, raw_header, }; - let index_keys = MultiIndex::new(BlockHeaderStorageTable::HEIGHT_TICKER_INDEX) - .with_value(&height) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + let index_keys = MultiIndex::new(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) .with_value(&ticker) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(BeBigUint::from(height)) .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; block_headers_db @@ -149,10 +149,10 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { .table::() .await .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; - let index_keys = MultiIndex::new(BlockHeaderStorageTable::HEIGHT_TICKER_INDEX) - .with_value(&height) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + let index_keys = MultiIndex::new(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) .with_value(&ticker) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(BeBigUint::from(height)) .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; Ok(block_headers_db @@ -178,21 +178,30 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { .await .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; - // Todo: use open_cursor with direction to optimze this process. - let res = block_headers_db - .open_cursor("ticker") - .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + let maybe_item = block_headers_db + .cursor_builder() .only("ticker", ticker.clone()) .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? - .collect() + // We need to provide any constraint on the `height` property + // since `ticker_height` consists of both `ticker` and `height` properties. + .bound("height", BeBigUint::from(0u64), BeBigUint::from(u64::MAX)) + // Cursor returns values from the lowest to highest key indexes. + // But we need to get the most highest height, so reverse the cursor direction. + .reverse() + .open_cursor(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) .await .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? - .into_iter() - .map(|(_item_id, item)| item.height) - .collect::>(); + .next() + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; - Ok(res.into_iter().max()) + maybe_item + .map(|(_, item)| { + item.height + .to_u64() + .ok_or_else(|| BlockHeaderStorageError::get_err(&ticker, "height is too large".to_string())) + }) + .transpose() } async fn get_last_block_header_with_non_max_bits( @@ -214,25 +223,29 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { .await .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; - // Todo: use open_cursor with direction to optimze this process. - let res = block_headers_db - .open_cursor("ticker") - .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + let mut cursor = block_headers_db + .cursor_builder() .only("ticker", ticker.clone()) .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? - .collect() + // We need to provide any constraint on the `height` property + // since `ticker_height` consists of both `ticker` and `height` properties. + .bound("height", BeBigUint::from(0u64), BeBigUint::from(u64::MAX)) + // Cursor returns values from the lowest to highest key indexes. + // But we need to get the most highest height, so reverse the cursor direction. + .reverse() + .open_cursor(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) + .await + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + + while let Some((_item_id, header)) = cursor + .next() .await .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? - .into_iter() - .map(|(_item_id, item)| item) - .collect::>(); - let res = res - .into_iter() - .filter_map(|e| if e.bits != max_bits { Some(e) } else { None }) - .collect::>(); - - for header in res { + { + if header.bits == max_bits { + continue; + } + let serialized = &hex::decode(header.raw_header).map_err(|e| BlockHeaderStorageError::DecodeError { coin: ticker.clone(), reason: e.to_string(), @@ -268,16 +281,23 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { .await .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; let index_keys = MultiIndex::new(BlockHeaderStorageTable::HASH_TICKER_INDEX) - .with_value(&hash.to_string()) + .with_value(hash.to_string()) .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? .with_value(&ticker) .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; - Ok(block_headers_db + let maybe_item = block_headers_db .get_item_by_unique_multi_index(index_keys) .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? - .map(|raw| raw.1.height as i64)) + .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + + maybe_item + .map(|(_, item)| { + item.height + .to_i64() + .ok_or_else(|| BlockHeaderStorageError::get_err(&ticker, "height is too large".to_string())) + }) + .transpose() } async fn remove_headers_up_to_height(&self, to_height: u64) -> Result<(), BlockHeaderStorageError> { @@ -297,10 +317,10 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; for height in 0..=to_height { - let index_keys = MultiIndex::new(BlockHeaderStorageTable::HEIGHT_TICKER_INDEX) - .with_value(&height) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + let index_keys = MultiIndex::new(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) .with_value(&ticker) + .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(BeBigUint::from(height)) .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; block_headers_db diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 19db7c9f32..5709002ec2 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -12,13 +12,14 @@ use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentI use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::tx_cache::TxCacheResult; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; -use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, HDAccountAddressId, - RawTransactionError, RawTransactionRequest, RawTransactionRes, SearchForSwapTxSpendInput, - SendMakerPaymentSpendPreimageInput, SendWatcherRefundsPaymentArgs, SignatureError, SignatureResult, - SwapOps, TradePreimageValue, TransactionFut, TxFeeDetails, TxMarshalingErr, ValidateAddressResult, - ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFrom, - WithdrawResult, WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, +use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, GetWithdrawSenderAddress, + HDAccountAddressId, RawTransactionError, RawTransactionRequest, RawTransactionRes, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, + SignatureResult, SpendPaymentArgs, SwapOps, TradePreimageValue, TransactionFut, TxFeeDetails, + TxMarshalingErr, ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, + VerificationError, VerificationResult, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFrom, WithdrawResult, WithdrawSenderAddress, + EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; pub use bitcrypto::{dhash160, sha256, ChecksumType}; use bitcrypto::{dhash256, ripemd160}; @@ -1189,28 +1190,21 @@ where send_outputs_from_my_address(coin, vec![output]) } -pub fn send_maker_payment( - coin: T, - time_lock: u32, - taker_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - swap_unique_data: &[u8], -) -> TransactionFut +pub fn send_maker_payment(coin: T, args: SendPaymentArgs) -> TransactionFut where T: UtxoCommonOps + GetUtxoListOps + SwapOps, { - let maker_htlc_key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let maker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let SwapPaymentOutputsResult { payment_address, outputs, } = try_tx_fus!(generate_swap_payment_outputs( &coin, - time_lock, + args.time_lock, maker_htlc_key_pair.public_slice(), - taker_pub, - secret_hash, - amount + args.other_pubkey, + args.secret_hash, + args.amount )); let send_fut = match &coin.as_ref().rpc_client { UtxoRpcClientEnum::Electrum(_) => Either::A(send_outputs_from_my_address(coin, outputs)), @@ -1227,28 +1221,21 @@ where Box::new(send_fut) } -pub fn send_taker_payment( - coin: T, - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - amount: BigDecimal, - swap_unique_data: &[u8], -) -> TransactionFut +pub fn send_taker_payment(coin: T, args: SendPaymentArgs) -> TransactionFut where T: UtxoCommonOps + GetUtxoListOps + SwapOps, { - let taker_htlc_key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let taker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let SwapPaymentOutputsResult { payment_address, outputs, } = try_tx_fus!(generate_swap_payment_outputs( &coin, - time_lock, + args.time_lock, taker_htlc_key_pair.public_slice(), - maker_pub, - secret_hash, - amount + args.other_pubkey, + args.secret_hash, + args.amount )); let send_fut = match &coin.as_ref().rpc_client { @@ -1266,36 +1253,29 @@ where Box::new(send_fut) } -pub fn send_maker_spends_taker_payment( - coin: T, - taker_payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret: &[u8], - secret_hash: &[u8], - swap_unique_data: &[u8], -) -> TransactionFut { +pub fn send_maker_spends_taker_payment(coin: T, args: SpendPaymentArgs) -> TransactionFut { let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); - let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); if prev_transaction.outputs.is_empty() { return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); } - let key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default() - .push_data(secret) + .push_data(args.secret) .push_opcode(Opcode::OP_0) .into_script(); let redeem_script = payment_script( - time_lock, - secret_hash, - &try_tx_fus!(Public::from_slice(taker_pub)), + args.time_lock, + args.secret_hash, + &try_tx_fus!(Public::from_slice(args.other_pubkey)), key_pair.public(), ) .into(); + let time_lock = args.time_lock; let fut = async move { let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) @@ -1499,36 +1479,30 @@ pub fn create_taker_payment_refund_preimage( Box::new(fut.boxed().compat()) } -pub fn send_taker_spends_maker_payment( - coin: T, - maker_payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret: &[u8], - secret_hash: &[u8], - swap_unique_data: &[u8], -) -> TransactionFut { +pub fn send_taker_spends_maker_payment(coin: T, args: SpendPaymentArgs) -> TransactionFut { let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); - let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); if prev_transaction.outputs.is_empty() { return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); } - let key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default() - .push_data(secret) + .push_data(args.secret) .push_opcode(Opcode::OP_0) .into_script(); let redeem_script = payment_script( - time_lock, - secret_hash, - &try_tx_fus!(Public::from_slice(maker_pub)), + args.time_lock, + args.secret_hash, + &try_tx_fus!(Public::from_slice(args.other_pubkey)), key_pair.public(), ) .into(); + + let time_lock = args.time_lock; let fut = async move { let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) @@ -1566,32 +1540,26 @@ pub fn send_taker_spends_maker_payment( Box::new(fut.boxed().compat()) } -pub fn send_taker_refunds_payment( - coin: T, - taker_payment_tx: &[u8], - time_lock: u32, - maker_pub: &[u8], - secret_hash: &[u8], - swap_unique_data: &[u8], -) -> TransactionFut { +pub fn send_taker_refunds_payment(coin: T, args: RefundPaymentArgs) -> TransactionFut { let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); let mut prev_transaction: UtxoTx = - try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); + try_tx_fus!(deserialize(args.payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); if prev_transaction.outputs.is_empty() { return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); } - let key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); let redeem_script = payment_script( - time_lock, - secret_hash, + args.time_lock, + args.secret_hash, key_pair.public(), - &try_tx_fus!(Public::from_slice(maker_pub)), + &try_tx_fus!(Public::from_slice(args.other_pubkey)), ) .into(); + let time_lock = args.time_lock; let fut = async move { let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) @@ -1631,7 +1599,7 @@ pub fn send_taker_refunds_payment( pub fn send_taker_payment_refund_preimage( coin: &T, - watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, + watcher_refunds_payment_args: RefundPaymentArgs, ) -> TransactionFut { let coin = coin.clone(); let transaction: UtxoTx = try_tx_fus!( @@ -1648,31 +1616,24 @@ pub fn send_taker_payment_refund_preimage( Box::new(fut.boxed().compat()) } -pub fn send_maker_refunds_payment( - coin: T, - maker_payment_tx: &[u8], - time_lock: u32, - taker_pub: &[u8], - secret_hash: &[u8], - swap_unique_data: &[u8], -) -> TransactionFut { +pub fn send_maker_refunds_payment(coin: T, args: RefundPaymentArgs) -> TransactionFut { let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); - let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); if prev_transaction.outputs.is_empty() { return try_tx_fus!(TX_PLAIN_ERR!("Transaction doesn't have any output")); } - - let key_pair = coin.derive_htlc_key_pair(swap_unique_data); + let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); let redeem_script = payment_script( - time_lock, - secret_hash, + args.time_lock, + args.secret_hash, key_pair.public(), - &try_tx_fus!(Public::from_slice(taker_pub)), + &try_tx_fus!(Public::from_slice(args.other_pubkey)), ) .into(); + let time_lock = args.time_lock; let fut = async move { let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) @@ -1904,67 +1865,77 @@ pub fn validate_fee( amount: &BigDecimal, min_block_number: u64, fee_addr: &[u8], -) -> Box + Send> { +) -> ValidatePaymentFut<()> { let amount = amount.clone(); - let address = try_fus!(address_from_raw_pubkey( + let address = try_f!(address_from_raw_pubkey( fee_addr, coin.as_ref().conf.pub_addr_prefix, coin.as_ref().conf.pub_t_addr_prefix, coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), - )); + ) + .map_to_mm(ValidatePaymentError::TxDeserializationError)); - if !try_fus!(check_all_utxo_inputs_signed_by_pub(&tx, sender_pubkey)) { - return Box::new(futures01::future::err(ERRL!("The dex fee was sent from wrong address"))); + let inputs_signed_by_pub = try_f!(check_all_utxo_inputs_signed_by_pub(&tx, sender_pubkey)); + if !inputs_signed_by_pub { + return Box::new(futures01::future::err( + ValidatePaymentError::WrongPaymentTx(format!( + "{INVALID_SENDER_ERR_LOG}: Taker payment does not belong to the verified public key" + )) + .into(), + )); } + let fut = async move { - let amount = try_s!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); - let tx_from_rpc = try_s!( - coin.as_ref() - .rpc_client - .get_verbose_transaction(&tx.hash().reversed().into()) - .compat() - .await - ); + let amount = sat_from_big_decimal(&amount, coin.as_ref().decimals)?; + let tx_from_rpc = coin + .as_ref() + .rpc_client + .get_verbose_transaction(&tx.hash().reversed().into()) + .compat() + .await?; - if try_s!(is_tx_confirmed_before_block(&coin, &tx_from_rpc, min_block_number).await) { - return ERR!( - "Fee tx {:?} confirmed before min_block {}", - tx_from_rpc, - min_block_number, - ); + let tx_confirmed_before_block = is_tx_confirmed_before_block(&coin, &tx_from_rpc, min_block_number) + .await + .map_to_mm(ValidatePaymentError::InternalError)?; + + if tx_confirmed_before_block { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Fee tx {:?} confirmed before min_block {}", + EARLY_CONFIRMATION_ERR_LOG, tx_from_rpc, min_block_number + ))); } if tx_from_rpc.hex.0 != serialize(&tx).take() && tx_from_rpc.hex.0 != serialize_with_flags(&tx, SERIALIZE_TRANSACTION_WITNESS).take() { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Provided dex fee tx {:?} doesn't match tx data from rpc {:?}", - tx, - tx_from_rpc - ); + tx, tx_from_rpc + ))); } match tx.outputs.get(output_index) { Some(out) => { let expected_script_pubkey = Builder::build_p2pkh(&address.hash).to_bytes(); if out.script_pubkey != expected_script_pubkey { - return ERR!( - "Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", - out.script_pubkey, - expected_script_pubkey - ); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", + INVALID_RECEIVER_ERR_LOG, out.script_pubkey, expected_script_pubkey + ))); } if out.value < amount { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Provided dex fee tx output value is less than expected {:?} {:?}", - out.value, - amount - ); + out.value, amount + ))); } }, None => { - return ERR!("Provided dex fee tx {:?} does not have output {}", tx, output_index); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided dex fee tx {:?} does not have output {}", + tx, output_index + ))) }, } Ok(()) @@ -2015,9 +1986,9 @@ pub fn watcher_validate_taker_payment( let fut = async move { let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(&taker_payment_tx, &input.taker_pub)?; if !inputs_signed_by_pub { - return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Taker payment does not belong to the verified public key".to_string(), - )); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{INVALID_SENDER_ERR_LOG}: Taker payment does not belong to the verified public key" + ))); } let taker_payment_locking_script = match taker_payment_tx.outputs.get(DEFAULT_SWAP_VOUT) { @@ -2031,8 +2002,7 @@ pub fn watcher_validate_taker_payment( if taker_payment_locking_script != Builder::build_p2sh(&dhash160(&expected_redeem).into()).to_bytes() { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Payment tx locking script {:?} doesn't match expected", - taker_payment_locking_script + "{INVALID_SCRIPT_ERR_LOG}: Payment tx locking script {taker_payment_locking_script:?} doesn't match expected" ))); } @@ -2057,7 +2027,7 @@ pub fn watcher_validate_taker_payment( if expected_redeem.as_slice() != redeem_script { return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Taker payment tx locking script doesn't match with taker payment refund redeem script".to_string(), + format!("{INVALID_REFUND_TX_ERR_LOG}: Taker payment tx locking script doesn't match with taker payment refund redeem script") )); } @@ -2353,21 +2323,17 @@ pub fn send_raw_tx_bytes( pub fn wait_for_confirmations( coin: &UtxoCoinFields, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, + input: ConfirmPaymentInput, ) -> Box + Send> { - let mut tx: UtxoTx = try_fus!(deserialize(tx).map_err(|e| ERRL!("{:?}", e))); + let mut tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.tx_hash_algo; coin.rpc_client.wait_for_confirmations( tx.hash().reversed().into(), tx.expiry_height, - confirmations as u32, - requires_nota, - wait_until, - check_every, + input.confirmations as u32, + input.requires_nota, + input.wait_until, + input.check_every, ) } diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index eef16a64ba..c8d9406cdc 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -19,9 +19,9 @@ use std::convert::TryFrom; use std::num::NonZeroUsize; use std::time::Duration; -pub(super) const TEST_COIN_NAME: &'static str = "RICK"; +pub(super) const TEST_COIN_NAME: &str = "RICK"; // Made-up hrp for rick to test p2wpkh script -pub(super) const TEST_COIN_HRP: &'static str = "rck"; +pub(super) const TEST_COIN_HRP: &str = "rck"; pub(super) const TEST_COIN_DECIMALS: u8 = 8; const MORTY_HD_TX_HISTORY_STR: &str = include_str!("../for_tests/MORTY_HD_tx_history_fixtures.json"); @@ -185,7 +185,7 @@ pub(super) fn get_morty_hd_transactions_ordered(tx_hashes: &[&str]) -> Vec TransactionFut { - utxo_common::send_maker_payment( - self.clone(), - maker_payment_args.time_lock, - maker_payment_args.other_pubkey, - maker_payment_args.secret_hash, - maker_payment_args.amount, - maker_payment_args.swap_unique_data, - ) + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs) -> TransactionFut { + utxo_common::send_maker_payment(self.clone(), maker_payment_args) } #[inline] - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs) -> TransactionFut { - utxo_common::send_taker_payment( - self.clone(), - taker_payment_args.time_lock, - taker_payment_args.other_pubkey, - taker_payment_args.secret_hash, - taker_payment_args.amount, - taker_payment_args.swap_unique_data, - ) + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs) -> TransactionFut { + utxo_common::send_taker_payment(self.clone(), taker_payment_args) } #[inline] - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs, - ) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment( - self.clone(), - maker_spends_payment_args.other_payment_tx, - maker_spends_payment_args.time_lock, - maker_spends_payment_args.other_pubkey, - maker_spends_payment_args.secret, - maker_spends_payment_args.secret_hash, - maker_spends_payment_args.swap_unique_data, - ) + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) } #[inline] - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs, - ) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment( - self.clone(), - taker_spends_payment_args.other_payment_tx, - taker_spends_payment_args.time_lock, - taker_spends_payment_args.other_pubkey, - taker_spends_payment_args.secret, - taker_spends_payment_args.secret_hash, - taker_spends_payment_args.swap_unique_data, - ) + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) } #[inline] - fn send_taker_refunds_payment(&self, taker_refunds_payment_args: SendTakerRefundsPaymentArgs) -> TransactionFut { - utxo_common::send_taker_refunds_payment( - self.clone(), - taker_refunds_payment_args.payment_tx, - taker_refunds_payment_args.time_lock, - taker_refunds_payment_args.other_pubkey, - taker_refunds_payment_args.secret_hash, - taker_refunds_payment_args.swap_unique_data, - ) + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + utxo_common::send_taker_refunds_payment(self.clone(), taker_refunds_payment_args) } #[inline] - fn send_maker_refunds_payment(&self, maker_refunds_payment_args: SendMakerRefundsPaymentArgs) -> TransactionFut { - utxo_common::send_maker_refunds_payment( - self.clone(), - maker_refunds_payment_args.payment_tx, - maker_refunds_payment_args.time_lock, - maker_refunds_payment_args.other_pubkey, - maker_refunds_payment_args.secret_hash, - maker_refunds_payment_args.swap_unique_data, - ) + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + utxo_common::send_maker_refunds_payment(self.clone(), maker_refunds_payment_args) } - fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> Box + Send> { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { let tx = match validate_fee_args.fee_tx { TransactionEnum::UtxoTx(tx) => tx.clone(), _ => panic!(), @@ -437,7 +385,12 @@ impl SwapOps for UtxoStandardCoin { } #[inline] - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + _watcher_reward: bool, + ) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -577,11 +530,8 @@ impl WatcherOps for UtxoStandardCoin { } #[inline] - fn send_taker_payment_refund_preimage( - &self, - watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { - utxo_common::send_taker_payment_refund_preimage(self, watcher_refunds_payment_args) + fn send_taker_payment_refund_preimage(&self, refund_payment_args: RefundPaymentArgs) -> TransactionFut { + utxo_common::send_taker_payment_refund_preimage(self, refund_payment_args) } #[inline] @@ -646,22 +596,8 @@ impl MarketCoinOps for UtxoStandardCoin { utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { - utxo_common::wait_for_confirmations( - &self.utxo_arc, - tx, - confirmations, - requires_nota, - wait_until, - check_every, - ) + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + utxo_common::wait_for_confirmations(&self.utxo_arc, input) } fn wait_for_htlc_tx_spend( @@ -781,9 +717,17 @@ impl MmCoin for UtxoStandardCoin { fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { + utxo_common::coin_protocol_info(self) + } - fn is_coin_protocol_supported(&self, info: &Option>) -> bool { + fn is_coin_protocol_supported( + &self, + info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { utxo_common::is_coin_protocol_supported(self, info) } diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index cec80eb40c..013441e6a8 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::coin_balance::HDAddressBalance; +use crate::coin_errors::ValidatePaymentError; use crate::hd_confirm_address::for_tests::MockableConfirmAddress; use crate::hd_confirm_address::{HDConfirmAddress, HDConfirmAddressError}; use crate::hd_wallet::HDAccountsMap; @@ -28,8 +29,9 @@ use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardC use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsParams, UtxoTxHistoryOps}; #[cfg(not(target_arch = "wasm32"))] use crate::WithdrawFee; use crate::{BlockHeightAndTime, CoinBalance, IguanaPrivKey, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, - SendMakerSpendsTakerPaymentArgs, StakingInfosDetails, SwapOps, TradePreimageValue, TxFeeDetails, - TxMarshalingErr, ValidateFeeArgs}; + SpendPaymentArgs, StakingInfosDetails, SwapOps, TradePreimageValue, TxFeeDetails, TxMarshalingErr, + ValidateFeeArgs}; +use crate::{ConfirmPaymentInput, INVALID_SENDER_ERR_LOG}; use chain::{BlockHeader, BlockHeaderBits, OutPoint}; use common::executor::Timer; use common::{block_on, now_ms, OrdRange, PagingOptionsEnum, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -149,7 +151,7 @@ fn test_extract_secret() { let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); let expected_secret = hex::decode("9da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365").unwrap(); let secret_hash = &*dhash160(&expected_secret); - let secret = block_on(coin.extract_secret(secret_hash, &tx_hex)).unwrap(); + let secret = block_on(coin.extract_secret(secret_hash, &tx_hex, false)).unwrap(); assert_eq!(secret, expected_secret); } @@ -159,14 +161,15 @@ fn test_send_maker_spends_taker_payment_recoverable_tx() { let coin = utxo_coin_for_test(client.into(), None, false); let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); let secret = hex::decode("9da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365").unwrap(); - let maker_spends_payment_args = SendMakerSpendsTakerPaymentArgs { + let maker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &tx_hex, time_lock: 777, - other_pubkey: &coin.my_public_key().unwrap().to_vec(), + other_pubkey: coin.my_public_key().unwrap(), secret: &secret, secret_hash: &*dhash160(&secret), swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let tx_err = coin .send_maker_spends_taker_payment(maker_spends_payment_args) @@ -492,12 +495,13 @@ fn test_search_for_swap_tx_spend_electrum_was_spent() { let search_input = SearchForSwapTxSpendInput { time_lock: 1591928233, - other_pub: &*coin.my_public_key().unwrap(), + other_pub: coin.my_public_key().unwrap(), secret_hash: &*dhash160(&secret), tx: &payment_tx_bytes, search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -526,12 +530,13 @@ fn test_search_for_swap_tx_spend_electrum_was_refunded() { let search_input = SearchForSwapTxSpendInput { time_lock: 1591933469, - other_pub: &coin.as_ref().priv_key_policy.key_pair_or_err().unwrap().public(), + other_pub: coin.as_ref().priv_key_policy.key_pair_or_err().unwrap().public(), secret_hash: &secret_hash, tx: &payment_tx_bytes, search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -1315,9 +1320,9 @@ fn test_get_median_time_past_from_native_does_not_have_median_in_get_block() { let blocks: Vec = json::from_str(blocks_json_str).unwrap(); let mut block_hashes: HashMap<_, _> = blocks .iter() - .map(|block| (block.height.unwrap() as u64, block.hash.clone())) + .map(|block| (block.height.unwrap() as u64, block.hash)) .collect(); - let mut blocks: HashMap<_, _> = blocks.into_iter().map(|block| (block.hash.clone(), block)).collect(); + let mut blocks: HashMap<_, _> = blocks.into_iter().map(|block| (block.hash, block)).collect(); let client = native_client_for_test(); NativeClientImpl::get_block_hash.mock_safe(move |_, block_num| { @@ -1475,7 +1480,7 @@ fn test_address_from_str_with_legacy_address_activated() { // https://github.com/KomodoPlatform/atomicDEX-API/issues/673 fn test_network_info_negative_time_offset() { let info_str = r#"{"version":1140200,"subversion":"/Shibetoshi:1.14.2/","protocolversion":70015,"localservices":"0000000000000005","localrelay":true,"timeoffset":-1,"networkactive":true,"connections":12,"networks":[{"name":"ipv4","limited":false,"reachable":true,"proxy":"","proxy_randomize_credentials":false},{"name":"ipv6","limited":false,"reachable":true,"proxy":"","proxy_randomize_credentials":false},{"name":"onion","limited":false,"reachable":true,"proxy":"127.0.0.1:9050","proxy_randomize_credentials":true}],"relayfee":1.00000000,"incrementalfee":0.00001000,"localaddresses":[],"warnings":""}"#; - let _info: NetworkInfo = json::from_str(&info_str).unwrap(); + let _info: NetworkInfo = json::from_str(info_str).unwrap(); } #[test] @@ -1631,7 +1636,7 @@ fn test_qtum_add_delegation() { }; let res = coin.add_delegation(request).wait().unwrap(); // Eligible for delegation - assert_eq!(res.my_balance_change.is_negative(), true); + assert!(res.my_balance_change.is_negative()); assert_eq!(res.total_amount, res.spent_by_me); assert!(res.spent_by_me > res.received_by_me); @@ -1641,7 +1646,7 @@ fn test_qtum_add_delegation() { }; let res = coin.add_delegation(request).wait(); // Wrong address - assert_eq!(res.is_err(), true); + assert!(res.is_err()); } #[test] @@ -1670,7 +1675,7 @@ fn test_qtum_add_delegation_on_already_delegating() { }; let res = coin.add_delegation(request).wait(); // Already Delegating - assert_eq!(res.is_err(), true); + assert!(res.is_err()); } #[test] @@ -1697,10 +1702,10 @@ fn test_qtum_get_delegation_infos() { let staking_infos = coin.get_delegation_infos().wait().unwrap(); match staking_infos.staking_infos_details { StakingInfosDetails::Qtum(staking_details) => { - assert_eq!(staking_details.am_i_staking, true); + assert!(staking_details.am_i_staking); assert_eq!(staking_details.staker.unwrap(), "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); // Will return false for segwit. - assert_eq!(staking_details.is_staking_supported, true); + assert!(staking_details.is_staking_supported); }, }; } @@ -1725,7 +1730,7 @@ fn test_qtum_remove_delegation() { )) .unwrap(); let res = coin.remove_delegation().wait(); - assert_eq!(res.is_err(), false); + assert!(res.is_ok()); } #[test] @@ -1874,7 +1879,7 @@ fn test_get_mature_unspent_ordered_map_from_cache_impl( block_on(coin.get_mature_unspent_ordered_list(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))) .expect("Expected an empty unspent list"); // unspents should be empty because `is_unspent_mature()` always returns false - assert!(unsafe { IS_UNSPENT_MATURE_CALLED == true }); + assert!(unsafe { IS_UNSPENT_MATURE_CALLED }); assert!(unspents.mature.is_empty()); assert_eq!(unspents.immature.len(), 1); } @@ -2019,12 +2024,12 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { let tx: UtxoTx = "0400008085202f89027f57730fcbbc2c72fb18bcc3766a713044831a117bb1cade3ed88644864f7333020000006a47304402206e3737b2fcf078b61b16fa67340cc3e79c5d5e2dc9ffda09608371552a3887450220460a332aa1b8ad8f2de92d319666f70751078b221199951f80265b4f7cef8543012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff42b916a80430b80a77e114445b08cf120735447a524de10742fac8f6a9d4170f000000006a473044022004aa053edafb9d161ea8146e0c21ed1593aa6b9404dd44294bcdf920a1695fd902202365eac15dbcc5e9f83e2eed56a8f2f0e5aded36206f9c3fabc668fd4665fa2d012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff03547b16000000000017a9143e8ad0e2bf573d32cb0b3d3a304d9ebcd0c2023b870000000000000000166a144e2b3c0323ab3c2dc6f86dc5ec0729f11e42f56103970400000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac89c5925f000000000000000000000000000000".into(); let spent_by_tx = vec![ UnspentInfo { - outpoint: tx.inputs[0].previous_output.clone(), + outpoint: tx.inputs[0].previous_output, value: 886737, height: Some(642293), }, UnspentInfo { - outpoint: tx.inputs[1].previous_output.clone(), + outpoint: tx.inputs[1].previous_output, value: 88843, height: Some(642293), }, @@ -2065,56 +2070,48 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx let tx_0: UtxoTx = "0400008085202f89027f57730fcbbc2c72fb18bcc3766a713044831a117bb1cade3ed88644864f7333020000006a47304402206e3737b2fcf078b61b16fa67340cc3e79c5d5e2dc9ffda09608371552a3887450220460a332aa1b8ad8f2de92d319666f70751078b221199951f80265b4f7cef8543012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff42b916a80430b80a77e114445b08cf120735447a524de10742fac8f6a9d4170f000000006a473044022004aa053edafb9d161ea8146e0c21ed1593aa6b9404dd44294bcdf920a1695fd902202365eac15dbcc5e9f83e2eed56a8f2f0e5aded36206f9c3fabc668fd4665fa2d012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff03547b16000000000017a9143e8ad0e2bf573d32cb0b3d3a304d9ebcd0c2023b870000000000000000166a144e2b3c0323ab3c2dc6f86dc5ec0729f11e42f56103970400000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac89c5925f000000000000000000000000000000".into(); let spent_by_tx_0 = vec![ UnspentInfo { - outpoint: tx_0.inputs[0].previous_output.clone(), + outpoint: tx_0.inputs[0].previous_output, value: 886737, height: Some(642293), }, UnspentInfo { - outpoint: tx_0.inputs[1].previous_output.clone(), + outpoint: tx_0.inputs[1].previous_output, value: 88843, height: Some(642293), }, ]; - block_on(coin.as_ref().recently_spent_outpoints.lock()).add_spent( - spent_by_tx_0.clone(), - tx_0.hash(), - tx_0.outputs.clone(), - ); + block_on(coin.as_ref().recently_spent_outpoints.lock()).add_spent(spent_by_tx_0.clone(), tx_0.hash(), tx_0.outputs); // https://morty.explorer.dexstats.info/tx/dbfc821e482747a3512ee6d5734f9df2aa73dab07e2fcd86abeadb462e795bf9 let tx_1: UtxoTx = "0400008085202f890347d329798b508dc28ec99d8c6f6c7ced860a19a364e1bafe391cab89aeaac731020000006a47304402203ea8b380d0a7e64348869ef7c4c2bfa966fc7b148633003332fa8d0ab0c1bc5602202cc63fabdd2a6578c52d8f4f549069b16505f2ead48edc2b8de299be15aadf9a012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff1d1fd3a6b01710647a7f4a08c6de6075cb8e78d5069fa50f10c4a2a10ded2a95000000006a47304402203868945edc0f6dc2ee43d70a69ee4ec46ca188dc493173ce58924ba9bf6ee7a50220648ff99ce458ca72800758f6a1bd3800cd05ff9c3122f23f3653c25e09d22c79012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff7932150df8b4a1852b8b84b89b0d5322bf74665fb7f76a728369fd6895d3fd48000000006a4730440220127918c6f79c11f7f2376a6f3b750ed4c7103183181ad1218afcb2625ece9599022028c05e88d3a2f97cebd84a718cda33b62b48b18f16278fa8e531fd2155e61ee8012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff0329fd12000000000017a914cafb62e3e8bdb8db3735c39b92743ac6ebc9ef20870000000000000000166a14a7416b070c9bb98f4bafae55616f005a2a30bd6014b40c00000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac8cc5925f000000000000000000000000000000".into(); let spent_by_tx_1 = vec![ UnspentInfo { - outpoint: tx_1.inputs[0].previous_output.clone(), + outpoint: tx_1.inputs[0].previous_output, value: 300803, height: Some(642293), }, UnspentInfo { - outpoint: tx_1.inputs[1].previous_output.clone(), + outpoint: tx_1.inputs[1].previous_output, value: 888544, height: Some(642293), }, UnspentInfo { - outpoint: tx_1.inputs[2].previous_output.clone(), + outpoint: tx_1.inputs[2].previous_output, value: 888642, height: Some(642293), }, ]; - block_on(coin.as_ref().recently_spent_outpoints.lock()).add_spent( - spent_by_tx_1.clone(), - tx_1.hash(), - tx_1.outputs.clone(), - ); + block_on(coin.as_ref().recently_spent_outpoints.lock()).add_spent(spent_by_tx_1.clone(), tx_1.hash(), tx_1.outputs); // https://morty.explorer.dexstats.info/tx/12ea22a7cde9efb66b76f9b84345ddfc4c34870e293bfa8eac68d7df83dffa4b let tx_2: UtxoTx = "0400008085202f8902f95b792e46dbeaab86cd2f7eb0da73aaf29d4f73d5e62e51a34727481e82fcdb020000006a4730440220347adefe33ed5afbbb8e5d453afd527319f9a50ab790023296a981da095ca4a2022029a68ef6fd5a4decf3793d4c33994eb8658408f3b14a6d439c4753b2dde954ee012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff75bd4348594f8ff2a216e5ad7533b37d47d2a2767b0b88d43972ad51895355e2000000006a473044022069b36c0f65d56e02bc179f7442806374c4163d07939090aba1da736abad9a77d022006dc39adf48e02033ae9d4a48540752ae3b3841e3ec60d2e86dececb88b9e518012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff03414111000000000017a914a153024c826a3a42c2e501eca5d7dacd3fc59976870000000000000000166a14db0e6f4d418d68dce8e5beb26cc5078e01e2e3ace2fe0800000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac8fc5925f000000000000000000000000000000".into(); let spent_by_tx_2 = vec![ UnspentInfo { - outpoint: tx_2.inputs[0].previous_output.clone(), + outpoint: tx_2.inputs[0].previous_output, value: 832532, height: Some(642293), }, UnspentInfo { - outpoint: tx_2.inputs[1].previous_output.clone(), + outpoint: tx_2.inputs[1].previous_output, value: 888823, height: Some(642293), }, @@ -2529,14 +2526,18 @@ fn test_validate_fee_wrong_sender() { let amount: BigDecimal = "0.0014157".parse().unwrap(); let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, - expected_sender: &*DEX_FEE_ADDR_RAW_PUBKEY, - fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + expected_sender: &DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, amount: &amount, min_block_number: 0, uuid: &[], }; - let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); - assert!(validate_err.contains("was sent from wrong address")); + let error = coin.validate_fee(validate_fee_args).wait().unwrap_err().into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains(INVALID_SENDER_ERR_LOG)), + _ => panic!("Expected `WrongPaymentTx` wrong sender address, found {:?}", error), + } } #[test] @@ -2555,13 +2556,16 @@ fn test_validate_fee_min_block() { let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, expected_sender: &sender_pub, - fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, amount: &amount, min_block_number: 810329, uuid: &[], }; - let validate_err = coin.validate_fee(validate_fee_args).wait().unwrap_err(); - assert!(validate_err.contains("confirmed before min_block")); + let error = coin.validate_fee(validate_fee_args).wait().unwrap_err().into_inner(); + match error { + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min_block")), + _ => panic!("Expected `WrongPaymentTx` early confirmation, found {:?}", error), + } } #[test] @@ -2581,7 +2585,7 @@ fn test_validate_fee_bch_70_bytes_signature() { let validate_fee_args = ValidateFeeArgs { fee_tx: &taker_fee_tx, expected_sender: &sender_pub, - fee_addr: &*DEX_FEE_ADDR_RAW_PUBKEY, + fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, amount: &amount, min_block_number: 0, uuid: &[], @@ -2736,8 +2740,7 @@ fn test_generate_tx_doge_fee() { TransactionOutput { value: 100000000, script_pubkey: vec![0; 26].into(), - } - .clone(); + }; 40 ]; @@ -2757,8 +2760,7 @@ fn test_generate_tx_doge_fee() { TransactionOutput { value: 100000000, script_pubkey: vec![0; 26].into(), - } - .clone(); + }; 60 ]; @@ -3067,7 +3069,7 @@ fn test_withdraw_to_p2pkh() { let client = NativeClient(Arc::new(NativeClientImpl::default())); - let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client.clone()), None, false); + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); // Create a p2pkh address for the test coin let p2pkh_address = Address { @@ -3115,7 +3117,7 @@ fn test_withdraw_to_p2sh() { let client = NativeClient(Arc::new(NativeClientImpl::default())); - let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client.clone()), None, false); + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); // Create a p2sh address for the test coin let p2sh_address = Address { @@ -3163,7 +3165,7 @@ fn test_withdraw_to_p2wpkh() { let client = NativeClient(Arc::new(NativeClientImpl::default())); - let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client.clone()), None, true); + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, true); // Create a p2wpkh address for the test coin let p2wpkh_address = Address { @@ -3469,7 +3471,7 @@ fn test_account_balance_rpc() { let address_str = address.to_string(); let balance = addresses_map .remove(&address_str) - .expect(&format!("Unexpected address: {}", address_str)); + .unwrap_or_else(|| panic!("Unexpected address: {}", address_str)); (address, BigDecimal::from(balance)) }) .collect(); @@ -3786,7 +3788,7 @@ fn test_scan_for_new_addresses() { let address = address.to_string(); let balance = display_balances .remove(&address) - .expect(&format!("Unexpected address: {}", address)); + .unwrap_or_else(|| panic!("Unexpected address: {}", address)); MockResult::Return(Box::new(futures01::future::ok(BigDecimal::from(balance)))) }); @@ -4123,11 +4125,14 @@ fn test_for_non_existent_tx_hex_utxo_electrum() { ); // bad transaction hex let tx = hex::decode("0400008085202f8902bf17bf7d1daace52e08f732a6b8771743ca4b1cb765a187e72fd091a0aabfd52000000006a47304402203eaaa3c4da101240f80f9c5e9de716a22b1ec6d66080de6a0cca32011cd77223022040d9082b6242d6acf9a1a8e658779e1c655d708379862f235e8ba7b8ca4e69c6012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffffff023ca13c0e9e085dd13f481f193e8a3e8fd609020936e98b5587342d994f4d020000006b483045022100c0ba56adb8de923975052312467347d83238bd8d480ce66e8b709a7997373994022048507bcac921fdb2302fa5224ce86e41b7efc1a2e20ae63aa738dfa99b7be826012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0300e1f5050000000017a9141ee6d4c38a3c078eab87ad1a5e4b00f21259b10d87000000000000000016611400000000000000000000000000000000000000001b94d736000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac2d08e35e000000000000000000000000000000").unwrap(); - let actual = coin - .wait_for_confirmations(&tx, 1, false, timeout, 1) - .wait() - .err() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx, + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + let actual = coin.wait_for_confirmations(confirm_payment_input).wait().err().unwrap(); assert!(actual.contains( "Tx d342ff9da528a2e262bddf2b6f9a27d1beb7aeb03f0fc8d9eac2987266447e44 was not found on chain after 10 tries" )); @@ -4343,7 +4348,7 @@ fn test_block_header_utxo_loop() { }); let ctx = mm_ctx_with_custom_db(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(IguanaPrivKey::from(H256Json::from([1u8; 32]))); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(H256Json::from([1u8; 32])); let servers: Vec<_> = RICK_ELECTRUM_ADDRS .iter() .map(|server| json!({ "url": server })) diff --git a/mm2src/coins/utxo/utxo_wasm_tests.rs b/mm2src/coins/utxo/utxo_wasm_tests.rs index 0d3dd0041d..65bd2fe20a 100644 --- a/mm2src/coins/utxo/utxo_wasm_tests.rs +++ b/mm2src/coins/utxo/utxo_wasm_tests.rs @@ -10,7 +10,7 @@ use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); -const TEST_COIN_NAME: &'static str = "RICK"; +const TEST_COIN_NAME: &str = "RICK"; pub async fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let ctx = MmCtxBuilder::default().into_mm_arc(); diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 72c86f38a1..416b6292c1 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -13,20 +13,18 @@ use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxDa RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; -use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, FeeApproxStage, - FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, + FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructions, PaymentInstructionsErr, PrivKeyActivationPolicy, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, - RawTransactionRequest, RefundError, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentArgs, - SendMakerPaymentSpendPreimageInput, SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, - SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, - SendWatcherRefundsPaymentArgs, SignatureError, SignatureResult, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionFut, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, - ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WatcherOps, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, - WithdrawRequest}; + RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, SignatureResult, SpendPaymentArgs, + SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionDetails, TransactionEnum, TransactionFut, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, + VerificationResult, WatcherOps, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; use crate::{Transaction, WithdrawError}; use async_trait::async_trait; use bitcrypto::dhash256; @@ -1049,15 +1047,8 @@ impl MarketCoinOps for ZCoin { Box::new(fut.boxed().compat()) } - fn wait_for_confirmations( - &self, - tx: &[u8], - confirmations: u64, - requires_nota: bool, - wait_until: u64, - check_every: u64, - ) -> Box + Send> { - utxo_common::wait_for_confirmations(self.as_ref(), tx, confirmations, requires_nota, wait_until, check_every) + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { + utxo_common::wait_for_confirmations(self.as_ref(), input) } fn wait_for_htlc_tx_spend( @@ -1115,7 +1106,7 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn send_maker_payment(&self, maker_payment_args: SendMakerPaymentArgs<'_>) -> TransactionFut { + fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionFut { let selfi = self.clone(); let maker_key_pair = self.derive_htlc_key_pair(maker_payment_args.swap_unique_data); let taker_pub = try_tx_fus!(Public::from_slice(maker_payment_args.other_pubkey)); @@ -1139,7 +1130,7 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn send_taker_payment(&self, taker_payment_args: SendTakerPaymentArgs<'_>) -> TransactionFut { + fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs<'_>) -> TransactionFut { let selfi = self.clone(); let taker_keypair = self.derive_htlc_key_pair(taker_payment_args.swap_unique_data); let maker_pub = try_tx_fus!(Public::from_slice(taker_payment_args.other_pubkey)); @@ -1163,10 +1154,7 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn send_maker_spends_taker_payment( - &self, - maker_spends_payment_args: SendMakerSpendsTakerPaymentArgs<'_>, - ) -> TransactionFut { + fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { let tx = try_tx_fus!(ZTransaction::read(maker_spends_payment_args.other_payment_tx)); let key_pair = self.derive_htlc_key_pair(maker_spends_payment_args.swap_unique_data); let time_lock = maker_spends_payment_args.time_lock; @@ -1197,10 +1185,7 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn send_taker_spends_maker_payment( - &self, - taker_spends_payment_args: SendTakerSpendsMakerPaymentArgs<'_>, - ) -> TransactionFut { + fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { let tx = try_tx_fus!(ZTransaction::read(taker_spends_payment_args.other_payment_tx)); let key_pair = self.derive_htlc_key_pair(taker_spends_payment_args.swap_unique_data); let time_lock = taker_spends_payment_args.time_lock; @@ -1231,10 +1216,7 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn send_taker_refunds_payment( - &self, - taker_refunds_payment_args: SendTakerRefundsPaymentArgs<'_>, - ) -> TransactionFut { + fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut { let tx = try_tx_fus!(ZTransaction::read(taker_refunds_payment_args.payment_tx)); let key_pair = self.derive_htlc_key_pair(taker_refunds_payment_args.swap_unique_data); let time_lock = taker_refunds_payment_args.time_lock; @@ -1262,10 +1244,7 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn send_maker_refunds_payment( - &self, - maker_refunds_payment_args: SendMakerRefundsPaymentArgs<'_>, - ) -> TransactionFut { + fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionFut { let tx = try_tx_fus!(ZTransaction::read(maker_refunds_payment_args.payment_tx)); let key_pair = self.derive_htlc_key_pair(maker_refunds_payment_args.swap_unique_data); let time_lock = maker_refunds_payment_args.time_lock; @@ -1293,41 +1272,41 @@ impl SwapOps for ZCoin { Box::new(fut.boxed().compat()) } - fn validate_fee( - &self, - validate_fee_args: ValidateFeeArgs<'_>, - ) -> Box + Send> { + fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { let z_tx = match validate_fee_args.fee_tx { TransactionEnum::ZTransaction(t) => t.clone(), _ => panic!("Unexpected tx {:?}", validate_fee_args.fee_tx), }; - let amount_sat = try_fus!(sat_from_big_decimal(validate_fee_args.amount, self.utxo_arc.decimals)); + let amount_sat = try_f!(sat_from_big_decimal(validate_fee_args.amount, self.utxo_arc.decimals)); let expected_memo = MemoBytes::from_bytes(validate_fee_args.uuid).expect("Uuid length < 512"); let min_block_number = validate_fee_args.min_block_number; let coin = self.clone(); let fut = async move { let tx_hash = H256::from(z_tx.txid().0).reversed(); - let tx_from_rpc = try_s!( - coin.utxo_rpc_client() - .get_verbose_transaction(&tx_hash.into()) - .compat() - .await - ); + let tx_from_rpc = coin + .utxo_rpc_client() + .get_verbose_transaction(&tx_hash.into()) + .compat() + .await + .map_err(|e| MmError::new(ValidatePaymentError::InvalidRpcResponse(e.into_inner().to_string())))?; + let mut encoded = Vec::with_capacity(1024); z_tx.write(&mut encoded).expect("Writing should not fail"); if encoded != tx_from_rpc.hex.0 { - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Encoded transaction {:?} does not match the tx {:?} from RPC", - encoded, - tx_from_rpc - ); + encoded, tx_from_rpc + ))); } let block_height = match tx_from_rpc.height { Some(h) => { if h < min_block_number { - return ERR!("Dex fee tx {:?} confirmed before min block {}", z_tx, min_block_number); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Dex fee tx {:?} confirmed before min block {}", + z_tx, min_block_number + ))); } else { BlockHeight::from_u32(h as u32) } @@ -1346,29 +1325,34 @@ impl SwapOps for ZCoin { z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &coin.z_fields.dex_fee_addr, ); - return ERR!( + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Dex fee was sent to the invalid address {}, expected {}", - encoded, - expected - ); + encoded, expected + ))); } if note.value != amount_sat { - return ERR!("Dex fee has invalid amount {}, expected {}", note.value, amount_sat); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Dex fee has invalid amount {}, expected {}", + note.value, amount_sat + ))); } if memo != expected_memo { - return ERR!("Dex fee has invalid memo {:?}, expected {:?}", memo, expected_memo); + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Dex fee has invalid memo {:?}, expected {:?}", + memo, expected_memo + ))); } return Ok(()); } } - ERR!( + MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "The dex fee tx {:?} has no shielded outputs or outputs decryption failed", z_tx - ) + ))) }; Box::new(fut.boxed().compat()) @@ -1419,7 +1403,12 @@ impl SwapOps for ZCoin { } #[inline] - async fn extract_secret(&self, secret_hash: &[u8], spend_tx: &[u8]) -> Result, String> { + async fn extract_secret( + &self, + secret_hash: &[u8], + spend_tx: &[u8], + _watcher_reward: bool, + ) -> Result, String> { utxo_common::extract_secret(secret_hash, spend_tx) } @@ -1539,10 +1528,7 @@ impl WatcherOps for ZCoin { unimplemented!(); } - fn send_taker_payment_refund_preimage( - &self, - _watcher_refunds_payment_args: SendWatcherRefundsPaymentArgs, - ) -> TransactionFut { + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { unimplemented!(); } @@ -1666,9 +1652,17 @@ impl MmCoin for ZCoin { fn mature_confirmations(&self) -> Option { Some(self.utxo_arc.conf.mature_confirmations) } - fn coin_protocol_info(&self) -> Vec { utxo_common::coin_protocol_info(self) } + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { + utxo_common::coin_protocol_info(self) + } - fn is_coin_protocol_supported(&self, info: &Option>) -> bool { + fn is_coin_protocol_supported( + &self, + info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { utxo_common::is_coin_protocol_supported(self, info) } diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index 7506494822..e6ae6401a0 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -6,6 +6,10 @@ edition = "2018" [lib] doctest = false +[features] +enable-solana = [] +default = [] + [dependencies] async-trait = "0.1" coins = { path = "../coins" } @@ -32,6 +36,6 @@ serde_json = { version = "1", features = ["preserve_order", "raw_value"] } mm2_metamask = { path = "../mm2_metamask" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -lightning = "0.0.110" -lightning-background-processor = "0.0.110" -lightning-invoice = { version = "0.18.0", features = ["serde"] } +lightning = "0.0.113" +lightning-background-processor = "0.0.113" +lightning-invoice = { version = "0.21.0", features = ["serde"] } diff --git a/mm2src/coins_activation/src/lib.rs b/mm2src/coins_activation/src/lib.rs index 5f4245af6c..34bd11e902 100644 --- a/mm2src/coins_activation/src/lib.rs +++ b/mm2src/coins_activation/src/lib.rs @@ -7,9 +7,19 @@ mod l2; mod platform_coin_with_tokens; mod prelude; mod slp_token_activation; -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] mod solana_with_tokens_activation; -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] mod spl_token_activation; mod standalone_coin; mod tendermint_token_activation; diff --git a/mm2src/coins_activation/src/lightning_activation.rs b/mm2src/coins_activation/src/lightning_activation.rs index b3da8c3f7b..c5aed05804 100644 --- a/mm2src/coins_activation/src/lightning_activation.rs +++ b/mm2src/coins_activation/src/lightning_activation.rs @@ -20,12 +20,12 @@ use common::executor::{SpawnFuture, Timer}; use crypto::hw_rpc_task::{HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use derive_more::Display; use futures::compat::Future01CompatExt; -use lightning::chain::keysinterface::{KeysInterface, Recipient}; +use lightning::chain::keysinterface::KeysInterface; use lightning::chain::Access; use lightning::routing::gossip; +use lightning::routing::router::DefaultRouter; use lightning_background_processor::{BackgroundProcessor, GossipSync}; use lightning_invoice::payment; -use lightning_invoice::utils::DefaultRouter; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use parking_lot::Mutex as PaMutex; @@ -392,10 +392,8 @@ async fn start_lightning( &platform, params.listening_port, channel_manager.clone(), + keys_manager.clone(), gossip_sync.clone(), - keys_manager - .get_node_secret(Recipient::Node) - .map_to_mm(|_| EnableLightningError::UnsupportedMode("'start_lightning'".into(), "local node".into()))?, logger.clone(), ) .await?; @@ -428,11 +426,15 @@ async fn start_lightning( // https://github.com/lightningdevkit/rust-lightning/pull/1286 // https://github.com/lightningdevkit/rust-lightning/pull/1359 let router_random_seed_bytes = keys_manager.get_secure_random_bytes(); - let router = DefaultRouter::new(network_graph.clone(), logger.clone(), router_random_seed_bytes); + let router = DefaultRouter::new( + network_graph.clone(), + logger.clone(), + router_random_seed_bytes, + scorer.clone(), + ); let invoice_payer = Arc::new(InvoicePayer::new( channel_manager.clone(), router, - scorer.clone(), logger.clone(), event_handler, // Todo: Add option for choosing payment::Retry::Timeout instead of Attempts in LightningParams @@ -468,7 +470,7 @@ async fn start_lightning( // Broadcast Node Announcement platform.spawner().spawn(ln_node_announcement_loop( - channel_manager.clone(), + peer_manager.clone(), params.node_name, params.node_color, params.listening_port, @@ -487,7 +489,12 @@ async fn start_lightning( db, open_channels_nodes, trusted_nodes, - router: Arc::new(DefaultRouter::new(network_graph, logger, router_random_seed_bytes)), - scorer, + router: Arc::new(DefaultRouter::new( + network_graph, + logger.clone(), + router_random_seed_bytes, + scorer, + )), + logger, }) } diff --git a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs index d6a3497640..47894c2a6f 100644 --- a/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs +++ b/mm2src/coins_activation/src/utxo_activation/init_utxo_standard_activation_error.rs @@ -115,7 +115,23 @@ impl InitUtxoStandardError { HwError::CannotChooseDevice { .. } => InitUtxoStandardError::HwError(HwRpcError::FoundMultipleDevices), HwError::ConnectionTimedOut { timeout } => InitUtxoStandardError::TaskTimedOut { duration: timeout }, HwError::FoundUnexpectedDevice => InitUtxoStandardError::HwError(HwRpcError::FoundUnexpectedDevice), - HwError::Failure(error) => InitUtxoStandardError::CoinCreationError { ticker, error }, + HwError::InvalidPin + | HwError::UnexpectedMessage + | HwError::ButtonExpected + | HwError::DataError + | HwError::PinExpected + | HwError::InvalidSignature + | HwError::ProcessError + | HwError::NotEnoughFunds + | HwError::NotInitialized + | HwError::WipeCodeMismatch + | HwError::InvalidSession + | HwError::FirmwareError + | HwError::FailureMessageNotFound + | HwError::UserCancelled => InitUtxoStandardError::CoinCreationError { + ticker, + error: hw_error.to_string(), + }, other => InitUtxoStandardError::Internal(other.to_string()), } } diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index 2327ebd957..dfe77611e4 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -18,7 +18,7 @@ async-trait = "0.1" backtrace = "0.3" bytes = "1.1" cfg-if = "1.0" -crossbeam = "0.7" +crossbeam = "0.8" fnv = "1.0.6" futures01 = { version = "0.1", package = "futures" } futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } @@ -32,7 +32,7 @@ log = "0.4.8" parking_lot = { version = "0.12.0", features = ["nightly"] } parking_lot_core = { version = "0.6", features = ["nightly"] } primitive-types = "0.11.1" -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } +rand = { version = "0.7", features = ["std", "small_rng"] } serde = "1" serde_derive = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } @@ -40,7 +40,7 @@ ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } sha2 = "0.9" shared_ref_counter = { path = "shared_ref_counter", optional = true } -uuid = { version = "0.7", features = ["serde", "v4"] } +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } wasm-timer = "0.2.4" [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -58,14 +58,13 @@ web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomExcepti [target.'cfg(not(target_arch = "wasm32"))'.dependencies] anyhow = "1.0" chrono = "0.4" -crossterm = "0.20" -gstuff = { version = "0.7", features = ["crossterm", "nightly"] } +gstuff = { version = "0.7", features = ["nightly"] } hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features hyper-rustls = { version = "0.23", default-features = false, features = ["http1", "http2", "webpki-tokio"] } libc = { version = "0.2" } -lightning = "0.0.110" +lightning = "0.0.113" log4rs = { version = "1.0", default-features = false, features = ["console_appender", "pattern_encoder"] } tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 5a8edbd7da..e9000bd0da 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -174,6 +174,7 @@ cfg_wasm32! { } pub const X_GRPC_WEB: &str = "x-grpc-web"; +pub const X_API_KEY: &str = "X-API-Key"; pub const APPLICATION_JSON: &str = "application/json"; pub const APPLICATION_GRPC_WEB: &str = "application/grpc-web"; pub const APPLICATION_GRPC_WEB_PROTO: &str = "application/grpc-web+proto"; @@ -798,7 +799,7 @@ pub struct PagingOptions { pub from_uuid: Option, } -#[cfg(not(target_arch = "wasm32"))] +#[inline] pub fn new_uuid() -> Uuid { Uuid::new_v4() } pub fn first_char_to_upper(input: &str) -> String { @@ -929,16 +930,13 @@ pub fn is_acceptable_input_on_repeated_characters(entry: &str, limit: usize) -> #[test] fn test_is_acceptable_input_on_repeated_characters() { - assert_eq!(is_acceptable_input_on_repeated_characters("Hello", 3), true); - assert_eq!(is_acceptable_input_on_repeated_characters("Hellooo", 3), false); - assert_eq!( - is_acceptable_input_on_repeated_characters("SuperStrongPassword123*", 3), - true - ); - assert_eq!( - is_acceptable_input_on_repeated_characters("SuperStrongaaaPassword123*", 3), - false - ); + assert!(is_acceptable_input_on_repeated_characters("Hello", 3)); + assert!(!is_acceptable_input_on_repeated_characters("Hellooo", 3)); + assert!(is_acceptable_input_on_repeated_characters("SuperStrongPassword123*", 3)); + assert!(!is_acceptable_input_on_repeated_characters( + "SuperStrongaaaPassword123*", + 3 + )); } #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] diff --git a/mm2src/common/crash_reports.rs b/mm2src/common/crash_reports.rs index 4d5afe39c9..a749824015 100644 --- a/mm2src/common/crash_reports.rs +++ b/mm2src/common/crash_reports.rs @@ -42,7 +42,7 @@ pub extern "C" fn rust_seh_handler(exception_code: u32) { #[inline(never)] #[allow(dead_code)] fn access_violation() { - let ptr: *mut i32 = 0 as *mut i32; + let ptr: *mut i32 = std::ptr::null_mut::(); unsafe { *ptr = 123 }; } diff --git a/mm2src/common/custom_futures/repeatable.rs b/mm2src/common/custom_futures/repeatable.rs index 1e31bc5a4d..80f5b635f8 100644 --- a/mm2src/common/custom_futures/repeatable.rs +++ b/mm2src/common/custom_futures/repeatable.rs @@ -532,6 +532,7 @@ mod tests { } #[test] + #[cfg(not(target_os = "macos"))] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 fn test_until_expired() { const ATTEMPTS_TO_FINISH: usize = 10; const LOWEST_TIMEOUT: Duration = Duration::from_millis(350); @@ -566,6 +567,7 @@ mod tests { } #[test] + #[cfg(not(target_os = "macos"))] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 fn test_until_ms() { const ATTEMPTS_TO_FINISH: usize = 5; const LOWEST_TIMEOUT: u64 = 350; diff --git a/mm2src/common/custom_futures/timeout.rs b/mm2src/common/custom_futures/timeout.rs index 0c54ed71be..594b07ad9e 100644 --- a/mm2src/common/custom_futures/timeout.rs +++ b/mm2src/common/custom_futures/timeout.rs @@ -81,6 +81,6 @@ mod tests { fn test_timeout() { let _err = crate::block_on(Timer::sleep(0.4).timeout(Duration::from_secs_f64(0.1))).expect_err("Expected timeout"); - let _ok = crate::block_on(Timer::sleep(0.1).timeout(Duration::from_secs_f64(0.2))).expect("Expected future"); + crate::block_on(Timer::sleep(0.1).timeout(Duration::from_secs_f64(0.2))).expect("Expected future"); } } diff --git a/mm2src/common/executor/abortable_system/abortable_queue.rs b/mm2src/common/executor/abortable_system/abortable_queue.rs index 9e74181e58..99ffc70ca3 100644 --- a/mm2src/common/executor/abortable_system/abortable_queue.rs +++ b/mm2src/common/executor/abortable_system/abortable_queue.rs @@ -258,7 +258,7 @@ mod tests { let fut1 = async { Timer::sleep(0.3).await }; let fut2 = async { Timer::sleep(0.7).await }; spawner.spawn_with_settings(fut1, settings.clone()); - spawner.spawn_with_settings(fut2, settings.clone()); + spawner.spawn_with_settings(fut2, settings); { let inner = abortable_system.inner.lock(); @@ -318,7 +318,7 @@ mod tests { Timer::sleep(0.2).await; unsafe { F2_FINISHED = true }; }; - spawner.spawn_with_settings(fut2, settings.clone()); + spawner.spawn_with_settings(fut2, settings); abortable_system.abort_all().unwrap(); diff --git a/mm2src/common/log.rs b/mm2src/common/log.rs index 733d0e8f83..700dc0b9dd 100644 --- a/mm2src/common/log.rs +++ b/mm2src/common/log.rs @@ -98,7 +98,7 @@ impl Gravity { fn flush(&self) { let logged_with_log_output = LOG_CALLBACK.lock().is_some(); let mut tail = self.tail.lock(); - while let Ok(chunk) = self.landing.pop() { + while let Some(chunk) = self.landing.pop() { if !logged_with_log_output { writeln(&chunk) } diff --git a/mm2src/common/shared_ref_counter/src/enable.rs b/mm2src/common/shared_ref_counter/src/enable.rs index bc4f6a0a67..205b36aab7 100644 --- a/mm2src/common/shared_ref_counter/src/enable.rs +++ b/mm2src/common/shared_ref_counter/src/enable.rs @@ -77,7 +77,7 @@ impl SharedRc { let existing_pointers = self.existing_pointers.read().expect(LOCKING_ERROR); log!(level, "{} exists at:", ident); for (_idx, location) in existing_pointers.iter() { - log!(level, "\t{}", stringify_location(*location)); + log!(level, "\t{}", stringify_location(location)); } } diff --git a/mm2src/common/wasm.rs b/mm2src/common/wasm.rs index a0162e6068..2342b59383 100644 --- a/mm2src/common/wasm.rs +++ b/mm2src/common/wasm.rs @@ -1,24 +1,10 @@ use crate::filename; -use rand::RngCore; use serde::de::DeserializeOwned; use serde::Serialize; use serde_wasm_bindgen::Serializer; use std::fmt; -use uuid::{Builder, Uuid, Variant, Version}; use wasm_bindgen::prelude::*; -pub fn new_uuid() -> Uuid { - let mut rng = rand::thread_rng(); - let mut bytes = [0; 16]; - - rng.fill_bytes(&mut bytes); - - Builder::from_bytes(bytes) - .set_variant(Variant::RFC4122) - .set_version(Version::Random) - .build() -} - /// Get only the first line of the error. /// Generally, the `JsValue` error contains the stack trace of an error. /// This function cuts off the stack trace. diff --git a/mm2src/crypto/src/hw_error.rs b/mm2src/crypto/src/hw_error.rs index d9e7b03610..1efd4b243f 100644 --- a/mm2src/crypto/src/hw_error.rs +++ b/mm2src/crypto/src/hw_error.rs @@ -3,7 +3,7 @@ use hw_common::primitives::Bip32Error; use mm2_err_handle::prelude::*; use serde::Serialize; use std::time::Duration; -use trezor::{TrezorError, TrezorUserInteraction}; +use trezor::{OperationFailure, TrezorError, TrezorUserInteraction}; pub type HwResult = Result>; @@ -28,11 +28,25 @@ pub enum HwError { }, #[display(fmt = "Invalid xpub received from a device: '{}'", _0)] InvalidXpub(String), - Failure(String), UnderlyingError(String), ProtocolError(String), UnexpectedUserInteractionRequest(TrezorUserInteraction), Internal(String), + InvalidPin, + UnexpectedMessage, + ButtonExpected, + DataError, + PinExpected, + InvalidSignature, + ProcessError, + NotEnoughFunds, + NotInitialized, + WipeCodeMismatch, + InvalidSession, + FirmwareError, + FailureMessageNotFound, + UserCancelled, + PongMessageMismatch, } impl From for HwError { @@ -44,10 +58,25 @@ impl From for HwError { TrezorError::DeviceDisconnected => HwError::DeviceDisconnected, TrezorError::UnderlyingError(_) => HwError::UnderlyingError(error), TrezorError::ProtocolError(_) | TrezorError::UnexpectedMessageType(_) => HwError::Internal(error), - // TODO handle the failure correctly later - TrezorError::Failure(_) => HwError::Failure(error), + TrezorError::Failure(failure) => match failure { + OperationFailure::InvalidPin => HwError::InvalidPin, + OperationFailure::UnexpectedMessage => HwError::UnexpectedMessage, + OperationFailure::ButtonExpected => HwError::ButtonExpected, + OperationFailure::DataError => HwError::DataError, + OperationFailure::PinExpected => HwError::PinExpected, + OperationFailure::InvalidSignature => HwError::InvalidSignature, + OperationFailure::ProcessError => HwError::ProcessError, + OperationFailure::NotEnoughFunds => HwError::NotEnoughFunds, + OperationFailure::NotInitialized => HwError::NotInitialized, + OperationFailure::WipeCodeMismatch => HwError::WipeCodeMismatch, + OperationFailure::InvalidSession => HwError::InvalidSession, + OperationFailure::FirmwareError => HwError::FirmwareError, + OperationFailure::FailureMessageNotFound => HwError::FailureMessageNotFound, + OperationFailure::UserCancelled => HwError::UserCancelled, + }, TrezorError::UnexpectedInteractionRequest(req) => HwError::UnexpectedUserInteractionRequest(req), TrezorError::Internal(_) => HwError::Internal(error), + TrezorError::PongMessageMismatch => HwError::PongMessageMismatch, } } } @@ -69,6 +98,36 @@ pub enum HwRpcError { FoundMultipleDevices, #[display(fmt = "Found unexpected device. Please re-initialize Hardware wallet")] FoundUnexpectedDevice, + #[display(fmt = "Pin is invalid")] + InvalidPin, + #[display(fmt = "Unexpected message")] + UnexpectedMessage, + #[display(fmt = "Button expected")] + ButtonExpected, + #[display(fmt = "Got data error")] + DataError, + #[display(fmt = "Pin expected")] + PinExpected, + #[display(fmt = "Invalid signature")] + InvalidSignature, + #[display(fmt = "Got process error")] + ProcessError, + #[display(fmt = "Not enough funds")] + NotEnoughFunds, + #[display(fmt = "Not initialized")] + NotInitialized, + #[display(fmt = "Wipe code mismatch")] + WipeCodeMismatch, + #[display(fmt = "Invalid session")] + InvalidSession, + #[display(fmt = "Got firmware error")] + FirmwareError, + #[display(fmt = "Failure message not found")] + FailureMessageNotFound, + #[display(fmt = "User cancelled action")] + UserCancelled, + #[display(fmt = "PONG message mismatch after ping")] + PongMessageMismatch, } /// The trait is implemented for those error enumerations that have `HwRpcError` variant. @@ -90,6 +149,21 @@ where HwError::CannotChooseDevice { .. } => T::hw_rpc_error(HwRpcError::FoundMultipleDevices), HwError::ConnectionTimedOut { timeout } => T::timeout(timeout), HwError::FoundUnexpectedDevice => T::hw_rpc_error(HwRpcError::FoundUnexpectedDevice), + HwError::InvalidPin => T::hw_rpc_error(HwRpcError::InvalidPin), + HwError::UnexpectedMessage => T::hw_rpc_error(HwRpcError::UnexpectedMessage), + HwError::ButtonExpected => T::hw_rpc_error(HwRpcError::ButtonExpected), + HwError::DataError => T::hw_rpc_error(HwRpcError::DataError), + HwError::PinExpected => T::hw_rpc_error(HwRpcError::PinExpected), + HwError::InvalidSignature => T::hw_rpc_error(HwRpcError::InvalidSignature), + HwError::ProcessError => T::hw_rpc_error(HwRpcError::ProcessError), + HwError::NotEnoughFunds => T::hw_rpc_error(HwRpcError::NotEnoughFunds), + HwError::NotInitialized => T::hw_rpc_error(HwRpcError::NotInitialized), + HwError::WipeCodeMismatch => T::hw_rpc_error(HwRpcError::WipeCodeMismatch), + HwError::InvalidSession => T::hw_rpc_error(HwRpcError::InvalidSession), + HwError::FirmwareError => T::hw_rpc_error(HwRpcError::FirmwareError), + HwError::FailureMessageNotFound => T::hw_rpc_error(HwRpcError::FailureMessageNotFound), + HwError::UserCancelled => T::hw_rpc_error(HwRpcError::UserCancelled), + HwError::PongMessageMismatch => T::hw_rpc_error(HwRpcError::PongMessageMismatch), other => T::internal(other.to_string()), } } diff --git a/mm2src/crypto/src/metamask_ctx.rs b/mm2src/crypto/src/metamask_ctx.rs index 4c9e0e681e..a36b957e79 100644 --- a/mm2src/crypto/src/metamask_ctx.rs +++ b/mm2src/crypto/src/metamask_ctx.rs @@ -58,7 +58,7 @@ impl MetamaskCtx { .await?; let sig = sig.strip_prefix("0x").unwrap_or(&sig); - let signature = Signature::from_str(&sig) + let signature = Signature::from_str(sig) .map_to_mm(|_| MetamaskError::Internal(format!("'{sig}' signature is invalid")))?; let pubkey = recover_pubkey(hash, signature).mm_err(|_| { let error = format!("Couldn't recover a public key from the signature: '{sig}'"); diff --git a/mm2src/db_common/Cargo.toml b/mm2src/db_common/Cargo.toml index 87d16bbbc5..40bff9f18e 100644 --- a/mm2src/db_common/Cargo.toml +++ b/mm2src/db_common/Cargo.toml @@ -10,7 +10,7 @@ doctest = false common = { path = "../common" } hex = "0.4.2" log = "0.4.8" -uuid = { version = "0.7", features = ["serde", "v4"] } +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rusqlite = { version = "0.24.2", features = ["bundled"] } diff --git a/mm2src/db_common/src/sql_query.rs b/mm2src/db_common/src/sql_query.rs index 287bd8db47..4c7b6eb362 100644 --- a/mm2src/db_common/src/sql_query.rs +++ b/mm2src/db_common/src/sql_query.rs @@ -457,7 +457,7 @@ mod tests { query .field("tx_hash") .unwrap() - .and_where_in_params("height", SEARCHING_HEIGHTS.clone()) + .and_where_in_params("height", SEARCHING_HEIGHTS) .unwrap(); assert_eq!( query.clone().sql().unwrap(), diff --git a/mm2src/floodsub/Cargo.toml b/mm2src/floodsub/Cargo.toml index 614cdd9b30..b54b60448c 100644 --- a/mm2src/floodsub/Cargo.toml +++ b/mm2src/floodsub/Cargo.toml @@ -22,4 +22,4 @@ rand = "0.7" smallvec = "1.0" [build-dependencies] -prost-build = "0.10.3" +prost-build = { version = "0.10.4", default-features = false } diff --git a/mm2src/gossipsub/Cargo.toml b/mm2src/gossipsub/Cargo.toml index 7df4785fc1..ad7ddbc03e 100644 --- a/mm2src/gossipsub/Cargo.toml +++ b/mm2src/gossipsub/Cargo.toml @@ -38,4 +38,4 @@ libp2p-yamux = { git = "https://github.com/libp2p/rust-libp2p.git", tag ="v0.45. quickcheck = "0.9.2" [build-dependencies] -prost-build = "0.10.3" +prost-build = { version = "0.10.4", default-features = false } diff --git a/mm2src/gossipsub/src/behaviour/tests.rs b/mm2src/gossipsub/src/behaviour/tests.rs index 5dee054c59..1c1923df0b 100644 --- a/mm2src/gossipsub/src/behaviour/tests.rs +++ b/mm2src/gossipsub/src/behaviour/tests.rs @@ -21,6 +21,7 @@ // collection of tests for the gossipsub network behaviour #[cfg(test)] +#[allow(clippy::module_inception)] mod tests { use super::super::*; use crate::GossipsubConfigBuilder; @@ -55,7 +56,7 @@ mod tests { for i in 0..peer_no { let peer = PeerId::random(); - peers.push(peer.clone()); + peers.push(peer); ::inject_connection_established( &mut gs, &peer, @@ -82,7 +83,7 @@ mod tests { }; } - return (gs, peers, topic_hashes); + (gs, peers, topic_hashes) } #[test] @@ -105,10 +106,9 @@ mod tests { let subscriptions = gs.events.iter().fold(vec![], |mut collected_subscriptions, e| match e { NetworkBehaviourAction::NotifyHandler { event, .. } => { for s in &event.subscriptions { - match s.action { - GossipsubSubscriptionAction::Subscribe => collected_subscriptions.push(s.clone()), - _ => {}, - }; + if s.action == GossipsubSubscriptionAction::Subscribe { + collected_subscriptions.push(s.clone()) + } } collected_subscriptions }, @@ -141,10 +141,10 @@ mod tests { for topic_hash in &topic_hashes { assert!( - gs.topic_peers.get(&topic_hash).is_some(), + gs.topic_peers.get(topic_hash).is_some(), "Topic_peers contain a topic entry" ); - assert!(gs.mesh.get(&topic_hash).is_some(), "mesh should contain a topic entry"); + assert!(gs.mesh.get(topic_hash).is_some(), "mesh should contain a topic entry"); } // unsubscribe from both topics @@ -160,10 +160,9 @@ mod tests { let subscriptions = gs.events.iter().fold(vec![], |mut collected_subscriptions, e| match e { NetworkBehaviourAction::NotifyHandler { event, .. } => { for s in &event.subscriptions { - match s.action { - GossipsubSubscriptionAction::Unsubscribe => collected_subscriptions.push(s.clone()), - _ => {}, - }; + if s.action == GossipsubSubscriptionAction::Unsubscribe { + collected_subscriptions.push(s.clone()) + } } collected_subscriptions }, @@ -179,7 +178,7 @@ mod tests { // check we clean up internal structures for topic_hash in &topic_hashes { assert!( - gs.mesh.get(&topic_hash).is_none(), + gs.mesh.get(topic_hash).is_none(), "All topics should have been removed from the mesh" ); } @@ -233,9 +232,8 @@ mod tests { .iter() .fold(vec![], |mut collected_grafts, (_, controls)| { for c in controls.iter() { - match c { - GossipsubControlAction::Graft { topic_hash: _ } => collected_grafts.push(c.clone()), - _ => {}, + if let GossipsubControlAction::Graft { topic_hash: _ } = c { + collected_grafts.push(c.clone()) } } collected_grafts @@ -278,9 +276,8 @@ mod tests { .iter() .fold(vec![], |mut collected_grafts, (_, controls)| { for c in controls.iter() { - match c { - GossipsubControlAction::Graft { topic_hash: _ } => collected_grafts.push(c.clone()), - _ => {}, + if let GossipsubControlAction::Graft { topic_hash: _ } = c { + collected_grafts.push(c.clone()) } } collected_grafts @@ -323,7 +320,7 @@ mod tests { _ => collected_publish, }); - let msg_id = (gs.config.message_id_fn)(&publishes.first().expect("Should contain > 0 entries")); + let msg_id = (gs.config.message_id_fn)(publishes.first().expect("Should contain > 0 entries")); assert!( publishes.len() == 20, @@ -366,7 +363,7 @@ mod tests { gs.publish(&Topic::new(fanout_topic.clone()), publish_data); assert_eq!( - gs.fanout.get(&TopicHash::from_raw(fanout_topic.clone())).unwrap().len(), + gs.fanout.get(&TopicHash::from_raw(fanout_topic)).unwrap().len(), gs.config.mesh_n, "Fanout should contain `mesh_n` peers for fanout topic" ); @@ -382,7 +379,7 @@ mod tests { _ => collected_publish, }); - let msg_id = (gs.config.message_id_fn)(&publishes.first().expect("Should contain > 0 entries")); + let msg_id = (gs.config.message_id_fn)(publishes.first().expect("Should contain > 0 entries")); assert_eq!( publishes.len(), @@ -415,22 +412,16 @@ mod tests { let send_events: Vec<&NetworkBehaviourAction>> = gs .events .iter() - .filter(|e| match e { - NetworkBehaviourAction::NotifyHandler { .. } => true, - _ => false, - }) + .filter(|e| matches!(e, NetworkBehaviourAction::NotifyHandler { .. })) .collect(); // check that there are two subscriptions sent to each peer for sevent in send_events.clone() { - match sevent { - NetworkBehaviourAction::NotifyHandler { event, .. } => { - assert!( - event.subscriptions.len() == 2, - "There should be two subscriptions sent to each peer (1 for each topic)." - ); - }, - _ => {}, + if let NetworkBehaviourAction::NotifyHandler { event, .. } = sevent { + assert!( + event.subscriptions.len() == 2, + "There should be two subscriptions sent to each peer (1 for each topic)." + ); }; } @@ -516,7 +507,7 @@ mod tests { // Peer 0 unsubscribes from the first topic gs.handle_received_subscriptions( - &vec![GossipsubSubscription { + &[GossipsubSubscription { action: GossipsubSubscriptionAction::Unsubscribe, topic_hash: topic_hashes[0].clone(), }], @@ -545,7 +536,7 @@ mod tests { let mut gs: Gossipsub = Gossipsub::new(PeerId::random(), gs_config); // create a topic and fill it with some peers - let topic_hash = Topic::new("Test".into()).no_hash().clone(); + let topic_hash = Topic::new("Test".into()).no_hash(); let mut peers = vec![]; for _ in 0..20 { peers.push(PeerId::random()) @@ -562,10 +553,10 @@ mod tests { assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); assert!(random_peers == peers, "Expected no shuffling"); let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 0, |_| true); - assert!(random_peers.len() == 0, "Expected 0 peers to be returned"); + assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); // test the filter let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 5, |_| false); - assert!(random_peers.len() == 0, "Expected 0 peers to be returned"); + assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); let random_peers = Gossipsub::get_random_peers(&gs.topic_peers, &topic_hash, 10, |peer| peers.contains(peer)); assert!(random_peers.len() == 10, "Expected 10 peers to be returned"); } @@ -578,13 +569,13 @@ mod tests { let id = gs.config.message_id_fn; let message = GossipsubMessage { - source: peers[11].clone(), + source: peers[11], data: vec![1, 2, 3, 4], sequence_number: 1u64, topics: Vec::new(), }; let msg_id = id(&message); - gs.mcache.put(message.clone()); + gs.mcache.put(message); gs.handle_iwant(&peers[7], vec![msg_id.clone()]); @@ -614,7 +605,7 @@ mod tests { // perform 10 memshifts and check that it leaves the cache for shift in 1..10 { let message = GossipsubMessage { - source: peers[11].clone(), + source: peers[11], data: vec![1, 2, 3, 4], sequence_number: shift, topics: Vec::new(), @@ -674,7 +665,9 @@ mod tests { // check that we sent an IWANT request for `unknown id` let iwant_exists = match gs.control_pool.get(&peers[7]) { Some(controls) => controls.iter().any(|c| match c { - GossipsubControlAction::IWant { message_ids } => { + GossipsubControlAction::IWant { message_ids } => + { + #[allow(clippy::cmp_owned)] message_ids.iter().any(|m| *m.0 == String::from("unknown id")) }, _ => false, @@ -759,8 +752,7 @@ mod tests { .map(|&t| String::from(t)) .collect(); - let (mut gs, peers, topic_hashes) = - build_and_inject_nodes(20, topics.clone(), GossipsubConfig::default(), true); + let (mut gs, peers, topic_hashes) = build_and_inject_nodes(20, topics, GossipsubConfig::default(), true); let mut their_topics = topic_hashes.clone(); // their_topics = [topic1, topic2, topic3] @@ -770,9 +762,9 @@ mod tests { gs.handle_graft(&peers[7], their_topics.clone()); - for i in 0..2 { + for item in topic_hashes.iter().take(2) { assert!( - gs.mesh.get(&topic_hashes[i]).unwrap().contains(&peers[7]), + gs.mesh.get(item).unwrap().contains(&peers[7]), "Expected peer to be in the mesh for the first 2 topics" ); } @@ -869,7 +861,7 @@ mod tests { let config = GossipsubConfigBuilder::default().i_am_relay(true).build(); let (mut gs, peers, _) = build_and_inject_nodes(peer_no, vec![], config, false); for peer in &peers { - gs.connected_relays.insert(peer.clone()); + gs.connected_relays.insert(*peer); } gs.handle_included_to_relays_mesh(&peers[0], true, 1); @@ -885,9 +877,9 @@ mod tests { let config = GossipsubConfigBuilder::default().i_am_relay(true).build(); let (mut gs, peers, _) = build_and_inject_nodes(peer_no, vec![], config, false); for (i, peer) in peers.iter().enumerate() { - gs.connected_relays.insert(peer.clone()); + gs.connected_relays.insert(*peer); if i < 13 { - gs.relays_mesh.insert(peer.clone(), 0); + gs.relays_mesh.insert(*peer, 0); } } diff --git a/mm2src/gossipsub/src/mcache.rs b/mm2src/gossipsub/src/mcache.rs index f9ae457052..fe92cd3c93 100644 --- a/mm2src/gossipsub/src/mcache.rs +++ b/mm2src/gossipsub/src/mcache.rs @@ -131,13 +131,12 @@ mod tests { let data: Vec = vec![u8x]; let sequence_number = x; - let m = GossipsubMessage { + GossipsubMessage { source, data, sequence_number, topics, - }; - m + } } #[test] @@ -160,8 +159,8 @@ mod tests { fn test_put_get_one() { let mut mc = MessageCache::new_default(10, 15); - let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); - let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); + let topic1_hash = Topic::new("topic1".into()).no_hash(); + let topic2_hash = Topic::new("topic2".into()).no_hash(); let m = gen_testm(10, vec![topic1_hash, topic2_hash]); @@ -171,13 +170,12 @@ mod tests { let fetched = mc.get(&(mc.msg_id)(&m)); - assert_eq!(fetched.is_none(), false); - assert_eq!(fetched.is_some(), true); + assert!(fetched.is_some()); // Make sure it is the same fetched message match fetched { Some(x) => assert_eq!(*x, m), - _ => assert!(false), + _ => panic!("expected {:?}", m), } } @@ -186,17 +184,17 @@ mod tests { fn test_get_wrong() { let mut mc = MessageCache::new_default(10, 15); - let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); - let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); + let topic1_hash = Topic::new("topic1".into()).no_hash(); + let topic2_hash = Topic::new("topic2".into()).no_hash(); let m = gen_testm(10, vec![topic1_hash, topic2_hash]); - mc.put(m.clone()); + mc.put(m); // Try to get an incorrect ID let wrong_id = MessageId(String::from("wrongid")); let fetched = mc.get(&wrong_id); - assert_eq!(fetched.is_none(), true); + assert!(fetched.is_none()); } #[test] @@ -207,7 +205,7 @@ mod tests { // Try to get an incorrect ID let wrong_string = MessageId(String::from("imempty")); let fetched = mc.get(&wrong_string); - assert_eq!(fetched.is_none(), true); + assert!(fetched.is_none()); } #[test] @@ -224,7 +222,7 @@ mod tests { // Make sure it is the same fetched message match fetched { Some(x) => assert_eq!(*x, m), - _ => assert!(false), + _ => panic!("expected {:?}", m), } } @@ -233,8 +231,8 @@ mod tests { fn test_shift() { let mut mc = MessageCache::new_default(1, 5); - let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); - let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); + let topic1_hash = Topic::new("topic1".into()).no_hash(); + let topic2_hash = Topic::new("topic2".into()).no_hash(); // Build the message for i in 0..10 { @@ -245,7 +243,7 @@ mod tests { mc.shift(); // Ensure the shift occurred - assert!(mc.history[0].len() == 0); + assert!(mc.history[0].is_empty()); assert!(mc.history[1].len() == 10); // Make sure no messages deleted @@ -257,8 +255,8 @@ mod tests { fn test_empty_shift() { let mut mc = MessageCache::new_default(1, 5); - let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); - let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); + let topic1_hash = Topic::new("topic1".into()).no_hash(); + let topic2_hash = Topic::new("topic2".into()).no_hash(); // Build the message for i in 0..10 { let m = gen_testm(i, vec![topic1_hash.clone(), topic2_hash.clone()]); @@ -268,14 +266,14 @@ mod tests { mc.shift(); // Ensure the shift occurred - assert!(mc.history[0].len() == 0); + assert!(mc.history[0].is_empty()); assert!(mc.history[1].len() == 10); mc.shift(); assert!(mc.history[2].len() == 10); - assert!(mc.history[1].len() == 0); - assert!(mc.history[0].len() == 0); + assert!(mc.history[1].is_empty()); + assert!(mc.history[0].is_empty()); } #[test] @@ -283,8 +281,8 @@ mod tests { fn test_remove_last_from_shift() { let mut mc = MessageCache::new_default(4, 5); - let topic1_hash = Topic::new("topic1".into()).no_hash().clone(); - let topic2_hash = Topic::new("topic2".into()).no_hash().clone(); + let topic1_hash = Topic::new("topic1".into()).no_hash(); + let topic2_hash = Topic::new("topic2".into()).no_hash(); // Build the message for i in 0..10 { let m = gen_testm(i, vec![topic1_hash.clone(), topic2_hash.clone()]); diff --git a/mm2src/gossipsub/tests/smoke.rs b/mm2src/gossipsub/tests/smoke.rs index b9c3063cc0..44c11416fb 100644 --- a/mm2src/gossipsub/tests/smoke.rs +++ b/mm2src/gossipsub/tests/smoke.rs @@ -144,7 +144,7 @@ fn build_node() -> (Multiaddr, Swarm) { .multiplex(yamux::YamuxConfig::default()) .boxed(); - let peer_id = public_key.clone().to_peer_id(); + let peer_id = public_key.to_peer_id(); // NOTE: The graph of created nodes can be disconnected from the mesh point of view as nodes // can reach their d_lo value and not add other nodes to their mesh. To speed up this test, we @@ -157,7 +157,7 @@ fn build_node() -> (Multiaddr, Swarm) { .history_length(10) .history_gossip(10) .build(); - let behaviour = Gossipsub::new(peer_id.clone(), config); + let behaviour = Gossipsub::new(peer_id, config); let mut swarm = Swarm::new(transport, behaviour, peer_id); let port = 1 + random::(); @@ -174,7 +174,7 @@ fn multi_hop_propagation() { let _ = env_logger::try_init(); fn prop(num_nodes: u8, seed: u64) -> TestResult { - if num_nodes < 2 || num_nodes > 50 { + if !(2..=50).contains(&num_nodes) { return TestResult::discard(); } diff --git a/mm2src/hw_common/Cargo.toml b/mm2src/hw_common/Cargo.toml index 06e9383b73..33e3a4e27a 100644 --- a/mm2src/hw_common/Cargo.toml +++ b/mm2src/hw_common/Cargo.toml @@ -18,7 +18,7 @@ serde = "1.0" serde_derive = "1.0" [target.'cfg(all(not(target_arch = "wasm32"), not(target_os = "ios")))'.dependencies] -rusb = { version = "0.7.0" } +rusb = { version = "0.7.0", features = ["vendored"] } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = { version = "0.3.27" } diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index c2788a80c3..6ab41e38a4 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "mm2_bin_lib" -version = "1.0.0-beta" +version = "1.0.1-beta" authors = ["James Lee", "Artem Pikulin", "Artem Grinblat", "Omar S.", "Onur Ozkan", "Alina Sharon", "Caglar Kaya", "Cipi", "Sergey Boiko", "Samuel Onoja", "Roman Sztergbaum", "Kadan Stadelmann "] edition = "2018" default-run = "mm2" diff --git a/mm2src/mm2_bin_lib/src/mm2_bin.rs b/mm2src/mm2_bin_lib/src/mm2_bin.rs index cad9e8be88..053d9dd9b4 100644 --- a/mm2src/mm2_bin_lib/src/mm2_bin.rs +++ b/mm2src/mm2_bin_lib/src/mm2_bin.rs @@ -1,6 +1,9 @@ #[cfg(not(target_arch = "wasm32"))] use mm2_main::mm2::mm2_main; +#[cfg(not(target_arch = "wasm32"))] const MM_VERSION: &str = env!("MM_VERSION"); + +#[cfg(not(target_arch = "wasm32"))] const MM_DATETIME: &str = env!("MM_DATETIME"); #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] diff --git a/mm2src/mm2_bitcoin/chain/Cargo.toml b/mm2src/mm2_bitcoin/chain/Cargo.toml index a107cf6735..58a1929c96 100644 --- a/mm2src/mm2_bitcoin/chain/Cargo.toml +++ b/mm2src/mm2_bitcoin/chain/Cargo.toml @@ -14,4 +14,4 @@ serialization = { path = "../serialization" } serialization_derive = { path = "../serialization_derive" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -bitcoin = "0.28.1" +bitcoin = "0.29" diff --git a/mm2src/mm2_bitcoin/chain/src/block_header.rs b/mm2src/mm2_bitcoin/chain/src/block_header.rs index 639670c7c7..2fce20a799 100644 --- a/mm2src/mm2_bitcoin/chain/src/block_header.rs +++ b/mm2src/mm2_bitcoin/chain/src/block_header.rs @@ -498,6 +498,7 @@ mod tests { #[test] fn test_doge_block_headers_serde_2() { // block headers of https://dogechain.info/block/3631810 and https://dogechain.info/block/3631811 + #[allow(clippy::zero_prefixed_literal)] let headers_bytes: &[u8] = &[ 02, 4, 1, 98, 0, 169, 253, 69, 196, 153, 115, 241, 239, 162, 112, 182, 254, 4, 175, 104, 238, 165, 178, 80, 67, 77, 109, 241, 134, 124, 3, 242, 203, 235, 211, 98, 185, 102, 124, 144, 105, 144, 228, 58, 25, 26, 29, diff --git a/mm2src/mm2_bitcoin/chain/src/transaction.rs b/mm2src/mm2_bitcoin/chain/src/transaction.rs index 8d250a583a..478d39bef5 100644 --- a/mm2src/mm2_bitcoin/chain/src/transaction.rs +++ b/mm2src/mm2_bitcoin/chain/src/transaction.rs @@ -8,7 +8,8 @@ use crypto::{dhash256, sha256}; use ext_bitcoin::blockdata::transaction::{OutPoint as ExtOutpoint, Transaction as ExtTransaction, TxIn, TxOut}; #[cfg(not(target_arch = "wasm32"))] use ext_bitcoin::hash_types::Txid; -#[cfg(not(target_arch = "wasm32"))] use ext_bitcoin::Witness; +#[cfg(not(target_arch = "wasm32"))] +use ext_bitcoin::{PackedLockTime, Sequence, Witness}; use hash::{CipherText, EncCipherText, OutCipherText, ZkProof, ZkProofSapling, H256, H512, H64}; use hex::FromHex; use ser::{deserialize, serialize, serialize_with_flags, SERIALIZE_TRANSACTION_WITNESS}; @@ -79,7 +80,7 @@ impl From for TxIn { TxIn { previous_output: txin.previous_output.into(), script_sig: txin.script_sig.take().into(), - sequence: txin.sequence, + sequence: Sequence(txin.sequence), witness: Witness::from_vec(txin.script_witness.into_iter().map(|s| s.take()).collect()), } } @@ -237,7 +238,7 @@ impl From for ExtTransaction { fn from(tx: Transaction) -> Self { ExtTransaction { version: tx.version, - lock_time: tx.lock_time, + lock_time: PackedLockTime(tx.lock_time), input: tx.inputs.into_iter().map(|i| i.into()).collect(), output: tx.outputs.into_iter().map(|o| o.into()).collect(), } diff --git a/mm2src/mm2_bitcoin/keys/Cargo.toml b/mm2src/mm2_bitcoin/keys/Cargo.toml index 3569f9ed84..7017d8ff24 100644 --- a/mm2src/mm2_bitcoin/keys/Cargo.toml +++ b/mm2src/mm2_bitcoin/keys/Cargo.toml @@ -9,7 +9,7 @@ doctest = false [dependencies] rustc-hex = "2" base58 = "0.2" -bech32 = "0.8.0" +bech32 = "0.9.1" bitcrypto = { path = "../crypto" } derive_more = "0.99" lazy_static = "1.4" diff --git a/mm2src/mm2_bitcoin/keys/src/cashaddress.rs b/mm2src/mm2_bitcoin/keys/src/cashaddress.rs index 80dcdab0f8..37066e9a62 100644 --- a/mm2src/mm2_bitcoin/keys/src/cashaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/cashaddress.rs @@ -404,7 +404,7 @@ mod tests { #[test] fn test_convert_bits() { - let (five, padded) = convert_bits(8, 5, &vec![0xFF], true); + let (five, padded) = convert_bits(8, 5, &[0xFF], true); assert!(padded, "Should have been padded"); assert_eq!(vec![0x1F, 0x1C], five); let (eight, padded) = convert_bits(5, 8, &five, false); @@ -494,7 +494,7 @@ mod tests { ]; for i in 0..4 { - let actual_address = CashAddress::decode(&encoded[i]).unwrap(); + let actual_address = CashAddress::decode(encoded[i]).unwrap(); let expected_address = expected_addresses[i].clone(); assert_eq!(actual_address, expected_address); let actual_encoded = actual_address.encode().unwrap(); diff --git a/mm2src/mm2_bitcoin/keys/src/keypair.rs b/mm2src/mm2_bitcoin/keys/src/keypair.rs index e3b3db7aae..0e29150f60 100644 --- a/mm2src/mm2_bitcoin/keys/src/keypair.rs +++ b/mm2src/mm2_bitcoin/keys/src/keypair.rs @@ -100,21 +100,21 @@ mod tests { /// Tests from: /// https://github.com/bitcoin/bitcoin/blob/a6a860796a44a2805a58391a009ba22752f64e32/src/test/key_tests.cpp - const SECRET_0: &'static str = "5KSCKP8NUyBZPCCQusxRwgmz9sfvJQEgbGukmmHepWw5Bzp95mu"; - const SECRET_1: &'static str = "5HxWvvfubhXpYYpS3tJkw6fq9jE9j18THftkZjHHfmFiWtmAbrj"; - const SECRET_2: &'static str = "5KC4ejrDjv152FGwP386VD1i2NYc5KkfSMyv1nGy1VGDxGHqVY3"; - const SECRET_1C: &'static str = "Kwr371tjA9u2rFSMZjTNun2PXXP3WPZu2afRHTcta6KxEUdm1vEw"; - const SECRET_2C: &'static str = "L3Hq7a8FEQwJkW1M2GNKDW28546Vp5miewcCzSqUD9kCAXrJdS3g"; - const SIGN_1: &'static str = "304402205dbbddda71772d95ce91cd2d14b592cfbc1dd0aabd6a394b6c2d377bbe59d31d022014ddda21494a4e221f0824f0b8b924c43fa43c0ad57dccdaa11f81a6bd4582f6"; - const SIGN_2: &'static str = "3044022052d8a32079c11e79db95af63bb9600c5b04f21a9ca33dc129c2bfa8ac9dc1cd5022061d8ae5e0f6c1a16bde3719c64c2fd70e404b6428ab9a69566962e8771b5944d"; + const SECRET_0: &str = "5KSCKP8NUyBZPCCQusxRwgmz9sfvJQEgbGukmmHepWw5Bzp95mu"; + const SECRET_1: &str = "5HxWvvfubhXpYYpS3tJkw6fq9jE9j18THftkZjHHfmFiWtmAbrj"; + const SECRET_2: &str = "5KC4ejrDjv152FGwP386VD1i2NYc5KkfSMyv1nGy1VGDxGHqVY3"; + const SECRET_1C: &str = "Kwr371tjA9u2rFSMZjTNun2PXXP3WPZu2afRHTcta6KxEUdm1vEw"; + const SECRET_2C: &str = "L3Hq7a8FEQwJkW1M2GNKDW28546Vp5miewcCzSqUD9kCAXrJdS3g"; + const SIGN_1: &str = "304402205dbbddda71772d95ce91cd2d14b592cfbc1dd0aabd6a394b6c2d377bbe59d31d022014ddda21494a4e221f0824f0b8b924c43fa43c0ad57dccdaa11f81a6bd4582f6"; + const SIGN_2: &str = "3044022052d8a32079c11e79db95af63bb9600c5b04f21a9ca33dc129c2bfa8ac9dc1cd5022061d8ae5e0f6c1a16bde3719c64c2fd70e404b6428ab9a69566962e8771b5944d"; #[allow(dead_code)] - const SIGN_COMPACT_1: &'static str = "1c5dbbddda71772d95ce91cd2d14b592cfbc1dd0aabd6a394b6c2d377bbe59d31d14ddda21494a4e221f0824f0b8b924c43fa43c0ad57dccdaa11f81a6bd4582f6"; + const SIGN_COMPACT_1: &str = "1c5dbbddda71772d95ce91cd2d14b592cfbc1dd0aabd6a394b6c2d377bbe59d31d14ddda21494a4e221f0824f0b8b924c43fa43c0ad57dccdaa11f81a6bd4582f6"; #[allow(dead_code)] - const SIGN_COMPACT_1C: &'static str = "205dbbddda71772d95ce91cd2d14b592cfbc1dd0aabd6a394b6c2d377bbe59d31d14ddda21494a4e221f0824f0b8b924c43fa43c0ad57dccdaa11f81a6bd4582f6"; + const SIGN_COMPACT_1C: &str = "205dbbddda71772d95ce91cd2d14b592cfbc1dd0aabd6a394b6c2d377bbe59d31d14ddda21494a4e221f0824f0b8b924c43fa43c0ad57dccdaa11f81a6bd4582f6"; #[allow(dead_code)] - const SIGN_COMPACT_2: &'static str = "1c52d8a32079c11e79db95af63bb9600c5b04f21a9ca33dc129c2bfa8ac9dc1cd561d8ae5e0f6c1a16bde3719c64c2fd70e404b6428ab9a69566962e8771b5944d"; + const SIGN_COMPACT_2: &str = "1c52d8a32079c11e79db95af63bb9600c5b04f21a9ca33dc129c2bfa8ac9dc1cd561d8ae5e0f6c1a16bde3719c64c2fd70e404b6428ab9a69566962e8771b5944d"; #[allow(dead_code)] - const SIGN_COMPACT_2C: &'static str = "2052d8a32079c11e79db95af63bb9600c5b04f21a9ca33dc129c2bfa8ac9dc1cd561d8ae5e0f6c1a16bde3719c64c2fd70e404b6428ab9a69566962e8771b5944d"; + const SIGN_COMPACT_2C: &str = "2052d8a32079c11e79db95af63bb9600c5b04f21a9ca33dc129c2bfa8ac9dc1cd561d8ae5e0f6c1a16bde3719c64c2fd70e404b6428ab9a69566962e8771b5944d"; fn check_compressed(secret: &'static str, compressed: bool) -> bool { let kp = KeyPair::from_private(secret.into()).unwrap(); diff --git a/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs b/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs index 4519deacf3..eb8112e238 100644 --- a/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/segwitaddress.rs @@ -209,7 +209,7 @@ mod tests { let public_key = Public::from_slice(&bytes).unwrap(); let hash = public_key.address_hash(); let hrp = "bc"; - let addr = SegwitAddress::new(&AddressHashEnum::AddressHash(hash.into()), hrp.to_string()); + let addr = SegwitAddress::new(&AddressHashEnum::AddressHash(hash), hrp.to_string()); assert_eq!(&addr.to_string(), "bc1qvzvkjn4q3nszqxrv3nraga2r822xjty3ykvkuw"); assert_eq!(addr.address_type(), Some(AddressType::P2wpkh)); } @@ -220,7 +220,7 @@ mod tests { let bytes = hex_to_bytes(script).unwrap(); let hash = sha256(&bytes); let hrp = "bc"; - let addr = SegwitAddress::new(&AddressHashEnum::WitnessScriptHash(hash.into()), hrp.to_string()); + let addr = SegwitAddress::new(&AddressHashEnum::WitnessScriptHash(hash), hrp.to_string()); assert_eq!( &addr.to_string(), "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3" diff --git a/mm2src/mm2_bitcoin/primitives/Cargo.toml b/mm2src/mm2_bitcoin/primitives/Cargo.toml index 568ee6aba0..3da53cdf33 100644 --- a/mm2src/mm2_bitcoin/primitives/Cargo.toml +++ b/mm2src/mm2_bitcoin/primitives/Cargo.toml @@ -8,6 +8,6 @@ doctest = false [dependencies] rustc-hex = "2" -bitcoin_hashes = "0.10.0" +bitcoin_hashes = "0.11" byteorder = "1.0" uint = "0.9.3" diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs index 30402a9396..1911d11781 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs @@ -194,9 +194,8 @@ mod tests { } let str_reversed = "XXXYYY"; - match H256::from_str(str_reversed) { - Ok(_) => panic!("unexpected"), - _ => (), + if H256::from_str(str_reversed).is_ok() { + panic!("unexpected"); } } diff --git a/mm2src/mm2_bitcoin/serialization_derive/tests/raw.rs b/mm2src/mm2_bitcoin/serialization_derive/tests/raw.rs index a67bbe0770..46389156eb 100644 --- a/mm2src/mm2_bitcoin/serialization_derive/tests/raw.rs +++ b/mm2src/mm2_bitcoin/serialization_derive/tests/raw.rs @@ -18,6 +18,7 @@ struct Bar { #[test] fn test_foo_serialize() { + #[allow(clippy::disallowed_names)] let foo = Foo { a: 1, b: 2, c: 3, d: 4 }; let expected = vec![1u8, 2, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0].into(); @@ -31,6 +32,7 @@ fn test_foo_serialize() { #[test] fn test_bar_serialize() { + #[allow(clippy::disallowed_names)] let foo = Foo { a: 1, b: 2, c: 3, d: 4 }; let foo2 = Foo { a: 5, b: 6, c: 7, d: 8 }; diff --git a/mm2src/mm2_bitcoin/spv_validation/src/helpers_validation.rs b/mm2src/mm2_bitcoin/spv_validation/src/helpers_validation.rs index 40b5e7fdc3..2f7046b3e5 100644 --- a/mm2src/mm2_bitcoin/spv_validation/src/helpers_validation.rs +++ b/mm2src/mm2_bitcoin/spv_validation/src/helpers_validation.rs @@ -402,7 +402,7 @@ mod tests { #[test] fn it_does_bitcoin_hash256() { test_utils::run_test(|fixtures| { - let test_cases = test_utils::get_test_cases("hash256", &fixtures); + let test_cases = test_utils::get_test_cases("hash256", fixtures); for case in test_cases { let input = force_deserialize_hex(case.input.as_str().unwrap()); let mut expected = H256::default(); @@ -416,7 +416,7 @@ mod tests { #[test] fn it_computes_hash256_merkle_steps() { test_utils::run_test(|fixtures| { - let test_cases = test_utils::get_test_cases("hash256MerkleStep", &fixtures); + let test_cases = test_utils::get_test_cases("hash256MerkleStep", fixtures); for case in test_cases { let inputs = case.input.as_array().unwrap(); let a = force_deserialize_hex(inputs[0].as_str().unwrap()); @@ -432,7 +432,7 @@ mod tests { #[test] fn it_determines_input_length() { test_utils::run_test(|fixtures| { - let test_cases = test_utils::get_test_cases("determineInputLength", &fixtures); + let test_cases = test_utils::get_test_cases("determineInputLength", fixtures); for case in test_cases { let input = force_deserialize_hex(case.input.as_str().unwrap()); let expected = case.output.as_u64().unwrap() as usize; @@ -444,7 +444,7 @@ mod tests { #[test] fn it_determines_output_length() { test_utils::run_test(|fixtures| { - let test_cases = test_utils::get_test_cases("determineOutputLength", &fixtures); + let test_cases = test_utils::get_test_cases("determineOutputLength", fixtures); for case in test_cases { let input = force_deserialize_hex(case.input.as_str().unwrap()); let expected = case.output.as_u64().unwrap() as usize; @@ -456,7 +456,7 @@ mod tests { #[test] fn it_validates_vin_syntax() { test_utils::run_test(|fixtures| { - let test_cases = test_utils::get_test_cases("validateVin", &fixtures); + let test_cases = test_utils::get_test_cases("validateVin", fixtures); for case in test_cases { let input = force_deserialize_hex(case.input.as_str().unwrap()); let expected = case.output.as_bool().unwrap(); @@ -468,7 +468,7 @@ mod tests { #[test] fn it_validates_vout_syntax() { test_utils::run_test(|fixtures| { - let test_cases = test_utils::get_test_cases("validateVout", &fixtures); + let test_cases = test_utils::get_test_cases("validateVout", fixtures); for case in test_cases { let input = force_deserialize_hex(case.input.as_str().unwrap()); let expected = case.output.as_bool().unwrap(); @@ -480,7 +480,7 @@ mod tests { #[test] fn it_verifies_hash256_merkles() { test_utils::run_test(|fixtures| { - let test_cases = test_utils::get_test_cases("verifyHash256Merkle", &fixtures); + let test_cases = test_utils::get_test_cases("verifyHash256Merkle", fixtures); for case in test_cases { let inputs = case.input.as_object().unwrap(); let extended_proof = force_deserialize_hex(inputs.get("proof").unwrap().as_str().unwrap()); @@ -489,12 +489,12 @@ mod tests { continue; } - let index = inputs.get("index").unwrap().as_u64().unwrap() as u64; + let index = inputs.get("index").unwrap().as_u64().unwrap(); let expected = case .output .as_bool() .unwrap() - .then(|| ()) + .then_some(()) .ok_or(SPVError::BadMerkleProof); // extract root and txid @@ -527,7 +527,7 @@ mod tests { let tx_id: H256 = H256::from_reversed_str("7e9797a05abafbc1542449766ef9a41838ebbf6d24cd3223d361aa07c51981df"); let merkle_pos = 1; let merkle_root: H256 = - H256::from_reversed_str("41f138275d13690e3c5d735e2f88eb6f1aaade1207eb09fa27a65b40711f3ae0").into(); + H256::from_reversed_str("41f138275d13690e3c5d735e2f88eb6f1aaade1207eb09fa27a65b40711f3ae0"); let merkle_nodes: Vec = vec![ H256::from_reversed_str("73dfb53e6f49854b09d98500d4899d5c4e703c4fa3a2ddadc2cd7f12b72d4182"), H256::from_reversed_str("4274d707b2308d39a04f2940024d382fa80d994152a50d4258f5a7feead2a563"), @@ -543,7 +543,7 @@ mod tests { let tx_id: H256 = H256::from_reversed_str("c06fbab289f723c6261d3030ddb6be121f7d2508d77862bb1e484f5cd7f92b25"); let merkle_pos = 0; let merkle_root: H256 = - H256::from_reversed_str("8fb300e3fdb6f30a4c67233b997f99fdd518b968b9a3fd65857bfe78b2600719").into(); + H256::from_reversed_str("8fb300e3fdb6f30a4c67233b997f99fdd518b968b9a3fd65857bfe78b2600719"); let merkle_nodes: Vec = vec![H256::from_reversed_str( "5a4ebf66822b0b2d56bd9dc64ece0bc38ee7844a23ff1d7320a88c5fdb2ad3e2", )]; @@ -558,7 +558,7 @@ mod tests { let tx_id: H256 = H256::from_reversed_str("b36bced99cc459506ad2b3af6990920b12f6dc84f9c7ed0dd2c3703f94a4b692"); let merkle_pos = 680; let merkle_root: H256 = - H256::from_reversed_str("def7a26d91789069dad448cb4b68658b7ba419f9fbd28dce7fe32ed0010e55df").into(); + H256::from_reversed_str("def7a26d91789069dad448cb4b68658b7ba419f9fbd28dce7fe32ed0010e55df"); let merkle_nodes: Vec = vec![ H256::from_reversed_str("39141331f2b7133e72913460384927b421ffdef3e24b88521e7ac54d30019409"), H256::from_reversed_str("39aeb77571ee0b0cf9feb7e121938b862f3994ff1254b34559378f6f2ed8b1fb"), diff --git a/mm2src/mm2_bitcoin/spv_validation/src/lib.rs b/mm2src/mm2_bitcoin/spv_validation/src/lib.rs index 4abc7e6466..4c57937ba2 100644 --- a/mm2src/mm2_bitcoin/spv_validation/src/lib.rs +++ b/mm2src/mm2_bitcoin/spv_validation/src/lib.rs @@ -49,8 +49,7 @@ pub(crate) mod test_utils { fn to_test_case(val: &serde_json::Value) -> TestCase { let o = val.get("output"); - let output: &serde_json::Value; - output = match o { + let output = match o { Some(v) => v, None => &serde_json::Value::Null, }; @@ -65,14 +64,14 @@ pub(crate) mod test_utils { let vals: &Vec = fixtures.get(name).unwrap().as_array().unwrap(); let mut cases = vec![]; for i in vals { - cases.push(to_test_case(&i)); + cases.push(to_test_case(i)); } cases } pub(crate) fn run_test(test: T) where - T: FnOnce(&serde_json::Value) -> () + panic::UnwindSafe, + T: FnOnce(&serde_json::Value) + panic::UnwindSafe, { let fixtures = setup(); diff --git a/mm2src/mm2_bitcoin/spv_validation/src/work.rs b/mm2src/mm2_bitcoin/spv_validation/src/work.rs index 11a148e1a7..21566be5ce 100644 --- a/mm2src/mm2_bitcoin/spv_validation/src/work.rs +++ b/mm2src/mm2_bitcoin/spv_validation/src/work.rs @@ -191,7 +191,7 @@ pub(crate) mod tests { BLOCK_HEADERS_MAP .get(coin) .unwrap() - .into_iter() + .iter() .map(|h| (h.height, h.hex.as_str().into())) .collect() } diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index 10d1cd1611..6d967b852b 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -22,11 +22,11 @@ rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } serde = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } shared_ref_counter = { path = "../common/shared_ref_counter" } -uuid = { version = "0.7", features = ["serde", "v4"] } +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] gstuff = { version = "0.7", features = ["nightly"] } mm2_rpc = { path = "../mm2_rpc" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gstuff = { version = "0.7", features = ["crossterm", "nightly"] } +gstuff = { version = "0.7", features = ["nightly"] } diff --git a/mm2src/mm2_core/src/event_dispatcher.rs b/mm2src/mm2_core/src/event_dispatcher.rs index 47de527142..479ca7d616 100644 --- a/mm2src/mm2_core/src/event_dispatcher.rs +++ b/mm2src/mm2_core/src/event_dispatcher.rs @@ -108,7 +108,7 @@ mod event_dispatcher_tests { impl Deref for ListenerSwapStatusChangedArc { type Target = ListenerSwapStatusChanged; - fn deref(&self) -> &ListenerSwapStatusChanged { &*self.0 } + fn deref(&self) -> &ListenerSwapStatusChanged { &self.0 } } #[async_trait] diff --git a/mm2src/mm2_db/Cargo.toml b/mm2src/mm2_db/Cargo.toml index 5933140509..5c3d53d005 100644 --- a/mm2src/mm2_db/Cargo.toml +++ b/mm2src/mm2_db/Cargo.toml @@ -10,6 +10,7 @@ doctest = false async-trait = "0.1" common = { path = "../common" } derive_more = "0.99" +enum_from = { path = "../derives/enum_from" } futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } itertools = "0.10" hex = "0.4.2" @@ -26,4 +27,4 @@ serde_json = { version = "1", features = ["preserve_order", "raw_value"] } wasm-bindgen = { version = "0.2.50", features = ["nightly"] } wasm-bindgen-futures = { version = "0.4.1" } wasm-bindgen-test = { version = "0.3.2" } -web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket"] } +web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbCursorDirection", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket"] } diff --git a/mm2src/mm2_db/src/indexed_db/db_lock.rs b/mm2src/mm2_db/src/indexed_db/db_lock.rs index b7106c252c..2a650f0c5f 100644 --- a/mm2src/mm2_db/src/indexed_db/db_lock.rs +++ b/mm2src/mm2_db/src/indexed_db/db_lock.rs @@ -26,7 +26,7 @@ impl ConstructibleDb { ConstructibleDb { mutex: AsyncMutex::new(None), db_namespace: ctx.db_namespace, - wallet_rmd160: ctx.rmd160().clone(), + wallet_rmd160: *ctx.rmd160(), } } @@ -37,7 +37,7 @@ impl ConstructibleDb { ConstructibleDb { mutex: AsyncMutex::new(None), db_namespace: ctx.db_namespace, - wallet_rmd160: ctx.shared_db_id().clone(), + wallet_rmd160: *ctx.shared_db_id(), } } @@ -50,7 +50,7 @@ impl ConstructibleDb { return Ok(unwrap_db_instance(locked_db)); } - let db_id = DbIdentifier::new::(self.db_namespace, self.wallet_rmd160.clone()); + let db_id = DbIdentifier::new::(self.db_namespace, self.wallet_rmd160); let db = Db::init(db_id).await?; *locked_db = Some(db); diff --git a/mm2src/mm2_db/src/indexed_db/drivers/builder.rs b/mm2src/mm2_db/src/indexed_db/drivers/builder.rs index ef8c6ea086..7d8c6bb79c 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/builder.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/builder.rs @@ -130,8 +130,8 @@ impl IdbDatabaseBuilder { }, }; - let db = Self::get_db_from_request(&db_request)?; - let transaction = Self::get_transaction_from_request(&db_request)?; + let db = Self::get_db_from_request(db_request)?; + let transaction = Self::get_transaction_from_request(db_request)?; let version_event = match event.dyn_into::() { Ok(version) => version, @@ -145,9 +145,8 @@ impl IdbDatabaseBuilder { let old_version = version_event.old_version() as u32; let new_version = version_event .new_version() - .ok_or(MmError::new(InitDbError::InvalidVersion( - "Expected a new_version".to_owned(), - )))? as u32; + .ok_or_else(|| MmError::new(InitDbError::InvalidVersion("Expected a new_version".to_owned())))? + as u32; let upgrader = DbUpgrader::new(db, transaction); for on_upgrade_needed_cb in handlers { diff --git a/mm2src/mm2_db/src/indexed_db/drivers/cursor/cursor.rs b/mm2src/mm2_db/src/indexed_db/drivers/cursor/cursor.rs index 694fec6f16..bba549d3bd 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/cursor/cursor.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/cursor/cursor.rs @@ -1,9 +1,9 @@ use super::construct_event_closure; use crate::indexed_db::db_driver::{InternalItem, ItemId}; use crate::indexed_db::BeBigUint; -use async_trait::async_trait; use common::wasm::{deserialize_from_js, serialize_to_js, stringify_js_error}; use derive_more::Display; +use enum_from::EnumFromTrait; use futures::channel::mpsc; use futures::StreamExt; use js_sys::Array; @@ -12,22 +12,23 @@ use serde_json::{self as json, Value as Json}; use std::convert::TryInto; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use web_sys::{IdbCursorWithValue, IdbIndex, IdbKeyRange, IdbRequest}; +use web_sys::{IdbCursorDirection, IdbCursorWithValue, IdbIndex, IdbKeyRange, IdbRequest}; +mod empty_cursor; mod multi_key_bound_cursor; mod multi_key_cursor; mod single_key_bound_cursor; mod single_key_cursor; -pub use multi_key_bound_cursor::IdbMultiKeyBoundCursor; -pub use multi_key_cursor::IdbMultiKeyCursor; -pub use single_key_bound_cursor::IdbSingleKeyBoundCursor; -pub use single_key_cursor::IdbSingleKeyCursor; +use empty_cursor::IdbEmptyCursor; +use multi_key_bound_cursor::IdbMultiKeyBoundCursor; +use multi_key_cursor::IdbMultiKeyCursor; +use single_key_bound_cursor::IdbSingleKeyBoundCursor; +use single_key_cursor::IdbSingleKeyCursor; pub type CursorResult = Result>; -pub type DbFilter = Box (CollectItemAction, CollectCursorAction) + Send>; -#[derive(Debug, Display, PartialEq)] +#[derive(Debug, Display, EnumFromTrait, PartialEq)] pub enum CursorError { #[display( fmt = "Error serializing the '{}' value of the index field '{}' : {:?}", @@ -59,6 +60,7 @@ pub enum CursorError { )] IncorrectNumberOfKeysPerIndex { expected: usize, found: usize }, #[display(fmt = "Error occurred due to an unexpected state: {:?}", _0)] + #[from_trait(WithInternal::internal)] UnexpectedState(String), #[display(fmt = "Incorrect usage of the cursor: {:?}", description)] IncorrectUsage { description: String }, @@ -81,6 +83,13 @@ pub enum CursorBoundValue { BigUint(BeBigUint), } +#[derive(Default)] +pub struct CursorFilters { + pub(crate) only_keys: Vec<(String, Json)>, + pub(crate) bound_keys: Vec<(String, CursorBoundValue, CursorBoundValue)>, + pub(crate) reverse: bool, +} + impl From for CursorBoundValue { fn from(uint: u32) -> Self { CursorBoundValue::Uint(uint) } } @@ -116,8 +125,8 @@ impl CursorBoundValue { pub fn to_js_value(&self) -> CursorResult { match self { - CursorBoundValue::Uint(uint) => Ok(JsValue::from(*uint as u32)), - CursorBoundValue::Int(int) => Ok(JsValue::from(*int as i32)), + CursorBoundValue::Uint(uint) => Ok(JsValue::from(*uint)), + CursorBoundValue::Int(int) => Ok(JsValue::from(*int)), CursorBoundValue::BigUint(int) => serialize_to_js(int).map_to_mm(|e| CursorError::InvalidKeyRange { description: e.to_string(), }), @@ -128,12 +137,13 @@ impl CursorBoundValue { // `matches` macro leads to the following error: // (CursorBoundValue::Uint(_), CursorBoundValue::Uint(_)) // ^ no rules expected this token in macro call - match (self, other) { + + matches!( + (self, other), (CursorBoundValue::Int(_), CursorBoundValue::Int(_)) - | (CursorBoundValue::Uint(_), CursorBoundValue::Uint(_)) - | (CursorBoundValue::BigUint(_), CursorBoundValue::BigUint(_)) => true, - _ => false, - } + | (CursorBoundValue::Uint(_), CursorBoundValue::Uint(_)) + | (CursorBoundValue::BigUint(_), CursorBoundValue::BigUint(_)) + ) } fn deserialize_with_expected_type(value: &Json, expected: &Self) -> CursorResult { @@ -160,143 +170,209 @@ impl CursorBoundValue { } #[derive(Debug, PartialEq)] -pub enum CollectCursorAction { +pub enum CursorAction { Continue, ContinueWithValue(JsValue), Stop, } #[derive(Debug, PartialEq)] -pub enum CollectItemAction { +pub enum CursorItemAction { Include, Skip, } -#[async_trait(?Send)] -pub trait CursorOps: Sized { - fn db_index(&self) -> &IdbIndex; - +pub trait CursorDriverImpl: Sized { fn key_range(&self) -> CursorResult>; - fn on_collect_iter(&mut self, key: JsValue, value: &Json) - -> CursorResult<(CollectItemAction, CollectCursorAction)>; + fn on_iteration(&mut self, key: JsValue) -> CursorResult<(CursorItemAction, CursorAction)>; +} + +pub(crate) struct CursorDriver { + /// An actual cursor implementation. + inner: IdbCursorEnum, + cursor_request: IdbRequest, + cursor_item_rx: mpsc::Receiver>, + /// Whether we got `CursorAction::Stop` at the last iteration or not. + stopped: bool, + /// We need to hold the closures in memory till `cursor` exists. + _onsuccess_closure: Closure, + _onerror_closure: Closure, +} - /// Collect items that match the specified bounds. - async fn collect(mut self) -> CursorResult> { - let (tx, mut rx) = mpsc::channel(1); +impl CursorDriver { + pub(crate) fn init_cursor(db_index: IdbIndex, filters: CursorFilters) -> CursorResult { + let reverse = filters.reverse; + let inner = IdbCursorEnum::new(filters)?; - let db_index = self.db_index(); - let cursor_request_result = match self.key_range()? { + let cursor_request_result = match inner.key_range()? { + Some(key_range) if reverse => { + db_index.open_cursor_with_range_and_direction(&key_range, IdbCursorDirection::Prev) + }, Some(key_range) => db_index.open_cursor_with_range(&key_range), + // Please note that `IndexedDb` doesn't allow to open a cursor with a direction + // but without a key range. + None if reverse => { + return MmError::err(CursorError::ErrorOpeningCursor { + description: "Direction cannot be specified without a range".to_owned(), + }); + }, None => db_index.open_cursor(), }; let cursor_request = cursor_request_result.map_err(|e| CursorError::ErrorOpeningCursor { description: stringify_js_error(&e), })?; - let onsuccess_closure = construct_event_closure(Ok, tx.clone()); - let onerror_closure = construct_event_closure(Err, tx); + let (cursor_item_tx, cursor_item_rx) = mpsc::channel(1); + + let onsuccess_closure = construct_event_closure(Ok, cursor_item_tx.clone()); + let onerror_closure = construct_event_closure(Err, cursor_item_tx); cursor_request.set_onsuccess(Some(onsuccess_closure.as_ref().unchecked_ref())); cursor_request.set_onerror(Some(onerror_closure.as_ref().unchecked_ref())); - let mut collected_items = Vec::new(); + Ok(CursorDriver { + inner, + cursor_request, + cursor_item_rx, + stopped: false, + _onsuccess_closure: onsuccess_closure, + _onerror_closure: onerror_closure, + }) + } + + pub(crate) async fn next(&mut self) -> CursorResult> { + loop { + // Check if we got `CursorAction::Stop` at the last iteration. + if self.stopped { + return Ok(None); + } + + let event = match self.cursor_item_rx.next().await { + Some(event) => event, + None => { + self.stopped = true; + return Ok(None); + }, + }; - while let Some(event) = rx.next().await { let _cursor_event = event.map_to_mm(|e| CursorError::ErrorOpeningCursor { description: stringify_js_error(&e), })?; - let cursor = match cursor_from_request(&cursor_request)? { + let cursor = match cursor_from_request(&self.cursor_request)? { Some(cursor) => cursor, - // no more items, stop the loop - None => break, + // No more items. + None => { + self.stopped = true; + return Ok(None); + }, }; let (key, js_value) = match (cursor.key(), cursor.value()) { (Ok(key), Ok(js_value)) => (key, js_value), - // no more items, stop the loop - _ => break, + // No more items. + _ => { + self.stopped = true; + return Ok(None); + }, }; let item: InternalItem = deserialize_from_js(js_value).map_to_mm(|e| CursorError::ErrorDeserializingItem(e.to_string()))?; - let (item_action, cursor_action) = self.on_collect_iter(key, &item.item)?; - match item_action { - CollectItemAction::Include => collected_items.push(item.into_pair()), - CollectItemAction::Skip => (), - } + let (item_action, cursor_action) = self.inner.on_iteration(key)?; match cursor_action { - CollectCursorAction::Continue => cursor.continue_().map_to_mm(|e| CursorError::AdvanceError { + CursorAction::Continue => cursor.continue_().map_to_mm(|e| CursorError::AdvanceError { description: stringify_js_error(&e), })?, - CollectCursorAction::ContinueWithValue(next_value) => { + CursorAction::ContinueWithValue(next_value) => { cursor .continue_with_key(&next_value) .map_to_mm(|e| CursorError::AdvanceError { description: stringify_js_error(&e), })? }, - // don't advance the cursor, just stop the loop - CollectCursorAction::Stop => break, + // Don't advance the cursor. + // Here we set the `stopped` flag so we return `Ok(None)` at the next iteration immediately. + // This is required because `item_action` can be `CollectItemAction::Include`, + // and at this iteration we will return `Ok(Some)`. + CursorAction::Stop => self.stopped = true, } - } - Ok(collected_items) + match item_action { + CursorItemAction::Include => return Ok(Some(item.into_pair())), + // Try to fetch the next item. + CursorItemAction::Skip => (), + } + } } } -pub struct IdbCursorBuilder { - db_index: IdbIndex, +pub(crate) enum IdbCursorEnum { + Empty(IdbEmptyCursor), + SingleKey(IdbSingleKeyCursor), + SingleKeyBound(IdbSingleKeyBoundCursor), + MultiKey(IdbMultiKeyCursor), + MultiKeyBound(IdbMultiKeyBoundCursor), } -impl IdbCursorBuilder { - pub fn new(db_index: IdbIndex) -> IdbCursorBuilder { IdbCursorBuilder { db_index } } - - /// Returns a cursor that is a representation of a range that includes records - /// whose value of the `field_name` field equals to the `field_value` value. - pub fn single_key_cursor( - self, - field_name: String, - field_value: Json, - collect_filter: Option, - ) -> IdbSingleKeyCursor { - IdbSingleKeyCursor::new(self.db_index, field_name, field_value, collect_filter) - } +impl IdbCursorEnum { + fn new(cursor_filters: CursorFilters) -> CursorResult { + if cursor_filters.only_keys.len() > 1 && cursor_filters.bound_keys.is_empty() { + return Ok(IdbCursorEnum::MultiKey(IdbMultiKeyCursor::new( + cursor_filters.only_keys, + ))); + } + if !cursor_filters.bound_keys.is_empty() + && (cursor_filters.only_keys.len() + cursor_filters.bound_keys.len() > 1) + { + return Ok(IdbCursorEnum::MultiKeyBound(IdbMultiKeyBoundCursor::new( + cursor_filters.only_keys, + cursor_filters.bound_keys, + )?)); + } // otherwise we're sure that there is either one `only`, or one `bound`, or no constraint specified. + + if let Some((field_name, field_value)) = cursor_filters.only_keys.into_iter().next() { + return Ok(IdbCursorEnum::SingleKey(IdbSingleKeyCursor::new( + field_name, + field_value, + ))); + } - /// Returns a cursor that is a representation of a range that includes records - /// whose value of the `field_name` field is lower than `lower_bound` and greater than `upper_bound`. - pub fn single_key_bound_cursor( - self, - field_name: String, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - collect_filter: Option, - ) -> CursorResult { - IdbSingleKeyBoundCursor::new(self.db_index, field_name, lower_bound, upper_bound, collect_filter) + if let Some((field_name, lower_bound, upper_bound)) = cursor_filters.bound_keys.into_iter().next() { + return Ok(IdbCursorEnum::SingleKeyBound(IdbSingleKeyBoundCursor::new( + field_name, + lower_bound, + upper_bound, + )?)); + } + + // There are no constraint specified. + Ok(IdbCursorEnum::Empty(IdbEmptyCursor)) } +} - /// Returns a cursor that is a representation of a range that includes records - /// whose fields have only the specified values `only_values`. - pub fn multi_key_cursor( - self, - only_values: Vec<(String, Json)>, - collect_filter: Option, - ) -> CursorResult { - IdbMultiKeyCursor::new(self.db_index, only_values, collect_filter) +impl CursorDriverImpl for IdbCursorEnum { + fn key_range(&self) -> CursorResult> { + match self { + IdbCursorEnum::Empty(empty) => empty.key_range(), + IdbCursorEnum::SingleKey(single) => single.key_range(), + IdbCursorEnum::SingleKeyBound(single_bound) => single_bound.key_range(), + IdbCursorEnum::MultiKey(multi) => multi.key_range(), + IdbCursorEnum::MultiKeyBound(multi_bound) => multi_bound.key_range(), + } } - /// Returns a cursor that is a representation of a range that includes records - /// with the multiple `only` and `bound` restrictions. - pub fn multi_key_bound_cursor( - self, - only_values: Vec<(String, Json)>, - bound_values: Vec<(String, CursorBoundValue, CursorBoundValue)>, - collect_filter: Option, - ) -> CursorResult { - IdbMultiKeyBoundCursor::new(self.db_index, only_values, bound_values, collect_filter) + fn on_iteration(&mut self, key: JsValue) -> CursorResult<(CursorItemAction, CursorAction)> { + match self { + IdbCursorEnum::Empty(empty) => empty.on_iteration(key), + IdbCursorEnum::SingleKey(single) => single.on_iteration(key), + IdbCursorEnum::SingleKeyBound(single_bound) => single_bound.on_iteration(key), + IdbCursorEnum::MultiKey(multi) => multi.on_iteration(key), + IdbCursorEnum::MultiKeyBound(multi_bound) => multi_bound.on_iteration(key), + } } } diff --git a/mm2src/mm2_db/src/indexed_db/drivers/cursor/empty_cursor.rs b/mm2src/mm2_db/src/indexed_db/drivers/cursor/empty_cursor.rs new file mode 100644 index 0000000000..441180d8bb --- /dev/null +++ b/mm2src/mm2_db/src/indexed_db/drivers/cursor/empty_cursor.rs @@ -0,0 +1,14 @@ +use super::{CursorAction, CursorDriverImpl, CursorItemAction, CursorResult}; +use wasm_bindgen::prelude::*; +use web_sys::IdbKeyRange; + +/// The representation of a range that includes all records. +pub struct IdbEmptyCursor; + +impl CursorDriverImpl for IdbEmptyCursor { + fn key_range(&self) -> CursorResult> { Ok(None) } + + fn on_iteration(&mut self, _key: JsValue) -> CursorResult<(CursorItemAction, CursorAction)> { + Ok((CursorItemAction::Include, CursorAction::Continue)) + } +} diff --git a/mm2src/mm2_db/src/indexed_db/drivers/cursor/multi_key_bound_cursor.rs b/mm2src/mm2_db/src/indexed_db/drivers/cursor/multi_key_bound_cursor.rs index 836204e94b..18a5962498 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/cursor/multi_key_bound_cursor.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/cursor/multi_key_bound_cursor.rs @@ -1,37 +1,29 @@ -use super::{index_key_as_array, CollectCursorAction, CollectItemAction, CursorBoundValue, CursorError, CursorOps, - CursorResult, DbFilter}; -use async_trait::async_trait; +use super::{index_key_as_array, CursorAction, CursorBoundValue, CursorDriverImpl, CursorError, CursorItemAction, + CursorResult}; use common::{deserialize_from_js, serialize_to_js, stringify_js_error}; use js_sys::Array; use mm2_err_handle::prelude::*; use serde_json::{json, Value as Json}; use wasm_bindgen::prelude::*; -use web_sys::{IdbIndex, IdbKeyRange}; +use web_sys::IdbKeyRange; /// The representation of a range that includes records /// with the multiple `only` and `bound` restrictions. /// https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/bound pub struct IdbMultiKeyBoundCursor { - db_index: IdbIndex, only_values: Vec<(String, Json)>, bound_values: Vec<(String, CursorBoundValue, CursorBoundValue)>, - /// An additional predicate that may be used to filter records. - collect_filter: Option, } impl IdbMultiKeyBoundCursor { pub(super) fn new( - db_index: IdbIndex, only_values: Vec<(String, Json)>, bound_values: Vec<(String, CursorBoundValue, CursorBoundValue)>, - collect_filter: Option, ) -> CursorResult { Self::check_bounds(&only_values, &bound_values)?; Ok(IdbMultiKeyBoundCursor { - db_index, only_values, bound_values, - collect_filter, }) } @@ -83,10 +75,7 @@ impl IdbMultiKeyBoundCursor { } } -#[async_trait(?Send)] -impl CursorOps for IdbMultiKeyBoundCursor { - fn db_index(&self) -> &IdbIndex { &self.db_index } - +impl CursorDriverImpl for IdbMultiKeyBoundCursor { fn key_range(&self) -> CursorResult> { let lower = Array::new(); let upper = Array::new(); @@ -119,11 +108,7 @@ impl CursorOps for IdbMultiKeyBoundCursor { /// The range `IDBKeyRange.bound([2,2], [4,4])` includes values like `[3,0]` and `[3,5]` as `[2,2] < [3,0] < [3,5] < [4,4]`, /// so we need to do additional filtering. /// For more information on why it's required, see https://stackoverflow.com/a/32976384. - fn on_collect_iter( - &mut self, - index_key: JsValue, - value: &Json, - ) -> CursorResult<(CollectItemAction, CollectCursorAction)> { + fn on_iteration(&mut self, index_key: JsValue) -> CursorResult<(CursorItemAction, CursorAction)> { let index_keys_js_array = index_key_as_array(index_key)?; let index_keys: Vec = index_keys_js_array .iter() @@ -172,8 +157,8 @@ impl CursorOps for IdbMultiKeyBoundCursor { return Ok(( // the `actual_index_value` is not in our expected bounds - CollectItemAction::Skip, - CollectCursorAction::ContinueWithValue(new_index.into()), + CursorItemAction::Skip, + CursorAction::ContinueWithValue(new_index.into()), )); } if &actual_index_value > upper_bound { @@ -192,13 +177,13 @@ impl CursorOps for IdbMultiKeyBoundCursor { return Ok(( // the `actual_index_value` is not in our expected bounds - CollectItemAction::Skip, - CollectCursorAction::ContinueWithValue(new_index.into()), + CursorItemAction::Skip, + CursorAction::ContinueWithValue(new_index.into()), )); } // otherwise there is no an index greater than actual `index`, stop the cursor - return Ok((CollectItemAction::Skip, CollectCursorAction::Stop)); + return Ok((CursorItemAction::Skip, CursorAction::Stop)); } let increased_index_key = actual_index_value.next(); @@ -209,19 +194,13 @@ impl CursorOps for IdbMultiKeyBoundCursor { idx_in_index += 1; idx_in_bounds += 1; } - - // `index_key` is in our expected bounds - if let Some(ref mut filter) = self.collect_filter { - return Ok(filter(value)); - } - Ok((CollectItemAction::Include, CollectCursorAction::Continue)) + Ok((CursorItemAction::Include, CursorAction::Continue)) } } mod tests { use super::*; use common::log::wasm_log::register_wasm_log; - use wasm_bindgen::JsCast; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -230,10 +209,10 @@ mod tests { fn test_in_bound_indexes(cursor: &mut IdbMultiKeyBoundCursor, input_indexes: Vec) { for input_index in input_indexes { let input_index_js_value = serialize_to_js(&input_index).unwrap(); - let result = cursor.on_collect_iter(input_index_js_value, &Json::Null); + let result = cursor.on_iteration(input_index_js_value); assert_eq!( result, - Ok((CollectItemAction::Include, CollectCursorAction::Continue)), + Ok((CursorItemAction::Include, CursorAction::Continue)), "'{}' index is expected to be in a bound", input_index ); @@ -247,19 +226,16 @@ mod tests { for (input_index, expected_next) in input_indexes { let input_index_js_value = serialize_to_js(&input_index).unwrap(); let (item_action, cursor_action) = cursor - .on_collect_iter(input_index_js_value, &Json::Null) - .expect(&format!("Error due to the index '{:?}'", input_index)); + .on_iteration(input_index_js_value) + .unwrap_or_else(|_| panic!("Error due to the index '{:?}'", input_index)); let actual_next: Json = match cursor_action { - CollectCursorAction::ContinueWithValue(next_index_js_value) => { + CursorAction::ContinueWithValue(next_index_js_value) => { deserialize_from_js(next_index_js_value).expect("Error deserializing next index}") }, - action => panic!( - "Expected 'CollectCursorAction::ContinueWithValue', found '{:?}'", - action - ), + action => panic!("Expected 'CursorAction::ContinueWithValue', found '{:?}'", action), }; - assert_eq!(item_action, CollectItemAction::Skip); + assert_eq!(item_action, CursorItemAction::Skip); assert_eq!(actual_next, expected_next); } } @@ -268,10 +244,10 @@ mod tests { fn test_out_of_bound_indexes(cursor: &mut IdbMultiKeyBoundCursor, input_indexes: Vec) { for input_index in input_indexes { let input_index_js_value = serialize_to_js(&input_index).unwrap(); - let result = cursor.on_collect_iter(input_index_js_value, &Json::Null); + let result = cursor.on_iteration(input_index_js_value); assert_eq!( result, - Ok((CollectItemAction::Skip, CollectCursorAction::Stop)), + Ok((CursorItemAction::Skip, CursorAction::Stop)), "'{}' index is expected to be out of bound", input_index ); @@ -280,7 +256,7 @@ mod tests { /// This test doesn't check [`IdbMultiKeyBoundCursor::filter`]. #[wasm_bindgen_test] - fn test_on_collect_iter_multiple_only_and_bound_values() { + fn test_on_iteration_multiple_only_and_bound_values() { register_wasm_log(); let only_values = vec![ @@ -311,10 +287,7 @@ mod tests { ), ]; - // Use [`wasm_bindgen::JsCast::unchecked_from_js`] to create not-valid `IdbIndex` - // that is not used by [`IdbMultiKeyBoundCursor::on_collect_iter`] anyway. - let db_index = IdbIndex::unchecked_from_js(JsValue::NULL); - let mut cursor = IdbMultiKeyBoundCursor::new(db_index, only_values, bound_values, None).unwrap(); + let mut cursor = IdbMultiKeyBoundCursor::new(only_values, bound_values).unwrap(); ////////////////// @@ -388,7 +361,7 @@ mod tests { /// This test doesn't check [`IdbMultiKeyBoundCursor::filter`]. #[wasm_bindgen_test] - fn test_on_collect_iter_multiple_bound_values() { + fn test_on_iteration_multiple_bound_values() { register_wasm_log(); let only_values = Vec::new(); @@ -405,10 +378,7 @@ mod tests { ), ]; - // Use [`wasm_bindgen::JsCast::unchecked_from_js`] to create not-valid `IdbIndex` - // that is not used by [`IdbMultiKeyBoundCursor::on_collect_iter`] anyway. - let db_index = IdbIndex::unchecked_from_js(JsValue::NULL); - let mut cursor = IdbMultiKeyBoundCursor::new(db_index, only_values, bound_values, None).unwrap(); + let mut cursor = IdbMultiKeyBoundCursor::new(only_values, bound_values).unwrap(); ////////////////// @@ -453,7 +423,7 @@ mod tests { /// This test doesn't check [`IdbMultiKeyBoundCursor::filter`]. #[wasm_bindgen_test] - fn test_on_collect_iter_single_only_and_bound_values() { + fn test_on_iteration_single_only_and_bound_values() { register_wasm_log(); let only_values = vec![("field1".to_owned(), json!(2))]; @@ -463,10 +433,7 @@ mod tests { CursorBoundValue::Uint(5), // upper bound )]; - // Use [`wasm_bindgen::JsCast::unchecked_from_js`] to create not-valid `IdbIndex` - // that is not used by [`IdbMultiKeyBoundCursor::on_collect_iter`] anyway. - let db_index = IdbIndex::unchecked_from_js(JsValue::NULL); - let mut cursor = IdbMultiKeyBoundCursor::new(db_index, only_values, bound_values, None).unwrap(); + let mut cursor = IdbMultiKeyBoundCursor::new(only_values, bound_values).unwrap(); ////////////////// @@ -502,7 +469,7 @@ mod tests { } #[wasm_bindgen_test] - fn test_on_collect_iter_error() { + fn test_on_iteration_error() { register_wasm_log(); let only_values = vec![("field1".to_owned(), json!(2u32))]; @@ -512,10 +479,7 @@ mod tests { CursorBoundValue::Uint(5), // upper bound )]; - // Use [`wasm_bindgen::JsCast::unchecked_from_js`] to create not-valid `IdbIndex` - // that is not used by [`IdbMultiKeyBoundCursor::on_collect_iter`] anyway. - let db_index = IdbIndex::unchecked_from_js(JsValue::NULL); - let mut cursor = IdbMultiKeyBoundCursor::new(db_index, only_values, bound_values, None).unwrap(); + let mut cursor = IdbMultiKeyBoundCursor::new(only_values, bound_values).unwrap(); ////////////////// @@ -531,7 +495,7 @@ mod tests { for (input_index, expected_type) in input_indexes { let input_index_js_value = serialize_to_js(&input_index).unwrap(); let error = cursor - .on_collect_iter(input_index_js_value, &Json::Null) + .on_iteration(input_index_js_value) .expect_err(&format!("'{:?}' must lead to 'CursorError::TypeMismatch'", input_index)); match error.into_inner() { CursorError::TypeMismatch { expected, .. } => assert_eq!(expected, expected_type), @@ -549,7 +513,7 @@ mod tests { for input_index in input_indexes { let input_index_js_value = serialize_to_js(&input_index).unwrap(); let error = cursor - .on_collect_iter(input_index_js_value, &Json::Null) + .on_iteration(input_index_js_value) .expect_err(&format!("'{:?}' must lead to 'CursorError::TypeMismatch'", input_index)); match error.into_inner() { CursorError::IncorrectNumberOfKeysPerIndex { .. } => (), diff --git a/mm2src/mm2_db/src/indexed_db/drivers/cursor/multi_key_cursor.rs b/mm2src/mm2_db/src/indexed_db/drivers/cursor/multi_key_cursor.rs index c161ada34e..f6b128aa1b 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/cursor/multi_key_cursor.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/cursor/multi_key_cursor.rs @@ -1,52 +1,23 @@ -use super::{CollectCursorAction, CollectItemAction, CursorError, CursorOps, CursorResult, DbFilter}; -use async_trait::async_trait; +use super::{CursorAction, CursorDriverImpl, CursorError, CursorItemAction, CursorResult}; use common::{serialize_to_js, stringify_js_error}; use js_sys::Array; use mm2_err_handle::prelude::*; use serde_json::Value as Json; use wasm_bindgen::prelude::*; -use web_sys::{IdbIndex, IdbKeyRange}; +use web_sys::IdbKeyRange; /// The representation of a range that includes records /// whose fields have only the specified [`IdbSingleCursor::only_values`] values. /// https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/only pub struct IdbMultiKeyCursor { - db_index: IdbIndex, only_values: Vec<(String, Json)>, - /// An additional predicate that can be used to filter records. - collect_filter: Option, } impl IdbMultiKeyCursor { - pub(super) fn new( - db_index: IdbIndex, - only_values: Vec<(String, Json)>, - collect_filter: Option, - ) -> CursorResult { - Self::check_only_values(&only_values)?; - Ok(IdbMultiKeyCursor { - db_index, - only_values, - collect_filter, - }) - } - - fn check_only_values(only_values: &Vec<(String, Json)>) -> CursorResult<()> { - if only_values.len() < 2 { - let description = format!( - "Incorrect usage of 'IdbMultiKeyCursor': expected more than one cursor bound, found '{}'", - only_values.len(), - ); - return MmError::err(CursorError::IncorrectUsage { description }); - } - Ok(()) - } + pub(super) fn new(only_values: Vec<(String, Json)>) -> IdbMultiKeyCursor { IdbMultiKeyCursor { only_values } } } -#[async_trait(?Send)] -impl CursorOps for IdbMultiKeyCursor { - fn db_index(&self) -> &IdbIndex { &self.db_index } - +impl CursorDriverImpl for IdbMultiKeyCursor { fn key_range(&self) -> CursorResult> { let only = Array::new(); @@ -65,14 +36,7 @@ impl CursorOps for IdbMultiKeyCursor { Ok(Some(key_range)) } - fn on_collect_iter( - &mut self, - _key: JsValue, - value: &Json, - ) -> CursorResult<(CollectItemAction, CollectCursorAction)> { - if let Some(ref mut filter) = self.collect_filter { - return Ok(filter(value)); - } - Ok((CollectItemAction::Include, CollectCursorAction::Continue)) + fn on_iteration(&mut self, _key: JsValue) -> CursorResult<(CursorItemAction, CursorAction)> { + Ok((CursorItemAction::Include, CursorAction::Continue)) } } diff --git a/mm2src/mm2_db/src/indexed_db/drivers/cursor/single_key_bound_cursor.rs b/mm2src/mm2_db/src/indexed_db/drivers/cursor/single_key_bound_cursor.rs index d3138f09e9..92e7ee087b 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/cursor/single_key_bound_cursor.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/cursor/single_key_bound_cursor.rs @@ -1,40 +1,31 @@ -use super::{CollectCursorAction, CollectItemAction, CursorBoundValue, CursorError, CursorOps, CursorResult, DbFilter}; -use async_trait::async_trait; +use super::{CursorAction, CursorBoundValue, CursorDriverImpl, CursorError, CursorItemAction, CursorResult}; use common::{log::warn, stringify_js_error}; use mm2_err_handle::prelude::*; -use serde_json::Value as Json; use wasm_bindgen::prelude::*; -use web_sys::{IdbIndex, IdbKeyRange}; +use web_sys::IdbKeyRange; /// The representation of a range that includes records /// whose value of the [`IdbSingleBoundCursor::field_name`] field is lower than [`IdbSingleBoundCursor::lower_bound_value`] /// and greater than [`IdbSingleBoundCursor::upper_bound_value`]. /// https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/bound pub struct IdbSingleKeyBoundCursor { - db_index: IdbIndex, #[allow(dead_code)] field_name: String, lower_bound: CursorBoundValue, upper_bound: CursorBoundValue, - /// An additional predicate that may be used to filter records. - collect_filter: Option, } impl IdbSingleKeyBoundCursor { pub(super) fn new( - db_index: IdbIndex, field_name: String, lower_bound: CursorBoundValue, upper_bound: CursorBoundValue, - collect_filter: Option, ) -> CursorResult { Self::check_bounds(&lower_bound, &upper_bound)?; Ok(IdbSingleKeyBoundCursor { - db_index, field_name, lower_bound, upper_bound, - collect_filter, }) } } @@ -56,10 +47,7 @@ impl IdbSingleKeyBoundCursor { } } -#[async_trait(?Send)] -impl CursorOps for IdbSingleKeyBoundCursor { - fn db_index(&self) -> &IdbIndex { &self.db_index } - +impl CursorDriverImpl for IdbSingleKeyBoundCursor { fn key_range(&self) -> CursorResult> { let key_range = IdbKeyRange::bound(&self.lower_bound.to_js_value()?, &self.upper_bound.to_js_value()?) .map_to_mm(|e| CursorError::InvalidKeyRange { @@ -68,14 +56,7 @@ impl CursorOps for IdbSingleKeyBoundCursor { Ok(Some(key_range)) } - fn on_collect_iter( - &mut self, - _key: JsValue, - value: &Json, - ) -> CursorResult<(CollectItemAction, CollectCursorAction)> { - if let Some(ref mut filter) = self.collect_filter { - return Ok(filter(value)); - } - Ok((CollectItemAction::Include, CollectCursorAction::Continue)) + fn on_iteration(&mut self, _key: JsValue) -> CursorResult<(CursorItemAction, CursorAction)> { + Ok((CursorItemAction::Include, CursorAction::Continue)) } } diff --git a/mm2src/mm2_db/src/indexed_db/drivers/cursor/single_key_cursor.rs b/mm2src/mm2_db/src/indexed_db/drivers/cursor/single_key_cursor.rs index f24f0da1f8..b3d6efa476 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/cursor/single_key_cursor.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/cursor/single_key_cursor.rs @@ -1,46 +1,29 @@ -use super::{CollectCursorAction, CollectItemAction, CursorError, CursorOps, CursorResult, DbFilter}; -use async_trait::async_trait; -use common::{log::warn, serialize_to_js, stringify_js_error}; +use super::{CursorAction, CursorDriverImpl, CursorError, CursorItemAction, CursorResult}; +use common::{serialize_to_js, stringify_js_error}; use mm2_err_handle::prelude::*; use serde_json::Value as Json; use wasm_bindgen::prelude::*; -use web_sys::{IdbIndex, IdbKeyRange}; +use web_sys::IdbKeyRange; /// The representation of a range that includes records /// whose value of the [`IdbSingleKeyCursor::field_name`] field equals to the [`IdbSingleKeyCursor::field_value`] value. /// https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/only pub struct IdbSingleKeyCursor { - db_index: IdbIndex, #[allow(dead_code)] field_name: String, field_value: Json, - /// An additional predicate that may be used to filter records. - collect_filter: Option, } impl IdbSingleKeyCursor { - pub(super) fn new( - db_index: IdbIndex, - field_name: String, - field_value: Json, - filter: Option, - ) -> IdbSingleKeyCursor { - if filter.is_none() { - warn!("Consider using 'IdbObjectStoreImpl::get_items' instead of 'IdbSingleKeyCursor'"); - } + pub(super) fn new(field_name: String, field_value: Json) -> IdbSingleKeyCursor { IdbSingleKeyCursor { - db_index, field_name, field_value, - collect_filter: None, } } } -#[async_trait(?Send)] -impl CursorOps for IdbSingleKeyCursor { - fn db_index(&self) -> &IdbIndex { &self.db_index } - +impl CursorDriverImpl for IdbSingleKeyCursor { fn key_range(&self) -> CursorResult> { let js_value = serialize_to_js(&self.field_value).map_to_mm(|e| CursorError::ErrorSerializingIndexFieldValue { @@ -55,14 +38,7 @@ impl CursorOps for IdbSingleKeyCursor { Ok(Some(key_range)) } - fn on_collect_iter( - &mut self, - _key: JsValue, - value: &Json, - ) -> CursorResult<(CollectItemAction, CollectCursorAction)> { - if let Some(ref mut filter) = self.collect_filter { - return Ok(filter(value)); - } - Ok((CollectItemAction::Include, CollectCursorAction::Continue)) + fn on_iteration(&mut self, _key: JsValue) -> CursorResult<(CursorItemAction, CursorAction)> { + Ok((CursorItemAction::Include, CursorAction::Continue)) } } diff --git a/mm2src/mm2_db/src/indexed_db/drivers/object_store.rs b/mm2src/mm2_db/src/indexed_db/drivers/object_store.rs index 53c9be6104..a7b8081e9c 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/object_store.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/object_store.rs @@ -1,5 +1,4 @@ use super::{construct_event_closure, DbTransactionError, DbTransactionResult, InternalItem, ItemId}; -use crate::indexed_db::db_driver::cursor::IdbCursorBuilder; use common::{deserialize_from_js, serialize_to_js, stringify_js_error}; use futures::channel::mpsc; use futures::StreamExt; @@ -9,7 +8,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use web_sys::{IdbObjectStore, IdbRequest}; +use web_sys::{IdbIndex, IdbObjectStore, IdbRequest}; pub struct IdbObjectStoreImpl { pub(crate) object_store: IdbObjectStore, @@ -58,10 +57,7 @@ impl IdbObjectStoreImpl { let index = index_str.to_owned(); let index_value_js = try_serialize_index_value!(serialize_to_js(&index_value), index_str); - let db_index = match self.object_store.index(index_str) { - Ok(index) => index, - Err(_) => return MmError::err(DbTransactionError::NoSuchIndex { index }), - }; + let db_index = self.open_index(index_str)?; let get_request = match db_index.get_all_with_key(&index_value_js) { Ok(request) => request, Err(e) => { @@ -90,10 +86,7 @@ impl IdbObjectStoreImpl { let index = index_str.to_owned(); let index_value_js = try_serialize_index_value!(serialize_to_js(&index_value), index); - let db_index = match self.object_store.index(index_str) { - Ok(index) => index, - Err(_) => return MmError::err(DbTransactionError::NoSuchIndex { index }), - }; + let db_index = self.open_index(index_str)?; let get_request = match db_index.get_all_keys_with_key(&index_value_js) { Ok(request) => request, Err(e) => { @@ -141,10 +134,7 @@ impl IdbObjectStoreImpl { let index = index_str.to_owned(); let index_value_js = try_serialize_index_value!(serialize_to_js(&index_value), index); - let db_index = match self.object_store.index(index_str) { - Ok(index) => index, - Err(_) => return MmError::err(DbTransactionError::NoSuchIndex { index }), - }; + let db_index = self.open_index(index_str)?; let count_request = match db_index.count_with_key(&index_value_js) { Ok(request) => request, Err(e) => { @@ -248,16 +238,13 @@ impl IdbObjectStoreImpl { Ok(()) } - pub(crate) fn cursor_builder(&self, index_str: &str) -> DbTransactionResult { - let db_index = match self.object_store.index(index_str) { - Ok(index) => index, - Err(_) => { - return MmError::err(DbTransactionError::NoSuchIndex { - index: index_str.to_owned(), - }) - }, - }; - Ok(IdbCursorBuilder::new(db_index)) + pub(crate) fn open_index(&self, index_str: &str) -> DbTransactionResult { + match self.object_store.index(index_str) { + Ok(index) => Ok(index), + Err(_) => MmError::err(DbTransactionError::NoSuchIndex { + index: index_str.to_owned(), + }), + } } async fn wait_for_request_complete(request: &IdbRequest) -> Result { diff --git a/mm2src/mm2_db/src/indexed_db/drivers/transaction.rs b/mm2src/mm2_db/src/indexed_db/drivers/transaction.rs index aef31a4e7d..f76794b2de 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/transaction.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/transaction.rs @@ -1,6 +1,7 @@ use super::IdbObjectStoreImpl; use common::wasm::stringify_js_error; use derive_more::Display; +use enum_from::EnumFromTrait; use mm2_err_handle::prelude::*; use serde_json::Value as Json; use std::collections::HashSet; @@ -11,7 +12,7 @@ use web_sys::IdbTransaction; pub type DbTransactionResult = Result>; -#[derive(Debug, Display, PartialEq)] +#[derive(Debug, Display, EnumFromTrait, PartialEq)] pub enum DbTransactionError { #[display(fmt = "No such table '{}'", table)] NoSuchTable { table: String }, @@ -44,6 +45,7 @@ pub enum DbTransactionError { description: String, }, #[display(fmt = "Error occurred due to an unexpected state: {:?}", _0)] + #[from_trait(WithInternal::internal)] UnexpectedState(String), #[display(fmt = "Transaction was aborted")] TransactionAborted, diff --git a/mm2src/mm2_db/src/indexed_db/indexed_cursor.rs b/mm2src/mm2_db/src/indexed_db/indexed_cursor.rs index e04c622ddf..cdbc019f1a 100644 --- a/mm2src/mm2_db/src/indexed_db/indexed_cursor.rs +++ b/mm2src/mm2_db/src/indexed_db/indexed_cursor.rs @@ -18,10 +18,13 @@ //! you can use [`WithBound::bound`] along with [`WithOnly::only`]: //! ```rust //! let table = open_table_somehow(); -//! let all_rick_morty_swaps = table.open_cursor("search_index") +//! let all_rick_morty_swaps = table +//! .cursor_builder() //! .only("base_coin", "RICK", "MORTY")? //! .bound("base_coin_value", 10, 13) //! .bound("started_at", 1000000030.into(), u32::MAX.into()) +//! .open_cursor("search_index") +//! .await? //! .collect() //! .await?; //! ``` @@ -49,13 +52,12 @@ //! because ['RICK', 'MORTY', 13] < ['RICK', 'MORTY', 13, 1000000030], //! although it is expected to be within the specified bounds. -use crate::indexed_db::db_driver::cursor::{CollectCursorAction, CollectItemAction, CursorBoundValue, CursorOps, - DbFilter, IdbCursorBuilder}; +use crate::indexed_db::db_driver::cursor::CursorBoundValue; +pub(crate) use crate::indexed_db::db_driver::cursor::{CursorDriver, CursorFilters}; pub use crate::indexed_db::db_driver::cursor::{CursorError, CursorResult}; -use crate::indexed_db::{ItemId, TableSignature}; -use async_trait::async_trait; +use crate::indexed_db::{DbTable, ItemId, TableSignature}; use futures::channel::{mpsc, oneshot}; -use futures::StreamExt; +use futures::{SinkExt, StreamExt}; use mm2_err_handle::prelude::*; use serde::Serialize; use serde_json::{self as json, Value as Json}; @@ -65,102 +67,20 @@ use std::marker::PhantomData; pub(super) type DbCursorEventTx = mpsc::UnboundedSender; pub(super) type DbCursorEventRx = mpsc::UnboundedReceiver; -pub enum DbCursorEvent { - Collect { - options: CursorCollectOptions, - result_tx: oneshot::Sender>>, - }, -} - -pub enum CursorCollectOptions { - SingleKey { - field_name: String, - field_value: Json, - filter: Option, - }, - SingleKeyBound { - field_name: String, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - filter: Option, - }, - MultiKey { - keys: Vec<(String, Json)>, - filter: Option, - }, - MultiKeyBound { - only_keys: Vec<(String, Json)>, - bound_keys: Vec<(String, CursorBoundValue, CursorBoundValue)>, - filter: Option, - }, -} - -pub async fn cursor_event_loop(mut rx: DbCursorEventRx, cursor_builder: IdbCursorBuilder) { - while let Some(event) = rx.next().await { - match event { - DbCursorEvent::Collect { options, result_tx } => { - on_collect_cursor_event(result_tx, options, cursor_builder).await; - return; - }, - } - } +pub struct CursorBuilder<'transaction, 'reference, Table: TableSignature> { + db_table: &'reference DbTable<'transaction, Table>, + filters: CursorFilters, } -async fn on_collect_cursor_event( - result_tx: oneshot::Sender>>, - options: CursorCollectOptions, - cursor_builder: IdbCursorBuilder, -) { - async fn on_collect_cursor_event_impl( - options: CursorCollectOptions, - cursor_builder: IdbCursorBuilder, - ) -> CursorResult> { - match options { - CursorCollectOptions::SingleKey { - field_name, - field_value, - filter, - } => { - cursor_builder - .single_key_cursor(field_name, field_value, filter) - .collect() - .await - }, - CursorCollectOptions::SingleKeyBound { - field_name, - lower_bound, - upper_bound, - filter, - } => { - cursor_builder - .single_key_bound_cursor(field_name, lower_bound, upper_bound, filter)? - .collect() - .await - }, - CursorCollectOptions::MultiKey { keys, filter } => { - cursor_builder.multi_key_cursor(keys, filter)?.collect().await - }, - CursorCollectOptions::MultiKeyBound { - only_keys, - bound_keys, - filter, - } => { - cursor_builder - .multi_key_bound_cursor(only_keys, bound_keys, filter)? - .collect() - .await - }, +impl<'transaction, 'reference, Table: TableSignature> CursorBuilder<'transaction, 'reference, Table> { + pub(crate) fn new(db_table: &'reference DbTable<'transaction, Table>) -> Self { + CursorBuilder { + db_table, + filters: CursorFilters::default(), } } - let result = on_collect_cursor_event_impl(options, cursor_builder).await; - result_tx.send(result).ok(); -} - -pub trait WithOnly: Sized { - type ResultCursor; - - fn only(self, field_name: &str, field_value: Value) -> CursorResult + pub fn only(mut self, field_name: &str, field_value: Value) -> CursorResult where Value: Serialize + fmt::Debug, { @@ -170,367 +90,93 @@ pub trait WithOnly: Sized { value: field_value_str, description: e.to_string(), })?; - Ok(self.only_json(field_name, field_value)) - } - fn only_json(self, field_name: &str, field_value: Json) -> Self::ResultCursor; -} - -pub trait WithBound: Sized { - type ResultCursor; + self.filters.only_keys.push((field_name.to_owned(), field_value)); + Ok(self) + } - fn bound(self, field_name: &str, lower_bound: Value, upper_bound: Value) -> Self::ResultCursor + pub fn bound(mut self, field_name: &str, lower_bound: Value, upper_bound: Value) -> Self where CursorBoundValue: From, { let lower_bound = CursorBoundValue::from(lower_bound); let upper_bound = CursorBoundValue::from(upper_bound); - self.bound_values(field_name, lower_bound, upper_bound) - } - - fn bound_values( - self, - field_name: &str, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - ) -> Self::ResultCursor; -} - -pub trait WithFilter: Sized { - fn filter(self, filter: F) -> Self - where - F: FnMut(&Json) -> (CollectItemAction, CollectCursorAction) + Send + 'static, - { - let filter = Box::new(filter); - self.filter_boxed(filter) - } - - fn filter_boxed(self, filter: DbFilter) -> Self; -} - -#[async_trait] -pub trait CollectCursor { - async fn collect(self) -> CursorResult>; -} - -#[async_trait] -impl + Send> CollectCursor for T { - async fn collect(self) -> CursorResult> { self.collect_impl().await } -} - -#[async_trait] -pub(crate) trait CollectCursorImpl: Sized { - fn into_collect_options(self) -> CursorCollectOptions; - - fn event_tx(&self) -> DbCursorEventTx; - - async fn collect_impl(self) -> CursorResult> { - let event_tx = self.event_tx(); - let options = self.into_collect_options(); - - let (result_tx, result_rx) = oneshot::channel(); - let event = DbCursorEvent::Collect { result_tx, options }; - let items: Vec<(ItemId, Json)> = send_event_recv_response(&event_tx, event, result_rx).await?; - - items - .into_iter() - .map(|(item_id, item)| json::from_value(item).map(|item| (item_id, item))) - .map(|res| res.map_to_mm(|e| CursorError::ErrorDeserializingItem(e.to_string()))) - // Item = CursorResult<(ItemId, Table)> - .collect() - } -} - -pub struct DbEmptyCursor<'a, Table: TableSignature> { - event_tx: DbCursorEventTx, - filter: Option, - phantom: PhantomData<&'a Table>, -} - -impl<'a, Table: TableSignature> WithOnly for DbEmptyCursor<'a, Table> { - type ResultCursor = DbSingleKeyCursor<'a, Table>; - - fn only_json(self, field_name: &str, field_value: Json) -> Self::ResultCursor { - DbSingleKeyCursor { - event_tx: self.event_tx, - field_name: field_name.to_owned(), - field_value, - filter: self.filter, - phantom: PhantomData::default(), - } - } -} - -impl<'a, Table: TableSignature> WithBound for DbEmptyCursor<'a, Table> { - type ResultCursor = DbSingleKeyBoundCursor<'a, Table>; - - fn bound_values( - self, - field_name: &str, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - ) -> Self::ResultCursor { - DbSingleKeyBoundCursor { - event_tx: self.event_tx, - field_name: field_name.to_owned(), - lower_bound, - upper_bound, - filter: self.filter, - phantom: PhantomData::default(), - } - } -} - -impl<'a, Table: TableSignature> WithFilter for DbEmptyCursor<'a, Table> { - fn filter_boxed(mut self, filter: DbFilter) -> Self { - self.filter = Some(filter); + self.filters + .bound_keys + .push((field_name.to_owned(), lower_bound, upper_bound)); self } -} - -impl<'a, Table: TableSignature> DbEmptyCursor<'a, Table> { - pub(super) fn new(event_tx: DbCursorEventTx) -> DbEmptyCursor<'a, Table> { - DbEmptyCursor { - event_tx, - filter: None, - phantom: PhantomData::default(), - } - } -} - -pub struct DbSingleKeyCursor<'a, Table: TableSignature> { - event_tx: DbCursorEventTx, - field_name: String, - field_value: Json, - filter: Option, - phantom: PhantomData<&'a Table>, -} - -impl<'a, Table: TableSignature> WithOnly for DbSingleKeyCursor<'a, Table> { - type ResultCursor = DbMultiKeyCursor<'a, Table>; - - fn only_json(self, field_name: &str, field_value: Json) -> Self::ResultCursor { - let keys = vec![ - (self.field_name, self.field_value), - (field_name.to_owned(), field_value), - ]; - DbMultiKeyCursor { - event_tx: self.event_tx, - keys, - filter: self.filter, - phantom: PhantomData::default(), - } - } -} - -impl<'a, Table: TableSignature> WithBound for DbSingleKeyCursor<'a, Table> { - type ResultCursor = DbMultiKeyBoundCursor<'a, Table>; - - fn bound_values( - self, - field_name: &str, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - ) -> Self::ResultCursor { - let only_keys = vec![(self.field_name, self.field_value)]; - let bound_keys = vec![(field_name.to_owned(), lower_bound, upper_bound)]; - DbMultiKeyBoundCursor { - event_tx: self.event_tx, - only_keys, - bound_keys, - filter: self.filter, - phantom: PhantomData::default(), - } - } -} -impl<'a, Table: TableSignature> WithFilter for DbSingleKeyCursor<'a, Table> { - fn filter_boxed(mut self, filter: DbFilter) -> Self { - self.filter = Some(filter); + pub fn reverse(mut self) -> Self { + self.filters.reverse = true; self } -} - -#[async_trait] -impl<'a, Table: TableSignature> CollectCursorImpl
for DbSingleKeyCursor<'a, Table> { - fn into_collect_options(self) -> CursorCollectOptions { - CursorCollectOptions::SingleKey { - field_name: self.field_name, - field_value: self.field_value, - filter: self.filter, - } - } - - fn event_tx(&self) -> DbCursorEventTx { self.event_tx.clone() } -} - -/// `DbSingleKeyBoundCursor` doesn't implement `WithOnly` trait, because indexes MUST start with `only` values. -pub struct DbSingleKeyBoundCursor<'a, Table: TableSignature> { - event_tx: DbCursorEventTx, - field_name: String, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - filter: Option, - phantom: PhantomData<&'a Table>, -} -impl<'a, Table: TableSignature> WithBound for DbSingleKeyBoundCursor<'a, Table> { - type ResultCursor = DbMultiKeyBoundCursor<'a, Table>; - - fn bound_values( - self, - field_name: &str, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - ) -> Self::ResultCursor { - let bound_keys = vec![ - (self.field_name, self.lower_bound, self.upper_bound), - (field_name.to_owned(), lower_bound, upper_bound), - ]; - DbMultiKeyBoundCursor { - event_tx: self.event_tx, - only_keys: Vec::new(), - bound_keys, - filter: self.filter, + /// Opens a cursor by the specified `index`. + /// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/openCursor + pub async fn open_cursor(self, index: &str) -> CursorResult> { + let event_tx = + self.db_table + .open_cursor(index, self.filters) + .await + .mm_err(|e| CursorError::ErrorOpeningCursor { + description: e.to_string(), + })?; + Ok(CursorIter { + event_tx, phantom: PhantomData::default(), - } - } -} - -impl<'a, Table: TableSignature> WithFilter for DbSingleKeyBoundCursor<'a, Table> { - fn filter_boxed(mut self, filter: DbFilter) -> Self { - self.filter = Some(filter); - self + }) } } -#[async_trait] -impl<'a, Table: TableSignature> CollectCursorImpl
for DbSingleKeyBoundCursor<'a, Table> { - fn into_collect_options(self) -> CursorCollectOptions { - CursorCollectOptions::SingleKeyBound { - field_name: self.field_name, - lower_bound: self.lower_bound, - upper_bound: self.upper_bound, - filter: self.filter, - } - } - - fn event_tx(&self) -> DbCursorEventTx { self.event_tx.clone() } -} - -pub struct DbMultiKeyCursor<'a, Table: TableSignature> { +pub struct CursorIter<'transaction, Table> { event_tx: DbCursorEventTx, - keys: Vec<(String, Json)>, - filter: Option, - phantom: PhantomData<&'a Table>, -} - -impl<'a, Table: TableSignature> WithOnly for DbMultiKeyCursor<'a, Table> { - type ResultCursor = DbMultiKeyCursor<'a, Table>; - - fn only_json(mut self, field_name: &str, field_value: Json) -> Self::ResultCursor { - self.keys.push((field_name.to_owned(), field_value)); - self - } + phantom: PhantomData<&'transaction Table>, } -impl<'a, Table: TableSignature> WithBound for DbMultiKeyCursor<'a, Table> { - type ResultCursor = DbMultiKeyBoundCursor<'a, Table>; - - fn bound_values( - self, - field_name: &str, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - ) -> Self::ResultCursor { - DbMultiKeyBoundCursor { - event_tx: self.event_tx, - only_keys: self.keys, - bound_keys: vec![(field_name.to_owned(), lower_bound, upper_bound)], - filter: self.filter, - phantom: PhantomData::default(), - } - } -} - -impl<'a, Table: TableSignature> WithFilter for DbMultiKeyCursor<'a, Table> { - fn filter_boxed(mut self, filter: DbFilter) -> Self { - self.filter = Some(filter); - self +impl<'transaction, Table: TableSignature> CursorIter<'transaction, Table> { + /// Advances the iterator and returns the next value. + /// Please note that the items are sorted by the index keys. + pub async fn next(&mut self) -> CursorResult> { + let (result_tx, result_rx) = oneshot::channel(); + self.event_tx + .send(DbCursorEvent::NextItem { result_tx }) + .await + .map_to_mm(|e| CursorError::UnexpectedState(format!("Error sending cursor event: {e}")))?; + let maybe_item = result_rx + .await + .map_to_mm(|e| CursorError::UnexpectedState(format!("Error receiving cursor item: {e}")))??; + let (item_id, item) = match maybe_item { + Some((item_id, item)) => (item_id, item), + None => return Ok(None), + }; + let item = json::from_value(item).map_to_mm(|e| CursorError::ErrorDeserializingItem(e.to_string()))?; + Ok(Some((item_id, item))) } -} -#[async_trait] -impl<'a, Table: TableSignature> CollectCursorImpl
for DbMultiKeyCursor<'a, Table> { - fn into_collect_options(self) -> CursorCollectOptions { - CursorCollectOptions::MultiKey { - keys: self.keys, - filter: self.filter, + pub async fn collect(mut self) -> CursorResult> { + let mut result = Vec::new(); + while let Some((item_id, item)) = self.next().await? { + result.push((item_id, item)); } - } - - fn event_tx(&self) -> DbCursorEventTx { self.event_tx.clone() } -} - -/// `DbMultiKeyBoundCursor` doesn't implement `WithOnly` trait, because indexes MUST start with `only` values. -pub struct DbMultiKeyBoundCursor<'a, Table: TableSignature> { - event_tx: DbCursorEventTx, - only_keys: Vec<(String, Json)>, - bound_keys: Vec<(String, CursorBoundValue, CursorBoundValue)>, - filter: Option, - phantom: PhantomData<&'a Table>, -} - -impl<'a, Table: TableSignature> WithBound for DbMultiKeyBoundCursor<'a, Table> { - type ResultCursor = DbMultiKeyBoundCursor<'a, Table>; - - fn bound_values( - mut self, - field_name: &str, - lower_bound: CursorBoundValue, - upper_bound: CursorBoundValue, - ) -> Self::ResultCursor { - self.bound_keys.push((field_name.to_owned(), lower_bound, upper_bound)); - self + Ok(result) } } -impl<'a, Table: TableSignature> WithFilter for DbMultiKeyBoundCursor<'a, Table> { - fn filter_boxed(mut self, filter: DbFilter) -> Self { - self.filter = Some(filter); - self - } +pub enum DbCursorEvent { + NextItem { + result_tx: oneshot::Sender>>, + }, } -#[async_trait] -impl<'a, Table: TableSignature> CollectCursorImpl
for DbMultiKeyBoundCursor<'a, Table> { - fn into_collect_options(self) -> CursorCollectOptions { - CursorCollectOptions::MultiKeyBound { - only_keys: self.only_keys, - bound_keys: self.bound_keys, - filter: self.filter, +pub(crate) async fn cursor_event_loop(mut rx: DbCursorEventRx, mut cursor: CursorDriver) { + while let Some(event) = rx.next().await { + match event { + DbCursorEvent::NextItem { result_tx } => { + result_tx.send(cursor.next().await).ok(); + }, } } - - fn event_tx(&self) -> DbCursorEventTx { self.event_tx.clone() } -} - -async fn send_event_recv_response( - event_tx: &mpsc::UnboundedSender, - event: Event, - result_rx: oneshot::Receiver>, -) -> CursorResult { - if let Err(e) = event_tx.unbounded_send(event) { - let error = format!("Error sending event: {}", e); - return MmError::err(CursorError::UnexpectedState(error)); - } - match result_rx.await { - Ok(result) => result, - Err(e) => { - let error = format!("Error receiving result: {}", e); - MmError::err(CursorError::UnexpectedState(error)) - }, - } } mod tests { @@ -605,10 +251,19 @@ mod tests { table .add_item(&item) .await - .expect(&format!("Error adding {:?} item", item)); + .unwrap_or_else(|_| panic!("Error adding {:?} item", item)); } } + #[track_caller] + async fn next_item(cursor_iter: &mut CursorIter<'_, Table>) -> Option
{ + cursor_iter + .next() + .await + .expect("!CursorIter::next") + .map(|(_item_id, item)| item) + } + /// The table with `BeBigUint` parameters. #[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[serde(deny_unknown_fields)] @@ -705,13 +360,14 @@ mod tests { // Get every item that satisfies the following [num_x, num_y] bound. let actual_items = table + .cursor_builder() + .bound("timestamp_x", num_x.clone(), num_y.clone()) .open_cursor("timestamp_x") .await - .expect("!DbTable::open_cursor") - .bound("timestamp_x", num_x.clone(), num_y.clone()) + .expect("!CursorBuilder::open_cursor") .collect() .await - .expect("!DbSingleKeyBoundCursor::collect") + .expect("!CursorIter::collect") .into_iter() // Map `(ItemId, TimestampTable)` into `BeBigUint`. .map(|(_item_id, item)| item.timestamp_x) @@ -759,14 +415,15 @@ mod tests { fill_table(&table, items).await; let mut actual_items = table + .cursor_builder() + .only("base_coin", "RICK") + .expect("!CursorBuilder::only") .open_cursor("base_coin") .await - .expect("!DbTable::open_cursor") - .only("base_coin", "RICK") - .expect("!DbEmptyCursor::only") + .expect("!CursorBuilder::open_cursor") .collect() .await - .expect("!DbSingleKeyCursor::collect") + .expect("!CursorIter::collect") .into_iter() .map(|(_item_id, item)| item) .collect::>(); @@ -812,13 +469,14 @@ mod tests { fill_table(&table, items).await; let mut actual_items = table + .cursor_builder() + .bound("rel_coin_value", 5u32, u32::MAX) .open_cursor("rel_coin_value") .await - .expect("!DbTable::open_cursor") - .bound("rel_coin_value", 5u32, u32::MAX) + .expect("!CursorBuilder::open_cursor") .collect() .await - .expect("!DbSingleKeyCursor::collect") + .expect("!CursorIter::collect") .into_iter() .map(|(_item_id, item)| item) .collect::>(); @@ -870,18 +528,19 @@ mod tests { fill_table(&table, items).await; let mut actual_items = table - .open_cursor("basecoin_basecoinvalue_startedat_index") - .await - .expect("!DbTable::open_cursor") + .cursor_builder() .only("base_coin", "RICK") - .expect("!DbEmptyCursor::only") + .expect("!CursorBuilder::only") .only("base_coin_value", 12) - .expect("!DbSingleKeyCursor::only") + .expect("!CursorBuilder::only") .only("started_at", 721) - .expect("!DbMultiKeyCursor::only") + .expect("!CursorBuilder::only") + .open_cursor("basecoin_basecoinvalue_startedat_index") + .await + .expect("!CursorBuilder::open_cursor") .collect() .await - .expect("!DbMultiKeyCursor::collect") + .expect("!CursorIter::collect") .into_iter() .map(|(_item_id, item)| item) .collect::>(); @@ -946,19 +605,20 @@ mod tests { fill_table(&table, items).await; let actual_items = table - .open_cursor("all_fields_index") - .await - .expect("!DbTable::open_cursor") + .cursor_builder() .only("base_coin", "RICK") - .expect("!DbEmptyCursor::only") + .expect("!CursorBuilder::only") .only("rel_coin", "QRC20") - .expect("!DbEmptyCursor::only") + .expect("!CursorBuilder::only") .bound("base_coin_value", 3u32, 8u32) .bound("rel_coin_value", 10u32, 12u32) .bound("started_at", 600i32, 800i32) + .open_cursor("all_fields_index") + .await + .expect("!CursorBuilder::open_cursor") .collect() .await - .expect("!DbMultiKeyCursor::collect") + .expect("!CursorIter::collect") .into_iter() .map(|(_item_id, item)| item) .collect::>(); @@ -1006,15 +666,16 @@ mod tests { fill_table(&table, items).await; let actual_items = table - .open_cursor("timestamp_xyz") - .await - .expect("!DbTable::open_cursor") + .cursor_builder() .bound("timestamp_x", BeBigUint::from(u64::MAX - 1), BeBigUint::from(u128::MAX)) .bound("timestamp_y", 0u32, 5u32) .bound("timestamp_z", BeBigUint::from(u64::MAX), BeBigUint::from(u128::MAX - 2)) + .open_cursor("timestamp_xyz") + .await + .expect("!CursorBuilder::open_cursor") .collect() .await - .expect("!DbMultiKeyCursor::collect") + .expect("!CursorIter::collect") .into_iter() .map(|(_item_id, item)| item) .collect::>(); @@ -1029,4 +690,198 @@ mod tests { assert_eq!(actual_items, expected_items); } + + #[wasm_bindgen_test] + async fn test_iter_without_constraints() { + const DB_NAME: &str = "TEST_ITER_WITHOUT_CONSTRAINTS"; + const DB_VERSION: u32 = 1; + + register_wasm_log(); + + let items = vec![ + swap_item!("uuid1", "RICK", "MORTY", 10, 3, 700), + swap_item!("uuid2", "MORTY", "KMD", 95000, 1, 721), + swap_item!("uuid3", "RICK", "XYZ", 7, u32::MAX, 1281), + swap_item!("uuid4", "RICK", "MORTY", 8, 6, 92), + ]; + + let db = IndexedDbBuilder::new(DbIdentifier::for_test(DB_NAME)) + .with_version(DB_VERSION) + .with_table::() + .build() + .await + .expect("!IndexedDb::init"); + let transaction = db.transaction().await.expect("!IndexedDb::transaction"); + let table = transaction + .table::() + .await + .expect("!DbTransaction::open_table"); + fill_table(&table, items).await; + + let mut cursor_iter = table + .cursor_builder() + .open_cursor("rel_coin_value") + .await + .expect("!CursorBuilder::open_cursor"); + + // The items must be sorted by `rel_coin_value`. + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid2", "MORTY", "KMD", 95000, 1, 721)) + ); + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid1", "RICK", "MORTY", 10, 3, 700)) + ); + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid4", "RICK", "MORTY", 8, 6, 92)) + ); + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid3", "RICK", "XYZ", 7, u32::MAX, 1281)) + ); + assert!(next_item(&mut cursor_iter).await.is_none()); + // Try to poll one more time. This should not fail but return `None`. + assert!(next_item(&mut cursor_iter).await.is_none()); + } + + #[wasm_bindgen_test] + async fn test_rev_iter_without_constraints() { + const DB_NAME: &str = "TEST_REV_ITER_WITHOUT_CONSTRAINTS"; + const DB_VERSION: u32 = 1; + + register_wasm_log(); + + let db = IndexedDbBuilder::new(DbIdentifier::for_test(DB_NAME)) + .with_version(DB_VERSION) + .with_table::() + .build() + .await + .expect("!IndexedDb::init"); + let transaction = db.transaction().await.expect("!IndexedDb::transaction"); + let table = transaction + .table::() + .await + .expect("!DbTransaction::open_table"); + + table + .cursor_builder() + .reverse() + .open_cursor("rel_coin_value") + .await + .map(|_| ()) + .expect_err( + "CursorBuilder::open_cursor should have failed because 'reverse' can be used with key range only", + ); + } + + #[wasm_bindgen_test] + async fn test_iter_single_key_bound_cursor() { + const DB_NAME: &str = "TEST_ITER_SINGLE_KEY_BOUND_CURSOR"; + const DB_VERSION: u32 = 1; + + register_wasm_log(); + + let items = vec![ + swap_item!("uuid1", "RICK", "MORTY", 10, 3, 700), + swap_item!("uuid2", "MORTY", "KMD", 95000, 1, 721), + swap_item!("uuid3", "RICK", "XYZ", 7, u32::MAX, 1281), // + + swap_item!("uuid4", "RICK", "MORTY", 8, 6, 92), // + + swap_item!("uuid5", "QRC20", "RICK", 2, 4, 721), + swap_item!("uuid6", "KMD", "MORTY", 12, 3124, 214), // + + ]; + + let db = IndexedDbBuilder::new(DbIdentifier::for_test(DB_NAME)) + .with_version(DB_VERSION) + .with_table::() + .build() + .await + .expect("!IndexedDb::init"); + let transaction = db.transaction().await.expect("!IndexedDb::transaction"); + let table = transaction + .table::() + .await + .expect("!DbTransaction::open_table"); + fill_table(&table, items).await; + + let mut cursor_iter = table + .cursor_builder() + .bound("rel_coin_value", 5u32, u32::MAX) + .open_cursor("rel_coin_value") + .await + .expect("!CursorBuilder::open_cursor"); + + // The items must be sorted by `rel_coin_value`. + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid4", "RICK", "MORTY", 8, 6, 92)) + ); + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid6", "KMD", "MORTY", 12, 3124, 214)) + ); + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid3", "RICK", "XYZ", 7, u32::MAX, 1281)) + ); + assert!(next_item(&mut cursor_iter).await.is_none()); + // Try to poll one more time. This should not fail but return `None`. + assert!(next_item(&mut cursor_iter).await.is_none()); + } + + #[wasm_bindgen_test] + async fn test_rev_iter_single_key_bound_cursor() { + const DB_NAME: &str = "TEST_REV_ITER_SINGLE_KEY_BOUND_CURSOR"; + const DB_VERSION: u32 = 1; + + register_wasm_log(); + + let items = vec![ + swap_item!("uuid1", "RICK", "MORTY", 10, 3, 700), + swap_item!("uuid2", "MORTY", "KMD", 95000, 1, 721), + swap_item!("uuid3", "RICK", "XYZ", 7, u32::MAX, 1281), // + + swap_item!("uuid4", "RICK", "MORTY", 8, 6, 92), // + + swap_item!("uuid5", "QRC20", "RICK", 2, 4, 721), + swap_item!("uuid6", "KMD", "MORTY", 12, 3124, 214), // + + ]; + + let db = IndexedDbBuilder::new(DbIdentifier::for_test(DB_NAME)) + .with_version(DB_VERSION) + .with_table::() + .build() + .await + .expect("!IndexedDb::init"); + let transaction = db.transaction().await.expect("!IndexedDb::transaction"); + let table = transaction + .table::() + .await + .expect("!DbTransaction::open_table"); + fill_table(&table, items).await; + + let mut cursor_iter = table + .cursor_builder() + .bound("rel_coin_value", 5u32, u32::MAX) + .reverse() + .open_cursor("rel_coin_value") + .await + .expect("!CursorBuilder::open_cursor"); + + // The items must be sorted in reverse order by `rel_coin_value`. + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid3", "RICK", "XYZ", 7, u32::MAX, 1281)) + ); + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid6", "KMD", "MORTY", 12, 3124, 214)) + ); + assert_eq!( + next_item(&mut cursor_iter).await, + Some(swap_item!("uuid4", "RICK", "MORTY", 8, 6, 92)) + ); + assert!(next_item(&mut cursor_iter).await.is_none()); + // Try to poll one more time. This should not fail but return `None`. + assert!(next_item(&mut cursor_iter).await.is_none()); + } } diff --git a/mm2src/mm2_db/src/indexed_db/indexed_db.rs b/mm2src/mm2_db/src/indexed_db/indexed_db.rs index 897161464f..3bad052869 100644 --- a/mm2src/mm2_db/src/indexed_db/indexed_db.rs +++ b/mm2src/mm2_db/src/indexed_db/indexed_db.rs @@ -48,15 +48,15 @@ pub use db_driver::{DbTransactionError, DbTransactionResult, DbUpgrader, InitDbE pub use db_lock::{ConstructibleDb, DbLocked, SharedDb, WeakDb}; use db_driver::{IdbDatabaseBuilder, IdbDatabaseImpl, IdbObjectStoreImpl, IdbTransactionImpl, OnUpgradeNeededCb}; -use indexed_cursor::{cursor_event_loop, DbCursorEventTx, DbEmptyCursor}; +use indexed_cursor::{cursor_event_loop, CursorBuilder, CursorDriver, CursorError, CursorFilters, CursorResult, + DbCursorEventTx}; type DbEventTx = mpsc::UnboundedSender; type DbTransactionEventTx = mpsc::UnboundedSender; type DbTableEventTx = mpsc::UnboundedSender; pub mod cursor_prelude { - pub use crate::indexed_db::indexed_cursor::{CollectCursor, CursorError, CursorResult, WithBound, WithFilter, - WithOnly}; + pub use crate::indexed_db::indexed_cursor::{CursorError, CursorResult}; } pub trait TableSignature: DeserializeOwned + Serialize + 'static { @@ -101,7 +101,7 @@ impl DbIdentifier { } } - pub fn display_rmd160(&self) -> String { hex::encode(&*self.wallet_rmd160) } + pub fn display_rmd160(&self) -> String { hex::encode(*self.wallet_rmd160) } } pub struct IndexedDbBuilder { @@ -174,21 +174,20 @@ pub struct IndexedDb { event_tx: DbEventTx, } -async fn send_event_recv_response( +async fn send_event_recv_response( event_tx: &mpsc::UnboundedSender, event: Event, - result_rx: oneshot::Receiver>, -) -> DbTransactionResult { + result_rx: oneshot::Receiver>, +) -> MmResult +where + Error: WithInternal + NotMmError, +{ if let Err(e) = event_tx.unbounded_send(event) { - let error = format!("Error sending event: {}", e); - return MmError::err(DbTransactionError::UnexpectedState(error)); + return MmError::err(Error::internal(format!("Error sending event: {}", e))); } match result_rx.await { Ok(result) => result, - Err(e) => { - let error = format!("Error receiving result: {}", e); - MmError::err(DbTransactionError::UnexpectedState(error)) - }, + Err(e) => MmError::err(Error::internal(format!("Error receiving result: {}", e))), } } @@ -232,9 +231,9 @@ impl IndexedDb { } } -pub struct DbTransaction<'a> { +pub struct DbTransaction<'transaction> { event_tx: DbTransactionEventTx, - phantom: PhantomData<&'a ()>, + phantom: PhantomData<&'transaction ()>, } impl DbTransaction<'_> { @@ -297,9 +296,9 @@ impl DbTransaction<'_> { } } -pub struct DbTable<'a, Table: TableSignature> { +pub struct DbTable<'transaction, Table: TableSignature> { event_tx: DbTableEventTx, - phantom: PhantomData<&'a Table>, + phantom: PhantomData<&'transaction Table>, } pub enum AddOrIgnoreResult { @@ -307,11 +306,11 @@ pub enum AddOrIgnoreResult { ExistAlready(ItemId), } -impl DbTable<'_, Table> { +impl<'transaction, Table: TableSignature> DbTable<'transaction, Table> { /// Adds the given item to the table. /// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/add pub async fn add_item(&self, item: &Table) -> DbTransactionResult { - let item = json::to_value(&item).map_to_mm(|e| DbTransactionError::ErrorSerializingItem(e.to_string()))?; + let item = json::to_value(item).map_to_mm(|e| DbTransactionError::ErrorSerializingItem(e.to_string()))?; let (result_tx, result_rx) = oneshot::channel(); let event = internal::DbTableEvent::AddItem { item, result_tx }; @@ -336,12 +335,10 @@ impl DbTable<'_, Table> { match ids.len() { 0 => self.add_item(item).await.map(AddOrIgnoreResult::Added), 1 => Ok(AddOrIgnoreResult::ExistAlready(ids[0])), - got_items => { - return MmError::err(DbTransactionError::MultipleItemsByUniqueIndex { - index: index.to_owned(), - got_items, - }); - }, + got_items => MmError::err(DbTransactionError::MultipleItemsByUniqueIndex { + index: index.to_owned(), + got_items, + }), } } @@ -529,12 +526,10 @@ impl DbTable<'_, Table> { let item_id = ids[0]; self.replace_item(item_id, item).await }, - got_items => { - return MmError::err(DbTransactionError::MultipleItemsByUniqueIndex { - index: index.to_owned(), - got_items, - }); - }, + got_items => MmError::err(DbTransactionError::MultipleItemsByUniqueIndex { + index: index.to_owned(), + got_items, + }), } } @@ -636,16 +631,10 @@ impl DbTable<'_, Table> { send_event_recv_response(&self.event_tx, event, result_rx).await } - /// Opens a cursor by the specified `index`. - /// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/openCursor - pub async fn open_cursor(&self, index: &str) -> DbTransactionResult> { - let (result_tx, result_rx) = oneshot::channel(); - let event = internal::DbTableEvent::OpenCursor { - index: index.to_owned(), - result_tx, - }; - let cursor_event_tx = send_event_recv_response(&self.event_tx, event, result_rx).await?; - Ok(DbEmptyCursor::new(cursor_event_tx)) + /// Returns a `CursorBuilder` builder. It can be used to open a cursor at the specified with specific key bounds. + /// See [`CursorBuilder::open_cursor`]. + pub fn cursor_builder<'reference>(&'reference self) -> CursorBuilder<'transaction, 'reference, Table> { + CursorBuilder::new(self) } /// Whether the transaction is aborted. @@ -655,6 +644,21 @@ impl DbTable<'_, Table> { send_event_recv_response(&self.event_tx, event, result_rx).await } + /// Opens a cursor by the specified `index`. + /// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/openCursor + async fn open_cursor(&self, index: &str, filters: CursorFilters) -> CursorResult { + let (result_tx, result_rx) = oneshot::channel(); + let event = internal::DbTableEvent::OpenCursor { + index: index.to_owned(), + filters, + result_tx, + }; + let cursor_event_tx = send_event_recv_response(&self.event_tx, event, result_rx) + .await + .mm_err(|e| CursorError::UnexpectedState(e.to_string()))?; + Ok(cursor_event_tx) + } + fn deserialize_items(items: Vec<(ItemId, Json)>) -> DbTransactionResult> { items .into_iter() @@ -726,8 +730,12 @@ async fn table_event_loop(mut rx: mpsc::UnboundedReceiver { result_tx.send(Ok(table.aborted())).ok(); }, - internal::DbTableEvent::OpenCursor { index, result_tx } => { - open_cursor(&table, index, result_tx); + internal::DbTableEvent::OpenCursor { + index, + filters, + result_tx, + } => { + open_cursor(&table, index, filters, result_tx); }, } } @@ -761,18 +769,30 @@ impl MultiIndex { fn open_cursor( table: &IdbObjectStoreImpl, index: String, - result_tx: oneshot::Sender>, + filters: CursorFilters, + result_tx: oneshot::Sender>, ) { - let cursor_builder = match table.cursor_builder(&index) { - Ok(builder) => builder, + let db_index = match table.open_index(&index) { + Ok(db_index) => db_index, + Err(tr_err) => { + let cursor_err = tr_err.map(|tr_err| CursorError::ErrorOpeningCursor { + description: tr_err.to_string(), + }); + result_tx.send(Err(cursor_err)).ok(); + return; + }, + }; + let cursor = match CursorDriver::init_cursor(db_index, filters) { + Ok(cursor) => cursor, Err(e) => { result_tx.send(Err(e)).ok(); return; }, }; + let (event_tx, event_rx) = mpsc::unbounded(); - let fut = async move { cursor_event_loop(event_rx, cursor_builder).await }; + let fut = async move { cursor_event_loop(event_rx, cursor).await }; // `cursor_event_loop` will finish almost immediately once `event_tx` is dropped. spawn_local(fut); @@ -843,7 +863,8 @@ mod internal { }, OpenCursor { index: String, - result_tx: oneshot::Sender>, + filters: CursorFilters, + result_tx: oneshot::Sender>, }, } } @@ -1271,9 +1292,10 @@ mod tests { version: u32, expected_old_new_versions: Option<(u32, u32)>, ) -> Result<(), String> { - let mut versions = LAST_VERSIONS.lock().expect("!LAST_VERSIONS.lock()"); - *versions = None; - drop(versions); + { + let mut versions = LAST_VERSIONS.lock().expect("!LAST_VERSIONS.lock()"); + *versions = None; + } let _db = IndexedDbBuilder::new(db_identifier) .with_version(version) diff --git a/mm2src/mm2_err_handle/src/mm_error.rs b/mm2src/mm2_err_handle/src/mm_error.rs index d30075351e..e044ac6a6c 100644 --- a/mm2src/mm2_err_handle/src/mm_error.rs +++ b/mm2src/mm2_err_handle/src/mm_error.rs @@ -359,7 +359,7 @@ mod tests { const FORWARDED_LINE: u32 = line!() + 2; fn forward_error(actual: u64, required: u64) -> Result<(), MmError> { - let _ = generate_error(actual, required)?; + generate_error(actual, required)?; unreachable!("'generate_error' must return an error") } @@ -456,7 +456,7 @@ mod tests { const FORWARDED_LINE: u32 = line!() + 2; fn forward_error_for_box(actual: u64, required: u64) -> Result<(), MmError> { - let _ = generate_error_for_box(actual, required)?; + generate_error_for_box(actual, required)?; unreachable!("'generate_error' must return an error") } diff --git a/mm2src/mm2_eth/src/eip712_encode.rs b/mm2src/mm2_eth/src/eip712_encode.rs index 1d854dcb41..dbda992b83 100644 --- a/mm2src/mm2_eth/src/eip712_encode.rs +++ b/mm2src/mm2_eth/src/eip712_encode.rs @@ -346,7 +346,7 @@ mod tests { #[test] fn test_hash_data() { - const JSON: &'static str = r#"{ + const JSON: &str = r#"{ "primaryType": "Mail", "domain": { "name": "Ether Mail", diff --git a/mm2src/mm2_git/Cargo.toml b/mm2src/mm2_git/Cargo.toml new file mode 100644 index 0000000000..ee06101400 --- /dev/null +++ b/mm2src/mm2_git/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "mm2_git" +version = "0.1.0" +edition = "2021" + +[lib] +doctest = false + +[dependencies] +async-trait = "0.1" +common = { path = "../common" } +http = "0.2" +mm2_err_handle = { path = "../mm2_err_handle" } +mm2_net = { path = "../mm2_net" } +serde = "1" +serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_git/src/github_client.rs b/mm2src/mm2_git/src/github_client.rs new file mode 100644 index 0000000000..5686e78814 --- /dev/null +++ b/mm2src/mm2_git/src/github_client.rs @@ -0,0 +1,150 @@ +use async_trait::async_trait; +use mm2_err_handle::prelude::MmError; +use mm2_net::transport::slurp_url_with_headers; +use serde::de::DeserializeOwned; + +use crate::{FileMetadata, GitCommons, GitControllerError, RepositoryOperations}; + +const GITHUB_CLIENT_USER_AGENT: &str = "mm2"; + +pub struct GithubClient { + api_address: String, +} + +impl GitCommons for GithubClient { + fn new(api_address: String) -> Self { Self { api_address } } +} + +#[async_trait] +impl RepositoryOperations for GithubClient { + async fn deserialize_json_source( + &self, + file_metadata: FileMetadata, + ) -> Result> { + let (_status_code, _headers, data_buffer) = slurp_url_with_headers(&file_metadata.download_url, vec![( + http::header::USER_AGENT.as_str(), + GITHUB_CLIENT_USER_AGENT, + )]) + .await + .map_err(|e| GitControllerError::HttpError(e.to_string()))?; + + #[cfg(test)] + #[allow(unused_must_use)] + { + dbg!(serde_json::from_slice::(&data_buffer)); + } + + Ok( + serde_json::from_slice(&data_buffer) + .map_err(|e| GitControllerError::DeserializationError(e.to_string()))?, + ) + } + + async fn get_file_metadata_list( + &self, + owner: &str, + repository_name: &str, + branch: &str, + dir: &str, + ) -> Result, MmError> { + let uri = format!( + "{}/repos/{}/{}/contents/{}?ref={}", + &self.api_address, owner, repository_name, dir, branch + ); + + let (_status_code, _headers, data_buffer) = slurp_url_with_headers(&uri, vec![( + http::header::USER_AGENT.as_str(), + GITHUB_CLIENT_USER_AGENT, + )]) + .await + .map_err(|e| GitControllerError::HttpError(e.to_string()))?; + + #[cfg(test)] + #[allow(unused_must_use)] + { + dbg!(serde_json::from_slice::(&data_buffer)); + } + + Ok( + serde_json::from_slice(&data_buffer) + .map_err(|e| GitControllerError::DeserializationError(e.to_string()))?, + ) + } +} + +#[cfg(test)] +#[allow(unused)] +mod tests { + use crate::{GitController, GITHUB_API_URI}; + + use super::*; + use serde::Deserialize; + + #[derive(Debug, Deserialize)] + struct ChainRegistry { + chain_1: ChainInfo, + chain_2: ChainInfo, + channels: Vec, + } + + #[derive(Debug, Deserialize)] + struct IbcChannel { + chain_1: ChannelInfo, + chain_2: ChannelInfo, + ordering: String, + version: String, + tags: Option, + } + + #[derive(Debug, Deserialize)] + struct ChainInfo { + chain_name: String, + client_id: String, + connection_id: String, + } + + #[derive(Debug, Deserialize)] + struct ChannelInfo { + channel_id: String, + port_id: String, + } + + #[derive(Debug, Deserialize)] + struct ChannelTag { + status: String, + preferred: bool, + dex: Option, + } + + #[test] + fn test_metadata_list_and_json_deserialization() { + // If we are using shared CI runners, + // this test may fail due to rate limiting. + if std::env::var("FROM_SHARED_RUNNER").is_ok() { + return; + } + + const REPO_OWNER: &str = "KomodoPlatform"; + const REPO_NAME: &str = "chain-registry"; + const BRANCH: &str = "master"; + const DIR_NAME: &str = "_IBC"; + + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = common::block_on( + git_controller + .client + .get_file_metadata_list(REPO_OWNER, REPO_NAME, BRANCH, DIR_NAME), + ) + .unwrap(); + + assert!(!metadata_list.is_empty()); + + common::block_on( + git_controller + .client + .deserialize_json_source::(metadata_list.first().unwrap().clone()), + ) + .unwrap(); + } +} diff --git a/mm2src/mm2_git/src/lib.rs b/mm2src/mm2_git/src/lib.rs new file mode 100644 index 0000000000..15bc8408f1 --- /dev/null +++ b/mm2src/mm2_git/src/lib.rs @@ -0,0 +1,59 @@ +//! This crate provides an abstraction layer on Git for doing query/parse +//! operations over the repositories. +//! +//! Implementation of generic `GitController` provides the flexibility of +//! adding any Git clients(like Gitlab, Bitbucket, etc) when needed. + +use async_trait::async_trait; +use mm2_err_handle::prelude::MmError; +use serde::{de::DeserializeOwned, Deserialize}; + +pub mod github_client; +pub use github_client::*; + +pub const GITHUB_API_URI: &str = "https://api.github.com"; + +#[derive(Clone, Debug, Deserialize)] +pub struct FileMetadata { + pub name: String, + pub download_url: String, + pub size: usize, +} + +pub trait GitCommons { + fn new(api_address: String) -> Self; +} + +#[async_trait] +pub trait RepositoryOperations { + async fn deserialize_json_source( + &self, + file_metadata: FileMetadata, + ) -> Result>; + + async fn get_file_metadata_list( + &self, + owner: &str, + repository_name: &str, + branch: &str, + dir: &str, + ) -> Result, MmError>; +} + +pub struct GitController { + pub client: T, +} + +impl GitController { + pub fn new(api_address: &str) -> Self { + Self { + client: T::new(api_address.to_owned()), + } + } +} + +#[derive(Debug)] +pub enum GitControllerError { + DeserializationError(String), + HttpError(String), +} diff --git a/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs b/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs index 8f2d97e9f0..8392294c78 100644 --- a/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs +++ b/mm2src/mm2_gui_storage/src/account/storage/wasm_storage.rs @@ -152,7 +152,7 @@ impl WasmAccountStorage { db_transaction: &DbTransaction<'_>, enabled_account_id: EnabledAccountId, ) -> AccountStorageResult<()> { - match Self::load_enabled_account_id(&db_transaction).await? { + match Self::load_enabled_account_id(db_transaction).await? { // If there is an enabled account **and** its ID is the same as `enabled_account_id`. Some(actual_enabled) if actual_enabled == enabled_account_id => (), _ => return Ok(()), @@ -390,17 +390,15 @@ impl TableSignature for AccountTable { fn table_name() -> &'static str { "gui_account" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_multi_index( - AccountTable::ACCOUNT_ID_INDEX, - &["account_type", "account_idx", "device_pubkey"], - true, - )?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index( + AccountTable::ACCOUNT_ID_INDEX, + &["account_type", "account_idx", "device_pubkey"], + true, + )?; } + Ok(()) } } @@ -476,12 +474,15 @@ impl TableSignature for EnabledAccountTable { fn table_name() -> &'static str { "gui_enabled_account" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let _table = upgrader.create_table(Self::table_name())?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_multi_index( + AccountTable::ACCOUNT_ID_INDEX, + &["account_type", "account_idx", "device_pubkey"], + true, + )?; } + Ok(()) } } diff --git a/mm2src/mm2_io/Cargo.toml b/mm2src/mm2_io/Cargo.toml index 67589d050f..925e0944b0 100644 --- a/mm2src/mm2_io/Cargo.toml +++ b/mm2src/mm2_io/Cargo.toml @@ -20,4 +20,4 @@ async-std = { version = "1.5", features = ["unstable"] } gstuff = { version = "0.7", features = ["nightly"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gstuff = { version = "0.7", features = ["crossterm", "nightly"] } +gstuff = { version = "0.7", features = ["nightly"] } diff --git a/mm2src/mm2_io/src/file_lock.rs b/mm2src/mm2_io/src/file_lock.rs index d529d5987f..24f064ea4d 100644 --- a/mm2src/mm2_io/src/file_lock.rs +++ b/mm2src/mm2_io/src/file_lock.rs @@ -127,7 +127,7 @@ mod file_lock_tests { fn test_file_lock_should_acquire_if_file_is_empty() { let now = now_ms() / 1000; let path = Path::new("test4.lock"); - std::fs::write(&path, &[]).unwrap(); + std::fs::write(path, []).unwrap(); let _new_lock = FileLock::lock(&path, 1.).unwrap().unwrap(); let timestamp = read_timestamp(&path).unwrap().unwrap(); assert!(timestamp >= now); @@ -137,7 +137,7 @@ mod file_lock_tests { fn test_file_lock_should_acquire_if_file_does_not_contain_parsable_timestamp() { let now = now_ms() / 1000; let path = Path::new("test5.lock"); - std::fs::write(&path, &[12, 13]).unwrap(); + std::fs::write(path, [12, 13]).unwrap(); let _new_lock = FileLock::lock(&path, 1.).unwrap().unwrap(); let timestamp = read_timestamp(&path).unwrap().unwrap(); assert!(timestamp >= now); diff --git a/mm2src/mm2_libp2p/src/atomicdex_behaviour/tests.rs b/mm2src/mm2_libp2p/src/atomicdex_behaviour/tests.rs index ea4bf4e289..166a950bb2 100644 --- a/mm2src/mm2_libp2p/src/atomicdex_behaviour/tests.rs +++ b/mm2src/mm2_libp2p/src/atomicdex_behaviour/tests.rs @@ -5,7 +5,8 @@ use futures::channel::{mpsc, oneshot}; use futures::{SinkExt, StreamExt}; use libp2p::PeerId; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +#[cfg(not(windows))] use std::sync::Mutex; use std::time::Duration; static TEST_LISTEN_PORT: AtomicU64 = AtomicU64::new(1); @@ -127,6 +128,7 @@ async fn test_request_response_ok() { } #[tokio::test] +#[cfg(not(windows))] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 async fn test_request_response_ok_three_peers() { let _ = env_logger::try_init(); @@ -265,6 +267,7 @@ async fn test_request_response_none() { } #[tokio::test] +#[cfg(target_os = "linux")] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 async fn test_request_peers_ok_three_peers() { let _ = env_logger::try_init(); diff --git a/mm2src/mm2_libp2p/src/relay_address.rs b/mm2src/mm2_libp2p/src/relay_address.rs index c1e6b6225a..d23c419632 100644 --- a/mm2src/mm2_libp2p/src/relay_address.rs +++ b/mm2src/mm2_libp2p/src/relay_address.rs @@ -148,7 +148,7 @@ fn test_relay_address_from_str() { ("/memory/71428421981", RelayAddress::Memory(71428421981)), ]; for (s, expected) in valid_addresses { - let actual = RelayAddress::from_str(s).expect(&format!("Error parsing '{}'", s)); + let actual = RelayAddress::from_str(s).unwrap_or_else(|_| panic!("Error parsing '{}'", s)); assert_eq!(actual, expected); } diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 5989c999c7..443055e4d1 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -16,10 +16,11 @@ custom-swap-locktime = [] # only for testing purposes, should never be activated native = [] # Deprecated track-ctx-pointer = ["common/track-ctx-pointer"] zhtlc-native-tests = ["coins/zhtlc-native-tests"] +enable-nft-integration = ["coins/enable-nft-integration"] +run-docker-tests = [] # TODO -# Remove this once the solana integration becomes stable/completed. -disable-solana-tests = [] -default = ["disable-solana-tests"] +enable-solana = [] +default = [] [dependencies] async-std = { version = "1.5", features = ["unstable"] } @@ -33,7 +34,7 @@ coins = { path = "../coins" } coins_activation = { path = "../coins_activation" } common = { path = "../common" } crc32fast = { version = "1.3.2", features = ["std", "nightly"] } -crossbeam = "0.7" +crossbeam = "0.8" crypto = { path = "../crypto" } db_common = { path = "../db_common" } derive_more = "0.99" @@ -41,6 +42,7 @@ either = "1.6" ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } enum_from = { path = "../derives/enum_from" } enum-primitive-derive = "0.2" +env_logger = "0.7.1" futures01 = { version = "0.1", package = "futures" } futures = { version = "0.3.1", package = "futures", features = ["compat", "async-await"] } gstuff = { version = "0.7", features = ["nightly"] } @@ -89,7 +91,7 @@ sp-runtime-interface = { version = "6.0.0", default-features = false, features = sp-trie = { version = "6.0", default-features = false } trie-db = { version = "0.23.1", default-features = false } trie-root = "0.16.0" -uuid = { version = "0.7", features = ["serde", "v4"] } +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } wasm-timer = "0.2.4" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/mm2src/mm2_main/src/database.rs b/mm2src/mm2_main/src/database.rs index fcb6704b67..3a6f1f3add 100644 --- a/mm2src/mm2_main/src/database.rs +++ b/mm2src/mm2_main/src/database.rs @@ -97,6 +97,10 @@ fn migration_7() -> Vec<(&'static str, Vec)> { db_common::sqlite::execute_batch(stats_swaps::ADD_COINS_PRICE_INFOMATION) } +fn migration_8() -> Vec<(&'static str, Vec)> { + db_common::sqlite::execute_batch(stats_swaps::ADD_MAKER_TAKER_PUBKEYS) +} + async fn statements_for_migration(ctx: &MmArc, current_migration: i64) -> Option)>> { match current_migration { 1 => Some(migration_1(ctx).await), @@ -106,6 +110,7 @@ async fn statements_for_migration(ctx: &MmArc, current_migration: i64) -> Option 5 => Some(migration_5()), 6 => Some(migration_6()), 7 => Some(migration_7()), + 8 => Some(migration_8()), _ => None, } } diff --git a/mm2src/mm2_main/src/database/my_orders.rs b/mm2src/mm2_main/src/database/my_orders.rs index 9a2ed6a278..776dca8487 100644 --- a/mm2src/mm2_main/src/database/my_orders.rs +++ b/mm2src/mm2_main/src/database/my_orders.rs @@ -8,6 +8,7 @@ use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Result as SqlRe use db_common::sqlite::sql_builder::SqlBuilder; use mm2_core::mm_ctx::MmArc; use std::convert::TryInto; +use std::string::ParseError; use uuid::Uuid; const MY_ORDERS_TABLE: &str = "my_orders"; @@ -179,7 +180,7 @@ fn apply_my_orders_filter(builder: &mut SqlBuilder, params: &mut Vec<(&str, Stri #[derive(Debug)] pub enum SelectRecentOrdersUuidsErr { Sql(SqlError), - Parse(uuid::parser::ParseError), + Parse(ParseError), } impl std::fmt::Display for SelectRecentOrdersUuidsErr { @@ -190,8 +191,8 @@ impl From for SelectRecentOrdersUuidsErr { fn from(err: SqlError) -> Self { SelectRecentOrdersUuidsErr::Sql(err) } } -impl From for SelectRecentOrdersUuidsErr { - fn from(err: uuid::parser::ParseError) -> Self { SelectRecentOrdersUuidsErr::Parse(err) } +impl From for SelectRecentOrdersUuidsErr { + fn from(err: ParseError) -> Self { SelectRecentOrdersUuidsErr::Parse(err) } } pub fn select_orders_by_filter( diff --git a/mm2src/mm2_main/src/database/my_swaps.rs b/mm2src/mm2_main/src/database/my_swaps.rs index e8d4f6b9bb..835de9f240 100644 --- a/mm2src/mm2_main/src/database/my_swaps.rs +++ b/mm2src/mm2_main/src/database/my_swaps.rs @@ -7,6 +7,7 @@ use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Result as SqlRe use db_common::sqlite::sql_builder::SqlBuilder; use mm2_core::mm_ctx::MmArc; use std::convert::TryInto; +use uuid::Error as UuidError; const MY_SWAPS_TABLE: &str = "my_swaps"; @@ -57,7 +58,7 @@ fn insert_saved_swap_sql(swap: SavedSwap) -> Option<(&'static str, Vec)> #[derive(Debug)] pub enum SelectRecentSwapsUuidsErr { Sql(SqlError), - Parse(uuid::parser::ParseError), + Parse(UuidError), } impl std::fmt::Display for SelectRecentSwapsUuidsErr { @@ -68,8 +69,8 @@ impl From for SelectRecentSwapsUuidsErr { fn from(err: SqlError) -> Self { SelectRecentSwapsUuidsErr::Sql(err) } } -impl From for SelectRecentSwapsUuidsErr { - fn from(err: uuid::parser::ParseError) -> Self { SelectRecentSwapsUuidsErr::Parse(err) } +impl From for SelectRecentSwapsUuidsErr { + fn from(err: UuidError) -> Self { SelectRecentSwapsUuidsErr::Parse(err) } } /// Adds where clauses determined by MySwapsFilter diff --git a/mm2src/mm2_main/src/database/stats_swaps.rs b/mm2src/mm2_main/src/database/stats_swaps.rs index e8b75be391..9ee2599aba 100644 --- a/mm2src/mm2_main/src/database/stats_swaps.rs +++ b/mm2src/mm2_main/src/database/stats_swaps.rs @@ -43,14 +43,23 @@ const INSERT_STATS_SWAP: &str = "INSERT INTO stats_swaps ( taker_amount, is_success, maker_coin_usd_price, - taker_coin_usd_price -) VALUES (:maker_coin, :maker_coin_ticker, :maker_coin_platform, :taker_coin, :taker_coin_ticker, :taker_coin_platform, :uuid, :started_at, :finished_at, :maker_amount, :taker_amount, :is_success, :maker_coin_usd_price, :taker_coin_usd_price)"; + taker_coin_usd_price, + maker_pubkey, + taker_pubkey +) VALUES (:maker_coin, :maker_coin_ticker, :maker_coin_platform, :taker_coin, :taker_coin_ticker, +:taker_coin_platform, :uuid, :started_at, :finished_at, :maker_amount, :taker_amount, :is_success, +:maker_coin_usd_price, :taker_coin_usd_price, :maker_pubkey, :taker_pubkey)"; pub const ADD_COINS_PRICE_INFOMATION: &[&str] = &[ "ALTER TABLE stats_swaps ADD COLUMN maker_coin_usd_price DECIMAL;", "ALTER TABLE stats_swaps ADD COLUMN taker_coin_usd_price DECIMAL;", ]; +pub const ADD_MAKER_TAKER_PUBKEYS: &[&str] = &[ + "ALTER TABLE stats_swaps ADD COLUMN maker_pubkey VARCHAR(255);", + "ALTER TABLE stats_swaps ADD COLUMN taker_pubkey VARCHAR(255);", +]; + pub const ADD_SPLIT_TICKERS: &[&str] = &[ "ALTER TABLE stats_swaps ADD COLUMN maker_coin_ticker VARCHAR(255) NOT NULL DEFAULT '';", "ALTER TABLE stats_swaps ADD COLUMN maker_coin_platform VARCHAR(255) NOT NULL DEFAULT '';", @@ -118,6 +127,7 @@ fn insert_stats_maker_swap_sql(swap: &MakerSavedSwap) -> Option<(&'static str, O return None; }, }; + let finished_at = match swap.finished_at() { Ok(t) => t.to_string(), Err(e) => { @@ -125,6 +135,15 @@ fn insert_stats_maker_swap_sql(swap: &MakerSavedSwap) -> Option<(&'static str, O return None; }, }; + + let pubkeys = match swap.swap_pubkeys() { + Ok(p) => p, + Err(e) => { + error!("Error {} on getting swap {} pubkeys", e, swap.uuid); + return None; + }, + }; + let is_success = swap .is_success() .expect("is_success can return error only when swap is not finished"); @@ -147,7 +166,10 @@ fn insert_stats_maker_swap_sql(swap: &MakerSavedSwap) -> Option<(&'static str, O ":is_success": (is_success as u32).to_string(), ":maker_coin_usd_price": swap.maker_coin_usd_price.as_ref().map(|p| p.to_string()), ":taker_coin_usd_price": swap.taker_coin_usd_price.as_ref().map(|p| p.to_string()), + ":maker_pubkey": pubkeys.maker, + ":taker_pubkey": pubkeys.taker, }; + Some((INSERT_STATS_SWAP, params)) } @@ -198,6 +220,15 @@ fn insert_stats_taker_swap_sql(swap: &TakerSavedSwap) -> Option<(&'static str, O return None; }, }; + + let pubkeys = match swap.swap_pubkeys() { + Ok(p) => p, + Err(e) => { + error!("Error {} on getting swap {} pubkeys", e, swap.uuid); + return None; + }, + }; + let is_success = swap .is_success() .expect("is_success can return error only when swap is not finished"); @@ -220,6 +251,8 @@ fn insert_stats_taker_swap_sql(swap: &TakerSavedSwap) -> Option<(&'static str, O ":is_success": (is_success as u32).to_string(), ":maker_coin_usd_price": swap.maker_coin_usd_price.as_ref().map(|p| p.to_string()), ":taker_coin_usd_price": swap.taker_coin_usd_price.as_ref().map(|p| p.to_string()), + ":maker_pubkey": pubkeys.maker, + ":taker_pubkey": pubkeys.taker, }; Some((INSERT_STATS_SWAP, params)) } diff --git a/mm2src/mm2_main/src/lib.rs b/mm2src/mm2_main/src/lib.rs index b72f558765..23c7b65312 100644 --- a/mm2src/mm2_main/src/lib.rs +++ b/mm2src/mm2_main/src/lib.rs @@ -1,4 +1,12 @@ #![feature(hash_raw_entry)] +// `mockable` implementation uses these +#![allow( + clippy::forget_ref, + clippy::forget_copy, + clippy::swap_ptr_to_ref, + clippy::forget_non_drop, + clippy::let_unit_value +)] #[macro_use] extern crate common; #[macro_use] extern crate gstuff; diff --git a/mm2src/mm2_main/src/lp_message_service.rs b/mm2src/mm2_main/src/lp_message_service.rs index 98a7b03a54..018d6bf971 100644 --- a/mm2src/mm2_main/src/lp_message_service.rs +++ b/mm2src/mm2_main/src/lp_message_service.rs @@ -150,7 +150,7 @@ mod message_service_tests { "RustTestChatId", true, )); - assert!(!res.is_err()); + assert!(res.is_ok()); assert!(res.unwrap()); } } diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 7fa4cb69d0..5fc9d248db 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -1425,6 +1425,16 @@ impl<'a> TakerOrderBuilder<'a> { None }; + let base_protocol_info = match &self.action { + TakerAction::Buy => self.base_coin.coin_protocol_info(Some(self.base_amount.clone())), + TakerAction::Sell => self.base_coin.coin_protocol_info(None), + }; + + let rel_protocol_info = match &self.action { + TakerAction::Buy => self.rel_coin.coin_protocol_info(None), + TakerAction::Sell => self.rel_coin.coin_protocol_info(Some(self.rel_amount.clone())), + }; + Ok(TakerOrder { created_at: now_ms(), request: TakerRequest { @@ -1438,8 +1448,8 @@ impl<'a> TakerOrderBuilder<'a> { dest_pub_key: Default::default(), match_by: self.match_by, conf_settings: self.conf_settings, - base_protocol_info: Some(self.base_coin.coin_protocol_info()), - rel_protocol_info: Some(self.rel_coin.coin_protocol_info()), + base_protocol_info: Some(base_protocol_info), + rel_protocol_info: Some(rel_protocol_info), }, matches: Default::default(), min_volume, @@ -1455,6 +1465,16 @@ impl<'a> TakerOrderBuilder<'a> { #[cfg(test)] /// skip validation for tests fn build_unchecked(self) -> TakerOrder { + let base_protocol_info = match &self.action { + TakerAction::Buy => self.base_coin.coin_protocol_info(Some(self.base_amount.clone())), + TakerAction::Sell => self.base_coin.coin_protocol_info(None), + }; + + let rel_protocol_info = match &self.action { + TakerAction::Buy => self.rel_coin.coin_protocol_info(None), + TakerAction::Sell => self.rel_coin.coin_protocol_info(Some(self.rel_amount.clone())), + }; + TakerOrder { created_at: now_ms(), request: TakerRequest { @@ -1468,8 +1488,8 @@ impl<'a> TakerOrderBuilder<'a> { dest_pub_key: Default::default(), match_by: self.match_by, conf_settings: self.conf_settings, - base_protocol_info: Some(self.base_coin.coin_protocol_info()), - rel_protocol_info: Some(self.rel_coin.coin_protocol_info()), + base_protocol_info: Some(base_protocol_info), + rel_protocol_info: Some(rel_protocol_info), }, matches: HashMap::new(), min_volume: Default::default(), @@ -1915,6 +1935,7 @@ impl<'a> MakerOrderBuilder<'a> { #[cfg(test)] fn build_unchecked(self) -> MakerOrder { let created_at = now_ms(); + #[allow(clippy::or_fun_call)] MakerOrder { base: self.base_coin.ticker().to_owned(), rel: self.rel_coin.ticker().to_owned(), @@ -2751,7 +2772,7 @@ impl OrdermatchContext { #[cfg(target_arch = "wasm32")] pub async fn ordermatch_db(&self) -> InitDbResult> { - Ok(self.ordermatch_db.get_or_initialize().await?) + self.ordermatch_db.get_or_initialize().await } } @@ -2871,8 +2892,12 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO // detect atomic lock time version implicitly by conf_settings existence in taker request let atomic_locktime_v = match maker_match.request.conf_settings { Some(_) => { - let other_conf_settings = - choose_taker_confs_and_notas(&maker_match.request, &maker_match.reserved, &maker_coin, &taker_coin); + let other_conf_settings = choose_taker_confs_and_notas( + &maker_match.request, + &maker_match.reserved.conf_settings, + &maker_coin, + &taker_coin, + ); AtomicLocktimeVersion::V2 { my_conf_settings, other_conf_settings, @@ -2961,8 +2986,12 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat let maker_amount = taker_match.reserved.get_base_amount().clone(); let taker_amount = taker_match.reserved.get_rel_amount().clone(); - let my_conf_settings = - choose_taker_confs_and_notas(&taker_order.request, &taker_match.reserved, &maker_coin, &taker_coin); + let my_conf_settings = choose_taker_confs_and_notas( + &taker_order.request, + &taker_match.reserved.conf_settings, + &maker_coin, + &taker_coin, + ); // detect atomic lock time version implicitly by conf_settings existence in maker reserved let atomic_locktime_v = match taker_match.reserved.conf_settings { Some(_) => { @@ -3118,8 +3147,8 @@ pub async fn lp_ordermatch_loop(ctx: MmArc) { maker_order_created_p2p_notify( ctx.clone(), &order, - base.coin_protocol_info(), - rel.coin_protocol_info(), + base.coin_protocol_info(None), + rel.coin_protocol_info(Some(order.max_base_vol.clone() * order.price.clone())), ); } } @@ -3216,8 +3245,8 @@ async fn handle_timed_out_taker_orders(ctx: MmArc, ordermatch_ctx: &OrdermatchCo maker_order_created_p2p_notify( ctx.clone(), &maker_order, - base.coin_protocol_info(), - rel.coin_protocol_info(), + base.coin_protocol_info(None), + rel.coin_protocol_info(Some(maker_order.max_base_vol.clone() * maker_order.price.clone())), ); } } @@ -3330,11 +3359,29 @@ async fn process_maker_reserved(ctx: MmArc, from_pubkey: H256Json, reserved_msg: reserved_messages.sort_unstable_by_key(|r| r.price()); for reserved_msg in reserved_messages { + let my_conf_settings = + choose_maker_confs_and_notas(reserved_msg.conf_settings, &my_order.request, &base_coin, &rel_coin); + let other_conf_settings = + choose_taker_confs_and_notas(&my_order.request, &reserved_msg.conf_settings, &base_coin, &rel_coin); + let atomic_locktime_v = AtomicLocktimeVersion::V2 { + my_conf_settings, + other_conf_settings, + }; + let lock_time = lp_atomic_locktime( + my_order.maker_orderbook_ticker(), + my_order.taker_orderbook_ticker(), + atomic_locktime_v, + ); // send "connect" message if reserved message targets our pubkey AND // reserved amounts match our order AND order is NOT reserved by someone else (empty matches) if (my_order.match_reserved(&reserved_msg) == MatchReservedResult::Matched && my_order.matches.is_empty()) - && base_coin.is_coin_protocol_supported(&reserved_msg.base_protocol_info) - && rel_coin.is_coin_protocol_supported(&reserved_msg.rel_protocol_info) + && base_coin.is_coin_protocol_supported(&reserved_msg.base_protocol_info, None, lock_time, false) + && rel_coin.is_coin_protocol_supported( + &reserved_msg.rel_protocol_info, + Some(reserved_msg.rel_amount.clone()), + lock_time, + false, + ) { let connect = TakerConnect { sender_pubkey: H256Json::from(our_public_id.bytes), @@ -3438,9 +3485,35 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: _ => return, // attempt to match with deactivated coin }; + let my_conf_settings = + choose_maker_confs_and_notas(order.conf_settings, &taker_request, &base_coin, &rel_coin); + let other_conf_settings = + choose_taker_confs_and_notas(&taker_request, &order.conf_settings, &base_coin, &rel_coin); + let atomic_locktime_v = AtomicLocktimeVersion::V2 { + my_conf_settings, + other_conf_settings, + }; + let maker_lock_duration = (lp_atomic_locktime( + order.base_orderbook_ticker(), + order.rel_orderbook_ticker(), + atomic_locktime_v, + ) as f64 + * rel_coin.maker_locktime_multiplier()) + .ceil() as u64; + if !order.matches.contains_key(&taker_request.uuid) - && base_coin.is_coin_protocol_supported(taker_request.base_protocol_info_for_maker()) - && rel_coin.is_coin_protocol_supported(taker_request.rel_protocol_info_for_maker()) + && base_coin.is_coin_protocol_supported( + taker_request.base_protocol_info_for_maker(), + Some(base_amount.clone()), + maker_lock_duration, + true, + ) + && rel_coin.is_coin_protocol_supported( + taker_request.rel_protocol_info_for_maker(), + None, + maker_lock_duration, + true, + ) { let reserved = MakerReserved { dest_pub_key: taker_request.sender_pubkey, @@ -3459,8 +3532,8 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: rel_nota: rel_coin.requires_notarization(), }) }), - base_protocol_info: Some(base_coin.coin_protocol_info()), - rel_protocol_info: Some(rel_coin.coin_protocol_info()), + base_protocol_info: Some(base_coin.coin_protocol_info(None)), + rel_protocol_info: Some(rel_coin.coin_protocol_info(Some(rel_amount.clone()))), }; let topic = order.orderbook_topic(); log::debug!("Request matched sending reserved {:?}", reserved); @@ -4494,9 +4567,9 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result Result Result }; let min_base_amount = base_coin.min_trading_vol(); + // Todo: Here min_trading_vol for lightning depends on inbound liquidity not outbound, will require to split min_trading_vol to two functions let min_rel_amount = rel_coin.min_trading_vol(); // Add min_volume to update_msg if min_volume is found in the request @@ -5575,7 +5649,7 @@ fn choose_maker_confs_and_notas( fn choose_taker_confs_and_notas( taker_req: &TakerRequest, - maker_reserved: &MakerReserved, + maker_conf_settings: &Option, maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum, ) -> SwapConfirmationsSettings { @@ -5599,7 +5673,7 @@ fn choose_taker_confs_and_notas( ), }, }; - if let Some(settings_from_maker) = maker_reserved.conf_settings { + if let Some(settings_from_maker) = maker_conf_settings { if settings_from_maker.rel_confs < taker_coin_confs { taker_coin_confs = settings_from_maker.rel_confs; } @@ -5625,6 +5699,7 @@ pub enum OrderbookAddress { #[derive(Debug, Display)] enum OrderbookAddrErr { AddrFromPubkeyError(String), + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] CoinIsNotSupported(String), DeserializationError(json::Error), InvalidPlatformCoinProtocol(String), @@ -5696,7 +5771,7 @@ fn orderbook_address( ))), } }, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] CoinProtocol::SOLANA | CoinProtocol::SPLTOKEN { .. } => { MmError::err(OrderbookAddrErr::CoinIsNotSupported(coin.to_owned())) }, diff --git a/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs b/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs index 0e841a214e..965fb743c2 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/best_orders.rs @@ -392,6 +392,7 @@ mod best_orders_test { use super::*; use crate::mm2::lp_ordermatch::ordermatch_tests::make_random_orders; use crate::mm2::lp_ordermatch::{OrderbookItem, TrieProof}; + use common::new_uuid; use std::iter::FromIterator; #[test] @@ -456,12 +457,11 @@ mod best_orders_test { let v1_serialized = rmp_serde::to_vec(&v1).unwrap(); let mut new: BestOrdersP2PRes = rmp_serde::from_read_ref(&v1_serialized).unwrap(); - new.protocol_infos.insert(Uuid::new_v4(), BaseRelProtocolInfo { + new.protocol_infos.insert(new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], }); - new.conf_infos - .insert(Uuid::new_v4(), OrderConfirmationsSettings::default()); + new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings::default()); let new_serialized = rmp_serde::to_vec(&new).unwrap(); @@ -485,7 +485,7 @@ mod best_orders_test { let v2 = BestOrdersResV2 { orders: HashMap::from_iter(std::iter::once(("RICK".into(), v2_orders))), - protocol_infos: HashMap::from_iter(std::iter::once((Uuid::new_v4(), BaseRelProtocolInfo { + protocol_infos: HashMap::from_iter(std::iter::once((new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], }))), @@ -494,8 +494,7 @@ mod best_orders_test { let v2_serialized = rmp_serde::to_vec(&v2).unwrap(); let mut new: BestOrdersP2PRes = rmp_serde::from_read_ref(&v2_serialized).unwrap(); - new.conf_infos - .insert(Uuid::new_v4(), OrderConfirmationsSettings::default()); + new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings::default()); let new_serialized = rmp_serde::to_vec(&new).unwrap(); diff --git a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs index 3fa0c7970a..1fbbba5ca6 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs @@ -755,7 +755,7 @@ mod tests { } async fn get_all_items(ctx: &MmArc) -> Vec
{ - let ordermatch_ctx = OrdermatchContext::from_ctx(&ctx).unwrap(); + let ordermatch_ctx = OrdermatchContext::from_ctx(ctx).unwrap(); let db = ordermatch_ctx.ordermatch_db().await.unwrap(); let transaction = db.transaction().await.unwrap(); let table = transaction.table::
().await.unwrap(); diff --git a/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs b/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs index 0801e0a534..4489ba0224 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/new_protocol.rs @@ -78,7 +78,7 @@ mod compact_uuid { } impl FromStr for CompactUuid { - type Err = uuid::parser::ParseError; + type Err = uuid::Error; fn from_str(str: &str) -> Result { let uuid = Uuid::parse_str(str)?; @@ -399,7 +399,7 @@ mod new_protocol_tests { } let old_msg = MakerOrderCreatedV1 { - uuid: Uuid::new_v4().into(), + uuid: new_uuid().into(), base: "RICK".to_string(), rel: "MORTY".to_string(), price: BigRational::from_integer(1.into()), diff --git a/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs b/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs index 264a696879..812a802999 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/ordermatch_wasm_db.rs @@ -105,13 +105,10 @@ pub mod tables { fn table_name() -> &'static str { "my_filtering_history_orders" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_index("uuid", true)?; - // TODO add other indexes during [`MyOrdersStorage::select_orders_by_filter`] implementation. - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_index("uuid", true)?; + // TODO add other indexes during [`MyOrdersStorage::select_orders_by_filter`] implementation. } Ok(()) } @@ -124,12 +121,9 @@ pub mod tables { new_version: u32, table_name: &'static str, ) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(table_name)?; - table.create_index("uuid", true)?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(table_name)?; + table.create_index("uuid", true)?; } Ok(()) } diff --git a/mm2src/mm2_main/src/lp_price.rs b/mm2src/mm2_main/src/lp_price.rs index e35222c2b8..164270d120 100644 --- a/mm2src/mm2_main/src/lp_price.rs +++ b/mm2src/mm2_main/src/lp_price.rs @@ -332,11 +332,11 @@ mod tests { assert_eq!(rates.price, MmNumber::from("0.02")); let usdt_infos = registry.get_infos("USDT-PLG20"); - assert_eq!(usdt_infos.is_some(), true); + assert!(usdt_infos.is_some()); assert_eq!(usdt_infos.unwrap().last_price, MmNumber::from(1)); let usdt_infos = registry.get_infos("USDT"); - assert_eq!(usdt_infos.is_some(), true); + assert!(usdt_infos.is_some()); assert_eq!(usdt_infos.unwrap().last_price, MmNumber::from(1)); } } diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 711dbdb064..53d5c1a1e2 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -59,6 +59,7 @@ use crate::mm2::lp_network::{broadcast_p2p_msg, Libp2pPeerId}; use bitcrypto::{dhash160, sha256}; +use coins::eth::Web3RpcError; use coins::{lp_coinfind, lp_coinfind_or_err, CoinFindError, MmCoinEnum, TradeFee, TransactionEnum}; use common::log::{debug, warn}; use common::time_cache::DuplicateCache; @@ -67,6 +68,7 @@ use common::{bits256, calc_total_pages, log::{error, info}, now_ms, var, HttpStatusCode, PagingOptions, StatusCode}; use derive_more::Display; +use futures::compat::Future01CompatExt; use http::Response; use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; @@ -442,7 +444,7 @@ impl SwapsContext { } #[cfg(target_arch = "wasm32")] - pub async fn swap_db(&self) -> InitDbResult> { Ok(self.swap_db.get_or_initialize().await?) } + pub async fn swap_db(&self) -> InitDbResult> { self.swap_db.get_or_initialize().await } } #[derive(Debug, Deserialize)] @@ -682,6 +684,72 @@ pub fn dex_fee_amount_from_taker_coin(taker_coin: &MmCoinEnum, maker_coin: &str, dex_fee_amount(taker_coin.ticker(), maker_coin, trade_amount, &dex_fee_threshold) } +#[derive(Debug, Display)] +pub enum WatcherRewardError { + RPCError(Web3RpcError), + InvalidCoinType(String), +} + +// This needs discussion. We need a way to determine the watcher reward amount, and a way to validate it at watcher side so +// that watchers won't accept it if it's less than the expected amount. This has to be done for all coin types, because watcher rewards +// will be required by both parties even if only one side is ETH coin. Artem's suggestion was first calculating the reward for ETH and +// converting the value to other coins, which is what I'm planning to do. I based the values to be higher than the gas amounts used +// when the watcher spends the maker payment or refunds the taker payment. For the validation, I check if the reward is higher +// than a minimum amount using the min_watcher_reward method. I can't make an exact comparison because the gas price and relative +// price of the coins will be different when the taker/maker sends their payment and when the watcher receives the message. This should +// work fine if we pick the WATCHER_REWARD_GAS and MIN_WATCHER_REWARD_GAS constants good. The advantage of this is that the reward will +// be directly based on the amount of gas burned when the watcher will call the contract functions (Artem's idea was to make it slightly +// profitable for the watchers). The disadvantage is the comparison during the validations using a separate minimum value. If we want +// validations with exact values, there are two other ways I could think of: +// 1. Precalculating fixed rewards for all coins. The disadvantage is that the gas price and the relative price of coins will change over +// time and the reward will deviate from the actual gas used by the watchers, and we can't keep updating the values. +// 2. Picking the reward to be a percentage of the trade amount like the taker fee. The disadvantage is it will be extremely hard to +// pick the right ratio such that it will be slightly profitable for watchers. +pub async fn watcher_reward_amount( + coin: &MmCoinEnum, + other_coin: &MmCoinEnum, +) -> Result> { + const WATCHER_REWARD_GAS: u64 = 100_000; + watcher_reward_from_gas(coin, other_coin, WATCHER_REWARD_GAS).await +} + +pub async fn min_watcher_reward( + coin: &MmCoinEnum, + other_coin: &MmCoinEnum, +) -> Result> { + const MIN_WATCHER_REWARD_GAS: u64 = 70_000; + watcher_reward_from_gas(coin, other_coin, MIN_WATCHER_REWARD_GAS).await +} + +pub async fn watcher_reward_from_gas( + coin: &MmCoinEnum, + other_coin: &MmCoinEnum, + gas: u64, +) -> Result> { + match (coin, other_coin) { + (MmCoinEnum::EthCoin(coin), _) | (_, MmCoinEnum::EthCoin(coin)) => { + let mut attempts = 0; + loop { + match coin.get_gas_price().compat().await { + Ok(gas_price) => return Ok(gas * gas_price.as_u64()), + Err(err) => { + if attempts >= 3 { + return MmError::err(WatcherRewardError::RPCError(err.into_inner())); + } else { + attempts += 1; + Timer::sleep(10.).await; + } + }, + }; + } + }, + _ => Err(WatcherRewardError::InvalidCoinType( + "At least one coin must be ETH to use watcher reward".to_string(), + ) + .into()), + } +} + #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] pub struct NegotiationDataV1 { started_at: u64, @@ -1382,6 +1450,11 @@ fn detect_secret_hash_algo(maker_coin: &MmCoinEnum, taker_coin: &MmCoinEnum) -> } } +pub struct SwapPubkeys { + pub maker: String, + pub taker: String, +} + #[cfg(all(test, not(target_arch = "wasm32")))] mod lp_swap_tests { use super::*; @@ -1391,7 +1464,7 @@ mod lp_swap_tests { use coins::utxo::{UtxoActivationParams, UtxoRpcMode}; use coins::MarketCoinOps; use coins::PrivKeyActivationPolicy; - use common::block_on; + use common::{block_on, new_uuid}; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_test_helpers::for_tests::{morty_conf, rick_conf, MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS}; @@ -1753,7 +1826,7 @@ mod lp_swap_tests { UtxoActivationParams { mode: UtxoRpcMode::Electrum { servers: electrums - .into_iter() + .iter() .map(|url| ElectrumRpcRequest { url: url.to_string(), protocol: Default::default(), @@ -1863,7 +1936,7 @@ mod lp_swap_tests { println!("Taker address {}", rick_taker.my_address().unwrap()); - let uuid = Uuid::new_v4(); + let uuid = new_uuid(); let maker_amount = BigDecimal::from_str("0.1").unwrap(); let taker_amount = BigDecimal::from_str("0.1").unwrap(); let conf_settings = SwapConfirmationsSettings { diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 7f12d7ec3d..320a65ff97 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -8,17 +8,18 @@ use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_messa get_locked_amount, recv_swap_msg, swap_topic, taker_payment_spend_deadline, tx_helper_topic, wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, - SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, SwapError, SwapMsg, SwapTxDataMsg, SwapsContext, - TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; + SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, + SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; use crate::mm2::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::{MakerOrderBuilder, OrderConfirmationsSettings}; use crate::mm2::lp_price::fetch_swap_coins_price; -use crate::mm2::lp_swap::{broadcast_swap_message, taker_payment_spend_duration}; -use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, - PaymentInstructions, PaymentInstructionsErr, SearchForSwapTxSpendInput, SendMakerPaymentArgs, - SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, TradeFee, TradePreimageValue, - TransactionEnum, ValidateFeeArgs, ValidatePaymentInput}; +use crate::mm2::lp_swap::{broadcast_swap_message, min_watcher_reward, taker_payment_spend_duration, + watcher_reward_amount}; +use coins::{CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, + MmCoinEnum, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, SearchForSwapTxSpendInput, + SendPaymentArgs, SpendPaymentArgs, TradeFee, TradePreimageValue, TransactionEnum, ValidateFeeArgs, + ValidatePaymentInput}; use common::log::{debug, error, info, warn}; use common::{bits256, executor::Timer, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::SerializableSecp256k1Keypair; @@ -184,6 +185,7 @@ pub struct MakerSwapMut { taker_payment_spend_confirmed: bool, maker_payment_refund: Option, payment_instructions: Option, + watcher_reward: bool, } #[cfg(test)] @@ -387,6 +389,7 @@ impl MakerSwap { maker_payment_refund: None, taker_payment_spend_confirmed: false, payment_instructions: None, + watcher_reward: false, }), ctx, secret, @@ -562,6 +565,11 @@ impl MakerSwap { p2p_privkey: self.p2p_privkey.map(SerializableSecp256k1Keypair::from), }; + // This value will be true if both sides support & want to use watchers and either the taker or the maker coin is ETH. + // This requires a communication between the parties before the swap starts, which will be done during the ordermatch phase + // or via negotiation messages in the next sprint. + self.w().watcher_reward = false; + Ok((Some(MakerSwapCommand::Negotiate), vec![MakerSwapEvent::Started(data)])) } @@ -756,7 +764,7 @@ impl MakerSwap { { Ok(_) => break, Err(err) => { - if attempts >= 3 { + if attempts >= 6 { return Ok((Some(MakerSwapCommand::Finish), vec![ MakerSwapEvent::TakerFeeValidateFailed(ERRL!("{}", err).into()), ])); @@ -802,11 +810,27 @@ impl MakerSwap { }) .compat(); + let reward_amount = if self.r().watcher_reward { + let reward = match watcher_reward_amount(&self.maker_coin, &self.taker_coin).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed(err.into_inner().to_string().into()), + ])) + }, + }; + Some(reward) + } else { + None + }; + let transaction = match transaction_f.await { Ok(res) => match res { Some(tx) => tx, None => { - let payment_fut = self.maker_coin.send_maker_payment(SendMakerPaymentArgs { + let maker_payment_wait_confirm = + wait_for_maker_payment_conf_until(self.r().data.started_at, self.r().data.lock_duration); + let payment_fut = self.maker_coin.send_maker_payment(SendPaymentArgs { time_lock_duration: self.r().data.lock_duration, time_lock: self.r().data.maker_payment_lock as u32, other_pubkey: &*self.r().other_maker_coin_htlc_pub, @@ -815,6 +839,8 @@ impl MakerSwap { swap_contract_address: &self.r().data.maker_coin_swap_contract_address, swap_unique_data: &unique_data, payment_instructions: &self.r().payment_instructions, + watcher_reward: reward_amount, + wait_for_confirmation_until: maker_payment_wait_confirm, }); match payment_fut.compat().await { @@ -867,13 +893,15 @@ impl MakerSwap { let maker_payment_wait_confirm = wait_for_maker_payment_conf_until(self.r().data.started_at, self.r().data.lock_duration); - let f = self.maker_coin.wait_for_confirmations( - &self.r().maker_payment.clone().unwrap().tx_hex, - self.r().data.maker_payment_confirmations, - self.r().data.maker_payment_requires_nota.unwrap_or(false), - maker_payment_wait_confirm, - WAIT_CONFIRM_INTERVAL, - ); + let confirm_maker_payment_input = ConfirmPaymentInput { + payment_tx: self.r().maker_payment.clone().unwrap().tx_hex.0, + confirmations: self.r().data.maker_payment_confirmations, + requires_nota: self.r().data.maker_payment_requires_nota.unwrap_or(false), + wait_until: maker_payment_wait_confirm, + check_every: WAIT_CONFIRM_INTERVAL, + }; + + let f = self.maker_coin.wait_for_confirmations(confirm_maker_payment_input); if let Err(err) = f.compat().await { return Ok((Some(MakerSwapCommand::PrepareForMakerPaymentRefund), vec![ MakerSwapEvent::MakerPaymentWaitConfirmFailed( @@ -937,16 +965,18 @@ impl MakerSwap { async fn validate_taker_payment(&self) -> Result<(Option, Vec), String> { let wait_taker_payment = taker_payment_spend_deadline(self.r().data.started_at, self.r().data.lock_duration); let confirmations = self.r().data.taker_payment_confirmations; + let taker_coin_swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); + let confirm_taker_payment_input = ConfirmPaymentInput { + payment_tx: self.r().taker_payment.clone().unwrap().tx_hex.0, + confirmations, + requires_nota: self.r().data.taker_payment_requires_nota.unwrap_or(false), + wait_until: wait_taker_payment, + check_every: WAIT_CONFIRM_INTERVAL, + }; let wait_f = self .taker_coin - .wait_for_confirmations( - &self.r().taker_payment.clone().unwrap().tx_hex, - confirmations, - self.r().data.taker_payment_requires_nota.unwrap_or(false), - wait_taker_payment, - WAIT_CONFIRM_INTERVAL, - ) + .wait_for_confirmations(confirm_taker_payment_input) .compat(); if let Err(err) = wait_f.await { return Ok((Some(MakerSwapCommand::PrepareForMakerPaymentRefund), vec![ @@ -959,6 +989,20 @@ impl MakerSwap { ])); } + let min_watcher_reward = if self.r().watcher_reward { + let reward = match min_watcher_reward(&self.taker_coin, &self.maker_coin).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed(err.into_inner().to_string().into()), + ])) + }, + }; + Some(reward) + } else { + None + }; + let validate_input = ValidatePaymentInput { payment_tx: self.r().taker_payment.clone().unwrap().tx_hex.0, time_lock: self.taker_payment_lock.load(Ordering::Relaxed) as u32, @@ -967,10 +1011,12 @@ impl MakerSwap { unique_swap_data: self.unique_swap_data(), secret_hash: self.secret_hash(), amount: self.taker_amount.clone(), - swap_contract_address: self.r().data.taker_coin_swap_contract_address.clone(), + swap_contract_address: taker_coin_swap_contract_address, try_spv_proof_until: wait_taker_payment, confirmations, + min_watcher_reward, }; + let validated_f = self.taker_coin.validate_taker_payment(validate_input).compat(); if let Err(e) = validated_f.await { @@ -1006,17 +1052,16 @@ impl MakerSwap { ])); } - let spend_fut = self - .taker_coin - .send_maker_spends_taker_payment(SendMakerSpendsTakerPaymentArgs { - other_payment_tx: &self.r().taker_payment.clone().unwrap().tx_hex, - time_lock: self.taker_payment_lock.load(Ordering::Relaxed) as u32, - other_pubkey: &*self.r().other_taker_coin_htlc_pub, - secret: &self.r().data.secret.0, - secret_hash: &self.secret_hash(), - swap_contract_address: &self.r().data.taker_coin_swap_contract_address, - swap_unique_data: &self.unique_swap_data(), - }); + let spend_fut = self.taker_coin.send_maker_spends_taker_payment(SpendPaymentArgs { + other_payment_tx: &self.r().taker_payment.clone().unwrap().tx_hex, + time_lock: self.taker_payment_lock.load(Ordering::Relaxed) as u32, + other_pubkey: &*self.r().other_taker_coin_htlc_pub, + secret: &self.r().data.secret.0, + secret_hash: &self.secret_hash(), + swap_contract_address: &self.r().data.taker_coin_swap_contract_address, + swap_unique_data: &self.unique_swap_data(), + watcher_reward: self.r().watcher_reward, + }); let transaction = match spend_fut.compat().await { Ok(t) => t, @@ -1069,13 +1114,14 @@ impl MakerSwap { // we should wait for only one confirmation to make sure our spend transaction is not failed let confirmations = std::cmp::min(1, self.r().data.taker_payment_confirmations); let requires_nota = false; - let wait_fut = self.taker_coin.wait_for_confirmations( - &self.r().taker_payment_spend.clone().unwrap().tx_hex, + let confirm_taker_payment_input = ConfirmPaymentInput { + payment_tx: self.r().taker_payment.clone().unwrap().tx_hex.0, confirmations, requires_nota, - self.wait_refund_until(), - WAIT_CONFIRM_INTERVAL, - ); + wait_until: self.wait_refund_until(), + check_every: WAIT_CONFIRM_INTERVAL, + }; + let wait_fut = self.taker_coin.wait_for_confirmations(confirm_taker_payment_input); if let Err(err) = wait_fut.compat().await { return Ok((Some(MakerSwapCommand::PrepareForMakerPaymentRefund), vec![ MakerSwapEvent::TakerPaymentSpendConfirmFailed( @@ -1141,13 +1187,14 @@ impl MakerSwap { } } - let spend_fut = self.maker_coin.send_maker_refunds_payment(SendMakerRefundsPaymentArgs { + let spend_fut = self.maker_coin.send_maker_refunds_payment(RefundPaymentArgs { payment_tx: &maker_payment, time_lock: locktime as u32, other_pubkey: &*self.r().other_maker_coin_htlc_pub, secret_hash: self.secret_hash().as_slice(), swap_contract_address: &self.r().data.maker_coin_swap_contract_address, swap_unique_data: &self.unique_swap_data(), + watcher_reward: self.r().watcher_reward, }); let transaction = match spend_fut.compat().await { @@ -1304,6 +1351,7 @@ impl MakerSwap { let secret = selfi.r().data.secret.0; let unique_data = selfi.unique_swap_data(); + let watcher_reward = selfi.r().watcher_reward; let search_input = SearchForSwapTxSpendInput { time_lock: timelock, @@ -1313,6 +1361,7 @@ impl MakerSwap { search_from_block: taker_coin_start_block, swap_contract_address: &taker_coin_swap_contract_address, swap_unique_data: &unique_data, + watcher_reward, }; // check if the taker payment is not spent yet match selfi.taker_coin.search_for_swap_tx_spend_other(search_input).await { @@ -1336,7 +1385,7 @@ impl MakerSwap { selfi .taker_coin - .send_maker_spends_taker_payment(SendMakerSpendsTakerPaymentArgs { + .send_maker_spends_taker_payment(SpendPaymentArgs { other_payment_tx: taker_payment_hex, time_lock: timelock, other_pubkey: other_taker_coin_htlc_pub.as_slice(), @@ -1344,6 +1393,7 @@ impl MakerSwap { secret_hash: &selfi.secret_hash(), swap_contract_address: &taker_coin_swap_contract_address, swap_unique_data: &selfi.unique_swap_data(), + watcher_reward, }) .compat() .await @@ -1374,6 +1424,7 @@ impl MakerSwap { let payment_instructions = self.r().payment_instructions.clone(); let maybe_maker_payment = self.r().maker_payment.clone(); + let watcher_reward = self.r().watcher_reward; let maker_payment = match maybe_maker_payment { Some(tx) => tx.tx_hex.0, None => { @@ -1407,6 +1458,7 @@ impl MakerSwap { search_from_block: maker_coin_start_block, swap_contract_address: &maker_coin_swap_contract_address, swap_unique_data: &unique_data, + watcher_reward, }; // validate that maker payment is not spent match self.maker_coin.search_for_swap_tx_spend_my(search_input).await { @@ -1440,13 +1492,14 @@ impl MakerSwap { if let CanRefundHtlc::HaveToWait(seconds_to_wait) = can_refund_htlc { return ERR!("Too early to refund, wait until {}", now_ms() / 1000 + seconds_to_wait); } - let fut = self.maker_coin.send_maker_refunds_payment(SendMakerRefundsPaymentArgs { + let fut = self.maker_coin.send_maker_refunds_payment(RefundPaymentArgs { payment_tx: &maker_payment, time_lock: maker_payment_lock, other_pubkey: other_maker_coin_htlc_pub.as_slice(), secret_hash: secret_hash.as_slice(), swap_contract_address: &maker_coin_swap_contract_address, swap_unique_data: &unique_data, + watcher_reward, }); let transaction = match fut.compat().await { @@ -1721,41 +1774,43 @@ pub struct MakerSavedSwap { #[cfg(test)] impl MakerSavedSwap { pub fn new(maker_amount: &MmNumber, taker_amount: &MmNumber) -> MakerSavedSwap { - let mut events: Vec = Vec::new(); - events.push(MakerSavedEvent { - timestamp: 0, - event: MakerSwapEvent::Started(MakerSwapData { - taker_coin: "".to_string(), - maker_coin: "".to_string(), - taker: Default::default(), - secret: Default::default(), - secret_hash: None, - my_persistent_pub: Default::default(), - lock_duration: 0, - maker_amount: maker_amount.to_decimal(), - taker_amount: taker_amount.to_decimal(), - maker_payment_confirmations: 0, - maker_payment_requires_nota: None, - taker_payment_confirmations: 0, - taker_payment_requires_nota: None, - maker_payment_lock: 0, - uuid: Default::default(), - started_at: 0, - maker_coin_start_block: 0, - taker_coin_start_block: 0, - maker_payment_trade_fee: None, - taker_payment_spend_trade_fee: None, - maker_coin_swap_contract_address: None, - taker_coin_swap_contract_address: None, - maker_coin_htlc_pubkey: None, - taker_coin_htlc_pubkey: None, - p2p_privkey: None, - }), - }); - events.push(MakerSavedEvent { - timestamp: 0, - event: MakerSwapEvent::Finished, - }); + let events = vec![ + MakerSavedEvent { + timestamp: 0, + event: MakerSwapEvent::Started(MakerSwapData { + taker_coin: "".to_string(), + maker_coin: "".to_string(), + taker: Default::default(), + secret: Default::default(), + secret_hash: None, + my_persistent_pub: Default::default(), + lock_duration: 0, + maker_amount: maker_amount.to_decimal(), + taker_amount: taker_amount.to_decimal(), + maker_payment_confirmations: 0, + maker_payment_requires_nota: None, + taker_payment_confirmations: 0, + taker_payment_requires_nota: None, + maker_payment_lock: 0, + uuid: Default::default(), + started_at: 0, + maker_coin_start_block: 0, + taker_coin_start_block: 0, + maker_payment_trade_fee: None, + taker_payment_spend_trade_fee: None, + maker_coin_swap_contract_address: None, + taker_coin_swap_contract_address: None, + maker_coin_htlc_pubkey: None, + taker_coin_htlc_pubkey: None, + p2p_privkey: None, + }), + }, + MakerSavedEvent { + timestamp: 0, + event: MakerSwapEvent::Finished, + }, + ]; + MakerSavedSwap { uuid: Default::default(), my_order_uuid: None, @@ -1877,6 +1932,33 @@ impl MakerSavedSwap { self.taker_coin_usd_price = Some(rates.rel); } } + + // TODO: Adjust for private coins when/if they are braodcasted + // TODO: Adjust for HD wallet when completed + pub fn swap_pubkeys(&self) -> Result { + let maker = match &self.events.first() { + Some(event) => match &event.event { + MakerSwapEvent::Started(started) => started.my_persistent_pub.to_string(), + _ => return ERR!("First swap event must be Started"), + }, + None => return ERR!("Can't get maker's pubkey while events are empty"), + }; + + let taker = match self.events.get(1) { + Some(event) => match &event.event { + MakerSwapEvent::Negotiated(neg) => { + let Some(key) = neg.taker_coin_htlc_pubkey else { + return ERR!("taker's pubkey is empty"); + }; + key.to_string() + }, + _ => return ERR!("Swap must be negotiated to get taker's pubkey"), + }, + None => return ERR!("Can't get taker's pubkey while there's no Negotiated event"), + }; + + Ok(SwapPubkeys { maker, taker }) + } } #[allow(clippy::large_enum_variant)] @@ -2387,7 +2469,6 @@ mod maker_swap_tests { let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let err = block_on(maker_swap.recover_funds()).expect_err("Expected an error"); - println!("{}", err); assert!(err.contains("Taker payment was already refunded")); assert!(unsafe { SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED }); assert!(unsafe { SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED }); @@ -2493,7 +2574,6 @@ mod maker_swap_tests { let taker_coin = MmCoinEnum::Test(TestCoin::default()); let (maker_swap, _) = MakerSwap::load_from_saved(ctx, maker_coin, taker_coin, maker_saved_swap).unwrap(); let err = block_on(maker_swap.recover_funds()).expect_err("Expected an error"); - println!("{}", err); assert!(err.contains("Taker payment was already spent")); assert!(unsafe { SEARCH_FOR_SWAP_TX_SPEND_MY_CALLED }); assert!(unsafe { SEARCH_FOR_SWAP_TX_SPEND_OTHER_CALLED }); diff --git a/mm2src/mm2_main/src/lp_swap/my_swaps_storage.rs b/mm2src/mm2_main/src/lp_swap/my_swaps_storage.rs index 6d2dc630bb..b396a4f54a 100644 --- a/mm2src/mm2_main/src/lp_swap/my_swaps_storage.rs +++ b/mm2src/mm2_main/src/lp_swap/my_swaps_storage.rs @@ -4,11 +4,10 @@ use common::PagingOptions; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use uuid::Uuid; pub type MySwapsResult = Result>; -use uuid::Uuid; - #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] #[derive(Debug, Display, PartialEq)] pub enum MySwapsError { @@ -23,7 +22,7 @@ pub enum MySwapsError { #[display(fmt = "'from_uuid' not found: {}", _0)] FromUuidNotFound(Uuid), #[display(fmt = "Error parsing uuid: {}", _0)] - UuidParse(uuid::parser::ParseError), + UuidParse(uuid::Error), #[display(fmt = "Unknown SQL error: {}", _0)] UnknownSqlError(String), #[display(fmt = "Internal error: {}", _0)] @@ -201,37 +200,41 @@ mod wasm_impl { let items = match (&filter.my_coin, &filter.other_coin) { (Some(my_coin), Some(other_coin)) => { my_swaps_table - .open_cursor("with_my_other_coins") - .await? + .cursor_builder() .only("my_coin", my_coin)? .only("other_coin", other_coin)? .bound("started_at", from_timestamp, to_timestamp) + .open_cursor("with_my_other_coins") + .await? .collect() .await? }, (Some(my_coin), None) => { my_swaps_table - .open_cursor("with_my_coin") - .await? + .cursor_builder() .only("my_coin", my_coin)? .bound("started_at", from_timestamp, to_timestamp) + .open_cursor("with_my_coin") + .await? .collect() .await? }, (None, Some(other_coin)) => { my_swaps_table - .open_cursor("with_other_coin") - .await? + .cursor_builder() .only("other_coin", other_coin)? .bound("started_at", from_timestamp, to_timestamp) + .open_cursor("with_other_coin") + .await? .collect() .await? }, (None, None) => { my_swaps_table + .cursor_builder() + .bound("started_at", from_timestamp, to_timestamp) .open_cursor("started_at") .await? - .bound("started_at", from_timestamp, to_timestamp) .collect() .await? }, @@ -447,7 +450,7 @@ mod wasm_tests { // unknown UUID from_uuid: Some(from_uuid), }; - let actual = take_according_to_paging_opts(uuids.clone(), &paging) + let actual = take_according_to_paging_opts(uuids, &paging) .expect_err("'take_according_to_paging_opts' must return an error"); assert_eq!(actual.into_inner(), MySwapsError::FromUuidNotFound(from_uuid)); } 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 7135c13306..a816695d84 100644 --- a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs +++ b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs @@ -448,7 +448,8 @@ async fn convert_maker_to_taker_events( return events; }, MakerSwapEvent::TakerPaymentSpent(tx_ident) => { - let secret = match maker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex).await { + //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()), Err(e) => { push_event!(TakerSwapEvent::TakerPaymentWaitForSpendFailed(ERRL!("{}", e).into())); @@ -528,7 +529,7 @@ mod tests { #[test] fn test_recreate_taker_swap() { - TestCoin::extract_secret.mock_safe(|_coin, _secret_hash, _spend_tx| { + TestCoin::extract_secret.mock_safe(|_coin, _secret_hash, _spend_tx, _watcher_reward| { let secret = hex::decode("23a6bb64bc0ab2cc14cb84277d8d25134b814e5f999c66e578c9bba3c5e2d3a4").unwrap(); MockResult::Return(Box::pin(async move { Ok(secret) })) }); diff --git a/mm2src/mm2_main/src/lp_swap/saved_swap.rs b/mm2src/mm2_main/src/lp_swap/saved_swap.rs index a4afb8dafe..3519d45596 100644 --- a/mm2src/mm2_main/src/lp_swap/saved_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/saved_swap.rs @@ -348,7 +348,7 @@ mod tests { wasm_bindgen_test_configure!(run_in_browser); async fn get_all_items(ctx: &MmArc) -> Vec<(ItemId, SavedSwapTable)> { - let swaps_ctx = SwapsContext::from_ctx(&ctx).unwrap(); + let swaps_ctx = SwapsContext::from_ctx(ctx).unwrap(); let db = swaps_ctx.swap_db().await.expect("Error getting SwapDb"); let transaction = db.transaction().await.expect("Error creating transaction"); let table = transaction diff --git a/mm2src/mm2_main/src/lp_swap/swap_lock.rs b/mm2src/mm2_main/src/lp_swap/swap_lock.rs index f9f84a6cb7..f1b6269e92 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_lock.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_lock.rs @@ -205,7 +205,7 @@ mod tests { wasm_bindgen_test_configure!(run_in_browser); async fn get_all_items(ctx: &MmArc) -> Vec<(ItemId, SwapLockTable)> { - let swaps_ctx = SwapsContext::from_ctx(&ctx).unwrap(); + let swaps_ctx = SwapsContext::from_ctx(ctx).unwrap(); let db = swaps_ctx.swap_db().await.expect("Error getting SwapDb"); let transaction = db.transaction().await.expect("Error creating transaction"); let table = transaction.table::().await.expect("Error opening table"); diff --git a/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs b/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs index 58511edb2d..d680ba4eb8 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_wasm_db.rs @@ -84,16 +84,13 @@ pub mod tables { fn table_name() -> &'static str { "my_swaps" } fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(Self::table_name())?; - table.create_index("uuid", true)?; - table.create_index("started_at", false)?; - table.create_multi_index("with_my_coin", &["my_coin", "started_at"], false)?; - table.create_multi_index("with_other_coin", &["other_coin", "started_at"], false)?; - table.create_multi_index("with_my_other_coins", &["my_coin", "other_coin", "started_at"], false)?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::table_name())?; + table.create_index("uuid", true)?; + table.create_index("started_at", false)?; + table.create_multi_index("with_my_coin", &["my_coin", "started_at"], false)?; + table.create_multi_index("with_other_coin", &["other_coin", "started_at"], false)?; + table.create_multi_index("with_my_other_coins", &["my_coin", "other_coin", "started_at"], false)?; } Ok(()) } @@ -106,12 +103,9 @@ pub mod tables { new_version: u32, table_name: &'static str, ) -> OnUpgradeResult<()> { - match (old_version, new_version) { - (0, 1) => { - let table = upgrader.create_table(table_name)?; - table.create_index("uuid", true)?; - }, - _ => (), + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(table_name)?; + table.create_index("uuid", true)?; } Ok(()) } diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 26067ae625..7441a4a855 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -1,9 +1,9 @@ -use super::{broadcast_p2p_tx_msg, lp_coinfind, taker_payment_spend_deadline, tx_helper_topic, H256Json, SwapsContext, - WAIT_CONFIRM_INTERVAL}; +use super::{broadcast_p2p_tx_msg, get_payment_locktime, lp_coinfind, min_watcher_reward, taker_payment_spend_deadline, + tx_helper_topic, H256Json, SwapsContext, WAIT_CONFIRM_INTERVAL}; use crate::mm2::MmError; use async_trait::async_trait; -use coins::{CanRefundHtlc, FoundSwapTxSpend, MmCoinEnum, SendMakerPaymentSpendPreimageInput, - SendWatcherRefundsPaymentArgs, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, +use coins::{CanRefundHtlc, ConfirmPaymentInput, FoundSwapTxSpend, MmCoinEnum, RefundPaymentArgs, + SendMakerPaymentSpendPreimageInput, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput}; use common::executor::{AbortSettings, SpawnAbortable, Timer}; use common::log::{debug, error, info}; @@ -33,6 +33,7 @@ struct WatcherContext { verified_pub: Vec, data: TakerSwapWatcherData, conf: WatcherConf, + watcher_reward: bool, } impl WatcherContext { @@ -141,6 +142,7 @@ enum WatcherError { MakerPaymentSpendFailed(String), MakerPaymentCouldNotBeFound(String), TakerPaymentRefundFailed(String), + InternalError(String), } impl Stopped { @@ -215,16 +217,17 @@ impl State for ValidateTakerPayment { }; let confirmations = min(watcher_ctx.data.taker_payment_confirmations, TAKER_SWAP_CONFIRMATIONS); + let confirm_taker_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment_hex.clone(), + confirmations, + requires_nota: watcher_ctx.data.taker_payment_requires_nota.unwrap_or(false), + wait_until: taker_payment_spend_deadline, + check_every: WAIT_CONFIRM_INTERVAL, + }; let wait_fut = watcher_ctx .taker_coin - .wait_for_confirmations( - &taker_payment_hex, - confirmations, - watcher_ctx.data.taker_payment_requires_nota.unwrap_or(false), - taker_payment_spend_deadline, - WAIT_CONFIRM_INTERVAL, - ) + .wait_for_confirmations(confirm_taker_payment_input) .compat(); if let Err(err) = wait_fut.await { return Self::change_state(Stopped::from_reason(StopReason::Error( @@ -232,6 +235,20 @@ impl State for ValidateTakerPayment { ))); } + let min_watcher_reward = if watcher_ctx.watcher_reward { + let reward = match min_watcher_reward(&watcher_ctx.taker_coin, &watcher_ctx.maker_coin).await { + Ok(reward) => reward, + Err(err) => { + return Self::change_state(Stopped::from_reason(StopReason::Error( + WatcherError::InternalError(err.into_inner().to_string()).into(), + ))) + }, + }; + Some(reward) + } else { + None + }; + let validate_input = WatcherValidatePaymentInput { payment_tx: taker_payment_hex.clone(), taker_payment_refund_preimage: watcher_ctx.data.taker_payment_refund_preimage.clone(), @@ -244,6 +261,7 @@ impl State for ValidateTakerPayment { secret_hash: watcher_ctx.data.secret_hash.clone(), try_spv_proof_until: taker_payment_spend_deadline, confirmations, + min_watcher_reward, }; let validated_f = watcher_ctx @@ -276,6 +294,7 @@ impl State for WaitForTakerPaymentSpend { secret_hash: &watcher_ctx.data.secret_hash, tx: &self.taker_payment_hex, search_from_block: watcher_ctx.data.taker_coin_start_block, + watcher_reward: watcher_ctx.watcher_reward, }; loop { @@ -349,7 +368,7 @@ impl State for WaitForTakerPaymentSpend { let tx_hex = tx.tx_hex(); let secret = match watcher_ctx .taker_coin - .extract_secret(&watcher_ctx.data.secret_hash, &tx_hex) + .extract_secret(&watcher_ctx.data.secret_hash, &tx_hex, watcher_ctx.watcher_reward) .await { Ok(bytes) => H256Json::from(bytes.as_slice()), @@ -377,6 +396,7 @@ impl State for SpendMakerPayment { secret: &self.secret.0, secret_hash: &watcher_ctx.data.secret_hash, taker_pub: &watcher_ctx.verified_pub, + watcher_reward: watcher_ctx.watcher_reward, }); let transaction = match spend_fut.compat().await { @@ -441,13 +461,14 @@ impl State for RefundTakerPayment { let refund_fut = watcher_ctx .taker_coin - .send_taker_payment_refund_preimage(SendWatcherRefundsPaymentArgs { + .send_taker_payment_refund_preimage(RefundPaymentArgs { payment_tx: &watcher_ctx.data.taker_payment_refund_preimage, swap_contract_address: &None, secret_hash: &watcher_ctx.data.secret_hash, other_pubkey: &watcher_ctx.verified_pub, time_lock: watcher_ctx.taker_locktime() as u32, swap_unique_data: &[], + watcher_reward: watcher_ctx.watcher_reward, }); let transaction = match refund_fut.compat().await { Ok(t) => t, @@ -557,6 +578,15 @@ impl Drop for SwapWatcherLock { } fn spawn_taker_swap_watcher(ctx: MmArc, watcher_data: TakerSwapWatcherData, verified_pub: Vec) { + // TODO: See if more data validations can be added here + if watcher_data.lock_duration != get_payment_locktime() + && watcher_data.lock_duration != get_payment_locktime() * 4 + && watcher_data.lock_duration != get_payment_locktime() * 10 + { + error!("Invalid lock duration"); + return; + } + let swap_ctx = SwapsContext::from_ctx(&ctx).unwrap(); if swap_ctx.swap_msgs.lock().unwrap().contains_key(&watcher_data.uuid) { return; @@ -595,6 +625,11 @@ fn spawn_taker_swap_watcher(ctx: MmArc, watcher_data: TakerSwapWatcherData, veri }, }; + if !taker_coin.is_supported_by_watchers() || !maker_coin.is_supported_by_watchers() { + log!("One of the coins or their contracts does not support watchers"); + return; + } + log_tag!( ctx, ""; @@ -605,6 +640,8 @@ fn spawn_taker_swap_watcher(ctx: MmArc, watcher_data: TakerSwapWatcherData, veri ); let conf = json::from_value::(ctx.conf["watcher_conf"].clone()).unwrap_or_default(); + //let watcher_reward = taker_coin.is_eth() || maker_coin.is_eth(); + let watcher_reward = false; let watcher_ctx = WatcherContext { ctx, maker_coin, @@ -612,6 +649,7 @@ fn spawn_taker_swap_watcher(ctx: MmArc, watcher_data: TakerSwapWatcherData, veri verified_pub, data: watcher_data, conf, + watcher_reward, }; let state_machine: StateMachine<_, ()> = StateMachine::from_ctx(watcher_ctx); state_machine.run(ValidateTakerFee {}).await; diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index f87cfb1b5e..1a41f1128b 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -8,17 +8,17 @@ use super::{broadcast_my_swap_status, broadcast_swap_message, broadcast_swap_mes check_other_coin_balance_for_swap, dex_fee_amount_from_taker_coin, dex_fee_rate, dex_fee_threshold, get_locked_amount, recv_swap_msg, swap_topic, wait_for_maker_payment_conf_until, AtomicSwap, LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, - SavedSwap, SavedSwapIo, SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapTxDataMsg, - SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; + SavedSwap, SavedSwapIo, SavedTradeFee, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, + SwapTxDataMsg, SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::{MatchBy, OrderConfirmationsSettings, TakerAction, TakerOrderBuilder}; use crate::mm2::lp_price::fetch_swap_coins_price; -use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, tx_helper_topic, wait_for_maker_payment_conf_duration, - TakerSwapWatcherData}; -use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, - PaymentInstructions, PaymentInstructionsErr, SearchForSwapTxSpendInput, SendSpendPaymentArgs, - SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, SendTakerSpendsMakerPaymentArgs, TradeFee, - TradePreimageValue, ValidatePaymentInput}; +use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, min_watcher_reward, tx_helper_topic, + wait_for_maker_payment_conf_duration, watcher_reward_amount, TakerSwapWatcherData}; +use coins::{lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, + FoundSwapTxSpend, MmCoinEnum, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, TradeFee, TradePreimageValue, + ValidatePaymentInput}; use common::executor::Timer; use common::log::{debug, error, info, warn}; use common::{bits256, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -303,6 +303,33 @@ impl TakerSavedSwap { self.taker_coin_usd_price = Some(rates.rel); } } + + // TODO: Adjust for private coins when/if they are braodcasted + // TODO: Adjust for HD wallet when completed + pub fn swap_pubkeys(&self) -> Result { + let taker = match &self.events.first() { + Some(event) => match &event.event { + TakerSwapEvent::Started(started) => started.my_persistent_pub.to_string(), + _ => return ERR!("First swap event must be Started"), + }, + None => return ERR!("Can't get taker's pubkey while events are empty"), + }; + + let maker = match self.events.get(1) { + Some(event) => match &event.event { + TakerSwapEvent::Negotiated(neg) => { + let Some(key) = neg.maker_coin_htlc_pubkey else { + return ERR!("maker's pubkey is empty"); + }; + key.to_string() + }, + _ => return ERR!("Swap must be negotiated to get maker's pubkey"), + }, + None => return ERR!("Can't get maker's pubkey while there's no Negotiated event"), + }; + + Ok(SwapPubkeys { maker, taker }) + } } #[allow(clippy::large_enum_variant)] @@ -511,6 +538,7 @@ pub struct TakerSwapMut { taker_payment_refund: Option, secret_hash: BytesJson, secret: H256Json, + watcher_reward: bool, payment_instructions: Option, } @@ -850,6 +878,7 @@ impl TakerSwap { taker_payment_refund: None, secret_hash: BytesJson::default(), secret: H256Json::default(), + watcher_reward: false, payment_instructions: None, }), ctx, @@ -1030,6 +1059,11 @@ impl TakerSwap { p2p_privkey: self.p2p_privkey.map(SerializableSecp256k1Keypair::from), }; + // This value will be true if both sides support & want to use watchers and either the taker or the maker coin is ETH. + // This requires a communication between the parties before the swap starts, which will be done during the ordermatch phase + // or via negotiation messages in the next sprint. + self.w().watcher_reward = false; + Ok((Some(TakerSwapCommand::Negotiate), vec![TakerSwapEvent::Started(data)])) } @@ -1131,6 +1165,7 @@ impl TakerSwap { let taker_coin_swap_contract_bytes = taker_coin_swap_contract_addr .clone() .map_or_else(Vec::new, |bytes| bytes.0); + let my_negotiation_data = self.get_my_negotiation_data( maker_data.secret_hash().to_vec(), maker_coin_swap_contract_bytes, @@ -1304,13 +1339,15 @@ impl TakerSwap { async fn validate_maker_payment(&self) -> Result<(Option, Vec), String> { info!("Before wait confirm"); let confirmations = self.r().data.maker_payment_confirmations; - let f = self.maker_coin.wait_for_confirmations( - &self.r().maker_payment.clone().unwrap().tx_hex, + let confirm_maker_payment_input = ConfirmPaymentInput { + payment_tx: self.r().maker_payment.clone().unwrap().tx_hex.0, confirmations, - self.r().data.maker_payment_requires_nota.unwrap_or(false), - self.r().data.maker_payment_wait, - WAIT_CONFIRM_INTERVAL, - ); + requires_nota: self.r().data.maker_payment_requires_nota.unwrap_or(false), + wait_until: self.r().data.maker_payment_wait, + check_every: WAIT_CONFIRM_INTERVAL, + }; + + let f = self.maker_coin.wait_for_confirmations(confirm_maker_payment_input); if let Err(err) = f.compat().await { return Ok((Some(TakerSwapCommand::Finish), vec![ TakerSwapEvent::MakerPaymentWaitConfirmFailed( @@ -1320,6 +1357,20 @@ impl TakerSwap { } info!("After wait confirm"); + let min_watcher_reward = if self.r().watcher_reward { + let reward = match min_watcher_reward(&self.maker_coin, &self.taker_coin).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed(err.into_inner().to_string().into()), + ])) + }, + }; + Some(reward) + } else { + None + }; + let validate_input = ValidatePaymentInput { payment_tx: self.r().maker_payment.clone().unwrap().tx_hex.0, time_lock_duration: self.r().data.lock_duration, @@ -1331,6 +1382,7 @@ impl TakerSwap { try_spv_proof_until: self.r().data.maker_payment_wait, confirmations, unique_swap_data: self.unique_swap_data(), + min_watcher_reward, }; let validated = self.maker_coin.validate_maker_payment(validate_input).compat().await; @@ -1398,6 +1450,21 @@ impl TakerSwap { amount: &self.taker_amount.to_decimal(), payment_instructions: &self.r().payment_instructions, }); + + let reward_amount = if self.r().watcher_reward { + let reward = match watcher_reward_amount(&self.taker_coin, &self.maker_coin).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed(err.into_inner().to_string().into()), + ])) + }, + }; + Some(reward) + } else { + None + }; + let transaction = match f.compat().await { Ok(res) => match res { Some(tx) => tx, @@ -1406,7 +1473,7 @@ impl TakerSwap { Ok(_) => self.r().data.started_at as u32, Err(_) => self.r().data.taker_payment_lock as u32, }; - let payment_fut = self.taker_coin.send_taker_payment(SendTakerPaymentArgs { + let payment_fut = self.taker_coin.send_taker_payment(SendPaymentArgs { time_lock_duration: self.r().data.lock_duration, time_lock, other_pubkey: &*self.r().other_taker_coin_htlc_pub, @@ -1415,6 +1482,8 @@ impl TakerSwap { swap_contract_address: &self.r().data.taker_coin_swap_contract_address, swap_unique_data: &unique_data, payment_instructions: &self.r().payment_instructions, + watcher_reward: reward_amount, + wait_for_confirmation_until: self.r().data.taker_payment_lock, }); match payment_fut.compat().await { @@ -1548,15 +1617,16 @@ impl TakerSwap { self.p2p_privkey, ); + let confirm_taker_payment_input = ConfirmPaymentInput { + payment_tx: self.r().taker_payment.clone().unwrap().tx_hex.0, + confirmations: self.r().data.taker_payment_confirmations, + requires_nota: self.r().data.taker_payment_requires_nota.unwrap_or(false), + wait_until: self.r().data.taker_payment_lock, + check_every: WAIT_CONFIRM_INTERVAL, + }; let wait_f = self .taker_coin - .wait_for_confirmations( - &self.r().taker_payment.clone().unwrap().tx_hex, - self.r().data.taker_payment_confirmations, - self.r().data.taker_payment_requires_nota.unwrap_or(false), - self.r().data.taker_payment_lock, - WAIT_CONFIRM_INTERVAL, - ) + .wait_for_confirmations(confirm_taker_payment_input) .compat(); if let Err(err) = wait_f.await { return Ok((Some(TakerSwapCommand::PrepareForTakerPaymentRefund), vec![ @@ -1598,7 +1668,12 @@ impl TakerSwap { }; let secret_hash = self.r().secret_hash.clone(); - let secret = match self.taker_coin.extract_secret(&secret_hash.0, &tx_ident.tx_hex).await { + let watcher_reward = self.r().watcher_reward; + let secret = match self + .taker_coin + .extract_secret(&secret_hash.0, &tx_ident.tx_hex, watcher_reward) + .await + { Ok(bytes) => H256Json::from(bytes.as_slice()), Err(e) => { return Ok((Some(TakerSwapCommand::Finish), vec![ @@ -1622,7 +1697,7 @@ impl TakerSwap { TakerSwapEvent::MakerPaymentSpendFailed("Explicit test failure".into()), ])); } - let spend_fut = self.maker_coin.send_taker_spends_maker_payment(SendSpendPaymentArgs { + let spend_fut = self.maker_coin.send_taker_spends_maker_payment(SpendPaymentArgs { other_payment_tx: &self.r().maker_payment.clone().unwrap().tx_hex, time_lock: self.maker_payment_lock.load(Ordering::Relaxed) as u32, other_pubkey: &*self.r().other_maker_coin_htlc_pub, @@ -1630,6 +1705,7 @@ impl TakerSwap { secret_hash: &self.r().secret_hash.0, swap_contract_address: &self.r().data.maker_coin_swap_contract_address, swap_unique_data: &self.unique_swap_data(), + watcher_reward: self.r().watcher_reward, }); let transaction = match spend_fut.compat().await { Ok(t) => t, @@ -1715,13 +1791,14 @@ impl TakerSwap { } } - let refund_fut = self.taker_coin.send_taker_refunds_payment(SendTakerRefundsPaymentArgs { + let refund_fut = self.taker_coin.send_taker_refunds_payment(RefundPaymentArgs { payment_tx: &taker_payment, time_lock: locktime as u32, other_pubkey: &*self.r().other_taker_coin_htlc_pub, secret_hash: &self.r().secret_hash.0, swap_contract_address: &self.r().data.taker_coin_swap_contract_address, swap_unique_data: &self.unique_swap_data(), + watcher_reward: self.r().watcher_reward, }); let transaction = match refund_fut.compat().await { @@ -1879,6 +1956,7 @@ impl TakerSwap { let taker_payment_lock = self.r().data.taker_payment_lock; 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 watcher_reward = self.r().watcher_reward; let unique_data = self.unique_swap_data(); macro_rules! check_maker_payment_is_not_spent { @@ -1892,6 +1970,7 @@ impl TakerSwap { search_from_block: maker_coin_start_block, swap_contract_address: &maker_coin_swap_contract_address, swap_unique_data: &unique_data, + watcher_reward, }; match self.maker_coin.search_for_swap_tx_spend_other(search_input).await { @@ -1951,17 +2030,16 @@ impl TakerSwap { let secret = self.r().secret.0; let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); - let fut = self - .maker_coin - .send_taker_spends_maker_payment(SendTakerSpendsMakerPaymentArgs { - other_payment_tx: &maker_payment, - time_lock: self.maker_payment_lock.load(Ordering::Relaxed) as u32, - other_pubkey: other_maker_coin_htlc_pub.as_slice(), - secret: &secret, - secret_hash: &secret_hash, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - }); + let fut = self.maker_coin.send_taker_spends_maker_payment(SpendPaymentArgs { + other_payment_tx: &maker_payment, + time_lock: self.maker_payment_lock.load(Ordering::Relaxed) as u32, + other_pubkey: other_maker_coin_htlc_pub.as_slice(), + secret: &secret, + secret_hash: &secret_hash, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + watcher_reward: self.r().watcher_reward, + }); let transaction = match fut.compat().await { Ok(t) => t, @@ -1994,6 +2072,7 @@ impl TakerSwap { search_from_block: taker_coin_start_block, swap_contract_address: &taker_coin_swap_contract_address, swap_unique_data: &unique_data, + watcher_reward, }; let taker_payment_spend = try_s!(self.taker_coin.search_for_swap_tx_spend_my(search_input).await); @@ -2003,19 +2082,22 @@ impl TakerSwap { check_maker_payment_is_not_spent!(); let secret_hash = self.r().secret_hash.clone(); let tx_hex = tx.tx_hex(); - let secret = try_s!(self.taker_coin.extract_secret(&secret_hash.0, &tx_hex).await); - - let fut = self - .maker_coin - .send_taker_spends_maker_payment(SendTakerSpendsMakerPaymentArgs { - other_payment_tx: &maker_payment, - time_lock: self.maker_payment_lock.load(Ordering::Relaxed) as u32, - other_pubkey: other_maker_coin_htlc_pub.as_slice(), - secret: &secret, - secret_hash: &secret_hash, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - }); + let secret = try_s!( + self.taker_coin + .extract_secret(&secret_hash.0, &tx_hex, watcher_reward) + .await + ); + + let fut = self.maker_coin.send_taker_spends_maker_payment(SpendPaymentArgs { + other_payment_tx: &maker_payment, + time_lock: self.maker_payment_lock.load(Ordering::Relaxed) as u32, + other_pubkey: other_maker_coin_htlc_pub.as_slice(), + secret: &secret, + secret_hash: &secret_hash, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + watcher_reward: self.r().watcher_reward, + }); let transaction = match fut.compat().await { Ok(t) => t, @@ -2054,13 +2136,14 @@ impl TakerSwap { return ERR!("Too early to refund, wait until {}", now_ms() / 1000 + seconds_to_wait); } - let fut = self.taker_coin.send_taker_refunds_payment(SendTakerRefundsPaymentArgs { + let fut = self.taker_coin.send_taker_refunds_payment(RefundPaymentArgs { payment_tx: &taker_payment, time_lock: taker_payment_lock as u32, other_pubkey: other_taker_coin_htlc_pub.as_slice(), secret_hash: &secret_hash, swap_contract_address: &taker_coin_swap_contract_address, swap_unique_data: &unique_data, + watcher_reward, }); let transaction = match fut.compat().await { @@ -2574,7 +2657,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(vec![]) }))); static mut MY_PAYMENT_SENT_CALLED: bool = false; TestCoin::check_if_my_payment_sent.mock_safe(|_, _| { @@ -2683,7 +2766,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(vec![]) }))); static mut SEARCH_TX_SPEND_CALLED: bool = false; TestCoin::search_for_swap_tx_spend_my.mock_safe(|_, _| { diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 2281cd11a9..c31aff6439 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -456,6 +456,8 @@ pub fn run_lp_main( version: String, datetime: String, ) -> Result<(), String> { + env_logger::init(); + let conf = get_mm2config(first_arg)?; let log_filter = LogLevel::from_env(); diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 9b35c5bb47..e550124c33 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -28,7 +28,7 @@ fn test_match_maker_order_and_taker_request() { price: 1.into(), matches: HashMap::new(), started_swaps: Vec::new(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -40,7 +40,7 @@ fn test_match_maker_order_and_taker_request() { let request = TakerRequest { base: "BASE".into(), rel: "REL".into(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), dest_pub_key: H256Json::default(), sender_pubkey: H256Json::default(), base_amount: 10.into(), @@ -66,7 +66,7 @@ fn test_match_maker_order_and_taker_request() { price: "0.5".into(), matches: HashMap::new(), started_swaps: Vec::new(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -78,7 +78,7 @@ fn test_match_maker_order_and_taker_request() { let request = TakerRequest { base: "BASE".into(), rel: "REL".into(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), dest_pub_key: H256Json::default(), sender_pubkey: H256Json::default(), base_amount: 10.into(), @@ -104,7 +104,7 @@ fn test_match_maker_order_and_taker_request() { price: "0.5".into(), matches: HashMap::new(), started_swaps: Vec::new(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -116,7 +116,7 @@ fn test_match_maker_order_and_taker_request() { let request = TakerRequest { base: "BASE".into(), rel: "REL".into(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), dest_pub_key: H256Json::default(), sender_pubkey: H256Json::default(), base_amount: 10.into(), @@ -142,7 +142,7 @@ fn test_match_maker_order_and_taker_request() { price: "0.5".into(), matches: HashMap::new(), started_swaps: Vec::new(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -154,7 +154,7 @@ fn test_match_maker_order_and_taker_request() { let request = TakerRequest { base: "REL".into(), rel: "BASE".into(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), dest_pub_key: H256Json::default(), sender_pubkey: H256Json::default(), base_amount: 5.into(), @@ -180,7 +180,7 @@ fn test_match_maker_order_and_taker_request() { price: "0.5".into(), matches: HashMap::new(), started_swaps: Vec::new(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -192,7 +192,7 @@ fn test_match_maker_order_and_taker_request() { let request = TakerRequest { base: "REL".into(), rel: "BASE".into(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), dest_pub_key: H256Json::default(), sender_pubkey: H256Json::default(), base_amount: 10.into(), @@ -218,7 +218,7 @@ fn test_match_maker_order_and_taker_request() { price: "1".into(), matches: HashMap::new(), started_swaps: Vec::new(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -230,7 +230,7 @@ fn test_match_maker_order_and_taker_request() { let request = TakerRequest { base: "REL".into(), rel: "BASE".into(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), dest_pub_key: H256Json::default(), sender_pubkey: H256Json::default(), base_amount: 1.into(), @@ -258,7 +258,7 @@ fn test_match_maker_order_and_taker_request() { rel: "KMD".to_owned(), matches: HashMap::new(), started_swaps: vec![], - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -272,7 +272,7 @@ fn test_match_maker_order_and_taker_request() { base_amount: "774.205645538427044180416545".into(), rel_amount: "0.2928826881884105".into(), action: TakerAction::Sell, - uuid: Uuid::new_v4(), + uuid: new_uuid(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), match_by: MatchBy::Any, @@ -298,7 +298,7 @@ fn test_match_maker_order_and_taker_request() { rel: "REL".to_owned(), matches: HashMap::new(), started_swaps: vec![], - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -312,7 +312,7 @@ fn test_match_maker_order_and_taker_request() { base_amount: "30".into(), rel_amount: "1.5".into(), action: TakerAction::Sell, - uuid: Uuid::new_v4(), + uuid: new_uuid(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), match_by: MatchBy::Any, @@ -369,7 +369,7 @@ fn test_maker_order_available_amount() { price: 1.into(), matches: HashMap::new(), started_swaps: Vec::new(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -377,9 +377,9 @@ fn test_maker_order_available_amount() { rel_orderbook_ticker: None, p2p_privkey: None, }; - maker.matches.insert(Uuid::new_v4(), MakerMatch { + maker.matches.insert(new_uuid(), MakerMatch { request: TakerRequest { - uuid: Uuid::new_v4(), + uuid: new_uuid(), base: "BASE".into(), rel: "REL".into(), base_amount: 5.into(), @@ -399,8 +399,8 @@ fn test_maker_order_available_amount() { rel_amount: 5.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), - taker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), + taker_order_uuid: new_uuid(), conf_settings: None, base_protocol_info: None, rel_protocol_info: None, @@ -409,9 +409,9 @@ fn test_maker_order_available_amount() { connected: None, last_updated: now_ms(), }); - maker.matches.insert(Uuid::new_v4(), MakerMatch { + maker.matches.insert(new_uuid(), MakerMatch { request: TakerRequest { - uuid: Uuid::new_v4(), + uuid: new_uuid(), base: "BASE".into(), rel: "REL".into(), base_amount: 1.into(), @@ -431,8 +431,8 @@ fn test_maker_order_available_amount() { rel_amount: 1.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), - taker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), + taker_order_uuid: new_uuid(), conf_settings: None, base_protocol_info: None, rel_protocol_info: None, @@ -449,7 +449,7 @@ fn test_maker_order_available_amount() { #[test] fn test_taker_match_reserved() { - let uuid = Uuid::new_v4(); + let uuid = new_uuid(); let request = TakerRequest { base: "BASE".into(), @@ -486,7 +486,7 @@ fn test_taker_match_reserved() { rel_amount: 10.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -530,7 +530,7 @@ fn test_taker_match_reserved() { rel_amount: 10.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -574,7 +574,7 @@ fn test_taker_match_reserved() { rel_amount: 1.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -618,7 +618,7 @@ fn test_taker_match_reserved() { rel_amount: 1.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -662,7 +662,7 @@ fn test_taker_match_reserved() { rel_amount: 1.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -706,7 +706,7 @@ fn test_taker_match_reserved() { rel_amount: 1.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -750,7 +750,7 @@ fn test_taker_match_reserved() { rel_amount: 1.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -794,7 +794,7 @@ fn test_taker_match_reserved() { rel_amount: 3.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -853,7 +853,7 @@ fn test_taker_order_cancellable() { let request = TakerRequest { base: "BASE".into(), rel: "REL".into(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), dest_pub_key: H256Json::default(), sender_pubkey: H256Json::default(), base_amount: 1.into(), @@ -883,7 +883,7 @@ fn test_taker_order_cancellable() { let request = TakerRequest { base: "BASE".into(), rel: "REL".into(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), dest_pub_key: H256Json::default(), sender_pubkey: H256Json::default(), base_amount: 1.into(), @@ -908,7 +908,7 @@ fn test_taker_order_cancellable() { p2p_privkey: None, }; - order.matches.insert(Uuid::new_v4(), TakerMatch { + order.matches.insert(new_uuid(), TakerMatch { last_updated: now_ms(), reserved: MakerReserved { base: "BASE".into(), @@ -917,8 +917,8 @@ fn test_taker_order_cancellable() { rel_amount: 3.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), - taker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), + taker_order_uuid: new_uuid(), conf_settings: None, base_protocol_info: None, rel_protocol_info: None, @@ -926,8 +926,8 @@ fn test_taker_order_cancellable() { connect: TakerConnect { sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), - taker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), + taker_order_uuid: new_uuid(), }, connected: None, }); @@ -1102,10 +1102,10 @@ fn test_cancel_by_all() { #[test] // https://github.com/KomodoPlatform/atomicDEX-API/issues/607 fn test_taker_order_match_by() { - let uuid = Uuid::new_v4(); + let uuid = new_uuid(); let mut not_matching_uuids = HashSet::new(); - not_matching_uuids.insert(Uuid::new_v4()); + not_matching_uuids.insert(new_uuid()); let request = TakerRequest { base: "BASE".into(), rel: "REL".into(), @@ -1141,7 +1141,7 @@ fn test_taker_order_match_by() { rel_amount: 10.into(), sender_pubkey: H256Json::default(), dest_pub_key: H256Json::default(), - maker_order_uuid: Uuid::new_v4(), + maker_order_uuid: new_uuid(), taker_order_uuid: uuid, conf_settings: None, base_protocol_info: None, @@ -1179,7 +1179,7 @@ fn test_maker_order_was_updated() { price: 1.into(), matches: HashMap::new(), started_swaps: Vec::new(), - uuid: Uuid::new_v4(), + uuid: new_uuid(), conf_settings: None, changes_history: None, save_in_history: false, @@ -1213,13 +1213,15 @@ fn lp_connect_start_bob_should_not_be_invoked_if_order_match_already_connected() static mut CONNECT_START_CALLED: bool = false; lp_connect_start_bob.mock_safe(|_, _, _| { - MockResult::Return(unsafe { + unsafe { CONNECT_START_CALLED = true; - }) + } + + MockResult::Return(()) }); let connect: TakerConnect = json::from_str(r#"{"taker_order_uuid":"2f9afe84-7a89-4194-8947-45fba563118f","maker_order_uuid":"5f6516ea-ccaa-453a-9e37-e1c2c0d527e3","method":"connect","sender_pubkey":"031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3","dest_pub_key":"c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed"}"#).unwrap(); - block_on(process_taker_connect(ctx, connect.sender_pubkey.clone(), connect)); + block_on(process_taker_connect(ctx, connect.sender_pubkey, connect)); assert!(unsafe { !CONNECT_START_CALLED }); } @@ -1391,7 +1393,7 @@ fn test_choose_taker_confs_settings_buy_action() { let maker_reserved = MakerReserved::default(); TestCoin::requires_notarization.mock_safe(|_| MockResult::Return(true)); TestCoin::required_confirmations.mock_safe(|_| MockResult::Return(8)); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // should pick settings from coins assert!(settings.taker_coin_nota); assert_eq!(settings.taker_coin_confs, 8); @@ -1409,7 +1411,7 @@ fn test_choose_taker_confs_settings_buy_action() { .build_unchecked(); // no confs and notas set let maker_reserved = MakerReserved::default(); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // should pick settings from taker request // as action is buy my_coin is rel and other coin is base assert!(!settings.taker_coin_nota); @@ -1434,7 +1436,7 @@ fn test_choose_taker_confs_settings_buy_action() { base_nota: true, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // should pick settings from maker reserved if he requires less confs // as action is buy my_coin is rel and other coin is base in request assert!(!settings.taker_coin_nota); @@ -1459,7 +1461,7 @@ fn test_choose_taker_confs_settings_buy_action() { base_nota: true, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // should allow maker to use more confirmations than we require, but it shouldn't affect our settings // as action is buy my_coin is rel and other coin is base in request assert!(!settings.taker_coin_nota); @@ -1484,7 +1486,7 @@ fn test_choose_taker_confs_settings_buy_action() { rel_nota: true, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // maker settings should have no effect on other_coin_confs and other_coin_nota // as action is buy my_coin is rel and other coin is base in request assert!(!settings.taker_coin_nota); @@ -1505,7 +1507,7 @@ fn test_choose_taker_confs_settings_sell_action() { let maker_reserved = MakerReserved::default(); TestCoin::requires_notarization.mock_safe(|_| MockResult::Return(true)); TestCoin::required_confirmations.mock_safe(|_| MockResult::Return(8)); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // should pick settings from coins assert!(settings.taker_coin_nota); assert_eq!(settings.taker_coin_confs, 8); @@ -1524,7 +1526,7 @@ fn test_choose_taker_confs_settings_sell_action() { .build_unchecked(); // no confs and notas set let maker_reserved = MakerReserved::default(); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // should pick settings from taker request // as action is sell my_coin is base and other coin is rel in request assert!(!settings.taker_coin_nota); @@ -1550,7 +1552,7 @@ fn test_choose_taker_confs_settings_sell_action() { rel_nota: false, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // should pick settings from maker reserved if he requires less confs // as action is sell my_coin is base and other coin is rel in request assert!(!settings.taker_coin_nota); @@ -1576,7 +1578,7 @@ fn test_choose_taker_confs_settings_sell_action() { base_nota: false, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // should allow maker to use more confirmations than we require, but it shouldn't affect our settings // as action is sell my_coin is base and other coin is rel in request assert!(!settings.taker_coin_nota); @@ -1602,7 +1604,7 @@ fn test_choose_taker_confs_settings_sell_action() { base_nota: false, }; maker_reserved.conf_settings = Some(maker_conf_settings); - let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved, &coin, &coin); + let settings = choose_taker_confs_and_notas(&taker_order.request, &maker_reserved.conf_settings, &coin, &coin); // maker settings should have no effect on other_coin_confs and other_coin_nota // as action is sell my_coin is base and other coin is rel in request assert!(!settings.taker_coin_nota); @@ -1633,7 +1635,7 @@ pub(super) fn make_random_orders( for _i in 0..n { let numer: u64 = rng.gen_range(2000, 10000000); let order = new_protocol::MakerOrderCreated { - uuid: Uuid::new_v4().into(), + uuid: new_uuid().into(), base: base.clone(), rel: rel.clone(), price: BigRational::new(numer.into(), 1000000.into()), @@ -1656,7 +1658,7 @@ pub(super) fn make_random_orders( fn pubkey_and_secret_for_test(passphrase: &str) -> (String, [u8; 32]) { let key_pair = key_pair_from_seed(passphrase).unwrap(); let pubkey = hex::encode(&**key_pair.public()); - let secret = *(&*key_pair.private().secret); + let secret = *key_pair.private().secret; (pubkey, secret) } @@ -1706,14 +1708,14 @@ fn test_process_get_orderbook_request() { let ordermatch_ctx = OrdermatchContext::from_ctx(&ctx).unwrap(); let mut orderbook = ordermatch_ctx.orderbook.lock(); - for order in orders_by_pubkeys.iter().map(|(_pubkey, orders)| orders).flatten() { + for order in orders_by_pubkeys.values().flatten() { orderbook.insert_or_update_order_update_trie(order.clone()); } // avoid dead lock on orderbook as process_get_orderbook_request also acquires it drop(orderbook); - let encoded = process_get_orderbook_request(ctx.clone(), "RICK".into(), "MORTY".into()) + let encoded = process_get_orderbook_request(ctx, "RICK".into(), "MORTY".into()) .unwrap() .unwrap(); @@ -1721,7 +1723,7 @@ fn test_process_get_orderbook_request() { for (pubkey, item) in orderbook.pubkey_orders { let expected = orders_by_pubkeys .get(&pubkey) - .expect(&format!("!best_orders_by_pubkeys is expected to contain {:?}", pubkey)); + .unwrap_or_else(|| panic!("!best_orders_by_pubkeys is expected to contain {:?}", pubkey)); let mut actual: Vec = item .orders @@ -1761,9 +1763,7 @@ fn test_process_get_orderbook_request_limit() { // avoid dead lock on orderbook as process_get_orderbook_request also acquires it drop(orderbook); - let err = process_get_orderbook_request(ctx.clone(), "RICK".into(), "MORTY".into()) - .err() - .expect("Expected an error"); + let err = process_get_orderbook_request(ctx, "RICK".into(), "MORTY".into()).expect_err("Expected an error"); log!("error: {}", err); assert!(err.contains("Orderbook too large")); @@ -1859,19 +1859,10 @@ fn test_request_and_fill_orderbook() { let ordermatch_ctx = OrdermatchContext::from_ctx(&ctx).unwrap(); let orderbook = ordermatch_ctx.orderbook.lock(); - let expected = expected_orders - .iter() - .map(|(_pubkey, orders)| orders.clone()) - .flatten() - .collect(); + let expected = expected_orders.values().flatten().cloned().collect(); assert_eq!(orderbook.order_set, expected); - let expected = expected_orders - .iter() - .map(|(_pubkey, orders)| orders) - .flatten() - .map(|(uuid, _order)| *uuid) - .collect(); + let expected = expected_orders.values().flatten().map(|(uuid, _order)| *uuid).collect(); let unordered = orderbook .unordered .get(&("RICK".to_owned(), "MORTY".to_owned())) @@ -1879,8 +1870,7 @@ fn test_request_and_fill_orderbook() { assert_eq!(*unordered, expected); let expected = expected_orders - .iter() - .map(|(_pubkey, orders)| orders) + .values() .flatten() .map(|(uuid, order)| OrderedByPriceOrder { uuid: *uuid, @@ -1940,7 +1930,7 @@ fn test_process_order_keep_alive_requested_from_peer() { let (_, mut cmd_rx) = p2p_context_mock(); let (ctx, pubkey, secret) = make_ctx_for_tests(); - let uuid = Uuid::new_v4(); + let uuid = new_uuid(); let peer = PeerId::random().to_string(); let order = new_protocol::MakerOrderCreated { @@ -2022,7 +2012,7 @@ fn test_process_get_order_request() { let mut orderbook = block_on(ordermatch_ctx.orderbook.lock()); let order = new_protocol::MakerOrderCreated { - uuid: Uuid::new_v4().into(), + uuid: new_uuid().into(), base: "RICK".into(), rel: "MORTY".into(), price: BigRational::from_integer(1000000.into()), @@ -2199,7 +2189,7 @@ fn test_taker_request_can_match_with_maker_pubkey() { assert!(order.request.can_match_with_maker_pubkey(&maker_pubkey)); let mut set = HashSet::new(); - set.insert(maker_pubkey.clone()); + set.insert(maker_pubkey); order.request.match_by = MatchBy::Pubkeys(set); assert!(order.request.can_match_with_maker_pubkey(&maker_pubkey)); @@ -2209,7 +2199,7 @@ fn test_taker_request_can_match_with_maker_pubkey() { #[test] fn test_taker_request_can_match_with_uuid() { - let uuid = Uuid::new_v4(); + let uuid = new_uuid(); let coin = MmCoinEnum::Test(TestCoin::default()); // default has MatchBy::Any @@ -2322,7 +2312,7 @@ fn test_diff_should_not_be_written_if_hash_not_changed_on_insert() { let alb_ordered_pair = alb_ordered_pair("C1", "C2"); let pair_trie_root = pair_trie_root_by_pub(&ctx, &pubkey, &alb_ordered_pair); - for order in orders.clone() { + for order in orders { insert_or_update_order(&ctx, order); } @@ -2581,7 +2571,7 @@ fn test_process_sync_pubkey_orderbook_state_points_to_not_uptodate_trie_root() { assert_eq!(full_trie, expected); } -fn check_if_orderbook_contains_only(orderbook: &Orderbook, pubkey: &str, orders: &Vec) { +fn check_if_orderbook_contains_only(orderbook: &Orderbook, pubkey: &str, orders: &[OrderbookItem]) { let pubkey_state = orderbook.pubkeys_state.get(pubkey).expect("!pubkeys_state"); // order_set @@ -2717,7 +2707,7 @@ fn test_orderbook_sync_trie_diff_time_cache() { let bob_root = bob_state.trie_roots.get(&rick_morty_pair).unwrap(); let bob_history_on_sync = DeltaOrFullTrie::from_history( - &rick_morty_history_bob, + rick_morty_history_bob, *alice_root, *bob_root, &orderbook_bob.memory_db, @@ -2768,7 +2758,7 @@ fn test_orderbook_sync_trie_diff_time_cache() { let bob_root = bob_state.trie_roots.get(&rick_morty_pair).unwrap(); let bob_history_on_sync = DeltaOrFullTrie::from_history( - &rick_morty_history_bob, + rick_morty_history_bob, *alice_root, *bob_root, &orderbook_bob.memory_db, @@ -2881,7 +2871,7 @@ fn test_trie_state_bytes() { let price = BigRational::from_integer(1.into()); let max_volume = BigRational::from_integer(u64::MAX.into()); let min_volume = BigRational::from_integer(1.into()); - let uuid = Uuid::new_v4(); + let uuid = new_uuid(); let created_at = now_ms() / 1000; #[derive(Serialize)] @@ -2998,11 +2988,11 @@ fn check_get_orderbook_p2p_res_serde() { let v1_serialized = rmp_serde::to_vec(&v1).unwrap(); let mut new: GetOrderbookRes = rmp_serde::from_read_ref(&v1_serialized).unwrap(); - new.protocol_infos.insert(Uuid::new_v4(), BaseRelProtocolInfo { + new.protocol_infos.insert(new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], }); - new.conf_infos.insert(Uuid::new_v4(), OrderConfirmationsSettings { + new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings { base_confs: 6, base_nota: false, rel_confs: 3, @@ -3030,7 +3020,7 @@ fn check_get_orderbook_p2p_res_serde() { let v2 = GetOrderbookResV2 { pubkey_orders: HashMap::from_iter(std::iter::once(("pubkey".into(), item))), - protocol_infos: HashMap::from_iter(std::iter::once((Uuid::new_v4(), BaseRelProtocolInfo { + protocol_infos: HashMap::from_iter(std::iter::once((new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], }))), @@ -3039,7 +3029,7 @@ fn check_get_orderbook_p2p_res_serde() { let v2_serialized = rmp_serde::to_vec(&v2).unwrap(); let mut new: GetOrderbookRes = rmp_serde::from_read_ref(&v2_serialized).unwrap(); - new.conf_infos.insert(Uuid::new_v4(), OrderConfirmationsSettings { + new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings { base_confs: 6, base_nota: false, rel_confs: 3, @@ -3107,11 +3097,11 @@ fn check_sync_pubkey_state_p2p_res_serde() { let v1_serialized = rmp_serde::to_vec(&v1).unwrap(); let mut new: SyncPubkeyOrderbookStateRes = rmp_serde::from_read_ref(&v1_serialized).unwrap(); - new.protocol_infos.insert(Uuid::new_v4(), BaseRelProtocolInfo { + new.protocol_infos.insert(new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], }); - new.conf_infos.insert(Uuid::new_v4(), OrderConfirmationsSettings { + new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings { base_confs: 6, base_nota: false, rel_confs: 3, @@ -3137,7 +3127,7 @@ fn check_sync_pubkey_state_p2p_res_serde() { alb_ordered_pair("RICK", "MORTY"), DeltaOrFullTrie::FullTrie(orders.into_iter().map(|order| (order.uuid, order.into())).collect()), ))), - protocol_infos: HashMap::from_iter(std::iter::once((Uuid::new_v4(), BaseRelProtocolInfo { + protocol_infos: HashMap::from_iter(std::iter::once((new_uuid(), BaseRelProtocolInfo { base: vec![1], rel: vec![2], }))), @@ -3146,7 +3136,7 @@ fn check_sync_pubkey_state_p2p_res_serde() { let v2_serialized = rmp_serde::to_vec(&v2).unwrap(); let mut new: SyncPubkeyOrderbookStateRes = rmp_serde::from_read_ref(&v2_serialized).unwrap(); - new.conf_infos.insert(Uuid::new_v4(), OrderConfirmationsSettings { + new.conf_infos.insert(new_uuid(), OrderConfirmationsSettings { base_confs: 6, base_nota: false, rel_confs: 3, diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index c94ec2714f..c0c338dab6 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -144,7 +144,7 @@ async fn process_json_batch_requests(ctx: MmArc, requests: &[Json], client: Sock #[cfg(target_arch = "wasm32")] async fn process_json_request(ctx: MmArc, req_json: Json, client: SocketAddr) -> Result { if let Some(requests) = req_json.as_array() { - return process_json_batch_requests(ctx, &requests, client) + return process_json_batch_requests(ctx, requests, client) .await .map_err(|e| ERRL!("{}", e)); } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index a553ff9c51..43ab6b8c9e 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -11,6 +11,8 @@ use crate::{mm2::lp_stats::{add_node_to_version_stat, remove_node_from_version_s mm2::rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; +#[cfg(feature = "enable-nft-integration")] use coins::nft; +use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels, ibc_withdraw}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, get_enabled_coins::get_enabled_coins, @@ -28,9 +30,14 @@ use coins::utxo::bch::BchCoin; use coins::utxo::qtum::QtumCoin; use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; -use coins::{add_delegation, get_raw_transaction, get_staking_infos, remove_delegation, sign_message, verify_message, - withdraw}; -#[cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))] +use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_infos, remove_delegation, sign_message, + verify_message, withdraw}; +#[cfg(all( + feature = "enable-solana", + not(target_os = "ios"), + not(target_os = "android"), + not(target_arch = "wasm32") +))] use coins::{SolanaCoin, SplToken}; use coins_activation::{cancel_init_l2, cancel_init_standalone_coin, enable_platform_coin_with_tokens, enable_token, init_l2, init_l2_status, init_l2_user_action, init_standalone_coin, @@ -42,6 +49,8 @@ use http::Response; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_rpc::mm_protocol::{MmRpcBuilder, MmRpcRequest, MmRpcVersion}; +#[cfg(feature = "enable-nft-integration")] +use nft::{get_nft_list, get_nft_metadata, get_nft_transfers, withdraw_nft}; use serde::de::DeserializeOwned; use serde_json::{self as json, Value as Json}; use std::net::SocketAddr; @@ -159,7 +168,14 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, get_current_mtp_rpc).await, "get_enabled_coins" => handle_mmrpc(ctx, request, get_enabled_coins).await, "get_locked_amount" => handle_mmrpc(ctx, request, get_locked_amount_rpc).await, + "get_my_address" => handle_mmrpc(ctx, request, get_my_address).await, "get_new_address" => handle_mmrpc(ctx, request, get_new_address).await, + #[cfg(feature = "enable-nft-integration")] + "get_nft_list" => handle_mmrpc(ctx, request, get_nft_list).await, + #[cfg(feature = "enable-nft-integration")] + "get_nft_metadata" => handle_mmrpc(ctx, request, get_nft_metadata).await, + #[cfg(feature = "enable-nft-integration")] + "get_nft_transfers" => handle_mmrpc(ctx, request, get_nft_transfers).await, "get_public_key" => handle_mmrpc(ctx, request, get_public_key).await, "get_public_key_hash" => handle_mmrpc(ctx, request, get_public_key_hash).await, "get_raw_transaction" => handle_mmrpc(ctx, request, get_raw_transaction).await, @@ -181,13 +197,18 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, update_version_stat_collection).await, "verify_message" => handle_mmrpc(ctx, request, verify_message).await, "withdraw" => handle_mmrpc(ctx, request, withdraw).await, + "ibc_withdraw" => handle_mmrpc(ctx, request, ibc_withdraw).await, + "ibc_chains" => handle_mmrpc(ctx, request, ibc_chains).await, + "ibc_transfer_channels" => handle_mmrpc(ctx, request, ibc_transfer_channels).await, + #[cfg(feature = "enable-nft-integration")] + "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, #[cfg(not(target_arch = "wasm32"))] native_only_methods => match native_only_methods { - #[cfg(all(not(target_os = "ios"), not(target_os = "android")))] + #[cfg(all(feature = "enable-solana", not(target_os = "ios"), not(target_os = "android")))] "enable_solana_with_tokens" => { handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await }, - #[cfg(all(not(target_os = "ios"), not(target_os = "android")))] + #[cfg(all(feature = "enable-solana", not(target_os = "ios"), not(target_os = "android")))] "enable_spl" => handle_mmrpc(ctx, request, enable_token::).await, "z_coin_tx_history" => handle_mmrpc(ctx, request, coins::my_tx_history_v2::z_coin_tx_history_rpc).await, _ => MmError::err(DispatcherError::NoSuchMethod), 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 9e81d2c3b3..40a6514c88 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -1,5 +1,7 @@ pub use common::{block_on, now_ms}; pub use mm2_number::MmNumber; +use mm2_test_helpers::for_tests::ETH_SEPOLIA_NODE; +use mm2_test_helpers::for_tests::ETH_SEPOLIA_SWAP_CONTRACT; pub use mm2_test_helpers::for_tests::{check_my_swap_status, check_recent_swaps, check_stats_swap_status, enable_native_bch, mm_dump, MarketMakerIt, MAKER_ERROR_EVENTS, MAKER_SUCCESS_EVENTS, TAKER_ERROR_EVENTS, TAKER_SUCCESS_EVENTS}; @@ -20,7 +22,7 @@ use coins::utxo::utxo_common::send_outputs_from_my_address; use coins::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; use coins::utxo::{coin_daemon_data_dir, sat_from_big_decimal, zcash_params_path, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps}; -use coins::{CoinProtocol, MarketCoinOps, PrivKeyBuildPolicy, Transaction}; +use coins::{CoinProtocol, ConfirmPaymentInput, MarketCoinOps, PrivKeyBuildPolicy, Transaction}; use crypto::privkey::key_pair_from_seed; use crypto::Secp256k1Secret; use ethereum_types::H160 as H160Eth; @@ -163,15 +165,16 @@ pub fn fill_eth(to_addr: &str) { .unwrap(); } -pub fn generate_eth_coin_with_random_privkey() -> EthCoin { +// Generates an ethereum coin in the sepolia network with the given seed +pub fn generate_eth_coin_with_seed(seed: &str) -> EthCoin { let req = json!({ "method": "enable", "coin": "ETH", - "urls": ETH_DEV_NODES, - "swap_contract_address": ETH_DEV_SWAP_CONTRACT, + "urls": ETH_SEPOLIA_NODE, + "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, }); - let priv_key = random_secp256k1_secret(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(priv_key); + let keypair = key_pair_from_seed(seed).unwrap(); + let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(keypair.private().secret); block_on(eth_coin_from_conf_and_request( &MM_CTX, "ETH", @@ -187,8 +190,8 @@ pub fn generate_jst_with_seed(seed: &str) -> EthCoin { let req = json!({ "method": "enable", "coin": "JST", - "urls": ETH_DEV_NODES, - "swap_contract_address": ETH_DEV_SWAP_CONTRACT, + "urls": ETH_SEPOLIA_NODE, + "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, }); let keypair = key_pair_from_seed(seed).unwrap(); @@ -200,7 +203,7 @@ pub fn generate_jst_with_seed(seed: &str) -> EthCoin { &req, CoinProtocol::ERC20 { platform: "ETH".into(), - contract_address: String::from("0x2b294F029Fde858b2c62184e8390591755521d8E"), + contract_address: String::from("0x948BF5172383F1Bc0Fdf3aBe0630b855694A5D2c"), }, priv_key_policy, )) @@ -274,10 +277,14 @@ impl BchDockerOps { let slp_genesis_tx = send_outputs_from_my_address(self.coin.clone(), bch_outputs) .wait() .unwrap(); - self.coin - .wait_for_confirmations(&slp_genesis_tx.tx_hex(), 1, false, now_ms() / 1000 + 30, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: slp_genesis_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: now_ms() / 1000 + 30, + check_every: 1, + }; + self.coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let adex_slp = SlpToken::new( 8, @@ -289,10 +296,14 @@ impl BchDockerOps { .unwrap(); let tx = block_on(adex_slp.send_slp_outputs(slp_outputs)).unwrap(); - self.coin - .wait_for_confirmations(&tx.tx_hex(), 1, false, now_ms() / 1000 + 30, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: now_ms() / 1000 + 30, + check_every: 1, + }; + self.coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); *SLP_TOKEN_OWNERS.lock().unwrap() = slp_privkeys; *SLP_TOKEN_ID.lock().unwrap() = slp_genesis_tx.tx_hash().as_slice().into(); } @@ -549,9 +560,14 @@ pub fn fill_qrc20_address(coin: &Qrc20Coin, amount: BigDecimal, timeout: u64) { let tx_bytes = client.get_transaction_bytes(&hash).wait().unwrap(); log!("{:02x}", tx_bytes); - coin.wait_for_confirmations(&tx_bytes, 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx_bytes.clone().0, + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); } /// Generate random privkey, create a QRC20 coin and fill it's address with the specified balance. @@ -662,9 +678,14 @@ where client.import_address(address, address, false).wait().unwrap(); let hash = client.send_to_address(address, &amount).wait().unwrap(); let tx_bytes = client.get_transaction_bytes(&hash).wait().unwrap(); - coin.wait_for_confirmations(&tx_bytes, 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx_bytes.clone().0, + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); log!("{:02x}", tx_bytes); loop { let unspents = client 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 891a817503..6a46d62661 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -5,9 +5,8 @@ use bitcrypto::dhash160; use chain::OutPoint; use coins::utxo::rpc_clients::UnspentInfo; use coins::utxo::{GetUtxoListOps, UtxoCommonOps}; -use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoin, SearchForSwapTxSpendInput, SendMakerPaymentArgs, - SendMakerRefundsPaymentArgs, SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, - SendTakerSpendsMakerPaymentArgs, SwapOps, TransactionEnum, WithdrawRequest}; +use coins::{ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapOps, TransactionEnum, WithdrawRequest}; use common::{block_on, now_ms}; use futures01::Future; use mm2_number::{BigDecimal, MmNumber}; @@ -28,7 +27,7 @@ fn test_search_for_swap_tx_spend_native_was_refunded_taker() { let my_public_key = coin.my_public_key().unwrap(); let time_lock = (now_ms() / 1000) as u32 - 3600; - let taker_payment_args = SendTakerPaymentArgs { + let taker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock, other_pubkey: my_public_key, @@ -37,28 +36,41 @@ fn test_search_for_swap_tx_spend_native_was_refunded_taker() { swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_taker_payment(taker_payment_args).wait().unwrap(); - coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); - let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + let maker_refunds_payment_args = RefundPaymentArgs { payment_tx: &tx.tx_hex(), time_lock, other_pubkey: my_public_key, secret_hash: &[0; 20], swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let refund_tx = coin .send_maker_refunds_payment(maker_refunds_payment_args) .wait() .unwrap(); - coin.wait_for_confirmations(&refund_tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock, @@ -68,6 +80,7 @@ fn test_search_for_swap_tx_spend_native_was_refunded_taker() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -82,11 +95,14 @@ fn test_for_non_existent_tx_hex_utxo() { let (_ctx, coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); // bad transaction hex let tx = hex::decode("0400008085202f8902bf17bf7d1daace52e08f732a6b8771743ca4b1cb765a187e72fd091a0aabfd52000000006a47304402203eaaa3c4da101240f80f9c5e9de716a22b1ec6d66080de6a0cca32011cd77223022040d9082b6242d6acf9a1a8e658779e1c655d708379862f235e8ba7b8ca4e69c6012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffffff023ca13c0e9e085dd13f481f193e8a3e8fd609020936e98b5587342d994f4d020000006b483045022100c0ba56adb8de923975052312467347d83238bd8d480ce66e8b709a7997373994022048507bcac921fdb2302fa5224ce86e41b7efc1a2e20ae63aa738dfa99b7be826012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0300e1f5050000000017a9141ee6d4c38a3c078eab87ad1a5e4b00f21259b10d87000000000000000016611400000000000000000000000000000000000000001b94d736000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac2d08e35e000000000000000000000000000000").unwrap(); - let actual = coin - .wait_for_confirmations(&tx, 1, false, timeout, 1) - .wait() - .err() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx, + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + let actual = coin.wait_for_confirmations(confirm_payment_input).wait().err().unwrap(); assert!(actual.contains( "Tx d342ff9da528a2e262bddf2b6f9a27d1beb7aeb03f0fc8d9eac2987266447e44 was not found on chain after 10 tries" )); @@ -99,7 +115,7 @@ fn test_search_for_swap_tx_spend_native_was_refunded_maker() { let my_public_key = coin.my_public_key().unwrap(); let time_lock = (now_ms() / 1000) as u32 - 3600; - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock, other_pubkey: my_public_key, @@ -108,28 +124,41 @@ fn test_search_for_swap_tx_spend_native_was_refunded_maker() { swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_maker_payment(maker_payment_args).wait().unwrap(); - coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); - let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + let maker_refunds_payment_args = RefundPaymentArgs { payment_tx: &tx.tx_hex(), time_lock, other_pubkey: my_public_key, secret_hash: &[0; 20], swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let refund_tx = coin .send_maker_refunds_payment(maker_refunds_payment_args) .wait() .unwrap(); - coin.wait_for_confirmations(&refund_tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock, @@ -139,6 +168,7 @@ fn test_search_for_swap_tx_spend_native_was_refunded_maker() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -155,7 +185,7 @@ fn test_search_for_taker_swap_tx_spend_native_was_spent_by_maker() { let secret_hash = dhash160(&secret); let time_lock = (now_ms() / 1000) as u32 - 3600; - let taker_payment_args = SendTakerPaymentArgs { + let taker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock, other_pubkey: my_pubkey, @@ -164,13 +194,20 @@ fn test_search_for_taker_swap_tx_spend_native_was_spent_by_maker() { swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_taker_payment(taker_payment_args).wait().unwrap(); - coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); - let maker_spends_payment_args = SendMakerSpendsTakerPaymentArgs { + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + let maker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &tx.tx_hex(), time_lock, other_pubkey: my_pubkey, @@ -178,15 +215,21 @@ fn test_search_for_taker_swap_tx_spend_native_was_spent_by_maker() { secret_hash: secret_hash.as_slice(), swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let spend_tx = coin .send_maker_spends_taker_payment(maker_spends_payment_args) .wait() .unwrap(); - coin.wait_for_confirmations(&spend_tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: spend_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock, @@ -196,6 +239,7 @@ fn test_search_for_taker_swap_tx_spend_native_was_spent_by_maker() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -212,7 +256,7 @@ fn test_search_for_maker_swap_tx_spend_native_was_spent_by_taker() { let time_lock = (now_ms() / 1000) as u32 - 3600; let secret_hash = dhash160(&secret); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock, other_pubkey: my_pubkey, @@ -221,13 +265,20 @@ fn test_search_for_maker_swap_tx_spend_native_was_spent_by_taker() { swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_maker_payment(maker_payment_args).wait().unwrap(); - coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); - let taker_spends_payment_args = SendTakerSpendsMakerPaymentArgs { + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + let taker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &tx.tx_hex(), time_lock, other_pubkey: my_pubkey, @@ -235,15 +286,21 @@ fn test_search_for_maker_swap_tx_spend_native_was_spent_by_taker() { secret_hash: secret_hash.as_slice(), swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let spend_tx = coin .send_taker_spends_maker_payment(taker_spends_payment_args) .wait() .unwrap(); - coin.wait_for_confirmations(&spend_tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: spend_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock, @@ -253,6 +310,7 @@ fn test_search_for_maker_swap_tx_spend_native_was_spent_by_taker() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -272,7 +330,7 @@ fn test_one_hundred_maker_payments_in_a_row_native() { let mut unspents = vec![]; let mut sent_tx = vec![]; for i in 0..100 { - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: time_lock + i, other_pubkey: my_pubkey, @@ -281,6 +339,8 @@ fn test_one_hundred_maker_payments_in_a_row_native() { swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_maker_payment(maker_payment_args).wait().unwrap(); if let TransactionEnum::UtxoTx(tx) = tx { @@ -2413,9 +2473,14 @@ fn test_maker_order_should_not_kick_start_and_appear_in_orderbook_if_balance_is_ .wait() .unwrap(); coin.send_raw_tx(&hex::encode(&withdraw.tx_hex.0)).wait().unwrap(); - coin.wait_for_confirmations(&withdraw.tx_hex.0, 1, false, (now_ms() / 1000) + 10, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: withdraw.tx_hex.0, + confirmations: 1, + requires_nota: false, + wait_until: (now_ms() / 1000) + 10, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let mm_bob_dup = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dup_dump_log, _bob_dup_dump_dashboard) = mm_dump(&mm_bob_dup.log_path); @@ -3376,6 +3441,7 @@ fn test_match_utxo_with_eth_taker_sell() { #[test] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1074 +#[ignore] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 fn test_match_utxo_with_eth_taker_buy() { let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000.into()); diff --git a/mm2src/mm2_main/tests/docker_tests/mod.rs b/mm2src/mm2_main/tests/docker_tests/mod.rs index 837d09fcd0..300ebbc63c 100644 --- a/mm2src/mm2_main/tests/docker_tests/mod.rs +++ b/mm2src/mm2_main/tests/docker_tests/mod.rs @@ -8,7 +8,7 @@ mod swap_watcher_tests; mod swaps_confs_settings_sync_tests; mod swaps_file_lock_tests; -#[cfg(not(feature = "disable-solana-tests"))] mod solana_tests; +#[cfg(feature = "enable-solana")] mod solana_tests; // dummy test helping IDE to recognize this as test module #[test] diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 838a148095..3601baa886 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -6,10 +6,9 @@ use coins::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin}; use coins::utxo::rpc_clients::UtxoRpcClientEnum; use coins::utxo::utxo_common::big_decimal_from_sat; use coins::utxo::{UtxoActivationParams, UtxoCommonOps}; -use coins::{CheckIfMyPaymentSentArgs, FeeApproxStage, FoundSwapTxSpend, MarketCoinOps, MmCoin, - SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerRefundsPaymentArgs, - SendMakerSpendsTakerPaymentArgs, SendTakerPaymentArgs, SendTakerRefundsPaymentArgs, - SendTakerSpendsMakerPaymentArgs, SwapOps, TradePreimageValue, TransactionEnum, ValidatePaymentInput}; +use coins::{CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, MarketCoinOps, MmCoin, + RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapOps, + TradePreimageValue, TransactionEnum, ValidatePaymentInput}; use common::log::debug; use common::{temp_dir, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::Secp256k1Secret; @@ -182,7 +181,7 @@ fn test_taker_spends_maker_payment() { let secret = &[1; 32]; let secret_hash = dhash160(secret).to_vec(); let amount = BigDecimal::try_from(0.2).unwrap(); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: &taker_pub, @@ -191,6 +190,8 @@ fn test_taker_spends_maker_payment() { swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = maker_coin.send_maker_payment(maker_payment_args).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -201,10 +202,14 @@ fn test_taker_spends_maker_payment() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - taker_coin - .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex.clone(), + confirmations, + requires_nota, + wait_until, + check_every, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let input = ValidatePaymentInput { payment_tx: payment_tx_hex.clone(), @@ -217,9 +222,10 @@ fn test_taker_spends_maker_payment() { try_spv_proof_until: wait_until + 30, confirmations, unique_swap_data: Vec::new(), + min_watcher_reward: None, }; taker_coin.validate_maker_payment(input).wait().unwrap(); - let taker_spends_payment_args = SendTakerSpendsMakerPaymentArgs { + let taker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &payment_tx_hex, time_lock: timelock, other_pubkey: &maker_pub, @@ -227,6 +233,7 @@ fn test_taker_spends_maker_payment() { secret_hash: &secret_hash, swap_contract_address: &taker_coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let spend = taker_coin .send_taker_spends_maker_payment(taker_spends_payment_args) @@ -237,10 +244,14 @@ fn test_taker_spends_maker_payment() { log!("Taker spends tx: {:?}", spend_tx_hash); let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run - taker_coin - .wait_for_confirmations(&spend_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: spend_tx_hex, + confirmations, + requires_nota, + wait_until, + check_every, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let maker_balance = maker_coin .my_spendable_balance() @@ -275,7 +286,7 @@ fn test_maker_spends_taker_payment() { let secret = &[1; 32]; let secret_hash = dhash160(secret).to_vec(); let amount = BigDecimal::try_from(0.2).unwrap(); - let taker_payment_args = SendTakerPaymentArgs { + let taker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: &maker_pub, @@ -284,6 +295,8 @@ fn test_maker_spends_taker_payment() { swap_contract_address: &taker_coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = taker_coin.send_taker_payment(taker_payment_args).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -294,10 +307,14 @@ fn test_maker_spends_taker_payment() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - maker_coin - .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex.clone(), + confirmations, + requires_nota, + wait_until, + check_every, + }; + maker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let input = ValidatePaymentInput { payment_tx: payment_tx_hex.clone(), @@ -310,9 +327,10 @@ fn test_maker_spends_taker_payment() { try_spv_proof_until: wait_until + 30, confirmations, unique_swap_data: Vec::new(), + min_watcher_reward: None, }; maker_coin.validate_taker_payment(input).wait().unwrap(); - let maker_spends_payment_args = SendMakerSpendsTakerPaymentArgs { + let maker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &payment_tx_hex, time_lock: timelock, other_pubkey: &taker_pub, @@ -320,6 +338,7 @@ fn test_maker_spends_taker_payment() { secret_hash: &secret_hash, swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let spend = maker_coin .send_maker_spends_taker_payment(maker_spends_payment_args) @@ -330,10 +349,14 @@ fn test_maker_spends_taker_payment() { log!("Maker spends tx: {:?}", spend_tx_hash); let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run - maker_coin - .wait_for_confirmations(&spend_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: spend_tx_hex, + confirmations, + requires_nota, + wait_until, + check_every, + }; + maker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let maker_balance = maker_coin .my_spendable_balance() @@ -357,7 +380,7 @@ fn test_maker_refunds_payment() { let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); let secret_hash = &[1; 20]; let amount = BigDecimal::from_str("0.2").unwrap(); - let maker_payment = SendMakerPaymentArgs { + let maker_payment = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: &taker_pub, @@ -366,6 +389,8 @@ fn test_maker_refunds_payment() { swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = coin.send_maker_payment(maker_payment).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -376,19 +401,25 @@ fn test_maker_refunds_payment() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - coin.wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex.clone(), + confirmations, + requires_nota, + wait_until, + check_every, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let balance_after_payment = coin.my_spendable_balance().wait().unwrap(); assert_eq!(expected_balance.clone() - amount, balance_after_payment); - let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + let maker_refunds_payment_args = RefundPaymentArgs { payment_tx: &payment_tx_hex, time_lock: timelock, other_pubkey: &taker_pub, secret_hash, swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let refund = coin .send_maker_refunds_payment(maker_refunds_payment_args) @@ -399,9 +430,14 @@ fn test_maker_refunds_payment() { log!("Maker refunds payment: {:?}", refund_tx_hash); let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run - coin.wait_for_confirmations(&refund_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx_hex, + confirmations, + requires_nota, + wait_until, + check_every, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let balance_after_refund = coin.my_spendable_balance().wait().unwrap(); assert_eq!(expected_balance, balance_after_refund); @@ -417,7 +453,7 @@ fn test_taker_refunds_payment() { let maker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); let secret_hash = &[1; 20]; let amount = BigDecimal::from_str("0.2").unwrap(); - let taker_payment_args = SendTakerPaymentArgs { + let taker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: &maker_pub, @@ -426,6 +462,8 @@ fn test_taker_refunds_payment() { swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = coin.send_taker_payment(taker_payment_args).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -436,19 +474,25 @@ fn test_taker_refunds_payment() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - coin.wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex.clone(), + confirmations, + requires_nota, + wait_until, + check_every, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let balance_after_payment = coin.my_spendable_balance().wait().unwrap(); assert_eq!(expected_balance.clone() - amount, balance_after_payment); - let taker_refunds_payment_args = SendTakerRefundsPaymentArgs { + let taker_refunds_payment_args = RefundPaymentArgs { payment_tx: &payment_tx_hex, time_lock: timelock, other_pubkey: &maker_pub, secret_hash, swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let refund = coin .send_taker_refunds_payment(taker_refunds_payment_args) @@ -459,9 +503,14 @@ fn test_taker_refunds_payment() { log!("Taker refunds payment: {:?}", refund_tx_hash); let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run - coin.wait_for_confirmations(&refund_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx_hex, + confirmations, + requires_nota, + wait_until, + check_every, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let balance_after_refund = coin.my_spendable_balance().wait().unwrap(); assert_eq!(expected_balance, balance_after_refund); @@ -474,7 +523,7 @@ fn test_check_if_my_payment_sent() { let taker_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); let secret_hash = &[1; 20]; let amount = BigDecimal::from_str("0.2").unwrap(); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: &taker_pub, @@ -483,6 +532,8 @@ fn test_check_if_my_payment_sent() { swap_contract_address: &coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = coin.send_maker_payment(maker_payment_args).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -493,9 +544,14 @@ fn test_check_if_my_payment_sent() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - coin.wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex, + confirmations, + requires_nota, + wait_until, + check_every, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_from_block = coin.current_block().wait().expect("!current_block") - 10; let if_my_payment_sent_args = CheckIfMyPaymentSentArgs { @@ -524,7 +580,7 @@ fn test_search_for_swap_tx_spend_taker_spent() { let secret = &[1; 32]; let secret_hash = dhash160(secret); let amount = BigDecimal::try_from(0.2).unwrap(); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: taker_pub, @@ -533,6 +589,8 @@ fn test_search_for_swap_tx_spend_taker_spent() { swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = maker_coin.send_maker_payment(maker_payment_args).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -543,11 +601,15 @@ fn test_search_for_swap_tx_spend_taker_spent() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - taker_coin - .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); - let taker_spends_payment_args = SendTakerSpendsMakerPaymentArgs { + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex.clone(), + confirmations, + requires_nota, + wait_until, + check_every, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + let taker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &payment_tx_hex, time_lock: timelock, other_pubkey: maker_pub, @@ -555,6 +617,7 @@ fn test_search_for_swap_tx_spend_taker_spent() { secret_hash: secret_hash.as_slice(), swap_contract_address: &taker_coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let spend = taker_coin .send_taker_spends_maker_payment(taker_spends_payment_args) @@ -565,10 +628,14 @@ fn test_search_for_swap_tx_spend_taker_spent() { log!("Taker spends tx: {:?}", spend_tx_hash); let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run - taker_coin - .wait_for_confirmations(&spend_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: spend_tx_hex, + confirmations, + requires_nota, + wait_until, + check_every, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock: timelock, @@ -578,6 +645,7 @@ fn test_search_for_swap_tx_spend_taker_spent() { search_from_block, swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let actual = block_on(maker_coin.search_for_swap_tx_spend_my(search_input)); let expected = Ok(Some(FoundSwapTxSpend::Spent(spend))); @@ -594,7 +662,7 @@ fn test_search_for_swap_tx_spend_maker_refunded() { let secret = &[1; 32]; let secret_hash = &*dhash160(secret); let amount = BigDecimal::try_from(0.2).unwrap(); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: &taker_pub, @@ -603,6 +671,8 @@ fn test_search_for_swap_tx_spend_maker_refunded() { swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = maker_coin.send_maker_payment(maker_payment_args).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -613,17 +683,22 @@ fn test_search_for_swap_tx_spend_maker_refunded() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - maker_coin - .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); - let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex.clone(), + confirmations, + requires_nota, + wait_until, + check_every, + }; + maker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + let maker_refunds_payment_args = RefundPaymentArgs { payment_tx: &payment_tx_hex, time_lock: timelock, other_pubkey: &taker_pub, secret_hash, swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let refund = maker_coin .send_maker_refunds_payment(maker_refunds_payment_args) @@ -634,10 +709,14 @@ fn test_search_for_swap_tx_spend_maker_refunded() { log!("Maker refunds tx: {:?}", refund_tx_hash); let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run - maker_coin - .wait_for_confirmations(&refund_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx_hex, + confirmations, + requires_nota, + wait_until, + check_every, + }; + maker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock: timelock, @@ -647,6 +726,7 @@ fn test_search_for_swap_tx_spend_maker_refunded() { search_from_block, swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let actual = block_on(maker_coin.search_for_swap_tx_spend_my(search_input)); let expected = Ok(Some(FoundSwapTxSpend::Refunded(refund))); @@ -663,7 +743,7 @@ fn test_search_for_swap_tx_spend_not_spent() { let secret = &[1; 32]; let secret_hash = &*dhash160(secret); let amount = BigDecimal::try_from(0.2).unwrap(); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: &taker_pub, @@ -672,6 +752,8 @@ fn test_search_for_swap_tx_spend_not_spent() { swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = maker_coin.send_maker_payment(maker_payment_args).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -682,10 +764,14 @@ fn test_search_for_swap_tx_spend_not_spent() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - maker_coin - .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex.clone(), + confirmations, + requires_nota, + wait_until, + check_every, + }; + maker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock: timelock, @@ -695,6 +781,7 @@ fn test_search_for_swap_tx_spend_not_spent() { search_from_block, swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let actual = block_on(maker_coin.search_for_swap_tx_spend_my(search_input)); // maker payment hasn't been spent or refunded yet @@ -713,7 +800,7 @@ fn test_wait_for_tx_spend() { let secret = &[1; 32]; let secret_hash = dhash160(secret); let amount = BigDecimal::try_from(0.2).unwrap(); - let maker_payment_args = SendMakerPaymentArgs { + let maker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: taker_pub, @@ -722,6 +809,8 @@ fn test_wait_for_tx_spend() { swap_contract_address: &maker_coin.swap_contract_address(), swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let payment = maker_coin.send_maker_payment(maker_payment_args).wait().unwrap(); let payment_tx_hash = payment.tx_hash(); @@ -732,10 +821,14 @@ fn test_wait_for_tx_spend() { let requires_nota = false; let wait_until = (now_ms() / 1000) + 40; // timeout if test takes more than 40 seconds to run let check_every = 1; - taker_coin - .wait_for_confirmations(&payment_tx_hex, confirmations, requires_nota, wait_until, check_every) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: payment_tx_hex.clone(), + confirmations, + requires_nota, + wait_until, + check_every, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); // first try to check if the wait_for_htlc_tx_spend() returns an error correctly let wait_until = (now_ms() / 1000) + 5; @@ -762,7 +855,7 @@ fn test_wait_for_tx_spend() { let payment_hex = payment_tx_hex.clone(); thread::spawn(move || { thread::sleep(Duration::from_secs(5)); - let taker_spends_payment_args = SendTakerSpendsMakerPaymentArgs { + let taker_spends_payment_args = SpendPaymentArgs { other_payment_tx: &payment_hex, time_lock: timelock, other_pubkey: &maker_pub_c, @@ -770,6 +863,7 @@ fn test_wait_for_tx_spend() { secret_hash: secret_hash.as_slice(), swap_contract_address: &taker_coin.swap_contract_address(), swap_unique_data: &[], + watcher_reward: false, }; let spend = taker_coin .send_taker_spends_maker_payment(taker_spends_payment_args) @@ -1028,7 +1122,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, dex_fee_amount.to_decimal(), &[]) .wait() .expect("!send_taker_fee"); - let taker_payment_args = SendTakerPaymentArgs { + let taker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock: timelock, other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, @@ -1037,6 +1131,8 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let _taker_payment_tx = coin @@ -1426,7 +1522,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { let my_public_key = coin.my_public_key().unwrap(); let time_lock = (now_ms() / 1000) as u32 - 3600; - let maker_payment = SendMakerPaymentArgs { + let maker_payment = SendPaymentArgs { time_lock_duration: 0, time_lock, other_pubkey: my_public_key, @@ -1435,28 +1531,41 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_maker_payment(maker_payment).wait().unwrap(); - coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); - let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + let maker_refunds_payment_args = RefundPaymentArgs { payment_tx: &tx.tx_hex(), time_lock, other_pubkey: my_public_key, secret_hash: &[0; 20], swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let refund_tx = coin .send_maker_refunds_payment(maker_refunds_payment_args) .wait() .unwrap(); - coin.wait_for_confirmations(&refund_tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock, @@ -1466,6 +1575,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_maker() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() @@ -1481,7 +1591,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { let my_public_key = coin.my_public_key().unwrap(); let time_lock = (now_ms() / 1000) as u32 - 3600; - let taker_payment = SendTakerPaymentArgs { + let taker_payment = SendPaymentArgs { time_lock_duration: 0, time_lock, other_pubkey: my_public_key, @@ -1490,28 +1600,41 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_taker_payment(taker_payment).wait().unwrap(); - coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); - let maker_refunds_payment_args = SendMakerRefundsPaymentArgs { + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + let maker_refunds_payment_args = RefundPaymentArgs { payment_tx: &tx.tx_hex(), time_lock, other_pubkey: my_public_key, secret_hash: &[0; 20], swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let refund_tx = coin .send_maker_refunds_payment(maker_refunds_payment_args) .wait() .unwrap(); - coin.wait_for_confirmations(&refund_tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock, @@ -1521,6 +1644,7 @@ fn test_search_for_segwit_swap_tx_spend_native_was_refunded_taker() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() 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 98ea02ce44..223b4f215c 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -1,18 +1,21 @@ -use crate::docker_tests::docker_tests_common::{eth_distributor, generate_eth_coin_with_random_privkey, - generate_jst_with_seed}; +use crate::docker_tests::docker_tests_common::{eth_distributor, generate_eth_coin_with_seed, generate_jst_with_seed}; use crate::integration_tests_common::*; use crate::{generate_utxo_coin_with_privkey, generate_utxo_coin_with_random_privkey, random_secp256k1_secret, SecretKey}; use coins::coin_errors::ValidatePaymentError; -use coins::utxo::UtxoCommonOps; -use coins::{FoundSwapTxSpend, MarketCoinOps, MmCoinEnum, SearchForSwapTxSpendInput, SendTakerPaymentArgs, - SendWatcherRefundsPaymentArgs, SwapOps, WatcherOps, WatcherValidateTakerFeeInput, - EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; +use coins::utxo::{dhash160, UtxoCommonOps}; +use coins::{ConfirmPaymentInput, FoundSwapTxSpend, MarketCoinOps, MmCoin, MmCoinEnum, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendPaymentArgs, SwapOps, WatcherOps, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, EARLY_CONFIRMATION_ERR_LOG, INSUFFICIENT_WATCHER_REWARD_ERR_LOG, + INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, + INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG, + OLD_TRANSACTION_ERR_LOG}; use common::{block_on, now_ms, DEX_FEE_ADDR_RAW_PUBKEY}; +use crypto::privkey::key_pair_from_secret; use futures01::Future; -use mm2_main::mm2::lp_swap::{dex_fee_amount_from_taker_coin, get_payment_locktime, MAKER_PAYMENT_SENT_LOG, - MAKER_PAYMENT_SPEND_FOUND_LOG, MAKER_PAYMENT_SPEND_SENT_LOG, - TAKER_PAYMENT_REFUND_SENT_LOG, WATCHER_MESSAGE_SENT_LOG}; +use mm2_main::mm2::lp_swap::{dex_fee_amount_from_taker_coin, get_payment_locktime, min_watcher_reward, + watcher_reward_amount, MakerSwap, MAKER_PAYMENT_SENT_LOG, MAKER_PAYMENT_SPEND_FOUND_LOG, + MAKER_PAYMENT_SPEND_SENT_LOG, TAKER_PAYMENT_REFUND_SENT_LOG, WATCHER_MESSAGE_SENT_LOG}; use mm2_number::BigDecimal; use mm2_number::MmNumber; use mm2_test_helpers::for_tests::{enable_eth_coin, eth_jst_conf, eth_testnet_conf, mm_dump, my_balance, mycoin1_conf, @@ -30,7 +33,8 @@ fn enable_eth_and_jst(mm_node: &MarketMakerIt) { "ETH", ETH_SEPOLIA_NODE, ETH_SEPOLIA_SWAP_CONTRACT, - Some(ETH_SEPOLIA_SWAP_CONTRACT) + Some(ETH_SEPOLIA_SWAP_CONTRACT), + true ))); dbg!(block_on(enable_eth_coin( @@ -38,7 +42,8 @@ fn enable_eth_and_jst(mm_node: &MarketMakerIt) { "JST", ETH_SEPOLIA_NODE, ETH_SEPOLIA_SWAP_CONTRACT, - Some(ETH_SEPOLIA_SWAP_CONTRACT) + Some(ETH_SEPOLIA_SWAP_CONTRACT), + true ))); } @@ -55,7 +60,7 @@ fn test_watcher_spends_maker_payment_spend_eth_erc20() { log!("Alice log path: {}", mm_alice.log_path.display()); let bob_passphrase = String::from("also shoot benefit prefer juice shell elder veteran woman mimic image kidney"); - let bob_conf = Mm2TestConf::light_node(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); + let bob_conf = Mm2TestConf::light_node_using_watchers(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); @@ -81,6 +86,7 @@ fn test_watcher_spends_maker_payment_spend_eth_erc20() { let alice_jst_balance_before = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); let bob_eth_balance_before = block_on(my_balance(&mm_bob, "ETH")).balance.with_scale(2); let bob_jst_balance_before = block_on(my_balance(&mm_bob, "JST")).balance.with_scale(2); + let watcher_eth_balance_before = block_on(my_balance(&mm_watcher, "ETH")).balance; block_on(start_swaps(&mut mm_bob, &mut mm_alice, &[("ETH", "JST")], 1., 1., 0.01)); @@ -96,12 +102,101 @@ fn test_watcher_spends_maker_payment_spend_eth_erc20() { let alice_jst_balance_after = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); let bob_eth_balance_after = block_on(my_balance(&mm_bob, "ETH")).balance.with_scale(2); let bob_jst_balance_after = block_on(my_balance(&mm_bob, "JST")).balance.with_scale(2); + let watcher_eth_balance_after = block_on(my_balance(&mm_watcher, "ETH")).balance; let volume = BigDecimal::from_str("0.01").unwrap(); assert_eq!(alice_jst_balance_before - volume.clone(), alice_jst_balance_after); assert_eq!(bob_jst_balance_before + volume.clone(), bob_jst_balance_after); assert_eq!(alice_eth_balance_before + volume.clone(), alice_eth_balance_after); assert_eq!(bob_eth_balance_before - volume.clone(), bob_eth_balance_after); + assert!(watcher_eth_balance_after > watcher_eth_balance_before); +} + +#[test] +#[ignore] +fn test_two_watchers_spend_maker_payment_eth_erc20() { + let coins = json!([eth_testnet_conf(), eth_jst_conf(ETH_SEPOLIA_TOKEN_CONTRACT)]); + + let alice_passphrase = + String::from("spice describe gravity federal blast come thank unfair canal monkey style afraid"); + let alice_conf = Mm2TestConf::seednode_using_watchers(&alice_passphrase, &coins); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf.clone(), alice_conf.rpc_password.clone(), None).unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + + let bob_passphrase = String::from("also shoot benefit prefer juice shell elder veteran woman mimic image kidney"); + let bob_conf = Mm2TestConf::light_node_using_watchers(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + let watcher1_passphrase = + String::from("also shoot benefit prefer juice shell thank unfair canal monkey style afraid"); + let watcher1_conf = + Mm2TestConf::watcher_light_node(&watcher1_passphrase, &coins, &[&mm_alice.ip.to_string()], WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 0., + refund_start_factor: 1.5, + search_interval: 1.0, + }) + .conf; + let mut mm_watcher1 = MarketMakerIt::start(watcher1_conf, DEFAULT_RPC_PASSWORD.to_string(), None).unwrap(); + let (_watcher_dump_log, _watcher_dump_dashboard) = mm_dump(&mm_watcher1.log_path); + + let watcher2_passphrase = + String::from("also shoot benefit shell thank prefer juice unfair canal monkey style afraid"); + let watcher2_conf = + Mm2TestConf::watcher_light_node(&watcher2_passphrase, &coins, &[&mm_alice.ip.to_string()], WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 0., + refund_start_factor: 1.5, + search_interval: 1.0, + }) + .conf; + let mut mm_watcher2 = MarketMakerIt::start(watcher2_conf, DEFAULT_RPC_PASSWORD.to_string(), None).unwrap(); + let (_watcher_dump_log, _watcher_dump_dashboard) = mm_dump(&mm_watcher1.log_path); + + enable_eth_and_jst(&mm_alice); + enable_eth_and_jst(&mm_bob); + enable_eth_and_jst(&mm_watcher1); + enable_eth_and_jst(&mm_watcher2); + + let alice_eth_balance_before = block_on(my_balance(&mm_alice, "ETH")).balance.with_scale(2); + let alice_jst_balance_before = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); + let bob_eth_balance_before = block_on(my_balance(&mm_bob, "ETH")).balance.with_scale(2); + let bob_jst_balance_before = block_on(my_balance(&mm_bob, "JST")).balance.with_scale(2); + let watcher1_eth_balance_before = block_on(my_balance(&mm_watcher1, "ETH")).balance; + let watcher2_eth_balance_before = block_on(my_balance(&mm_watcher2, "ETH")).balance; + + block_on(start_swaps(&mut mm_bob, &mut mm_alice, &[("ETH", "JST")], 1., 1., 0.01)); + + block_on(mm_alice.wait_for_log(180., |log| log.contains(WATCHER_MESSAGE_SENT_LOG))).unwrap(); + block_on(mm_alice.stop()).unwrap(); + block_on(mm_watcher1.wait_for_log(180., |log| log.contains(MAKER_PAYMENT_SPEND_SENT_LOG))).unwrap(); + block_on(mm_watcher2.wait_for_log(180., |log| log.contains(MAKER_PAYMENT_SPEND_SENT_LOG))).unwrap(); + thread::sleep(Duration::from_secs(25)); + + let mm_alice = MarketMakerIt::start(alice_conf.conf.clone(), alice_conf.rpc_password.clone(), None).unwrap(); + enable_eth_and_jst(&mm_alice); + + let alice_eth_balance_after = block_on(my_balance(&mm_alice, "ETH")).balance.with_scale(2); + let alice_jst_balance_after = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); + let bob_eth_balance_after = block_on(my_balance(&mm_bob, "ETH")).balance.with_scale(2); + let bob_jst_balance_after = block_on(my_balance(&mm_bob, "JST")).balance.with_scale(2); + let watcher1_eth_balance_after = block_on(my_balance(&mm_watcher1, "ETH")).balance; + let watcher2_eth_balance_after = block_on(my_balance(&mm_watcher2, "ETH")).balance; + + let volume = BigDecimal::from_str("0.01").unwrap(); + assert_eq!(alice_jst_balance_before - volume.clone(), alice_jst_balance_after); + assert_eq!(bob_jst_balance_before + volume.clone(), bob_jst_balance_after); + assert_eq!(alice_eth_balance_before + volume.clone(), alice_eth_balance_after); + assert_eq!(bob_eth_balance_before - volume.clone(), bob_eth_balance_after); + if watcher1_eth_balance_after > watcher1_eth_balance_before { + assert_eq!(watcher2_eth_balance_after, watcher2_eth_balance_after); + } + if watcher2_eth_balance_after > watcher2_eth_balance_before { + assert_eq!(watcher1_eth_balance_after, watcher1_eth_balance_after); + } } #[test] @@ -117,7 +212,7 @@ fn test_watcher_spends_maker_payment_spend_erc20_eth() { log!("Alice log path: {}", mm_alice.log_path.display()); let bob_passphrase = String::from("also shoot benefit prefer juice shell elder veteran woman mimic image kidney"); - let bob_conf = Mm2TestConf::light_node(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); + let bob_conf = Mm2TestConf::light_node_using_watchers(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); @@ -144,6 +239,7 @@ fn test_watcher_spends_maker_payment_spend_erc20_eth() { let alice_jst_balance_before = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); let bob_eth_balance_before = block_on(my_balance(&mm_bob, "ETH")).balance.with_scale(2); let bob_jst_balance_before = block_on(my_balance(&mm_bob, "JST")).balance.with_scale(2); + let watcher_eth_balance_before = block_on(my_balance(&mm_watcher, "ETH")).balance; block_on(start_swaps(&mut mm_bob, &mut mm_alice, &[("JST", "ETH")], 1., 1., 0.01)); @@ -159,6 +255,7 @@ fn test_watcher_spends_maker_payment_spend_erc20_eth() { let alice_jst_balance_after = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); let bob_eth_balance_after = block_on(my_balance(&mm_bob, "ETH")).balance.with_scale(2); let bob_jst_balance_after = block_on(my_balance(&mm_bob, "JST")).balance.with_scale(2); + let watcher_eth_balance_after = block_on(my_balance(&mm_watcher, "ETH")).balance; let volume = BigDecimal::from_str("0.01").unwrap(); @@ -166,6 +263,48 @@ fn test_watcher_spends_maker_payment_spend_erc20_eth() { assert_eq!(bob_jst_balance_before - volume.clone(), bob_jst_balance_after); assert_eq!(alice_eth_balance_before - volume.clone(), alice_eth_balance_after); assert_eq!(bob_eth_balance_before + volume.clone(), bob_eth_balance_after); + assert!(watcher_eth_balance_after > watcher_eth_balance_before); +} + +#[test] +#[ignore] +fn test_watcher_waits_for_taker_eth() { + let coins = json!([eth_testnet_conf(), eth_jst_conf(ETH_SEPOLIA_TOKEN_CONTRACT)]); + + let alice_passphrase = + String::from("spice describe gravity federal blast come thank unfair canal monkey style afraid"); + let alice_conf = Mm2TestConf::seednode_using_watchers(&alice_passphrase, &coins); + let mut mm_alice = MarketMakerIt::start(alice_conf.conf.clone(), alice_conf.rpc_password.clone(), None).unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); + log!("Alice log path: {}", mm_alice.log_path.display()); + + let bob_passphrase = String::from("also shoot benefit prefer juice shell elder veteran woman mimic image kidney"); + let bob_conf = Mm2TestConf::light_node_using_watchers(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); + let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + let watcher_passphrase = + String::from("also shoot benefit prefer juice shell thank unfair canal monkey style afraid"); + let watcher_conf = + Mm2TestConf::watcher_light_node(&watcher_passphrase, &coins, &[&mm_alice.ip.to_string()], WatcherConf { + wait_taker_payment: 0., + wait_maker_payment_spend_factor: 1., + refund_start_factor: 1.5, + search_interval: 1., + }) + .conf; + + let mut mm_watcher = MarketMakerIt::start(watcher_conf, DEFAULT_RPC_PASSWORD.to_string(), None).unwrap(); + let (_watcher_dump_log, _watcher_dump_dashboard) = mm_dump(&mm_watcher.log_path); + + enable_eth_and_jst(&mm_alice); + enable_eth_and_jst(&mm_bob); + enable_eth_and_jst(&mm_watcher); + + block_on(start_swaps(&mut mm_bob, &mut mm_alice, &[("ETH", "JST")], 1., 1., 0.01)); + + block_on(mm_watcher.wait_for_log(160., |log| log.contains(MAKER_PAYMENT_SPEND_FOUND_LOG))).unwrap(); } #[test] @@ -187,7 +326,7 @@ fn test_watcher_refunds_taker_payment_erc20() { log!("Alice log path: {}", mm_alice.log_path.display()); let bob_passphrase = String::from("also shoot benefit prefer juice shell elder veteran woman mimic image kidney"); - let bob_conf = Mm2TestConf::light_node(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); + let bob_conf = Mm2TestConf::light_node_using_watchers(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); @@ -217,6 +356,7 @@ fn test_watcher_refunds_taker_payment_erc20() { let alice_eth_balance_before = block_on(my_balance(&mm_alice, "ETH")).balance.with_scale(2); let alice_jst_balance_before = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); + let watcher_eth_balance_before = block_on(my_balance(&mm_watcher, "ETH")).balance; block_on(start_swaps(&mut mm_bob, &mut mm_alice, &[("ETH", "JST")], 1., 1., 0.01)); @@ -232,9 +372,11 @@ fn test_watcher_refunds_taker_payment_erc20() { let alice_eth_balance_after = block_on(my_balance(&mm_alice, "ETH")).balance.with_scale(2); let alice_jst_balance_after = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); + let watcher_eth_balance_after = block_on(my_balance(&mm_watcher, "ETH")).balance; assert_eq!(alice_jst_balance_before, alice_jst_balance_after); assert_eq!(alice_eth_balance_before, alice_eth_balance_after); + assert!(watcher_eth_balance_after > watcher_eth_balance_before); } #[test] @@ -256,7 +398,7 @@ fn test_watcher_refunds_taker_payment_eth() { log!("Alice log path: {}", mm_alice.log_path.display()); let bob_passphrase = String::from("also shoot benefit prefer juice shell elder veteran woman mimic image kidney"); - let bob_conf = Mm2TestConf::light_node(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); + let bob_conf = Mm2TestConf::light_node_using_watchers(&bob_passphrase, &coins, &[&mm_alice.ip.to_string()]); let mut mm_bob = MarketMakerIt::start(bob_conf.conf, bob_conf.rpc_password, None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); log!("Bob log path: {}", mm_bob.log_path.display()); @@ -286,6 +428,7 @@ fn test_watcher_refunds_taker_payment_eth() { let alice_eth_balance_before = block_on(my_balance(&mm_alice, "ETH")).balance.with_scale(2); let alice_jst_balance_before = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); + let watcher_eth_balance_before = block_on(my_balance(&mm_watcher, "ETH")).balance; block_on(start_swaps(&mut mm_bob, &mut mm_alice, &[("JST", "ETH")], 1., 1., 0.01)); @@ -301,12 +444,15 @@ fn test_watcher_refunds_taker_payment_eth() { let alice_eth_balance_after = block_on(my_balance(&mm_alice, "ETH")).balance.with_scale(2); let alice_jst_balance_after = block_on(my_balance(&mm_alice, "JST")).balance.with_scale(2); + let watcher_eth_balance_after = block_on(my_balance(&mm_watcher, "ETH")).balance; assert_eq!(alice_jst_balance_before, alice_jst_balance_after); assert_eq!(alice_eth_balance_before, alice_eth_balance_after); + assert!(watcher_eth_balance_after > watcher_eth_balance_before); } #[test] +#[ignore] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 fn test_watcher_validate_taker_fee_eth() { let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 120 seconds to run let lock_duration = get_payment_locktime(); @@ -315,16 +461,8 @@ fn test_watcher_validate_taker_fee_eth() { let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); let taker_pubkey = taker_keypair.public(); - let maker_coin = generate_eth_coin_with_random_privkey(); - let maker_keypair = maker_coin.derive_htlc_key_pair(&[]); - let maker_pubkey = maker_keypair.public(); - let taker_amount = MmNumber::from((10, 1)); - let fee_amount = dex_fee_amount_from_taker_coin( - &MmCoinEnum::EthCoin(taker_coin.clone()), - maker_coin.ticker(), - &taker_amount, - ); + let fee_amount = dex_fee_amount_from_taker_coin(&MmCoinEnum::EthCoin(taker_coin.clone()), "ETH", &taker_amount); let taker_fee = taker_coin .send_taker_fee( &DEX_FEE_ADDR_RAW_PUBKEY, @@ -334,10 +472,14 @@ fn test_watcher_validate_taker_fee_eth() { .wait() .unwrap(); - taker_coin - .wait_for_confirmations(&taker_fee.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_fee.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let validate_taker_fee_res = taker_coin .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { @@ -350,10 +492,11 @@ fn test_watcher_validate_taker_fee_eth() { .wait(); assert!(validate_taker_fee_res.is_ok()); + let wrong_keypair = key_pair_from_secret(random_secp256k1_secret().as_slice()).unwrap(); let error = taker_coin .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash().into_vec(), - sender_pubkey: maker_pubkey.to_vec(), + sender_pubkey: wrong_keypair.public().to_vec(), min_block_number: 0, fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, @@ -416,6 +559,7 @@ fn test_watcher_validate_taker_fee_eth() { } #[test] +#[ignore] fn test_watcher_validate_taker_fee_erc20() { let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 120 seconds to run let lock_duration = get_payment_locktime(); @@ -425,16 +569,8 @@ fn test_watcher_validate_taker_fee_erc20() { let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); let taker_pubkey = taker_keypair.public(); - let maker_coin = generate_eth_coin_with_random_privkey(); - let maker_keypair = maker_coin.derive_htlc_key_pair(&[]); - let maker_pubkey = maker_keypair.public(); - let taker_amount = MmNumber::from((10, 1)); - let fee_amount = dex_fee_amount_from_taker_coin( - &MmCoinEnum::EthCoin(taker_coin.clone()), - maker_coin.ticker(), - &taker_amount, - ); + let fee_amount = dex_fee_amount_from_taker_coin(&MmCoinEnum::EthCoin(taker_coin.clone()), "ETH", &taker_amount); let taker_fee = taker_coin .send_taker_fee( &DEX_FEE_ADDR_RAW_PUBKEY, @@ -444,10 +580,14 @@ fn test_watcher_validate_taker_fee_erc20() { .wait() .unwrap(); - taker_coin - .wait_for_confirmations(&taker_fee.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_fee.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let validate_taker_fee_res = taker_coin .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { @@ -460,10 +600,11 @@ fn test_watcher_validate_taker_fee_erc20() { .wait(); assert!(validate_taker_fee_res.is_ok()); + let wrong_keypair = key_pair_from_secret(random_secp256k1_secret().as_slice()).unwrap(); let error = taker_coin .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { taker_fee_hash: taker_fee.tx_hash().into_vec(), - sender_pubkey: maker_pubkey.to_vec(), + sender_pubkey: wrong_keypair.public().to_vec(), min_block_number: 0, fee_addr: DEX_FEE_ADDR_RAW_PUBKEY.to_vec(), lock_duration, @@ -525,6 +666,553 @@ fn test_watcher_validate_taker_fee_erc20() { } } +#[test] +#[ignore] +fn test_watcher_validate_taker_payment_eth() { + let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 120 seconds to run + + let seed = String::from("spice describe gravity federal blast come thank unfair canal monkey style afraid"); + let taker_coin = generate_eth_coin_with_seed(&seed); + let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); + let taker_pub = taker_keypair.public(); + + let maker_keypair = key_pair_from_secret(random_secp256k1_secret().as_slice()).unwrap(); + let maker_pub = maker_keypair.public(); + + let time_lock_duration = get_payment_locktime(); + let wait_for_confirmation_until = now_ms() / 1000 + time_lock_duration; + let time_lock = wait_for_confirmation_until as u32; + let amount = BigDecimal::from_str("0.01").unwrap(); + let secret_hash = dhash160(&MakerSwap::generate_secret()); + let watcher_reward = Some( + block_on(watcher_reward_amount( + &MmCoinEnum::from(taker_coin.clone()), + &MmCoinEnum::from(taker_coin.clone()), + )) + .unwrap(), + ); + + let min_watcher_reward = Some( + block_on(min_watcher_reward( + &MmCoinEnum::from(taker_coin.clone()), + &MmCoinEnum::from(taker_coin.clone()), + )) + .unwrap(), + ); + + let taker_payment = taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pub, + secret_hash: secret_hash.as_slice(), + amount: amount.clone(), + swap_contract_address: &taker_coin.swap_contract_address(), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward, + wait_for_confirmation_until, + }) + .wait() + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + + let validate_taker_payment_res = taker_coin + .watcher_validate_taker_payment(coins::WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait(); + assert!(validate_taker_payment_res.is_ok()); + + let error = taker_coin + .watcher_validate_taker_payment(coins::WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: maker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SENDER_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_SENDER_ERR_LOG, error + ), + } + + let taker_payment_wrong_contract = taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pub, + secret_hash: secret_hash.as_slice(), + amount: amount.clone(), + swap_contract_address: &Some("9130b257d37a52e52f21054c4da3450c72f595ce".into()), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward, + wait_for_confirmation_until, + }) + .wait() + .unwrap(); + + let error = taker_coin + .watcher_validate_taker_payment(coins::WatcherValidatePaymentInput { + payment_tx: taker_payment_wrong_contract.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_CONTRACT_ADDRESS_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_CONTRACT_ADDRESS_ERR_LOG, error + ), + } + + // Used to get wrong swap id + let wrong_secret_hash = dhash160(&MakerSwap::generate_secret()); + let error = taker_coin + .watcher_validate_taker_payment(coins::WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: wrong_secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::UnexpectedPaymentState(err) => { + assert!(err.contains(INVALID_PAYMENT_STATE_ERR_LOG)) + }, + _ => panic!( + "Expected `UnexpectedPaymentState` {}, found {:?}", + INVALID_PAYMENT_STATE_ERR_LOG, error + ), + } + + let taker_payment_wrong_secret = taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pub, + secret_hash: wrong_secret_hash.as_slice(), + amount, + swap_contract_address: &taker_coin.swap_contract_address(), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward, + wait_for_confirmation_until, + }) + .wait() + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment_wrong_secret.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: wrong_secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SWAP_ID_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_SWAP_ID_ERR_LOG, error + ), + } + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: taker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_RECEIVER_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_RECEIVER_ERR_LOG, error + ), + } + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward: min_watcher_reward.map(|min_reward| min_reward * 3), + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INSUFFICIENT_WATCHER_REWARD_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INSUFFICIENT_WATCHER_REWARD_ERR_LOG, error + ), + } +} + +#[test] +#[ignore] +fn test_watcher_validate_taker_payment_erc20() { + let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 120 seconds to run + + let seed = String::from("spice describe gravity federal blast come thank unfair canal monkey style afraid"); + let taker_coin = generate_jst_with_seed(&seed); + let taker_keypair = taker_coin.derive_htlc_key_pair(&[]); + let taker_pub = taker_keypair.public(); + + let maker_keypair = key_pair_from_secret(random_secp256k1_secret().as_slice()).unwrap(); + let maker_pub = maker_keypair.public(); + + let time_lock_duration = get_payment_locktime(); + let wait_for_confirmation_until = now_ms() / 1000 + time_lock_duration; + let time_lock = wait_for_confirmation_until as u32; + + let secret_hash = dhash160(&MakerSwap::generate_secret()); + let watcher_reward = Some( + block_on(watcher_reward_amount( + &MmCoinEnum::from(taker_coin.clone()), + &MmCoinEnum::from(taker_coin.clone()), + )) + .unwrap(), + ); + let min_watcher_reward = Some( + block_on(min_watcher_reward( + &MmCoinEnum::from(taker_coin.clone()), + &MmCoinEnum::from(taker_coin.clone()), + )) + .unwrap(), + ); + + let taker_payment = taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pub, + secret_hash: secret_hash.as_slice(), + amount: BigDecimal::from(10), + swap_contract_address: &taker_coin.swap_contract_address(), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward, + wait_for_confirmation_until, + }) + .wait() + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + + let validate_taker_payment_res = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait(); + assert!(validate_taker_payment_res.is_ok()); + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: maker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SENDER_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_SENDER_ERR_LOG, error + ), + } + + let taker_payment_wrong_contract = taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pub, + secret_hash: secret_hash.as_slice(), + amount: BigDecimal::from(10), + swap_contract_address: &Some("9130b257d37a52e52f21054c4da3450c72f595ce".into()), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward, + wait_for_confirmation_until, + }) + .wait() + .unwrap(); + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment_wrong_contract.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_CONTRACT_ADDRESS_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_CONTRACT_ADDRESS_ERR_LOG, error + ), + } + + // Used to get wrong swap id + let wrong_secret_hash = dhash160(&MakerSwap::generate_secret()); + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: wrong_secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::UnexpectedPaymentState(err) => { + assert!(err.contains(INVALID_PAYMENT_STATE_ERR_LOG)) + }, + _ => panic!( + "Expected `UnexpectedPaymentState` {}, found {:?}", + INVALID_PAYMENT_STATE_ERR_LOG, error + ), + } + + let taker_payment_wrong_secret = taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pub, + secret_hash: wrong_secret_hash.as_slice(), + amount: BigDecimal::from(10), + swap_contract_address: &taker_coin.swap_contract_address(), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward, + wait_for_confirmation_until, + }) + .wait() + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment_wrong_secret.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: wrong_secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SWAP_ID_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_SWAP_ID_ERR_LOG, error + ), + } + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: taker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward, + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_RECEIVER_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_RECEIVER_ERR_LOG, error + ), + } + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: Vec::new(), + time_lock, + taker_pub: taker_pub.to_vec(), + maker_pub: maker_pub.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward: min_watcher_reward.map(|min_reward| min_reward * 3), + }) + .wait() + .unwrap_err() + .into_inner(); + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INSUFFICIENT_WATCHER_REWARD_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INSUFFICIENT_WATCHER_REWARD_ERR_LOG, error + ), + } +} + #[test] fn test_watcher_spends_maker_payment_spend_utxo() { let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 100.into()); @@ -540,10 +1228,11 @@ fn test_watcher_spends_maker_payment_spend_utxo() { let mut mm_alice = MarketMakerIt::start(alice_conf.clone(), DEFAULT_RPC_PASSWORD.to_string(), None).unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - let bob_conf = Mm2TestConf::light_node(&format!("0x{}", hex::encode(bob_priv_key)), &coins, &[&mm_alice - .ip - .to_string()]) - .conf; + let bob_conf = + Mm2TestConf::light_node_using_watchers(&format!("0x{}", hex::encode(bob_priv_key)), &coins, &[&mm_alice + .ip + .to_string()]) + .conf; let mut mm_bob = MarketMakerIt::start(bob_conf, DEFAULT_RPC_PASSWORD.to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -620,10 +1309,11 @@ fn test_watcher_waits_for_taker_utxo() { let mut mm_alice = MarketMakerIt::start(alice_conf.clone(), DEFAULT_RPC_PASSWORD.to_string(), None).unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - let bob_conf = Mm2TestConf::light_node(&format!("0x{}", hex::encode(bob_priv_key)), &coins, &[&mm_alice - .ip - .to_string()]) - .conf; + let bob_conf = + Mm2TestConf::light_node_using_watchers(&format!("0x{}", hex::encode(bob_priv_key)), &coins, &[&mm_alice + .ip + .to_string()]) + .conf; let mut mm_bob = MarketMakerIt::start(bob_conf, DEFAULT_RPC_PASSWORD.to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -680,10 +1370,11 @@ fn test_watcher_refunds_taker_payment_utxo() { .unwrap(); let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); - let bob_conf = Mm2TestConf::light_node(&format!("0x{}", hex::encode(bob_priv_key)), &coins, &[&mm_alice - .ip - .to_string()]) - .conf; + let bob_conf = + Mm2TestConf::light_node_using_watchers(&format!("0x{}", hex::encode(bob_priv_key)), &coins, &[&mm_alice + .ip + .to_string()]) + .conf; let mut mm_bob = MarketMakerIt::start(bob_conf, DEFAULT_RPC_PASSWORD.to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -769,10 +1460,14 @@ fn test_watcher_validate_taker_fee_utxo() { .wait() .unwrap(); - taker_coin - .wait_for_confirmations(&taker_fee.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_fee.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let validate_taker_fee_res = taker_coin .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { @@ -869,6 +1564,192 @@ fn test_watcher_validate_taker_fee_utxo() { } } +#[test] +fn test_watcher_validate_taker_payment_utxo() { + let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 120 seconds to run + let time_lock_duration = get_payment_locktime(); + let wait_for_confirmation_until = now_ms() / 1000 + time_lock_duration; + let time_lock = wait_for_confirmation_until as u32; + + let (_ctx, taker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let taker_pubkey = taker_coin.my_public_key().unwrap(); + + let (_ctx, maker_coin, _) = generate_utxo_coin_with_random_privkey("MYCOIN", 1000u64.into()); + let maker_pubkey = maker_coin.my_public_key().unwrap(); + + let secret_hash = dhash160(&MakerSwap::generate_secret()); + + let taker_payment = taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration, + time_lock, + other_pubkey: maker_pubkey, + secret_hash: secret_hash.as_slice(), + amount: BigDecimal::from(10), + swap_contract_address: &None, + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until, + }) + .wait() + .unwrap(); + + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: taker_payment.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + taker_coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); + + let taker_payment_refund_preimage = taker_coin + .create_taker_payment_refund_preimage( + &taker_payment.tx_hex(), + time_lock, + maker_pubkey, + secret_hash.as_slice(), + &None, + &[], + ) + .wait() + .unwrap(); + let validate_taker_payment_res = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), + time_lock, + taker_pub: taker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward: None, + }) + .wait(); + assert!(validate_taker_payment_res.is_ok()); + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), + time_lock, + taker_pub: maker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward: None, + }) + .wait() + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SENDER_ERR_LOG)) + }, + _ => panic!("Expected `WrongPaymentTx` {INVALID_SENDER_ERR_LOG}, found {:?}", error), + } + + let wrong_secret_hash = dhash160(&MakerSwap::generate_secret()); + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), + time_lock, + taker_pub: taker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: wrong_secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward: None, + }) + .wait() + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SCRIPT_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_SCRIPT_ERR_LOG, error + ), + } + + // Wrong time lock + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: taker_payment_refund_preimage.tx_hex(), + time_lock: 500, + taker_pub: taker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: wrong_secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward: None, + }) + .wait() + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_SCRIPT_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_SCRIPT_ERR_LOG, error + ), + } + + let wrong_taker_payment_refund_preimage = taker_coin + .create_taker_payment_refund_preimage( + &taker_payment.tx_hex(), + time_lock, + maker_pubkey, + wrong_secret_hash.as_slice(), + &None, + &[], + ) + .wait() + .unwrap(); + + let error = taker_coin + .watcher_validate_taker_payment(WatcherValidatePaymentInput { + payment_tx: taker_payment.tx_hex(), + taker_payment_refund_preimage: wrong_taker_payment_refund_preimage.tx_hex(), + time_lock, + taker_pub: taker_pubkey.to_vec(), + maker_pub: maker_pubkey.to_vec(), + secret_hash: secret_hash.to_vec(), + try_spv_proof_until: timeout, + confirmations: 1, + min_watcher_reward: None, + }) + .wait() + .unwrap_err() + .into_inner(); + + log!("error: {:?}", error); + match error { + ValidatePaymentError::WrongPaymentTx(err) => { + assert!(err.contains(INVALID_REFUND_TX_ERR_LOG)) + }, + _ => panic!( + "Expected `WrongPaymentTx` {}, found {:?}", + INVALID_REFUND_TX_ERR_LOG, error + ), + } +} + #[test] fn test_send_taker_payment_refund_preimage_utxo() { let timeout = (now_ms() / 1000) + 120; // timeout if test takes more than 120 seconds to run @@ -876,7 +1757,7 @@ fn test_send_taker_payment_refund_preimage_utxo() { let my_public_key = coin.my_public_key().unwrap(); let time_lock = (now_ms() / 1000) as u32 - 3600; - let taker_payment_args = SendTakerPaymentArgs { + let taker_payment_args = SendPaymentArgs { time_lock_duration: 0, time_lock, other_pubkey: my_public_key, @@ -885,12 +1766,19 @@ fn test_send_taker_payment_refund_preimage_utxo() { swap_contract_address: &None, swap_unique_data: &[], payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, }; let tx = coin.send_taker_payment(taker_payment_args).wait().unwrap(); - coin.wait_for_confirmations(&tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let refund_tx = coin .create_taker_payment_refund_preimage(&tx.tx_hex(), time_lock, my_public_key, &[0; 20], &None, &[]) @@ -898,20 +1786,26 @@ fn test_send_taker_payment_refund_preimage_utxo() { .unwrap(); let refund_tx = coin - .send_taker_payment_refund_preimage(SendWatcherRefundsPaymentArgs { + .send_taker_payment_refund_preimage(RefundPaymentArgs { payment_tx: &refund_tx.tx_hex(), swap_contract_address: &None, secret_hash: &[0; 20], other_pubkey: my_public_key, time_lock, swap_unique_data: &[], + watcher_reward: false, }) .wait() .unwrap(); - coin.wait_for_confirmations(&refund_tx.tx_hex(), 1, false, timeout, 1) - .wait() - .unwrap(); + let confirm_payment_input = ConfirmPaymentInput { + payment_tx: refund_tx.tx_hex(), + confirmations: 1, + requires_nota: false, + wait_until: timeout, + check_every: 1, + }; + coin.wait_for_confirmations(confirm_payment_input).wait().unwrap(); let search_input = SearchForSwapTxSpendInput { time_lock, @@ -921,6 +1815,7 @@ fn test_send_taker_payment_refund_preimage_utxo() { search_from_block: 0, swap_contract_address: &None, swap_unique_data: &[], + watcher_reward: false, }; let found = block_on(coin.search_for_swap_tx_spend_my(search_input)) .unwrap() diff --git a/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs b/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs index 5068aa09f3..42447ae35b 100644 --- a/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs @@ -424,6 +424,7 @@ fn test_buy_maker_should_not_use_taker_confs_and_notas_for_maker_payment_if_take } #[test] +#[ignore] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 fn test_buy_taker_should_use_maker_confs_and_notas_for_taker_payment_if_maker_requires_less() { let maker_settings = OrderConfirmationsSettings { base_confs: 1, @@ -463,6 +464,7 @@ fn test_buy_taker_should_use_maker_confs_and_notas_for_taker_payment_if_maker_re } #[test] +#[ignore] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 fn test_buy_taker_should_not_use_maker_confs_and_notas_for_taker_payment_if_maker_requires_more() { let maker_settings = OrderConfirmationsSettings { base_confs: 1, @@ -619,6 +621,7 @@ fn test_sell_maker_should_not_use_taker_confs_and_notas_for_maker_payment_if_tak } #[test] +#[ignore] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 fn test_sell_taker_should_use_maker_confs_and_notas_for_taker_payment_if_maker_requires_less() { let maker_settings = OrderConfirmationsSettings { base_confs: 1, @@ -658,6 +661,7 @@ fn test_sell_taker_should_use_maker_confs_and_notas_for_taker_payment_if_maker_r } #[test] +#[ignore] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1712 fn test_sell_taker_should_not_use_maker_confs_and_notas_for_taker_payment_if_maker_requires_more() { let maker_settings = OrderConfirmationsSettings { base_confs: 1, diff --git a/mm2src/mm2_main/tests/docker_tests_main.rs b/mm2src/mm2_main/tests/docker_tests_main.rs index e5a1140967..6ef9cba2d5 100644 --- a/mm2src/mm2_main/tests/docker_tests_main.rs +++ b/mm2src/mm2_main/tests/docker_tests_main.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "run-docker-tests")] #![feature(async_closure)] #![feature(custom_test_frameworks)] #![feature(test)] diff --git a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs index b0887667ca..38830e6d24 100644 --- a/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/bch_and_slp_tests.rs @@ -30,7 +30,7 @@ const T_BCH_ELECTRUMS: &[&str] = &[ const BIP39_PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; -fn t_bch_electrums_legacy_json() -> Vec { T_BCH_ELECTRUMS.into_iter().map(|url| json!({ "url": url })).collect() } +fn t_bch_electrums_legacy_json() -> Vec { T_BCH_ELECTRUMS.iter().map(|url| json!({ "url": url })).collect() } #[test] #[cfg(not(target_arch = "wasm32"))] diff --git a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs index e817d15d51..0685731781 100644 --- a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs @@ -821,9 +821,9 @@ fn test_best_orders_address_and_confirmations() { assert_eq!(best_orders[0].coin, "RICK"); assert_eq!(best_orders[0].address, rick_address); assert_eq!(best_orders[0].base_confs, 5); - assert_eq!(best_orders[0].base_nota, false); + assert!(!best_orders[0].base_nota); assert_eq!(best_orders[0].rel_confs, 10); - assert_eq!(best_orders[0].rel_nota, true); + assert!(best_orders[0].rel_nota); let rc = block_on(mm_alice.rpc(&json! ({ "userpass": mm_alice.userpass, @@ -840,9 +840,9 @@ fn test_best_orders_address_and_confirmations() { assert_eq!(best_orders[0].coin, "tBTC"); assert_eq!(best_orders[0].address, tbtc_segwit_address); assert_eq!(best_orders[0].base_confs, 10); - assert_eq!(best_orders[0].base_nota, true); + assert!(best_orders[0].base_nota); assert_eq!(best_orders[0].rel_confs, 5); - assert_eq!(best_orders[0].rel_nota, false); + assert!(!best_orders[0].rel_nota); // checking buy and sell best_orders against ("RICK", "tBTC", "0.7", "0.0002", Some("0.00015")) let rc = block_on(mm_alice.rpc(&json! ({ @@ -860,9 +860,9 @@ fn test_best_orders_address_and_confirmations() { assert_eq!(best_orders[0].coin, "tBTC"); assert_eq!(best_orders[0].address, tbtc_segwit_address); assert_eq!(best_orders[0].base_confs, 10); - assert_eq!(best_orders[0].base_nota, true); + assert!(best_orders[0].base_nota); assert_eq!(best_orders[0].rel_confs, 5); - assert_eq!(best_orders[0].rel_nota, false); + assert!(!best_orders[0].rel_nota); let rc = block_on(mm_alice.rpc(&json! ({ "userpass": mm_alice.userpass, @@ -879,9 +879,9 @@ fn test_best_orders_address_and_confirmations() { assert_eq!(best_orders[0].coin, "RICK"); assert_eq!(best_orders[0].address, rick_address); assert_eq!(best_orders[0].base_confs, 5); - assert_eq!(best_orders[0].base_nota, false); + assert!(!best_orders[0].base_nota); assert_eq!(best_orders[0].rel_confs, 10); - assert_eq!(best_orders[0].rel_nota, true); + assert!(best_orders[0].rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); diff --git a/mm2src/mm2_main/tests/mm2_tests/eth_tests.rs b/mm2src/mm2_main/tests/mm2_tests/eth_tests.rs index 62c82f0de8..d1ae803d03 100644 --- a/mm2src/mm2_main/tests/mm2_tests/eth_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/eth_tests.rs @@ -62,7 +62,7 @@ fn test_disable_eth_coin_with_token() { }); let make_test_order = block_on(mm.rpc(&req)).unwrap(); assert_eq!(make_test_order.0, StatusCode::OK); - let order_uuid = Json::from_str(&*make_test_order.1).unwrap(); + let order_uuid = Json::from_str(&make_test_order.1).unwrap(); let order_uuid = order_uuid.get("result").unwrap().get("uuid").unwrap().as_str().unwrap(); // Try to disable platform coin, ETH. This should fail due to the dependent tokens. diff --git a/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs index c6dd0265b6..de0af16b3e 100644 --- a/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs +++ b/mm2src/mm2_main/tests/mm2_tests/iris_swap.rs @@ -17,15 +17,13 @@ const TBNB_URLS: &[&str] = &["https://data-seed-prebsc-1-s3.binance.org:8545/"]; const TBNB_SWAP_CONTRACT: &str = "0xB1Ad803ea4F57401639c123000C75F5B66E4D123"; #[test] -#[ignore] -// TODO https://github.com/KomodoPlatform/atomicDEX-API/issues/1569 fn start_swap_operation() { let pairs = [ ("USDC-IBC-IRIS", "IRIS-NIMDA"), ("IRIS-NIMDA", "RICK"), - ("USDC-IBC-IRIS", "tBNB"), + // ("USDC-IBC-IRIS", "tBNB"), having fund problems ]; - block_on(trade_base_rel_iris(&pairs, 1, 2, 0.01)); + block_on(trade_base_rel_iris(&pairs, 1, 2, 0.008)); } pub async fn trade_base_rel_iris( @@ -110,8 +108,8 @@ pub async fn trade_base_rel_iris( .await ); dbg!(enable_electrum(&mm_alice, "RICK", false, RICK_ELECTRUM_ADDRS).await); - dbg!(enable_eth_coin(&mm_bob, "tBNB", TBNB_URLS, TBNB_SWAP_CONTRACT, None).await); - dbg!(enable_eth_coin(&mm_alice, "tBNB", TBNB_URLS, TBNB_SWAP_CONTRACT, None).await); + dbg!(enable_eth_coin(&mm_bob, "tBNB", TBNB_URLS, TBNB_SWAP_CONTRACT, None, false).await); + dbg!(enable_eth_coin(&mm_alice, "tBNB", TBNB_URLS, TBNB_SWAP_CONTRACT, None, false).await); for (base, rel) in pairs.iter() { log!("Issue bob {}/{} sell request", base, rel); diff --git a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs index ff2b5040fc..537bc3aae1 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs @@ -1,5 +1,5 @@ use crate::integration_tests_common::{enable_coins_rick_morty_electrum, enable_electrum}; -use coins::lightning::ln_events::{SUCCESSFUL_CLAIM_LOG, SUCCESSFUL_SEND_LOG}; +use coins::lightning::ln_events::{CHANNEL_READY_LOG, PAYMENT_CLAIMABLE_LOG, SUCCESSFUL_CLAIM_LOG, SUCCESSFUL_SEND_LOG}; use common::executor::Timer; use common::{block_on, log}; use gstuff::now_ms; @@ -74,7 +74,11 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, "decimals": 11, "our_channels_configs": { "inbound_channels_confirmations": 1, - "max_inbound_in_flight_htlc_percent": 100 + // todo: When this was 100% I got "lightning:channelmanager:2525] ERROR Cannot send value that would put our balance under counterparty-announced channel reserve value (1000000)" + // todo: This seems to be a bug in rust-lightning for mpp, I informed their team and will revert this to 100 if it was fixed https://github.com/lightningdevkit/rust-lightning/issues/1126#issuecomment-1414308252 + "max_inbound_in_flight_htlc_percent": 90, + // If this is set to 0 it will default to 1000 sats since it's the min allowed value + "their_channel_reserve_sats": 1000, }, "counterparty_channel_config_limits": { "outbound_channels_confirmations": 1, @@ -150,6 +154,165 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, (mm_node_1, mm_node_2, node_1_address, node_2_address) } +async fn open_channel( + mm: &mut MarketMakerIt, + coin: &str, + address: &str, + amount: f64, + wait_for_ready_signal: bool, +) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "lightning::channels::open_channel", + "params": { + "coin": coin, + "node_address": address, + "amount": { + "type": "Exact", + "value": amount, + }, + }, + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'lightning::channels::open_channel' failed: {}", + request.1 + ); + + let res: Json = json::from_str(&request.1).unwrap(); + let uuid = res["result"]["uuid"].as_str().unwrap(); + if wait_for_ready_signal { + mm.wait_for_log(3600., |log| log.contains(&format!("{}: {}", CHANNEL_READY_LOG, uuid))) + .await + .unwrap(); + } + res +} + +async fn close_channel(mm: &MarketMakerIt, uuid: &str, force_close: bool) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "lightning::channels::close_channel", + "params": { + "coin": "tBTC-TEST-lightning", + "uuid": uuid, + "force_close": force_close, + }, + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'lightning::channels::close_channel' failed: {}", + request.1 + ); + + json::from_str(&request.1).unwrap() +} + +async fn add_trusted_node(mm: &MarketMakerIt, node_id: &str) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "lightning::nodes::add_trusted_node", + "params": { + "coin": "tBTC-TEST-lightning", + "node_id": node_id + }, + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'lightning::nodes::add_trusted_node' failed: {}", + request.1 + ); + json::from_str(&request.1).unwrap() +} + +async fn generate_invoice(mm: &MarketMakerIt, amount_in_msat: u64) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "lightning::payments::generate_invoice", + "params": { + "coin": "tBTC-TEST-lightning", + "description": "test invoice", + "amount_in_msat": amount_in_msat + }, + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'lightning::payments::generate_invoice' failed: {}", + request.1 + ); + + json::from_str(&request.1).unwrap() +} + +async fn pay_invoice(mm: &MarketMakerIt, invoice: &str) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "lightning::payments::send_payment", + "params": { + "coin": "tBTC-TEST-lightning", + "payment": { + "type": "invoice", + "invoice": invoice + } + }, + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'lightning::payments::send_payment' failed: {}", + request.1 + ); + + json::from_str(&request.1).unwrap() +} + +async fn get_payment_details(mm: &MarketMakerIt, payment_hash: &str) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "mmrpc": "2.0", + "method": "lightning::payments::get_payment_details", + "params": { + "coin": "tBTC-TEST-lightning", + "payment_hash": payment_hash + }, + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'lightning::payments::get_payment_details' failed: {}", + request.1 + ); + + json::from_str(&request.1).unwrap() +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_enable_lightning() { @@ -259,7 +422,7 @@ fn test_enable_lightning() { #[cfg(not(target_arch = "wasm32"))] fn test_connect_to_node() { let (mm_node_1, mm_node_2, node_1_id, _) = start_lightning_nodes(false); - let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip.to_string()); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); let connect = block_on(mm_node_2.rpc(&json!({ "userpass": mm_node_2.userpass, @@ -290,28 +453,15 @@ fn test_connect_to_node() { #[cfg(not(target_arch = "wasm32"))] fn test_open_channel() { let (mm_node_1, mut mm_node_2, node_1_id, node_2_id) = start_lightning_nodes(false); - let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip.to_string()); - - let open_channel = block_on(mm_node_2.rpc(&json!({ - "userpass": mm_node_2.userpass, - "mmrpc": "2.0", - "method": "lightning::channels::open_channel", - "params": { - "coin": "tBTC-TEST-lightning", - "node_address": node_1_address, - "amount": { - "type": "Exact", - "value": 0.0002, - }, - }, - }))) - .unwrap(); - assert!( - open_channel.0.is_success(), - "!lightning::channels::open_channel: {}", - open_channel.1 - ); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + false, + )); block_on(mm_node_2.wait_for_log(60., |log| log.contains("Transaction broadcasted successfully"))).unwrap(); let list_channels_node_1 = block_on(mm_node_1.rpc(&json!({ @@ -382,45 +532,16 @@ fn test_open_channel() { // This also tests 0_confs_channels fn test_send_payment() { let (mut mm_node_2, mut mm_node_1, node_2_id, node_1_id) = start_lightning_nodes(true); - let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip.to_string()); - - let add_trusted_node = block_on(mm_node_1.rpc(&json!({ - "userpass": mm_node_1.userpass, - "mmrpc": "2.0", - "method": "lightning::nodes::add_trusted_node", - "params": { - "coin": "tBTC-TEST-lightning", - "node_id": node_2_id - }, - }))) - .unwrap(); - assert!( - add_trusted_node.0.is_success(), - "!lightning::nodes::add_trusted_node: {}", - add_trusted_node.1 - ); - - let open_channel = block_on(mm_node_2.rpc(&json!({ - "userpass": mm_node_2.userpass, - "mmrpc": "2.0", - "method": "lightning::channels::open_channel", - "params": { - "coin": "tBTC-TEST-lightning", - "node_address": node_1_address, - "amount": { - "type": "Exact", - "value": 0.0002, - }, - }, - }))) - .unwrap(); - assert!( - open_channel.0.is_success(), - "!lightning::channels::open_channel: {}", - open_channel.1 - ); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); - block_on(mm_node_2.wait_for_log(60., |log| log.contains("Received message ChannelReady"))).unwrap(); + block_on(add_trusted_node(&mm_node_1, &node_2_id)); + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); let send_payment = block_on(mm_node_2.rpc(&json!({ "userpass": mm_node_2.userpass, @@ -450,151 +571,108 @@ fn test_send_payment() { block_on(mm_node_2.wait_for_log(60., |log| log.contains(SUCCESSFUL_SEND_LOG))).unwrap(); // Check payment on the sending node side - let get_payment_details = block_on(mm_node_2.rpc(&json!({ - "userpass": mm_node_2.userpass, - "mmrpc": "2.0", - "method": "lightning::payments::get_payment_details", - "params": { - "coin": "tBTC-TEST-lightning", - "payment_hash": payment_hash - }, - }))) - .unwrap(); - assert!( - get_payment_details.0.is_success(), - "!lightning::payments::get_payment_details: {}", - get_payment_details.1 - ); - - let get_payment_details_res: Json = json::from_str(&get_payment_details.1).unwrap(); - let payment = &get_payment_details_res["result"]["payment_details"]; + let sender_payment_details = block_on(get_payment_details(&mm_node_2, payment_hash)); + let payment = &sender_payment_details["result"]["payment_details"]; assert_eq!(payment["status"], "succeeded"); assert_eq!(payment["amount_in_msat"], 1000); assert_eq!(payment["payment_type"]["type"], "Outbound Payment"); // Check payment on the receiving node side - let get_payment_details = block_on(mm_node_1.rpc(&json!({ - "userpass": mm_node_1.userpass, - "mmrpc": "2.0", - "method": "lightning::payments::get_payment_details", - "params": { - "coin": "tBTC-TEST-lightning", - "payment_hash": payment_hash - }, - }))) - .unwrap(); - assert!( - get_payment_details.0.is_success(), - "!lightning::payments::get_payment_details: {}", - get_payment_details.1 - ); - - let get_payment_details_res: Json = json::from_str(&get_payment_details.1).unwrap(); - let payment = &get_payment_details_res["result"]["payment_details"]; + let receiver_payment_details = block_on(get_payment_details(&mm_node_1, payment_hash)); + let payment = &receiver_payment_details["result"]["payment_details"]; assert_eq!(payment["status"], "succeeded"); assert_eq!(payment["amount_in_msat"], 1000); assert_eq!(payment["payment_type"]["type"], "Inbound Payment"); // Test generate and pay invoice - let generate_invoice = block_on(mm_node_1.rpc(&json!({ - "userpass": mm_node_1.userpass, - "mmrpc": "2.0", - "method": "lightning::payments::generate_invoice", - "params": { - "coin": "tBTC-TEST-lightning", - "description": "test invoice", - "amount_in_msat": 10000 - }, - }))) - .unwrap(); - assert!( - generate_invoice.0.is_success(), - "!lightning::payments::generate_invoice: {}", - generate_invoice.1 - ); - - let generate_invoice_res: Json = json::from_str(&generate_invoice.1).unwrap(); - log!("generate_invoice_res {:?}", generate_invoice_res); - let invoice = generate_invoice_res["result"]["invoice"].as_str().unwrap(); - let invoice_payment_hash = generate_invoice_res["result"]["payment_hash"].as_str().unwrap(); - - let pay_invoice = block_on(mm_node_2.rpc(&json!({ - "userpass": mm_node_2.userpass, - "mmrpc": "2.0", - "method": "lightning::payments::send_payment", - "params": { - "coin": "tBTC-TEST-lightning", - "payment": { - "type": "invoice", - "invoice": invoice - } - }, - }))) - .unwrap(); - assert!( - pay_invoice.0.is_success(), - "!lightning::payments::send_payment: {}", - pay_invoice.1 - ); + let generate_invoice = block_on(generate_invoice(&mm_node_1, 10000)); + let invoice = generate_invoice["result"]["invoice"].as_str().unwrap(); + let invoice_payment_hash = generate_invoice["result"]["payment_hash"].as_str().unwrap(); - let pay_invoice_res: Json = json::from_str(&pay_invoice.1).unwrap(); - log!("pay_invoice_res {:?}", pay_invoice_res); - let payment_hash = pay_invoice_res["result"]["payment_hash"].as_str().unwrap(); + let pay_invoice = block_on(pay_invoice(&mm_node_2, invoice)); + let payment_hash = pay_invoice["result"]["payment_hash"].as_str().unwrap(); block_on(mm_node_1.wait_for_log(60., |log| log.contains(SUCCESSFUL_CLAIM_LOG))).unwrap(); block_on(mm_node_2.wait_for_log(60., |log| { - log.contains(&format!( - "{} of 10000 millisatoshis with payment hash {}", - SUCCESSFUL_SEND_LOG, payment_hash - )) + log.contains(&format!("{} with payment hash {}", SUCCESSFUL_SEND_LOG, payment_hash)) })) .unwrap(); // Check payment on the sending node side - let get_payment_details = block_on(mm_node_2.rpc(&json!({ - "userpass": mm_node_2.userpass, - "mmrpc": "2.0", - "method": "lightning::payments::get_payment_details", - "params": { - "coin": "tBTC-TEST-lightning", - "payment_hash": payment_hash - }, - }))) - .unwrap(); - assert!( - get_payment_details.0.is_success(), - "!lightning::payments::get_payment_details: {}", - get_payment_details.1 - ); - - let get_payment_details_res: Json = json::from_str(&get_payment_details.1).unwrap(); - let payment = &get_payment_details_res["result"]["payment_details"]; + let sender_payment_details = block_on(get_payment_details(&mm_node_2, invoice_payment_hash)); + let payment = &sender_payment_details["result"]["payment_details"]; assert_eq!(payment["status"], "succeeded"); assert_eq!(payment["amount_in_msat"], 10000); assert_eq!(payment["payment_type"]["type"], "Outbound Payment"); assert_eq!(payment["description"], "test invoice"); // Check payment on the receiving node side - let get_payment_details = block_on(mm_node_1.rpc(&json!({ - "userpass": mm_node_1.userpass, - "mmrpc": "2.0", - "method": "lightning::payments::get_payment_details", - "params": { - "coin": "tBTC-TEST-lightning", - "payment_hash": invoice_payment_hash - }, - }))) + let receiver_payment_details = block_on(get_payment_details(&mm_node_1, invoice_payment_hash)); + let payment = &receiver_payment_details["result"]["payment_details"]; + assert_eq!(payment["status"], "succeeded"); + assert_eq!(payment["amount_in_msat"], 10000); + assert_eq!(payment["payment_type"]["type"], "Inbound Payment"); + assert_eq!(payment["description"], "test invoice"); + + block_on(mm_node_1.stop()).unwrap(); + block_on(mm_node_2.stop()).unwrap(); +} + +#[test] +// This test is ignored because it requires refilling the tBTC addresses with test coins periodically. +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_mpp() { + let (mut mm_node_2, mut mm_node_1, node_2_id, node_1_id) = start_lightning_nodes(true); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); + + block_on(add_trusted_node(&mm_node_1, &node_2_id)); + + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + + // Wait a few seconds for both channels to be included in the invoice routing hints, since both channels are private. + block_on(Timer::sleep(3.)); + + // Test generate and pay invoice, invoice amount is larger than one channel so payment will use the 2 channels + let generate_invoice = block_on(generate_invoice(&mm_node_1, 30000000)); + let invoice = generate_invoice["result"]["invoice"].as_str().unwrap(); + let invoice_payment_hash = generate_invoice["result"]["payment_hash"].as_str().unwrap(); + + let pay_invoice = block_on(pay_invoice(&mm_node_2, invoice)); + let payment_hash = pay_invoice["result"]["payment_hash"].as_str().unwrap(); + + block_on(mm_node_1.wait_for_log(60., |log| log.contains(SUCCESSFUL_CLAIM_LOG))).unwrap(); + block_on(mm_node_2.wait_for_log(60., |log| { + log.contains(&format!("{} with payment hash {}", SUCCESSFUL_SEND_LOG, payment_hash)) + })) .unwrap(); - assert!( - get_payment_details.0.is_success(), - "!lightning::payments::get_payment_details: {}", - get_payment_details.1 - ); - let get_payment_details_res: Json = json::from_str(&get_payment_details.1).unwrap(); - let payment = &get_payment_details_res["result"]["payment_details"]; + // Check payment on the sending node side + let sender_payment_details = block_on(get_payment_details(&mm_node_2, invoice_payment_hash)); + let payment = &sender_payment_details["result"]["payment_details"]; assert_eq!(payment["status"], "succeeded"); - assert_eq!(payment["amount_in_msat"], 10000); + assert_eq!(payment["amount_in_msat"], 30000000); + assert_eq!(payment["payment_type"]["type"], "Outbound Payment"); + assert_eq!(payment["description"], "test invoice"); + + // Check payment on the receiving node side + let receiver_payment_details = block_on(get_payment_details(&mm_node_1, invoice_payment_hash)); + let payment = &receiver_payment_details["result"]["payment_details"]; + assert_eq!(payment["status"], "succeeded"); + assert_eq!(payment["amount_in_msat"], 30000000); assert_eq!(payment["payment_type"]["type"], "Inbound Payment"); assert_eq!(payment["description"], "test invoice"); @@ -608,45 +686,17 @@ fn test_send_payment() { #[cfg(not(target_arch = "wasm32"))] fn test_lightning_swaps() { let (mut mm_node_1, mut mm_node_2, node_1_id, node_2_id) = start_lightning_nodes(true); - let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip.to_string()); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); - let add_trusted_node = block_on(mm_node_1.rpc(&json!({ - "userpass": mm_node_1.userpass, - "mmrpc": "2.0", - "method": "lightning::nodes::add_trusted_node", - "params": { - "coin": "tBTC-TEST-lightning", - "node_id": node_2_id - }, - }))) - .unwrap(); - assert!( - add_trusted_node.0.is_success(), - "!lightning::nodes::add_trusted_node: {}", - add_trusted_node.1 - ); - - let open_channel = block_on(mm_node_2.rpc(&json!({ - "userpass": mm_node_2.userpass, - "mmrpc": "2.0", - "method": "lightning::channels::open_channel", - "params": { - "coin": "tBTC-TEST-lightning", - "node_address": node_1_address, - "amount": { - "type": "Exact", - "value": 0.0002, - }, - }, - }))) - .unwrap(); - assert!( - open_channel.0.is_success(), - "!lightning::channels::open_channel: {}", - open_channel.1 - ); + block_on(add_trusted_node(&mm_node_1, &node_2_id)); - block_on(mm_node_2.wait_for_log(60., |log| log.contains("Received message ChannelReady"))).unwrap(); + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); // Enable coins on mm_node_1 side. Print the replies in case we need the "address". log!( @@ -708,6 +758,244 @@ fn test_lightning_swaps() { block_on(mm_node_2.stop()).unwrap(); } +#[test] +// This test is ignored because it requires refilling the tBTC and RICK addresses with test coins periodically. +// This test also takes a lot of time so it should always be ignored. +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_lightning_taker_swap_mpp() { + let (mut mm_node_1, mut mm_node_2, node_1_id, node_2_id) = start_lightning_nodes(true); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); + + block_on(add_trusted_node(&mm_node_1, &node_2_id)); + + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + + // Enable coins on mm_node_1 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_1): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_1)) + ); + + // Enable coins on mm_node_2 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_2): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_2)) + ); + + let price = 0.0025; + let volume = 0.1; + let uuids = block_on(start_swaps( + &mut mm_node_1, + &mut mm_node_2, + &[("RICK", "tBTC-TEST-lightning")], + price, + price, + volume, + )); + block_on(wait_for_swaps_finish_and_check_status( + &mut mm_node_1, + &mut mm_node_2, + &uuids, + volume, + price, + )); + block_on(mm_node_1.stop()).unwrap(); + block_on(mm_node_2.stop()).unwrap(); +} + +#[test] +// This test is ignored because it requires refilling the tBTC and RICK addresses with test coins periodically. +// This test also takes a lot of time so it should always be ignored. +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_lightning_maker_swap_mpp() { + let (mut mm_node_1, mut mm_node_2, node_1_id, node_2_id) = start_lightning_nodes(true); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); + + block_on(add_trusted_node(&mm_node_1, &node_2_id)); + + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + + // Enable coins on mm_node_1 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_1): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_1)) + ); + + // Enable coins on mm_node_2 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_2): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_2)) + ); + + let price = 10.; + let volume = 0.00025; + let uuids = block_on(start_swaps( + &mut mm_node_2, + &mut mm_node_1, + &[("tBTC-TEST-lightning", "RICK")], + price, + price, + volume, + )); + block_on(wait_for_swaps_finish_and_check_status( + &mut mm_node_2, + &mut mm_node_1, + &uuids, + volume, + price, + )); + block_on(mm_node_1.stop()).unwrap(); + block_on(mm_node_2.stop()).unwrap(); +} + +// Todo: not working for now, should work once on-chain claiming is implemented https://github.com/lightningdevkit/rust-lightning/issues/2017 +// Todo: watchtowers implementation is needed for such cases, if taker is offline +#[test] +// This test is ignored because it requires refilling the tBTC and RICK addresses with test coins periodically. +// This test also takes a lot of time so it should always be ignored. +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_lightning_taker_gets_swap_preimage_onchain() { + let (mut mm_node_1, mut mm_node_2, node_1_id, _) = start_lightning_nodes(false); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); + + let open_channel = block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + let uuid = open_channel["result"]["uuid"].as_str().unwrap(); + + // Enable coins on mm_node_1 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_1): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_1)) + ); + + // Enable coins on mm_node_2 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_2): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_2)) + ); + + let price = 0.0005; + let volume = 0.1; + let uuids = block_on(start_swaps( + &mut mm_node_1, + &mut mm_node_2, + &[("RICK", "tBTC-TEST-lightning")], + price, + price, + volume, + )); + block_on(mm_node_1.wait_for_log(60., |log| log.contains(PAYMENT_CLAIMABLE_LOG))).unwrap(); + + // Taker node force closes the channel after the maker receives the payment but before the maker claims the payment and releases the preimage + block_on(close_channel(&mm_node_2, uuid, true)); + + block_on(mm_node_1.wait_for_log(7200., |log| log.contains(&format!("[swap uuid={}] Finished", uuids[0])))).unwrap(); + block_on(mm_node_2.wait_for_log(7200., |log| log.contains(&format!("[swap uuid={}] Finished", uuids[0])))).unwrap(); + + // Todo: If the test passes the payment will be added to the tBTC balance, add a check here, find a way to inform the user of this. + + block_on(mm_node_1.stop()).unwrap(); + block_on(mm_node_2.stop()).unwrap(); +} + +// Todo: not working for now, should work once on-chain claiming is implemented https://github.com/lightningdevkit/rust-lightning/issues/2017 +// Todo: watchtowers implementation is needed for such cases, if taker is offline +#[test] +// This test is ignored because it requires refilling the tBTC and RICK addresses with test coins periodically. +// This test also takes a lot of time so it should always be ignored. +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_lightning_taker_claims_mpp() { + let (mut mm_node_1, mut mm_node_2, node_1_id, _) = start_lightning_nodes(false); + let node_1_address = format!("{}@{}:9735", node_1_id, mm_node_1.ip); + + let open_channel_1 = block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + let uuid = open_channel_1["result"]["uuid"].as_str().unwrap(); + block_on(open_channel( + &mut mm_node_2, + "tBTC-TEST-lightning", + &node_1_address, + 0.0002, + true, + )); + + // Enable coins on mm_node_1 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_1): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_1)) + ); + + // Enable coins on mm_node_2 side. Print the replies in case we need the "address". + log!( + "enable_coins (mm_node_2): {:?}", + block_on(enable_coins_rick_morty_electrum(&mm_node_2)) + ); + + let price = 0.0025; + let volume = 0.1; + let uuids = block_on(start_swaps( + &mut mm_node_1, + &mut mm_node_2, + &[("RICK", "tBTC-TEST-lightning")], + price, + price, + volume, + )); + + block_on(mm_node_1.wait_for_log(60., |log| log.contains(PAYMENT_CLAIMABLE_LOG))).unwrap(); + + // Taker node force closes the channel after the maker receives the payment but before the maker claims the payment and releases the preimage + block_on(close_channel(&mm_node_2, uuid, true)); + + block_on(mm_node_1.wait_for_log(7200., |log| log.contains(&format!("[swap uuid={}] Finished", uuids[0])))).unwrap(); + block_on(mm_node_2.wait_for_log(7200., |log| log.contains(&format!("[swap uuid={}] Finished", uuids[0])))).unwrap(); + + // Todo: If the test passes the payment will be added to the tBTC balance, add a check here, find a way to inform the user of this. + + block_on(mm_node_1.stop()).unwrap(); + block_on(mm_node_2.stop()).unwrap(); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_verify_message_lightning() { 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 d6d4042ace..345b4960bf 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -1,6 +1,6 @@ use crate::integration_tests_common::*; use common::executor::Timer; -use common::{cfg_native, cfg_wasm32, get_utc_timestamp, log}; +use common::{cfg_native, cfg_wasm32, get_utc_timestamp, log, new_uuid}; use crypto::privkey::key_pair_from_seed; use http::{HeaderMap, StatusCode}; use mm2_main::mm2::lp_ordermatch::MIN_ORDER_KEEP_ALIVE_INTERVAL; @@ -1417,7 +1417,7 @@ fn test_swap_status() { "userpass": mm.userpass, "method": "my_swap_status", "params": { - "uuid":Uuid::new_v4(), + "uuid": new_uuid(), } }))) .unwrap(); @@ -1428,7 +1428,7 @@ fn test_swap_status() { "userpass": mm.userpass, "method": "stats_swap_status", "params": { - "uuid":Uuid::new_v4(), + "uuid": new_uuid(), } }))) .unwrap(); @@ -3349,7 +3349,7 @@ fn test_add_delegation_qtum() { "pass".into(), None, ) - .unwrap(); + .unwrap(); let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ "electrum1.cipig.net:10071", @@ -3434,7 +3434,7 @@ fn test_remove_delegation_qtum() { "pass".into(), None, ) - .unwrap(); + .unwrap(); let json = block_on(enable_electrum(&mm, "tQTUM", false, &[ "electrum1.cipig.net:10071", @@ -4518,7 +4518,7 @@ fn test_tx_history_tbtc_non_segwit() { "mm2": 1, "tx_history": true, }))) - .unwrap(); + .unwrap(); assert_eq!( electrum.0, StatusCode::OK, @@ -4533,6 +4533,8 @@ fn test_tx_history_tbtc_non_segwit() { ); let expected = vec![ + // https://live.blockcypher.com/btc-testnet/tx/a41b2e5f0741d1dcbc309ce4c43fde1ad44c5e61bb34778ab0bf9f3d9fd6fb6c/ + "a41b2e5f0741d1dcbc309ce4c43fde1ad44c5e61bb34778ab0bf9f3d9fd6fb6c", // https://live.blockcypher.com/btc-testnet/tx/9c1ca9de9f3a47d71c8113209123410f44048c67951bf49cdfb1a84c2cc6a55b/ "9c1ca9de9f3a47d71c8113209123410f44048c67951bf49cdfb1a84c2cc6a55b", // https://live.blockcypher.com/btc-testnet/tx/ac6218b33d02e069c4055af709bbb6ca92ce11e55450cde96bc17411e281e5e7/ @@ -5710,7 +5712,7 @@ fn test_orderbook_is_mine_orders() { let asks = bob_orderbook["asks"].as_array().unwrap(); assert_eq!(asks.len(), 1, "Bob RICK/MORTY orderbook must have exactly 1 ask"); let is_mine = asks[0]["is_mine"].as_bool().unwrap(); - assert_eq!(is_mine, true); + assert!(is_mine); // Alice orderbook must show 1 not-mine order log!("Get RICK/MORTY orderbook on Alice side"); @@ -5728,7 +5730,7 @@ fn test_orderbook_is_mine_orders() { let asks = alice_orderbook["asks"].as_array().unwrap(); assert_eq!(asks.len(), 1, "Alice RICK/MORTY orderbook must have exactly 1 ask"); let is_mine = asks[0]["is_mine"].as_bool().unwrap(); - assert_eq!(is_mine, false); + assert!(!is_mine); // make another order by Alice let rc = block_on(mm_alice.rpc(&json! ({ @@ -6232,7 +6234,7 @@ fn test_get_current_mtp() { ]); let passphrase = "cMhHM3PMpMrChygR4bLF7QsTdenhWpFrrmf2UezBG3eeFsz41rtL"; - let conf = Mm2TestConf::seednode(&passphrase, &coins); + let conf = Mm2TestConf::seednode(passphrase, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let (_dump_log, _dump_dashboard) = mm.mm_dump(); @@ -6254,10 +6256,10 @@ fn test_get_current_mtp() { .unwrap(); // Test if request is successful before proceeding. - assert_eq!(true, rc.0.is_success()); + assert!(rc.0.is_success()); let mtp_result: Json = json::from_str(&rc.1).unwrap(); // Test if mtp returns a u32 Number. - assert_eq!(true, mtp_result["result"]["mtp"].is_number()); + assert!(mtp_result["result"]["mtp"].is_number()); } #[test] @@ -6295,7 +6297,7 @@ fn test_get_public_key() { // Must be 200 assert_eq!(resp.0, 200); - let v: RpcV2Response = serde_json::from_str(&*resp.1).unwrap(); + let v: RpcV2Response = serde_json::from_str(&resp.1).unwrap(); assert_eq!( v.result.public_key, "022cd3021a2197361fb70b862c412bc8e44cff6951fa1de45ceabfdd9b4c520420" @@ -6334,7 +6336,7 @@ fn test_get_public_key_hash() { // Must be 200 assert_eq!(resp.0, StatusCode::OK); - let v: RpcV2Response = serde_json::from_str(&*resp.1).unwrap(); + let v: RpcV2Response = serde_json::from_str(&resp.1).unwrap(); // Public key hash must be "b506088aa2a3b4bb1da3a29bf00ce1a550ea1df9" assert_eq!(v.result.public_key_hash, "b506088aa2a3b4bb1da3a29bf00ce1a550ea1df9") } @@ -6481,9 +6483,9 @@ fn test_conf_settings_in_orderbook() { "Alice RICK/MORTY orderbook must have exactly 1 ask" ); assert_eq!(alice_orderbook.asks[0].base_confs, 10); - assert_eq!(alice_orderbook.asks[0].base_nota, true); + assert!(alice_orderbook.asks[0].base_nota); assert_eq!(alice_orderbook.asks[0].rel_confs, 5); - assert_eq!(alice_orderbook.asks[0].rel_nota, false); + assert!(!alice_orderbook.asks[0].rel_nota); assert_eq!( alice_orderbook.bids.len(), @@ -6491,9 +6493,9 @@ fn test_conf_settings_in_orderbook() { "Alice RICK/MORTY orderbook must have exactly 1 bid" ); assert_eq!(alice_orderbook.bids[0].base_confs, 10); - assert_eq!(alice_orderbook.bids[0].base_nota, true); + assert!(alice_orderbook.bids[0].base_nota); assert_eq!(alice_orderbook.bids[0].rel_confs, 5); - assert_eq!(alice_orderbook.bids[0].rel_nota, false); + assert!(!alice_orderbook.bids[0].rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -6622,9 +6624,9 @@ fn alice_can_see_confs_in_orderbook_after_sync() { .find(|entry| entry.pubkey == bob_pubkey) .unwrap(); assert_eq!(bob_order_in_orderbook.base_confs, 10); - assert_eq!(bob_order_in_orderbook.base_nota, true); + assert!(bob_order_in_orderbook.base_nota); assert_eq!(bob_order_in_orderbook.rel_confs, 5); - assert_eq!(bob_order_in_orderbook.rel_nota, false); + assert!(!bob_order_in_orderbook.rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -6827,9 +6829,9 @@ fn test_sign_verify_message_eth() { "0xcdf11a9c4591fb7334daa4b21494a2590d3f7de41c7d2b333a5b61ca59da9b311b492374cc0ba4fbae53933260fa4b1c18f15d95b694629a7b0620eec77a938600" ); - let response = block_on(verify_message(&mm_bob, "ETH", - "0xcdf11a9c4591fb7334daa4b21494a2590d3f7de41c7d2b333a5b61ca59da9b311b492374cc0ba4fbae53933260fa4b1c18f15d95b694629a7b0620eec77a938600", - "0xbAB36286672fbdc7B250804bf6D14Be0dF69fa29")); + let response = block_on(verify_message(&mm_bob, "ETH", + "0xcdf11a9c4591fb7334daa4b21494a2590d3f7de41c7d2b333a5b61ca59da9b311b492374cc0ba4fbae53933260fa4b1c18f15d95b694629a7b0620eec77a938600", + "0xbAB36286672fbdc7B250804bf6D14Be0dF69fa29")); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -7450,7 +7452,8 @@ fn test_eth_swap_contract_addr_negotiation_same_fallback() { ETH_DEV_NODES, // using arbitrary address "0x2b294F029Fde858b2c62184e8390591755521d8E", - Some(ETH_DEV_SWAP_CONTRACT) + Some(ETH_DEV_SWAP_CONTRACT), + false ))); dbg!(block_on(enable_eth_coin( @@ -7459,7 +7462,8 @@ fn test_eth_swap_contract_addr_negotiation_same_fallback() { ETH_DEV_NODES, // using arbitrary address "0x2b294F029Fde858b2c62184e8390591755521d8E", - Some(ETH_DEV_SWAP_CONTRACT) + Some(ETH_DEV_SWAP_CONTRACT), + false ))); dbg!(block_on(enable_eth_coin( @@ -7468,7 +7472,8 @@ fn test_eth_swap_contract_addr_negotiation_same_fallback() { ETH_DEV_NODES, // using arbitrary address ETH_MAINNET_SWAP_CONTRACT, - Some(ETH_DEV_SWAP_CONTRACT) + Some(ETH_DEV_SWAP_CONTRACT), + false ))); dbg!(block_on(enable_eth_coin( @@ -7477,7 +7482,8 @@ fn test_eth_swap_contract_addr_negotiation_same_fallback() { ETH_DEV_NODES, // using arbitrary address ETH_MAINNET_SWAP_CONTRACT, - Some(ETH_DEV_SWAP_CONTRACT) + Some(ETH_DEV_SWAP_CONTRACT), + false ))); let uuids = block_on(start_swaps( @@ -7538,6 +7544,7 @@ fn test_eth_swap_negotiation_fails_maker_no_fallback() { // using arbitrary address "0x2b294F029Fde858b2c62184e8390591755521d8E", None, + false ))); dbg!(block_on(enable_eth_coin( @@ -7547,6 +7554,7 @@ fn test_eth_swap_negotiation_fails_maker_no_fallback() { // using arbitrary address "0x2b294F029Fde858b2c62184e8390591755521d8E", None, + false ))); dbg!(block_on(enable_eth_coin( @@ -7555,7 +7563,8 @@ fn test_eth_swap_negotiation_fails_maker_no_fallback() { ETH_DEV_NODES, // using arbitrary address ETH_MAINNET_SWAP_CONTRACT, - Some(ETH_DEV_SWAP_CONTRACT) + Some(ETH_DEV_SWAP_CONTRACT), + false ))); dbg!(block_on(enable_eth_coin( @@ -7564,7 +7573,8 @@ fn test_eth_swap_negotiation_fails_maker_no_fallback() { ETH_DEV_NODES, // using arbitrary address ETH_MAINNET_SWAP_CONTRACT, - Some(ETH_DEV_SWAP_CONTRACT) + Some(ETH_DEV_SWAP_CONTRACT), + false ))); let uuids = block_on(start_swaps( diff --git a/mm2src/mm2_main/tests/mm2_tests/mod.rs b/mm2src/mm2_main/tests/mm2_tests/mod.rs index 3959e9e94d..d331a269c6 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mod.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mod.rs @@ -12,4 +12,5 @@ mod z_coin_tests; // dummy test helping IDE to recognize this as test module #[test] +#[allow(clippy::assertions_on_constants)] fn dummy() { assert!(true) } diff --git a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs index f45aae813b..ac7ec3b1bf 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -701,9 +701,9 @@ fn test_conf_settings_in_orderbook() { "Alice RICK/MORTY orderbook must have exactly 1 ask" ); assert_eq!(alice_orderbook.asks[0].base_confs, 10); - assert_eq!(alice_orderbook.asks[0].base_nota, true); + assert!(alice_orderbook.asks[0].base_nota); assert_eq!(alice_orderbook.asks[0].rel_confs, 5); - assert_eq!(alice_orderbook.asks[0].rel_nota, false); + assert!(!alice_orderbook.asks[0].rel_nota); assert_eq!( alice_orderbook.bids.len(), @@ -711,9 +711,9 @@ fn test_conf_settings_in_orderbook() { "Alice RICK/MORTY orderbook must have exactly 1 bid" ); assert_eq!(alice_orderbook.bids[0].base_confs, 10); - assert_eq!(alice_orderbook.bids[0].base_nota, true); + assert!(alice_orderbook.bids[0].base_nota); assert_eq!(alice_orderbook.bids[0].rel_confs, 5); - assert_eq!(alice_orderbook.bids[0].rel_nota, false); + assert!(!alice_orderbook.bids[0].rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); @@ -841,9 +841,9 @@ fn alice_can_see_confs_in_orderbook_after_sync() { .find(|entry| entry.pubkey == bob_pubkey) .unwrap(); assert_eq!(bob_order_in_orderbook.base_confs, 10); - assert_eq!(bob_order_in_orderbook.base_nota, true); + assert!(bob_order_in_orderbook.base_nota); assert_eq!(bob_order_in_orderbook.rel_confs, 5); - assert_eq!(bob_order_in_orderbook.rel_nota, false); + assert!(!bob_order_in_orderbook.rel_nota); block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); diff --git a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs index 796162de48..660e60c4e5 100644 --- a/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/tendermint_tests.rs @@ -1,12 +1,13 @@ use common::block_on; use mm2_number::BigDecimal; use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_err, enable_tendermint, - enable_tendermint_token, get_tendermint_my_tx_history, iris_nimda_testnet_conf, - iris_testnet_conf, my_balance, send_raw_transaction, withdraw_v1, MarketMakerIt, - Mm2TestConf}; + enable_tendermint_token, get_tendermint_my_tx_history, ibc_withdraw, + iris_nimda_testnet_conf, iris_testnet_conf, my_balance, send_raw_transaction, + withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{RpcV2Response, TendermintActivationResult, TransactionDetails}; use serde_json::{self as json, json}; +const IRIS_TEST_SEED: &str = "iris test seed"; const ATOM_TEST_BALANCE_SEED: &str = "atom test seed"; const ATOM_TEST_WITHDRAW_SEED: &str = "atom test withdraw seed"; const ATOM_TICKER: &str = "ATOM"; @@ -32,7 +33,7 @@ fn test_tendermint_activation_and_balance() { let result: RpcV2Response = json::from_value(activation_result).unwrap(); assert_eq!(result.result.address, expected_address); - let expected_balance: BigDecimal = "0.0959".parse().unwrap(); + let expected_balance: BigDecimal = "8.0959".parse().unwrap(); assert_eq!(result.result.balance.spendable, expected_balance); let my_balance = block_on(my_balance(&mm, ATOM_TICKER)); @@ -108,18 +109,56 @@ fn test_tendermint_withdraw() { "cosmos1w5h6wud7a8zpa539rc99ehgl9gwkad3wjsjq8v".to_owned() ]); + let tx_details = block_on(withdraw_v1( + &mm, + ATOM_TICKER, + "cosmos1w5h6wud7a8zpa539rc99ehgl9gwkad3wjsjq8v", + "0.1", + )); let send_raw_tx = block_on(send_raw_transaction(&mm, ATOM_TICKER, &tx_details.tx_hex)); println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); } +#[test] +fn test_tendermint_ibc_withdraw() { + const IBC_SOURCE_CHANNEL: &str = "channel-81"; + const IBC_TARGET_ADDRESS: &str = "cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl"; + const MY_ADDRESS: &str = "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2"; + + let coins = json!([iris_testnet_conf(), iris_nimda_testnet_conf()]); + let platform_coin = coins[0]["coin"].as_str().unwrap(); + let token = coins[1]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(IRIS_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint(&mm, platform_coin, &[], IRIS_TESTNET_RPC_URLS, false)); + println!("Activation with assets {}", json::to_string(&activation_res).unwrap()); + + let activation_res = block_on(enable_tendermint_token(&mm, token)); + println!("Token activation {}", json::to_string(&activation_res).unwrap()); + + let tx_details = block_on(ibc_withdraw(&mm, IBC_SOURCE_CHANNEL, token, IBC_TARGET_ADDRESS, "0.1")); + println!("IBC transfer to atom address {}", json::to_string(&tx_details).unwrap()); + + let expected_spent: BigDecimal = "0.1".parse().unwrap(); + assert_eq!(tx_details.spent_by_me, expected_spent); + + assert_eq!(tx_details.to, vec![IBC_TARGET_ADDRESS.to_owned()]); + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + + let tx_details = block_on(ibc_withdraw(&mm, IBC_SOURCE_CHANNEL, token, IBC_TARGET_ADDRESS, "0.1")); + let send_raw_tx = block_on(send_raw_transaction(&mm, token, &tx_details.tx_hex)); + println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); +} + #[test] fn test_tendermint_token_activation_and_withdraw() { - const TEST_SEED: &str = "iris test seed"; let coins = json!([iris_testnet_conf(), iris_nimda_testnet_conf()]); let platform_coin = coins[0]["coin"].as_str().unwrap(); let token = coins[1]["coin"].as_str().unwrap(); - let conf = Mm2TestConf::seednode(TEST_SEED, &coins); + let conf = Mm2TestConf::seednode(IRIS_TEST_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); let activation_res = block_on(enable_tendermint(&mm, platform_coin, &[], IRIS_TESTNET_RPC_URLS, false)); @@ -188,6 +227,12 @@ fn test_tendermint_token_activation_and_withdraw() { "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2".to_owned() ]); + let tx_details = block_on(withdraw_v1( + &mm, + token, + "iaa1e0rx87mdj79zejewuc4jg7ql9ud2286g2us8f2", + "0.1", + )); let send_raw_tx = block_on(send_raw_transaction(&mm, token, &tx_details.tx_hex)); println!("Send raw tx {}", json::to_string(&send_raw_tx).unwrap()); } @@ -223,9 +268,9 @@ fn test_tendermint_tx_history() { true, )); - if let Err(_) = block_on(mm.wait_for_log(60., |log| log.contains(TX_FINISHED_LOG))) { + if block_on(mm.wait_for_log(60., |log| log.contains(TX_FINISHED_LOG))).is_err() { println!("{}", mm.log_as_utf8().unwrap()); - assert!(false, "Tx history didn't finish which is not expected"); + panic!("Tx history didn't finish which is not expected"); } // testing IRIS-TEST history @@ -277,13 +322,7 @@ fn test_disable_tendermint_platform_coin_with_token() { let conf = Mm2TestConf::seednode(TEST_SEED, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); // Enable platform coin IRIS-TEST - let activation_res = block_on(enable_tendermint( - &mm, - platform_coin, - &[], - &["http://34.80.202.172:26657"], - false, - )); + let activation_res = block_on(enable_tendermint(&mm, platform_coin, &[], IRIS_TESTNET_RPC_URLS, false)); assert!(&activation_res.get("result").unwrap().get("address").is_some()); // Enable platform coin token IRIS-NIMDA diff --git a/mm2src/mm2_metamask/src/metamask_error.rs b/mm2src/mm2_metamask/src/metamask_error.rs index 29e3201b9a..68db8af025 100644 --- a/mm2src/mm2_metamask/src/metamask_error.rs +++ b/mm2src/mm2_metamask/src/metamask_error.rs @@ -1,7 +1,7 @@ use derive_more::Display; use jsonrpc_core::{Error as RPCError, ErrorCode as RpcErrorCode}; use mm2_err_handle::prelude::*; -use serde_derive::Serialize; +use serde_derive::{Deserialize, Serialize}; use web3::Error as Web3Error; const USER_CANCELLED_ERROR_CODE: RpcErrorCode = RpcErrorCode::ServerError(4001); @@ -55,7 +55,7 @@ impl From for MetamaskError { /// so please extend it if it's required **only**. /// /// Please also note that this enum is fieldless. -#[derive(Clone, Debug, Display, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Display, Serialize, PartialEq)] pub enum MetamaskRpcError { EthProviderNotFound, #[display(fmt = "User cancelled request")] diff --git a/mm2src/mm2_metrics/src/mm_metrics.rs b/mm2src/mm2_metrics/src/mm_metrics.rs index ad3c5749b0..0814c0abde 100644 --- a/mm2src/mm2_metrics/src/mm_metrics.rs +++ b/mm2src/mm2_metrics/src/mm_metrics.rs @@ -468,10 +468,10 @@ mod test { let mut actual = metrics.collect_json().unwrap(); let actual = actual["metrics"].as_array_mut().unwrap(); for expected in expected["metrics"].as_array().unwrap() { - let index = actual.iter().position(|metric| metric == expected).expect(&format!( - "Couldn't find expected metric: {:#?} \n in {:#?}", - expected, actual - )); + let index = actual + .iter() + .position(|metric| metric == expected) + .unwrap_or_else(|| panic!("Couldn't find expected metric: {:#?} \n in {:#?}", expected, actual)); actual.remove(index); } diff --git a/mm2src/mm2_net/Cargo.toml b/mm2src/mm2_net/Cargo.toml index 045b255120..50d7cc5a3f 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -33,4 +33,4 @@ js-sys = "0.3.27" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] hyper = { version = "0.14.11", features = ["client", "http2", "server", "tcp"] } -gstuff = { version = "0.7", features = ["crossterm", "nightly"] } +gstuff = { version = "0.7", features = ["nightly"] } diff --git a/mm2src/mm2_net/src/native_http.rs b/mm2src/mm2_net/src/native_http.rs index ea4b0b2912..700bdb1efa 100644 --- a/mm2src/mm2_net/src/native_http.rs +++ b/mm2src/mm2_net/src/native_http.rs @@ -1,10 +1,11 @@ -use crate::transport::{SlurpError, SlurpResult}; +use crate::transport::{SlurpError, SlurpResult, SlurpResultJson}; use common::wio::{drive03, HYPER}; use common::APPLICATION_JSON; use futures::channel::oneshot::Canceled; -use http::{header, Request}; +use http::{header, HeaderValue, Request}; use hyper::Body; use mm2_err_handle::prelude::*; +use serde_json::Value as Json; impl From for SlurpError { fn from(_: Canceled) -> Self { SlurpError::Internal("Spawned Slurp future has been canceled".to_owned()) } @@ -49,12 +50,47 @@ pub async fn slurp_req(request: Request>) -> SlurpResult { Ok((status, headers, output.to_vec())) } +/// Executes a Hyper request, requires [`Request`] and return the response status, headers and body as Json. +pub async fn slurp_req_body(request: Request) -> SlurpResultJson { + let uri = request.uri().to_string(); + + let request_f = HYPER.request(request); + let response = drive03(request_f) + .await? + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let status = response.status(); + let headers = response.headers().clone(); + // Get the response body bytes. + let body_bytes = hyper::body::to_bytes(response.into_body()) + .await + .map_to_mm(|e| SlurpError::from_hyper_error(e, uri.clone()))?; + let body_str = String::from_utf8(body_bytes.to_vec()).map_to_mm(|e| SlurpError::Internal(e.to_string()))?; + let body: Json = serde_json::from_str(&body_str)?; + Ok((status, headers, body)) +} + /// Executes a GET request, returning the response status, headers and body. pub async fn slurp_url(url: &str) -> SlurpResult { let req = Request::builder().uri(url).body(Vec::new())?; slurp_req(req).await } +/// Executes a GET request with additional headers. +/// Returning the response status, headers and body. +pub async fn slurp_url_with_headers(url: &str, headers: Vec<(&'static str, &'static str)>) -> SlurpResult { + let mut req = Request::builder(); + let h = req + .headers_mut() + .or_mm_err(|| SlurpError::Internal("An error occured while accessing to the request headers.".to_string()))?; + + for (key, value) in headers { + h.insert(key, HeaderValue::from_static(value)); + } + + let req = req.uri(url).body(Vec::new())?; + slurp_req(req).await +} + /// Executes a POST request, returning the response status, headers and body. pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { let request = Request::builder() diff --git a/mm2src/mm2_net/src/transport.rs b/mm2src/mm2_net/src/transport.rs index ad51122993..2ba04b65e1 100644 --- a/mm2src/mm2_net/src/transport.rs +++ b/mm2src/mm2_net/src/transport.rs @@ -4,15 +4,18 @@ use ethkey::Secret; use http::{HeaderMap, StatusCode}; use mm2_err_handle::prelude::*; use serde::{Deserialize, Serialize}; +use serde_json::{Error, Value as Json}; #[cfg(not(target_arch = "wasm32"))] -pub use crate::native_http::{slurp_post_json, slurp_req, slurp_url}; +pub use crate::native_http::{slurp_post_json, slurp_req, slurp_req_body, slurp_url, slurp_url_with_headers}; #[cfg(target_arch = "wasm32")] -pub use crate::wasm_http::{slurp_post_json, slurp_url}; +pub use crate::wasm_http::{slurp_post_json, slurp_url, slurp_url_with_headers}; pub type SlurpResult = Result<(StatusCode, HeaderMap, Vec), MmError>; +pub type SlurpResultJson = Result<(StatusCode, HeaderMap, Json), MmError>; + #[derive(Debug, Deserialize, Display, Serialize)] pub enum SlurpError { #[display(fmt = "Error deserializing '{}' response: {}", uri, error)] @@ -27,6 +30,10 @@ pub enum SlurpError { Internal(String), } +impl From for SlurpError { + fn from(e: Error) -> Self { SlurpError::Internal(e.to_string()) } +} + impl From for JsonRpcErrorType { fn from(err: SlurpError) -> Self { match err { diff --git a/mm2src/mm2_net/src/wasm_http.rs b/mm2src/mm2_net/src/wasm_http.rs index afa24d1848..3767c2f40c 100644 --- a/mm2src/mm2_net/src/wasm_http.rs +++ b/mm2src/mm2_net/src/wasm_http.rs @@ -24,6 +24,17 @@ pub async fn slurp_url(url: &str) -> SlurpResult { .map(|(status_code, response)| (status_code, HeaderMap::new(), response.into_bytes())) } +/// Executes a GET request with additional headers. +/// Returning the response status, headers and body. +/// Please note the return header map is empty, because `wasm_bindgen` doesn't provide the way to extract all headers. +pub async fn slurp_url_with_headers(url: &str, headers: Vec<(&str, &str)>) -> SlurpResult { + FetchRequest::get(url) + .headers(headers) + .request_str() + .await + .map(|(status_code, response)| (status_code, HeaderMap::new(), response.into_bytes())) +} + /// Executes a POST request, returning the response status, headers and body. /// Please note the return header map is empty, because `wasm_bindgen` doesn't provide the way to extract all headers. pub async fn slurp_post_json(url: &str, body: String) -> SlurpResult { @@ -86,6 +97,13 @@ impl FetchRequest { self } + pub fn headers(mut self, headers: Vec<(&str, &str)>) -> FetchRequest { + for (key, value) in headers { + self.headers.insert(key.to_owned(), value.to_owned()); + } + self + } + pub async fn request_str(self) -> FetchResult { let (tx, rx) = oneshot::channel(); Self::spawn_fetch_str(self, tx); diff --git a/mm2src/mm2_net/src/wasm_ws.rs b/mm2src/mm2_net/src/wasm_ws.rs index 40daa603dd..bf8008de54 100644 --- a/mm2src/mm2_net/src/wasm_ws.rs +++ b/mm2src/mm2_net/src/wasm_ws.rs @@ -313,7 +313,7 @@ impl WebSocketImpl { let (tx, rx) = mpsc::channel(1024); let onopen_closure = construct_ws_event_closure(|_: JsValue| WsTransportEvent::Establish, tx.clone()); - let onclose_closure = construct_ws_event_closure(|close: CloseEvent| WsTransportEvent::from(close), tx.clone()); + let onclose_closure = construct_ws_event_closure::(WsTransportEvent::from, tx.clone()); let onerror_closure = construct_ws_event_closure( |_: JsValue| WsTransportEvent::Error(WsTransportError::UnderlyingError), tx.clone(), @@ -323,7 +323,7 @@ impl WebSocketImpl { Ok(response) => WsTransportEvent::Incoming(response), Err(e) => WsTransportEvent::Error(WsTransportError::ErrorDecodingIncoming(e)), }, - tx.clone(), + tx, ); ws.set_onopen(Some(onopen_closure.as_ref().unchecked_ref())); diff --git a/mm2src/mm2_number/src/big_int_str.rs b/mm2src/mm2_number/src/big_int_str.rs index 8648548c47..d0855cfbbf 100644 --- a/mm2src/mm2_number/src/big_int_str.rs +++ b/mm2src/mm2_number/src/big_int_str.rs @@ -69,11 +69,11 @@ mod big_int_str_tests { fn test_bigint_str_deserialize() { let num = r#""1023""#; let expected: BigInt = 1023.into(); - let actual: BigIntStr = json::from_str(&num).unwrap(); + let actual: BigIntStr = json::from_str(num).unwrap(); assert_eq!(expected, actual.0); let err_num = "abc"; - let res = json::from_str::(&err_num); + let res = json::from_str::(err_num); assert!(res.is_err()); } diff --git a/mm2src/mm2_number/src/mm_number.rs b/mm2src/mm2_number/src/mm_number.rs index 64bf5d112f..ba6bdd112c 100644 --- a/mm2src/mm2_number/src/mm_number.rs +++ b/mm2src/mm2_number/src/mm_number.rs @@ -282,7 +282,7 @@ mod tests { for num in vals { let decimal: BigDecimal = BigDecimal::from_str(num).unwrap(); let expected: MmNumber = from_dec_to_ratio(&decimal).into(); - let actual: MmNumber = json::from_str(&num).unwrap(); + let actual: MmNumber = json::from_str(num).unwrap(); assert_eq!(expected, actual); } } diff --git a/mm2src/mm2_rpc/Cargo.toml b/mm2src/mm2_rpc/Cargo.toml index 2f08a6d9b9..5f6fc9d0b1 100644 --- a/mm2src/mm2_rpc/Cargo.toml +++ b/mm2src/mm2_rpc/Cargo.toml @@ -21,4 +21,4 @@ ser_error_derive = { path = "../derives/ser_error_derive" } gstuff = { version = "0.7", features = ["nightly"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gstuff = { version = "0.7", features = ["crossterm", "nightly"] } +gstuff = { version = "0.7", features = ["nightly"] } diff --git a/mm2src/mm2_rpc/src/mm_protocol.rs b/mm2src/mm2_rpc/src/mm_protocol.rs index bc032c1465..9788236a61 100644 --- a/mm2src/mm2_rpc/src/mm_protocol.rs +++ b/mm2src/mm2_rpc/src/mm_protocol.rs @@ -186,7 +186,7 @@ mod tests { #[test] fn test_mm_rpc_response_serialize() { let ok: MmRpcResponse<_, AnError> = MmRpcBuilder::ok(vec![1, 2, 3]).build(); - let actual = json::to_value(&ok).expect("Couldn't serialize MmRpcResponse"); + let actual = json::to_value(ok).expect("Couldn't serialize MmRpcResponse"); let expected = json!({ "mmrpc": "2.0", "result": [1, 2, 3], @@ -195,7 +195,7 @@ mod tests { assert_eq!(actual, expected); let ok_with_id: MmRpcResponse<_, AnError> = MmRpcBuilder::ok(vec![1, 2, 3]).id(Some(2)).build(); - let actual = json::to_value(&ok_with_id).expect("Couldn't serialize MmRpcResponse"); + let actual = json::to_value(ok_with_id).expect("Couldn't serialize MmRpcResponse"); let expected = json!({ "mmrpc": "2.0", "result": [1, 2, 3], @@ -206,7 +206,7 @@ mod tests { let error_type = AnError::NotSufficientBalance { missing: 123 }; let err_line = line!() + 1; let err: MmRpcResponse = MmRpcBuilder::err(MmError::new(error_type)).build(); - let actual = json::to_value(&err).expect("Couldn't serialize MmRpcResponse"); + let actual = json::to_value(err).expect("Couldn't serialize MmRpcResponse"); let expected = json!({ "mmrpc": "2.0", "error": "Not sufficient balance. Top up your balance by 123", diff --git a/mm2src/mm2_test_helpers/Cargo.toml b/mm2src/mm2_test_helpers/Cargo.toml index 9b8e284947..286c9cc3bf 100644 --- a/mm2src/mm2_test_helpers/Cargo.toml +++ b/mm2src/mm2_test_helpers/Cargo.toml @@ -26,7 +26,7 @@ rpc = { path = "../mm2_bitcoin/rpc" } serde = "1" serde_derive = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -uuid = { version = "0.7", features = ["serde", "v4"] } +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] chrono = { version = "0.4", features = ["wasmbind"] } @@ -34,5 +34,4 @@ gstuff = { version = "0.7", features = ["nightly"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] chrono = "0.4" -crossterm = "0.20" -gstuff = { version = "0.7", features = ["crossterm", "nightly"] } +gstuff = { version = "0.7", features = ["nightly"] } diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 2680ced419..910837c666 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -20,6 +20,7 @@ use serde_json::{self as json, json, Value as Json}; use std::collections::HashMap; use std::convert::TryFrom; use std::env; +#[cfg(not(target_arch = "wasm32"))] use std::io::Write; use std::net::IpAddr; use std::num::NonZeroUsize; use std::process::Child; @@ -36,7 +37,6 @@ cfg_native! { use bytes::Bytes; use futures::channel::oneshot; use futures::task::SpawnExt; - use gstuff::ISATTY; use http::Request; use regex::Regex; use std::fs; @@ -124,10 +124,10 @@ pub const MORTY_ELECTRUM_ADDRS: &[&str] = &[ ]; pub const ZOMBIE_TICKER: &str = "ZOMBIE"; pub const ARRR: &str = "ARRR"; -pub const ZOMBIE_ELECTRUMS: &[&str] = &["zombie.sirseven.me:10033"]; -pub const ZOMBIE_LIGHTWALLETD_URLS: &[&str] = &["http://zombie.sirseven.me:443"]; -pub const PIRATE_ELECTRUMS: &[&str] = &["pirate.sirseven.me:10032"]; -pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &["http://pirate.sirseven.me:443"]; +pub const ZOMBIE_ELECTRUMS: &[&str] = &["zombie.dragonhound.info:10033"]; +pub const ZOMBIE_LIGHTWALLETD_URLS: &[&str] = &["http://zombie.dragonhound.info:443"]; +pub const PIRATE_ELECTRUMS: &[&str] = &["pirate.dragonhound.info:10032"]; +pub const PIRATE_LIGHTWALLETD_URLS: &[&str] = &["http://pirate.dragonhound.info:443"]; pub const DEFAULT_RPC_PASSWORD: &str = "pass"; pub const QRC20_ELECTRUMS: &[&str] = &[ "electrum1.cipig.net:10071", @@ -147,7 +147,7 @@ pub const ETH_DEV_NODES: &[&str] = &["http://195.201.0.6:8565"]; pub const ETH_DEV_SWAP_CONTRACT: &str = "0xa09ad3cd7e96586ebd05a2607ee56b56fb2db8fd"; pub const ETH_SEPOLIA_NODE: &[&str] = &["https://rpc-sepolia.rockx.com/"]; -pub const ETH_SEPOLIA_SWAP_CONTRACT: &str = "0xA25E0e06fB139CDc2f9f11675877DaD9EdD1C352"; +pub const ETH_SEPOLIA_SWAP_CONTRACT: &str = "0x5BCC05dD32a87fABEDBcbbfeb77476eaD1F7051C"; pub const ETH_SEPOLIA_TOKEN_CONTRACT: &str = "0x948BF5172383F1Bc0Fdf3aBe0630b855694A5D2c"; pub const BCHD_TESTNET_URLS: &[&str] = &["https://bchd-testnet.greyh.at:18335"]; @@ -216,6 +216,21 @@ impl Mm2TestConf { } } + pub fn light_node_using_watchers(passphrase: &str, coins: &Json, seednodes: &[&str]) -> Self { + Mm2TestConf { + conf: json!({ + "gui": "nogui", + "netid": 9998, + "passphrase": passphrase, + "coins": coins, + "rpc_password": DEFAULT_RPC_PASSWORD, + "seednodes": seednodes, + "use_watchers": true + }), + rpc_password: DEFAULT_RPC_PASSWORD.into(), + } + } + pub fn watcher_light_node(passphrase: &str, coins: &Json, seednodes: &[&str], conf: WatcherConf) -> Self { Mm2TestConf { conf: json!({ @@ -844,8 +859,9 @@ pub struct RaiiDump { #[cfg(not(target_arch = "wasm32"))] impl Drop for RaiiDump { fn drop(&mut self) { - use crossterm::execute; - use crossterm::style::{Color, Print, SetForegroundColor}; + const DARK_YELLOW_ANSI_CODE: &str = "\x1b[33m"; + const YELLOW_ANSI_CODE: &str = "\x1b[93m"; + const RESET_COLOR_ANSI_CODE: &str = "\x1b[0m"; // `term` bypasses the stdout capturing, we should only use it if the capturing was disabled. let nocapture = env::args().any(|a| a == "--nocapture"); @@ -857,15 +873,16 @@ impl Drop for RaiiDump { let log = String::from_utf8_lossy(&log); let log = log.trim(); - if let (true, true, mut stdout) = (nocapture, *ISATTY, std::io::stdout()) { - execute!( - stdout, - SetForegroundColor(Color::DarkYellow), - Print(format!("vvv {:?} vvv\n", self.log_path)), - SetForegroundColor(Color::Yellow), - Print(log), - ) - .expect("Printing to stdout failed"); + // If we want to determine is a tty or not here and write logs to stdout only if it's tty, + // we can use something like https://docs.rs/atty/latest/atty/ here, look like it's more cross-platform than gstuff::ISATTY . + + if nocapture { + std::io::stdout() + .write_all(format!("{}vvv {:?} vvv\n", DARK_YELLOW_ANSI_CODE, self.log_path).as_bytes()) + .expect("Printing to stdout failed"); + std::io::stdout() + .write_all(format!("{}{}{}\n", YELLOW_ANSI_CODE, log, RESET_COLOR_ANSI_CODE).as_bytes()) + .expect("Printing to stdout failed"); } else { log!("vvv {:?} vvv\n{}", self.log_path, log); } @@ -1139,7 +1156,8 @@ impl MarketMakerIt { } else { StatusCode::INTERNAL_SERVER_ERROR }; - let body_str = json::to_string(&body).expect(&format!("Response {:?} is not a valid JSON", body)); + let body_str = + json::to_string(&body).unwrap_or_else(|_| panic!("Response {:?} is not a valid JSON", body)); Ok((status_code, body_str, HeaderMap::new())) }, Err(e) => Ok((StatusCode::INTERNAL_SERVER_ERROR, e, HeaderMap::new())), @@ -1576,6 +1594,7 @@ pub async fn enable_eth_coin( urls: &[&str], swap_contract_address: &str, fallback_swap_contract: Option<&str>, + contract_supports_watcher: bool, ) -> Json { let enable = mm .rpc(&json!({ @@ -1586,6 +1605,7 @@ pub async fn enable_eth_coin( "swap_contract_address": swap_contract_address, "fallback_swap_contract": fallback_swap_contract, "mm2": 1, + "contract_supports_watchers": contract_supports_watcher })) .await .unwrap(); @@ -2190,6 +2210,33 @@ pub async fn withdraw_v1(mm: &MarketMakerIt, coin: &str, to: &str, amount: &str) json::from_str(&request.1).unwrap() } +pub async fn ibc_withdraw( + mm: &MarketMakerIt, + source_channel: &str, + coin: &str, + to: &str, + amount: &str, +) -> TransactionDetails { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "ibc_withdraw", + "mmrpc": "2.0", + "params": { + "ibc_source_channel": source_channel, + "coin": coin, + "to": to, + "amount": amount + } + })) + .await + .unwrap(); + assert_eq!(request.0, StatusCode::OK, "'ibc_withdraw' failed: {}", request.1); + + let json: Json = json::from_str(&request.1).unwrap(); + json::from_value(json["result"].clone()).unwrap() +} + pub async fn withdraw_status(mm: &MarketMakerIt, task_id: u64) -> Json { let request = mm .rpc(&json!({ diff --git a/mm2src/trezor/src/client.rs b/mm2src/trezor/src/client.rs index cb4bcc3bf5..3e06023ad5 100644 --- a/mm2src/trezor/src/client.rs +++ b/mm2src/trezor/src/client.rs @@ -116,11 +116,10 @@ impl<'a> TrezorSession<'a> { }; let result_handler = ResultHandler::<()>::new(move |pong: proto_common::Success| { - if pong.message == Some(ping_message.clone()) { + if pong.message == Some(ping_message) { Ok(()) } else { - let error = format!("Expected '{ping_message}' PONG message, found: {:?}", pong.message); - MmError::err(TrezorError::Failure(OperationFailure::Other(error))) + MmError::err(TrezorError::PongMessageMismatch) } }); self.call(req, result_handler).await diff --git a/mm2src/trezor/src/error.rs b/mm2src/trezor/src/error.rs index dfcf5303ba..60397e55a1 100644 --- a/mm2src/trezor/src/error.rs +++ b/mm2src/trezor/src/error.rs @@ -32,14 +32,25 @@ pub enum TrezorError { #[display(fmt = "Unexpected interaction request: {:?}", _0)] UnexpectedInteractionRequest(TrezorUserInteraction), Internal(String), + PongMessageMismatch, } -#[derive(Debug, Display)] +#[derive(Clone, Debug, Display)] pub enum OperationFailure { InvalidPin, - /// TODO expand it to other types. - #[display(fmt = "Operation failed due to unknown reason: {}", _0)] - Other(String), + UnexpectedMessage, + ButtonExpected, + DataError, + PinExpected, + InvalidSignature, + ProcessError, + NotEnoughFunds, + NotInitialized, + WipeCodeMismatch, + InvalidSession, + FirmwareError, + FailureMessageNotFound, + UserCancelled, } impl From for OperationFailure { @@ -48,15 +59,25 @@ impl From for OperationFailure { Some(FailureType::FailurePinInvalid) | Some(FailureType::FailurePinMismatch) => { OperationFailure::InvalidPin }, - _ => OperationFailure::Other(format!("{:?}", failure)), + Some(FailureType::FailureActionCancelled) | Some(FailureType::FailurePinCancelled) => { + OperationFailure::UserCancelled + }, + Some(FailureType::FailureUnexpectedMessage) => OperationFailure::UnexpectedMessage, + Some(FailureType::FailureButtonExpected) => OperationFailure::ButtonExpected, + Some(FailureType::FailureDataError) => OperationFailure::DataError, + Some(FailureType::FailurePinExpected) => OperationFailure::PinExpected, + Some(FailureType::FailureInvalidSignature) => OperationFailure::InvalidSignature, + Some(FailureType::FailureProcessError) => OperationFailure::ProcessError, + Some(FailureType::FailureNotEnoughFunds) => OperationFailure::NotEnoughFunds, + Some(FailureType::FailureNotInitialized) => OperationFailure::NotInitialized, + Some(FailureType::FailureWipeCodeMismatch) => OperationFailure::WipeCodeMismatch, + Some(FailureType::FailureInvalidSession) => OperationFailure::InvalidSession, + Some(FailureType::FailureFirmwareError) => OperationFailure::FirmwareError, + None => OperationFailure::FailureMessageNotFound, } } } -impl From for TrezorError { - fn from(failure: OperationFailure) -> Self { TrezorError::Failure(failure) } -} - impl From for TrezorError { fn from(e: DecodeError) -> Self { TrezorError::ProtocolError(e.to_string()) } }