diff --git a/.cargo/config.toml b/.cargo/config.toml index 5704896780..d59b360d40 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,7 +2,7 @@ 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" ] +rustflags = [ "-Zshare-generics=y", '--cfg=curve25519_dalek_backend="fiat"' ] # # Install lld using package manager # [target.x86_64-unknown-linux-gnu] @@ -25,3 +25,6 @@ rustflags = [ "-Zshare-generics=y" ] [target.wasm32-unknown-unknown] runner = 'wasm-bindgen-test-runner' rustflags = [ "--cfg=web_sys_unstable_apis" ] + +[target.x86_64-pc-windows-msvc] +rustflags = ["-Ctarget-feature=+crt-static"] diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 585f9d4856..0849cca89a 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -51,17 +51,6 @@ jobs: - name: Build run: cargo build --release - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-linux-x86-64.zip" - zip $NAME target/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -70,6 +59,7 @@ jobs: NAME="kdf_$KDF_BUILD_TAG-linux-x86-64.zip" zip $NAME target/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -125,16 +115,6 @@ jobs: - name: Build run: cargo build --release --target x86_64-apple-darwin - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-x86-64.zip" - zip $NAME target/x86_64-apple-darwin/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -143,6 +123,7 @@ jobs: NAME="kdf_$KDF_BUILD_TAG-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -186,17 +167,6 @@ jobs: - name: Build run: cargo build --release --target aarch64-apple-darwin - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-arm64.zip" - zip $NAME target/aarch64-apple-darwin/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -205,6 +175,7 @@ jobs: NAME="kdf_$KDF_BUILD_TAG-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -247,25 +218,15 @@ jobs: - name: Build run: cargo build --release - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - $NAME="mm2_$Env:KDF_BUILD_TAG-win-x86-64.zip" - 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll - $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | $NAME="kdf_$Env:KDF_BUILD_TAG-win-x86-64.zip" - 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll + 7z a $NAME .\target\release\kdf.exe $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -309,18 +270,6 @@ jobs: - name: Build run: cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" - cp target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libmm2.a - zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -330,6 +279,7 @@ jobs: mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -438,18 +388,6 @@ jobs: - name: Build run: cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-ios-aarch64.zip" - cp target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libmm2.a - zip $NAME target/aarch64-apple-ios/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -459,6 +397,7 @@ jobs: mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a zip $NAME target/aarch64-apple-ios/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -516,18 +455,6 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-android-aarch64.zip" - cp target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a - zip $NAME target/aarch64-linux-android/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -537,6 +464,7 @@ jobs: mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a zip $NAME target/aarch64-linux-android/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -594,18 +522,6 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress mm2 build output - env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - run: | - NAME="mm2_$KDF_BUILD_TAG-android-armv7.zip" - cp target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a - zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} @@ -615,6 +531,7 @@ jobs: mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 7fec248c88..75e9262b55 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -51,19 +51,12 @@ jobs: - name: Build run: cargo build --release - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-linux-x86-64.zip" - zip $NAME target/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-linux-x86-64.zip" zip $NAME target/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -116,19 +109,12 @@ jobs: - name: Build run: cargo build --release --target x86_64-apple-darwin - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-x86-64.zip" - zip $NAME target/x86_64-apple-darwin/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -172,19 +158,12 @@ jobs: - name: Build run: cargo build --release --target aarch64-apple-darwin - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-arm64.zip" - zip $NAME target/aarch64-apple-darwin/release/mm2 -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-mac-arm64.zip" zip $NAME target/aarch64-apple-darwin/release/kdf -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -227,19 +206,12 @@ jobs: - name: Build run: cargo build --release - - name: Compress mm2 build output - run: | - $NAME="mm2_$Env:KDF_BUILD_TAG-win-x86-64.zip" - 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll - $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | $NAME="kdf_$Env:KDF_BUILD_TAG-win-x86-64.zip" 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll $SAFE_DIR_NAME = $Env:BRANCH_NAME -replace '/', '-' + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -283,21 +255,13 @@ jobs: - name: Build run: cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" - cp target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libmm2.a - zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-mac-dylib-x86-64.zip" mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -403,21 +367,13 @@ jobs: - name: Build run: cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-ios-aarch64.zip" - mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libmm2.a - zip $NAME target/aarch64-apple-ios/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-ios-aarch64.zip" mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a zip $NAME target/aarch64-apple-ios/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -475,21 +431,13 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-android-aarch64.zip" - mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a - zip $NAME target/aarch64-linux-android/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-android-aarch64.zip" mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a zip $NAME target/aarch64-linux-android/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact @@ -547,21 +495,13 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress mm2 build output - run: | - NAME="mm2_$KDF_BUILD_TAG-android-armv7.zip" - mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a - zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j - SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') - mkdir $SAFE_DIR_NAME - mv $NAME ./$SAFE_DIR_NAME/ - - name: Compress kdf build output run: | NAME="kdf_$KDF_BUILD_TAG-android-armv7.zip" mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j SAFE_DIR_NAME=$(echo "$BRANCH_NAME" | tr '/' '-') + mkdir $SAFE_DIR_NAME mv $NAME ./$SAFE_DIR_NAME/ - name: Upload build artifact diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0da60616b..bc242f6547 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -268,4 +268,4 @@ jobs: uses: ./.github/actions/build-cache - name: Test - run: WASM_BINDGEN_TEST_TIMEOUT=600 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main + run: WASM_BINDGEN_TEST_TIMEOUT=1200 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main diff --git a/CHANGELOG.md b/CHANGELOG.md index 132dda9919..831bc7bc68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,105 @@ +## v2.5.0-beta - 2025-07-04 + +### Features: + +**WalletConnect Integration**: +- WalletConnect v2 support for EVM and Cosmos coins was implemented, enabling wallet connection and transaction signing via the WalletConnect protocol. [#2223](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2223) [#2485](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2485) [#2508](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2508) + +--- + +### Work in Progress (WIP) Features: + +**Cosmos Network and IBC Swaps**: +- Pre-swap validation logic was implemented for maker order creation, requiring HTLC assets and healthy IBC channels on the Cosmos network, with all changes gated behind the `ibc-routing-for-swaps` feature. [#2459](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2459) +- The taker and maker order types were extended with an `order_metadata` field to carry protocol/IBC details, and cross-checks for IBC channels were added (also feature-gated), enabling both parties to validate IBC routing before a swap. [#2476](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2476) + +**TRON Integration**: +- Initial groundwork for TRON integration was started, including the addition of basic structures and boilerplate code; no end-to-end functionality is yet available. [#2425](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2425) + +--- + +### Enhancements/Fixes: + +**Event Streaming**: +- Streamer IDs in the event-streaming system were strongly typed to improve type safety and code clarity. [#2441](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2441) + +**Peer-to-Peer Network**: +- Hardcoded seed node IP addresses were removed from the peer-to-peer network configuration to improve maintainability. [#2439](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2439) + +**Orders and Trading Protocol**: +- The minimum trading volume logic was revised to remove BTC-specific volume behavior, standardizing the calculation across all coins. [#2483](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2483) + +**Tendermint / Cosmos**: +- A helper for generating internal transaction IDs for Tendermint transactions was introduced. [#2438](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2438) +- The IBC channel handler was improved to enhance safety and reliability when interacting with IBC channels. [#2298](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2298) + +**Wallet**: +- Unconfirmed z-coin notes are now correctly tracked. [#2331](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331) +- HD multi-address support for message signing was implemented, allowing message signatures from multiple derived addresses. [#2432](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2432) [#2474](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2474) +- A `delete_wallet` RPC was introduced to securely remove wallets after password confirmation, while preventing the deletion of the currently active wallet. [#2497](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2497) +- A race condition during the initialization of Trezor-based hardware wallets was resolved by ensuring the correct account and address are loaded before fetching the enabled address, preventing startup errors. [#2504](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2504) + +**UTXO**: +- Validation of expected public keys for p2pk inputs was corrected, resolving an error in p2pk transaction processing. [#2408](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2408) +- Transaction fee calculation and minimum relay fee handling for UTXO coins were improved for accurate fee estimation. [#2316](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2316) + +**EVM / ERC20**: +- ETH address serialization in event streaming was updated to use the `AddrToString` trait for consistency. [#2440](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2440) + +**Pubkey Banning**: +- Expirable bans for pubkeys were introduced, allowing temporary exclusion of certain public keys. [#2455](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2455) + +**RPC Interface**: +- A unified interface was implemented for legacy and current RPC methods. [#2450](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2450) + +**DNS Resolution**: +- IP resolution logic was improved to fail only if no IPv4 address is found. [#2487](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2487) + +**Database and File System**: +- More replacements of `dbdir` with `address_dir` were made as part of an ongoing improvement to database architecture. [#2398](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2398) + +**Build and Dependency Management**: +- Duplicated mm2 build artifacts were removed from the build process to reduce clutter. [#2448](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2448) +- Static CRT linking was enabled for MSVC builds, improving the portability of Windows binaries. [#2464](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2464) +- The `timed-map` dependency was bumped to version `1.4.1` for improved performance and stability. [#2413](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2413) [#2481](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2481) +- The `base58` crate was removed and replaced with `bs58` throughout the codebase for consistency and security. [#2427](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2427) +- Dependencies were reorganized using the `workspace.dependencies` feature for centralized management. [#2449](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2449) + +--- + +### Other Changes: + +**Documentation**: +- Old URLs referencing atomicDEX or previous documentation pages were updated throughout the documentation. [#2428](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2428) +- A DeepWiki badge was added to the README to highlight documentation resources. [#2463](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2463) + +**Core Maintenance**: +- Workspace dependencies were organized for consistent dependency management across the project. [#2449](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2449) +- A unit test was added to validate DEX fee handling for ZCoin. [#2460](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2460) +- Improved ERC20 token lookup to use platform ticker alongside contract address for proper token identification across platforms. [#2445](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2445) + +--- + +### NB - Backwards compatibility breaking changes: + +**WalletConnect/EVM Coin Activation Policy**: +- The `priv_key_policy` field for EVM coin activation now requires the new enum variant format: `"priv_key_policy": { "type": "ContextPrivKey" }`. [#2223](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2223) + +**TRON/EVM Chain Specification**: +- EVM coin configurations must now include `chain_id` inside the `protocol_data` field. Legacy `chain_id` fields are deprecated. [#2425](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2425) + +**mm2 Build Artifacts**: +- The `mm2` binaries have been removed from build outputs. Users must reference new artifact locations. [#2448](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2448) + +**Seednode Configuration**: +- Hardcoded seed nodes were removed. KDF will no longer connect to 8762 mainnet by default without proper `seednodes` configuration. [#2439](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2439) + +**IBC/Cosmos Changes**: +- The `ibc_chains` and `ibc_transfer_channels` RPC endpoints have been removed. [#2459](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2459) +- The `ibc_source_channel` field now requires numeric values only (e.g., `12` instead of `channel-12`). [#2459](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2459) + +--- + ## v2.4.0-beta - 2025-05-02 ### Features: diff --git a/Cargo.lock b/Cargo.lock index e2ebd94a86..c6cd5560d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,25 +35,14 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922b33332f54fc0ad13fa3e514601e8d30fb54e1f3eadc36643f6526db645621" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ + "crypto-common", "generic-array", ] -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if 1.0.0", - "cipher 0.3.0", - "cpufeatures 0.2.11", - "opaque-debug", -] - [[package]] name = "aes" version = "0.8.3" @@ -61,19 +50,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if 1.0.0", - "cipher 0.4.4", - "cpufeatures 0.2.11", + "cipher", + "cpufeatures", ] [[package]] name = "aes-gcm" -version = "0.9.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", - "aes 0.7.5", - "cipher 0.3.0", + "aes", + "cipher", "ctr", "ghash", "subtle", @@ -130,9 +119,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.42" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "argon2" @@ -142,7 +131,7 @@ checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.11", + "cpufeatures", "password-hash", "zeroize", ] @@ -241,7 +230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -258,8 +247,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -268,7 +257,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0de5164e5edbf51c45fb8c2d9664ae1c095cce1b265ecf7569093c0d66ef690" dependencies = [ - "bytes 1.4.0", + "bytes", "futures-sink", "futures-util", "memchr", @@ -307,12 +296,12 @@ dependencies = [ "async-trait", "axum-core", "bitflags 1.3.2", - "bytes 1.4.0", + "bytes", "futures-util", "http 0.2.12", - "http-body 0.4.5", + "http-body", "hyper", - "itoa 1.0.10", + "itoa", "matchit", "memchr", "mime", @@ -333,10 +322,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes 1.4.0", + "bytes", "futures-util", "http 0.2.12", - "http-body 0.4.5", + "http-body", "mime", "rustversion", "tower-layer", @@ -370,12 +359,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base58" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" - [[package]] name = "base64" version = "0.11.0" @@ -681,16 +664,6 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - [[package]] name = "bytes" version = "1.4.0" @@ -712,7 +685,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher 0.4.4", + "cipher", ] [[package]] @@ -735,25 +708,24 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if 1.0.0", - "cipher 0.3.0", - "cpufeatures 0.2.11", - "zeroize", + "cipher", + "cpufeatures", ] [[package]] name = "chacha20poly1305" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", "chacha20", - "cipher 0.3.0", + "cipher", "poly1305", "zeroize", ] @@ -786,15 +758,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" @@ -803,6 +766,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -830,15 +794,15 @@ version = "0.1.0" dependencies = [ "async-std", "async-trait", - "base58", "base64 0.21.7", "bip32", "bitcoin", "bitcoin_hashes", "bitcrypto", "blake2b_simd", + "bs58 0.4.0", "byteorder", - "bytes 0.4.12", + "bytes", "cfg-if 1.0.0", "chain", "chrono", @@ -849,7 +813,7 @@ dependencies = [ "db_common", "derive_more", "dirs", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "enum_derives", "ethabi", "ethcore-transaction", @@ -870,6 +834,7 @@ dependencies = [ "js-sys", "jsonrpc-core", "jubjub", + "kdf_walletconnect", "keys", "lazy_static", "libc", @@ -881,7 +846,6 @@ dependencies = [ "mm2_db", "mm2_err_handle", "mm2_event_stream", - "mm2_git", "mm2_io", "mm2_metamask", "mm2_metrics", @@ -965,6 +929,7 @@ dependencies = [ "ethereum-types", "futures 0.3.28", "hex", + "kdf_walletconnect", "lightning", "lightning-background-processor", "lightning-invoice", @@ -977,6 +942,7 @@ dependencies = [ "parking_lot", "rpc", "rpc_task", + "secp256k1 0.24.3", "ser_error", "ser_error_derive", "serde", @@ -993,7 +959,7 @@ dependencies = [ "arrayref", "async-trait", "backtrace", - "bytes 1.4.0", + "bytes", "cc", "cfg-if 1.0.0", "chrono", @@ -1008,7 +974,7 @@ dependencies = [ "gstuff", "hex", "http 0.2.12", - "http-body 0.1.0", + "http-body", "hyper", "hyper-rustls 0.24.2", "itertools", @@ -1138,15 +1104,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cpufeatures" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.2.11" @@ -1314,7 +1271,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" name = "crypto" version = "1.0.0" dependencies = [ - "aes 0.8.3", + "aes", "argon2", "arrayref", "async-trait", @@ -1325,7 +1282,7 @@ dependencies = [ "bs58 0.4.0", "cbc", "cfg-if 1.0.0", - "cipher 0.4.4", + "cipher", "common", "derive_more", "enum-primitive-derive", @@ -1380,6 +1337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1420,11 +1378,11 @@ dependencies = [ [[package]] name = "ctr" -version = "0.7.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.3.0", + "cipher", ] [[package]] @@ -1453,18 +1411,31 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.1" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", "fiat-crypto", - "packed_simd_2", - "platforms", + "rustc_version 0.4.0", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "syn 2.0.77", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1500,7 +1471,7 @@ dependencies = [ "codespan-reporting", "lazy_static", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "scratch", "syn 1.0.95", ] @@ -1518,7 +1489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1541,7 +1512,7 @@ dependencies = [ "fnv", "ident_case", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "strsim", "syn 1.0.95", ] @@ -1553,7 +1524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1615,7 +1586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1693,12 +1664,12 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ "serde", - "signature 1.4.0", + "signature 1.6.4", ] [[package]] @@ -1731,7 +1702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", - "ed25519 1.5.2", + "ed25519 1.5.3", "rand 0.7.3", "serde", "serde_bytes", @@ -1739,6 +1710,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "rand_core 0.6.4", + "serde", + "sha2 0.10.7", + "subtle", + "zeroize", +] + [[package]] name = "edit-distance" version = "2.1.0" @@ -1791,9 +1777,9 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1804,7 +1790,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" dependencies = [ "num-traits", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1814,7 +1800,7 @@ version = "0.1.0" dependencies = [ "itertools", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -1834,7 +1820,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "blake2b_simd", "byteorder", @@ -2012,9 +1998,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "findshlibs" @@ -2033,7 +2019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" dependencies = [ "byteorder", - "rand 0.8.4", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -2087,8 +2073,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" dependencies = [ "cbc", - "cipher 0.4.4", - "libm 0.2.7", + "cipher", + "libm", "num-bigint", "num-integer", "num-traits", @@ -2200,8 +2186,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -2315,9 +2301,9 @@ dependencies = [ [[package]] name = "ghash" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbd60caa311237d508927dbba7594b483db3ef05faa55172fcf89b1bcda7853" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", "polyval", @@ -2392,7 +2378,7 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ - "bytes 1.4.0", + "bytes", "fnv", "futures-core", "futures-sink", @@ -2469,7 +2455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", - "bytes 1.4.0", + "bytes", "headers-core", "http 0.2.12", "httpdate", @@ -2492,6 +2478,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.14" @@ -2531,6 +2523,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.8.1" @@ -2582,38 +2583,26 @@ dependencies = [ "winapi", ] -[[package]] -name = "http" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" -dependencies = [ - "bytes 0.4.12", - "fnv", - "itoa 0.4.6", -] - [[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.4.0", + "bytes", "fnv", - "itoa 1.0.10", + "itoa", ] [[package]] -name = "http-body" -version = "0.1.0" +name = "http" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ - "bytes 0.4.12", - "futures 0.1.29", - "http 0.1.21", - "tokio-buf", + "bytes", + "fnv", + "itoa", ] [[package]] @@ -2622,7 +2611,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.4.0", + "bytes", "http 0.2.12", "pin-project-lite 0.2.9", ] @@ -2672,16 +2661,16 @@ version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ - "bytes 1.4.0", + "bytes", "futures-channel", "futures-core", "futures-util", "h2", "http 0.2.12", - "http-body 0.4.5", + "http-body", "httparse", "httpdate", - "itoa 1.0.10", + "itoa", "pin-project-lite 0.2.9", "socket2 0.4.9", "tokio", @@ -2848,7 +2837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -2910,15 +2899,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "ipconfig" version = "0.3.0" @@ -2946,12 +2926,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" - [[package]] name = "itoa" version = "1.0.10" @@ -3003,6 +2977,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.7", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "jubjub" version = "0.5.1" @@ -3029,6 +3017,47 @@ dependencies = [ "sha2 0.10.7", ] +[[package]] +name = "kdf_walletconnect" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "cfg-if 1.0.0", + "chrono", + "common", + "db_common", + "derive_more", + "enum_derives", + "futures 0.3.28", + "hex", + "hkdf", + "js-sys", + "mm2_core", + "mm2_db", + "mm2_err_handle", + "mm2_test_helpers", + "pairing_api", + "parking_lot", + "rand 0.8.5", + "relay_client", + "relay_rpc", + "secp256k1 0.20.3", + "serde", + "serde_json", + "sha2 0.10.7", + "thiserror", + "timed-map", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "wc_common", + "web-sys", + "x25519-dalek 2.0.1", +] + [[package]] name = "keccak" version = "0.1.0" @@ -3049,9 +3078,9 @@ dependencies = [ name = "keys" version = "0.1.0" dependencies = [ - "base58", "bech32", "bitcrypto", + "bs58 0.4.0", "derive_more", "lazy_static", "primitives", @@ -3096,12 +3125,6 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "libm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" - [[package]] name = "libm" version = "0.2.7" @@ -3113,7 +3136,7 @@ name = "libp2p" version = "0.52.1" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ - "bytes 1.4.0", + "bytes", "futures 0.3.28", "futures-timer", "getrandom 0.2.9", @@ -3181,7 +3204,7 @@ dependencies = [ "parking_lot", "pin-project", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "rw-stream-sink", "smallvec 1.6.1", "thiserror", @@ -3218,7 +3241,7 @@ dependencies = [ "log", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "thiserror", ] @@ -3231,7 +3254,7 @@ dependencies = [ "asynchronous-codec", "base64 0.21.7", "byteorder", - "bytes 1.4.0", + "bytes", "either", "fnv", "futures 0.3.28", @@ -3246,7 +3269,7 @@ dependencies = [ "prometheus-client", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.4", + "rand 0.8.5", "regex", "sha2 0.10.7", "smallvec 1.6.1", @@ -3283,12 +3306,12 @@ checksum = "d2874d9c6575f1d7a151022af5c42bb0ffdcdfbafe0a6fd039de870b384835a2" dependencies = [ "asn1_der", "bs58 0.5.0", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "libsecp256k1", "log", "multihash", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "sha2 0.10.7", "thiserror", "zeroize", @@ -3306,7 +3329,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "socket2 0.5.3", "tokio", @@ -3335,7 +3358,7 @@ name = "libp2p-noise" version = "0.43.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ - "bytes 1.4.0", + "bytes", "curve25519-dalek 3.2.0", "futures 0.3.28", "libp2p-core", @@ -3345,12 +3368,12 @@ dependencies = [ "multihash", "once_cell", "quick-protobuf", - "rand 0.8.4", + "rand 0.8.5", "sha2 0.10.7", "snow", "static_assertions", "thiserror", - "x25519-dalek", + "x25519-dalek 1.1.0", "zeroize", ] @@ -3367,7 +3390,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "void", ] @@ -3383,7 +3406,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "log", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "void", ] @@ -3404,7 +3427,7 @@ dependencies = [ "log", "multistream-select", "once_cell", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "tokio", "void", @@ -3415,11 +3438,11 @@ name = "libp2p-swarm-derive" version = "0.33.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-warning", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -3497,7 +3520,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand 0.8.4", + "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", @@ -3791,8 +3814,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -3852,7 +3875,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "2.4.0-beta" +version = "2.5.0-beta" dependencies = [ "chrono", "common", @@ -3890,6 +3913,7 @@ dependencies = [ "libp2p", "mm2_err_handle", "mm2_event_stream", + "mm2_io", "mm2_metrics", "mm2_rpc", "primitives", @@ -4038,7 +4062,7 @@ dependencies = [ "async-trait", "bitcrypto", "blake2", - "bytes 0.4.12", + "bytes", "cfg-if 1.0.0", "chain", "chrono", @@ -4070,6 +4094,7 @@ dependencies = [ "instant", "itertools", "js-sys", + "kdf_walletconnect", "keys", "lazy_static", "libc", @@ -4118,6 +4143,7 @@ dependencies = [ "sp-runtime-interface", "sp-trie", "spv_validation", + "tempfile", "testcontainers", "timed-map", "tokio", @@ -4185,7 +4211,7 @@ dependencies = [ "async-stream", "async-trait", "base64 0.21.7", - "bytes 1.4.0", + "bytes", "cfg-if 1.0.0", "common", "derive_more", @@ -4194,7 +4220,7 @@ dependencies = [ "futures-util", "gstuff", "http 0.2.12", - "http-body 0.4.5", + "http-body", "httparse", "hyper", "js-sys", @@ -4264,7 +4290,6 @@ dependencies = [ "serde_json", "sha2 0.10.7", "smallvec 1.6.1", - "syn 2.0.38", "timed-map", "tokio", "void", @@ -4303,7 +4328,7 @@ dependencies = [ name = "mm2_test_helpers" version = "0.1.0" dependencies = [ - "bytes 1.4.0", + "bytes", "cfg-if 1.0.0", "chrono", "common", @@ -4344,7 +4369,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3048ef3680533a27f9f8e7d6a0bce44dc61e4895ea0f42709337fa1c8616fefe" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -4399,7 +4424,7 @@ name = "multistream-select" version = "0.13.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ - "bytes 1.4.0", + "bytes", "futures 0.3.28", "log", "pin-project", @@ -4450,7 +4475,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" dependencies = [ - "bytes 1.4.0", + "bytes", "futures 0.3.28", "log", "netlink-packet-core", @@ -4465,7 +4490,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" dependencies = [ - "bytes 1.4.0", + "bytes", "futures 0.3.28", "libc", "log", @@ -4530,8 +4555,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4612,16 +4637,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if 1.0.0", - "libm 0.1.4", -] - [[package]] name = "pairing" version = "0.18.0" @@ -4632,6 +4647,27 @@ dependencies = [ "group 0.8.0", ] +[[package]] +name = "pairing_api" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2fb03ac19186fd2372a0eef71897ef8a6ae9653" +dependencies = [ + "anyhow", + "chrono", + "hex", + "lazy_static", + "paste", + "rand 0.8.5", + "regex", + "relay_client", + "relay_rpc", + "serde", + "serde_json", + "thiserror", + "url", + "wc_common", +] + [[package]] name = "parity-scale-codec" version = "3.1.2" @@ -4654,7 +4690,7 @@ checksum = "c45ed1f39709f5a89338fab50e59816b2e8815f5bb58276e7ddf9afd495f73f8" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -4750,9 +4786,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "peg" @@ -4772,7 +4808,7 @@ checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" dependencies = [ "peg-runtime", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", ] [[package]] @@ -4822,8 +4858,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -4860,12 +4896,6 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" -[[package]] -name = "platforms" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" - [[package]] name = "polling" version = "3.7.4" @@ -4883,23 +4913,23 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe800695325da85083cd23b56826fccb2e2dc29b218e7811a6f33bc93f414be" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures 0.1.4", + "cpufeatures", "opaque-debug", "universal-hash", ] [[package]] name = "polyval" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e597450cbf209787f0e6de80bf3795c6b2356a380ee87837b545aded8dbc1823" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.1.4", + "cpufeatures", "opaque-debug", "universal-hash", ] @@ -4923,7 +4953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.38", + "syn 2.0.77", ] [[package]] @@ -4967,15 +4997,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70550716265d1ec349c41f70dd4f964b4fd88394efe4405f0c1da679c4799a07" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -4987,7 +5017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c2f43e8969d51935d2a7284878ae053ba30034cd563f673cde37ba5205685e" dependencies = [ "dtoa", - "itoa 1.0.10", + "itoa", "parking_lot", "prometheus-client-derive-encode", ] @@ -4999,7 +5029,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b6a5217beb0ad503ee7fa752d451c905113d70721b937126158f3106a48cc1" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -5009,7 +5039,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ - "bytes 1.4.0", + "bytes", "prost-derive", ] @@ -5019,8 +5049,8 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ - "bytes 1.4.0", - "heck", + "bytes", + "heck 0.5.0", "itertools", "log", "multimap", @@ -5030,7 +5060,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.38", + "syn 2.0.77", "tempfile", ] @@ -5043,8 +5073,8 @@ dependencies = [ "anyhow", "itertools", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -5130,7 +5160,7 @@ version = "0.2.0" source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.12#8bcc1fda79d56a2f398df3d45a29729b8ce0148d" dependencies = [ "asynchronous-codec", - "bytes 1.4.0", + "bytes", "quick-protobuf", "thiserror", "unsigned-varint", @@ -5155,9 +5185,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -5225,14 +5255,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", - "rand_hc 0.3.1", ] [[package]] @@ -5316,15 +5345,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand_isaac" version = "0.1.1" @@ -5470,7 +5490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c523ccaed8ac4b0288948849a350b37d3035827413c458b6a40ddb614bb4f72" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -5491,6 +5511,61 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "relay_client" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2fb03ac19186fd2372a0eef71897ef8a6ae9653" +dependencies = [ + "chrono", + "data-encoding", + "futures-util", + "getrandom 0.2.9", + "http 1.1.0", + "js-sys", + "pin-project", + "rand 0.8.5", + "relay_rpc", + "serde", + "serde_json", + "serde_qs", + "thiserror", + "tokio", + "tokio-tungstenite-wasm", + "tokio-util", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "relay_rpc" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2fb03ac19186fd2372a0eef71897ef8a6ae9653" +dependencies = [ + "anyhow", + "bs58 0.4.0", + "chrono", + "data-encoding", + "derive_more", + "ed25519-dalek 2.1.1", + "getrandom 0.2.9", + "hex", + "jsonwebtoken", + "once_cell", + "paste", + "rand 0.8.5", + "regex", + "serde", + "serde-aux", + "serde_json", + "sha2 0.10.7", + "strum", + "thiserror", + "url", +] + [[package]] name = "reqwest" version = "0.11.9" @@ -5498,13 +5573,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" dependencies = [ "base64 0.13.0", - "bytes 1.4.0", + "bytes", "encoding_rs", "futures-core", "futures-util", "h2", "http 0.2.12", - "http-body 0.4.5", + "http-body", "hyper", "hyper-rustls 0.23.0", "ipnet", @@ -5604,7 +5679,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ - "bytes 1.4.0", + "bytes", "rustc-hex", ] @@ -5913,7 +5988,7 @@ checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6097,20 +6172,30 @@ name = "ser_error_derive" version = "0.1.0" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "ser_error", "syn 1.0.95", ] [[package]] name = "serde" -version = "1.0.189" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-aux" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "serde-wasm-bindgen" version = "0.4.3" @@ -6133,27 +6218,39 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ - "indexmap 1.9.3", - "itoa 1.0.10", + "indexmap 2.2.3", + "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_qs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_repr" version = "0.1.6" @@ -6161,7 +6258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6181,7 +6278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.10", + "itoa", "ryu", "serde", ] @@ -6204,7 +6301,7 @@ checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ "darling", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6235,7 +6332,7 @@ checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.9.0", "opaque-debug", ] @@ -6247,7 +6344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.10.7", ] @@ -6259,7 +6356,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.9.0", "opaque-debug", ] @@ -6271,7 +6368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.2.11", + "cpufeatures", "digest 0.10.7", ] @@ -6313,7 +6410,7 @@ dependencies = [ "blake2b_simd", "chrono", "derive_more", - "ed25519-dalek", + "ed25519-dalek 1.0.1", "hex", "nom", "reqwest", @@ -6335,9 +6432,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.4.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "signature" @@ -6349,6 +6446,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.20", +] + [[package]] name = "siphasher" version = "0.1.3" @@ -6408,16 +6517,16 @@ dependencies = [ [[package]] name = "snow" -version = "0.9.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" dependencies = [ "aes-gcm", "blake2", "chacha20poly1305", - "curve25519-dalek 4.0.0-rc.1", + "curve25519-dalek 4.1.3", "rand_core 0.6.4", - "ring 0.16.20", + "ring 0.17.3", "rustc_version 0.4.0", "sha2 0.10.7", "subtle", @@ -6461,11 +6570,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "083624472e8817d44d02c0e55df043737ff11f279af924abdf93845717c2b75c" dependencies = [ "base64 0.13.0", - "bytes 1.4.0", + "bytes", "futures 0.3.28", "httparse", "log", - "rand 0.8.4", + "rand 0.8.5", "sha-1", ] @@ -6501,7 +6610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d676664972e22a0796176e81e7bec41df461d1edf52090955cdab55f2c956ff2" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6531,7 +6640,7 @@ dependencies = [ "Inflector", "proc-macro-crate", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", ] @@ -6652,7 +6761,7 @@ checksum = "2f9799e6d412271cb2414597581128b03f3285f260ea49f5363d07df6a332b3e" dependencies = [ "Inflector", "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "serde", "serde_json", "unicode-xid 0.2.0", @@ -6670,6 +6779,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote 1.0.37", + "rustversion", + "syn 2.0.77", +] + [[package]] name = "subtle" version = "2.4.0" @@ -6709,18 +6840,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.38" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "unicode-ident", ] @@ -6746,7 +6877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", "unicode-xid 0.2.0", ] @@ -6797,7 +6928,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43f8a10105d0a7c4af0a242e23ed5a12519afe5cc0e68419da441bb5981a6802" dependencies = [ - "bytes 1.4.0", + "bytes", "digest 0.10.7", "ed25519 2.2.3", "ed25519-consensus", @@ -6842,7 +6973,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff525d5540a9fc535c38dc0d92a98da3ee36fcdfbda99cecb9f3cce5cd4d41d7" dependencies = [ - "bytes 1.4.0", + "bytes", "flex-error", "num-derive", "num-traits", @@ -6861,12 +6992,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d8fe61b1772cd50038bdeeadf53773bb37a09e639dd8e6d996668fd220ddb29" dependencies = [ "async-trait", - "bytes 1.4.0", + "bytes", "flex-error", "getrandom 0.2.9", "peg", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "semver 1.0.6", "serde", "serde_bytes", @@ -6910,7 +7041,7 @@ dependencies = [ "hex", "hmac 0.12.1", "log", - "rand 0.8.4", + "rand 0.8.5", "serde", "serde_json", "sha2 0.10.7", @@ -6932,8 +7063,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -6952,7 +7083,7 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ - "itoa 1.0.10", + "itoa", "js-sys", "serde", "time-core", @@ -6976,11 +7107,12 @@ dependencies = [ [[package]] name = "timed-map" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30565aee368a9b233f397f46cd803c59285b61d54c5b3ae378611bd467beecbe" +checksum = "6f664a6b916d03d3e32c312c3b6ce31c24697c0f7ea6d87e20eb6372053ddf29" dependencies = [ "rustc-hash", + "serde", "web-time", ] @@ -7024,7 +7156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg 1.1.0", - "bytes 1.4.0", + "bytes", "libc", "mio", "num_cpus", @@ -7035,16 +7167,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "tokio-buf" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.29", -] - [[package]] name = "tokio-io-timeout" version = "1.2.0" @@ -7062,8 +7184,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7117,7 +7239,7 @@ dependencies = [ [[package]] name = "tokio-tungstenite-wasm" version = "0.1.1-alpha.0" -source = "git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm?rev=d20abdb#d20abdbbb2f03e302e3a8d11a1736ec8b50d0f58" +source = "git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm?rev=8fc7e2f#8fc7e2ff4c970bee0c0867399cb9a941881ea183" dependencies = [ "futures-channel", "futures-util", @@ -7137,7 +7259,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ - "bytes 1.4.0", + "bytes", "futures-core", "futures-sink", "pin-project-lite 0.2.9", @@ -7198,11 +7320,11 @@ dependencies = [ "async-trait", "axum", "base64 0.21.7", - "bytes 1.4.0", + "bytes", "flate2", "h2", "http 0.2.12", - "http-body 0.4.5", + "http-body", "hyper", "hyper-timeout", "percent-encoding", @@ -7229,8 +7351,8 @@ dependencies = [ "prettyplease", "proc-macro2", "prost-build", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7244,7 +7366,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite 0.2.9", - "rand 0.8.4", + "rand 0.8.5", "slab", "tokio", "tokio-util", @@ -7284,8 +7406,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", ] [[package]] @@ -7392,7 +7514,7 @@ dependencies = [ "idna 0.2.3", "ipnet", "lazy_static", - "rand 0.8.4", + "rand 0.8.5", "smallvec 1.6.1", "socket2 0.4.9", "thiserror", @@ -7436,11 +7558,11 @@ checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" dependencies = [ "base64 0.13.0", "byteorder", - "bytes 1.4.0", + "bytes", "http 0.2.12", "httparse", "log", - "rand 0.8.4", + "rand 0.8.5", "rustls 0.20.4", "sha-1", "thiserror", @@ -7451,9 +7573,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uint" @@ -7513,11 +7635,11 @@ checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" [[package]] name = "universal-hash" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "generic-array", + "crypto-common", "subtle", ] @@ -7528,7 +7650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86a8dc7f45e4c1b0d30e43038c38f274e77af056aa5f74b93c2cf9eb3c1c836" dependencies = [ "asynchronous-codec", - "bytes 1.4.0", + "bytes", ] [[package]] @@ -7586,7 +7708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.9", - "rand 0.8.4", + "rand 0.8.5", "serde", ] @@ -7723,8 +7845,8 @@ dependencies = [ "log", "once_cell", "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -7746,7 +7868,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote 1.0.33", + "quote 1.0.37", "wasm-bindgen-macro-support", ] @@ -7757,8 +7879,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", - "quote 1.0.33", - "syn 2.0.38", + "quote 1.0.37", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7790,7 +7912,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c2e18093f11c19ca4e188c177fecc7c372304c311189f12c2f9bea5b7324ac7" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", +] + +[[package]] +name = "wc_common" +version = "0.1.0" +source = "git+https://github.com/komodoplatform/walletconnectrust?tag=k-0.1.3#e2fb03ac19186fd2372a0eef71897ef8a6ae9653" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "thiserror", ] [[package]] @@ -7816,11 +7948,11 @@ dependencies = [ [[package]] name = "web3" version = "0.19.0" -source = "git+https://github.com/KomodoPlatform/rust-web3?tag=v0.20.0#01de1d732e61c920cfb2fb1533db7d7110c8a457" +source = "git+https://github.com/komodoplatform/rust-web3?tag=v0.20.0#01de1d732e61c920cfb2fb1533db7d7110c8a457" dependencies = [ "arrayvec 0.7.1", "base64 0.13.0", - "bytes 1.4.0", + "bytes", "derive_more", "ethabi", "ethereum-types", @@ -7835,7 +7967,7 @@ dependencies = [ "log", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "reqwest", "rlp", "serde", @@ -8263,6 +8395,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "yamux" version = "0.12.1" @@ -8274,7 +8418,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "static_assertions", ] @@ -8290,7 +8434,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.8.4", + "rand 0.8.5", "static_assertions", ] @@ -8312,7 +8456,7 @@ checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "base64 0.13.0", @@ -8337,7 +8481,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "bech32", @@ -8359,7 +8503,7 @@ dependencies = [ [[package]] name = "zcash_extras" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "async-trait", "ff 0.8.0", @@ -8375,7 +8519,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.0.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "blake2b_simd", "byteorder", @@ -8389,9 +8533,9 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ - "aes 0.8.3", + "aes", "bitvec 0.18.5", "blake2b_simd", "blake2s_simd", @@ -8419,7 +8563,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" +source = "git+https://github.com/komodoplatform/librustzcash.git?tag=k-1.4.2#4e030a0f44cc17f100bf5f019563be25c5b8755f" dependencies = [ "bellman", "blake2b_simd", @@ -8450,7 +8594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", - "quote 1.0.33", + "quote 1.0.37", "syn 1.0.95", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 507c2e5c31..dd317f0d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,6 @@ [workspace] +# https://doc.rust-lang.org/beta/cargo/reference/features.html#feature-resolver-version-2 +resolver = "2" members = [ "mm2src/coins_activation", "mm2src/coins", @@ -10,6 +12,7 @@ members = [ "mm2src/derives/ser_error_derive", "mm2src/derives/ser_error", "mm2src/hw_common", + "mm2src/kdf_walletconnect", "mm2src/mm2_bin_lib", "mm2src/mm2_bitcoin/chain", "mm2src/mm2_bitcoin/crypto", @@ -47,8 +50,187 @@ exclude = [ "mm2src/mm2_test_helpers", ] -# https://doc.rust-lang.org/beta/cargo/reference/features.html#feature-resolver-version-2 -resolver = "2" +[workspace.dependencies] +aes = "0.8.3" +argon2 = { version = "0.5.2", features = ["zeroize"] } +arrayref = "0.3" +anyhow = "1.0.89" +async-std = "1.5" +async-trait = "0.1.52" +async-stream = "0.3" +backtrace = "0.3" +base64 = "0.21.2" +bech32 = "0.9.1" +bs58 = "0.4.0" +bigdecimal = { version = "0.3", features = ["serde"] } +bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } +bip39 = { version = "2.0.0", features = ["rand_core", "zeroize"], default-features = false } +bitcoin = "0.29" +bitcoin_hashes = "0.11" +blake2 = "0.10.6" +blake2b_simd = "0.5.10" +bytes = "1.1" +byteorder = "1.3" +cbc = "0.1.2" +cc = "1.0" +cipher = "0.4.4" +chrono = "0.4.23" +cfg-if = "1.0" +clap = { version = "4.2", features = ["derive"] } +cosmrs = { version = "0.16", default-features = false } +crossbeam = "0.8" +crossbeam-channel = "0.5.1" +compatible-time = { version = "1.1.0", package = "web-time" } +crc32fast = { version = "1.3.2", features = ["std", "nightly"] } +derive_more = "0.99" +directories = "5.0" +dirs = "1" +ed25519-dalek = { version = "1.0.1", features = ["serde"] } +either = "1.6" +enum-primitive-derive = "0.2" +env_logger = "0.9.3" +ethabi = "17.0.0" +ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } +# Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. +#enum_dispatch = "0.1" +ff = "0.8" +findshlibs = "0.5" +# using select macro requires the crate to be named futures, compilation failed with futures03 name +futures = { version = "0.3.1", default-features = false } +futures01 = { version = "0.1", package = "futures" } +futures-rustls = { version = "0.24", default-features = false } +futures-ticker = "0.0.3" +futures-timer = "3.0" +futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +fnv = "1.0.6" +group = "0.8.0" +gstuff = { version = "0.7", features = ["nightly"] } +hash256-std-hasher = "0.15.2" +hash-db = "0.15.2" +hex = "0.4.2" +hmac = "0.12.1" +hkdf = "0.12.4" +http = "0.2" +http-body = "0.4" +httparse = "1.8.0" +hyper = "0.14.26" +hyper-rustls = { version = "0.24", default-features = false } +indexmap = "1.7.0" +inquire = "0.6" +itertools = "0.10" +jemallocator = "0.5.0" +jubjub = "0.5.1" +js-sys = "0.3.27" +# Same version as `web3` depends on. +jsonrpc-core = "18.0.0" +lazy_static = "1.4" +libc = "0.2" +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false } +lightning = "0.0.113" +lightning-background-processor = "0.0.113" +lightning-invoice = { version = "0.21.0", features = ["serde"] } +lightning-net-tokio = "0.0.113" +instant = "0.1.12" +log = "0.4" +metrics = "0.21" +metrics-exporter-prometheus = "0.12.1" +metrics-util = "0.15" +mocktopus = "0.8.0" +nom = "6.1.2" +num-bigint = { version = "0.4", features = ["serde", "std"] } +num-rational = { version = "0.4", features = ["serde"] } +parity-util-mem = "0.11" +num-traits = "0.2" +pairing_api = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } +parking_lot = { version = "0.12.0", default-features = false } +parking_lot_core = { version = "0.6", features = ["nightly"] } +passwords = "3.1" +paste = "1.0" +pin-project = "1.1.2" +primitive-types = "0.11.1" +prost = "0.12" +prost-build = { version = "0.12", default-features = false } +protobuf = "2.20" +proc-macro2 = "1.0" +quote = "1.0" +regex = "1" +relay_client = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } +relay_rpc = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } +reqwest = { version = "0.11.9", default-features = false, features = ["json"] } +rand = { version = "0.7", default-features = false, features = ["std", "small_rng", "wasm-bindgen"] } +rcgen = "0.10" +ripemd160 = "0.9.0" +rlp = "0.5" +rmp-serde = "0.14.3" +rusb = { version = "0.7.0", features = ["vendored"] } +rustc-hash = "2.0" +rustc-hex = "2" +rust-ini = "0.13" +rustls = { version = "0.21", default-features = false } +rustls-pemfile = "1.0.2" +rusqlite = { version = "0.28", features = ["bundled"] } +secp256k1 = "0.20" +secp256k1v24 = { version = "0.24", package = "secp256k1" } +serde = { version = "1", default-features = false } +serde_bytes = "0.11.5" +serde_derive = { version = "1", default-features = false } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +serde_with = "1.14.0" +serde_repr = "0.1.6" +serde-wasm-bindgen = "0.4.3" +sha-1 = "0.9" +sha2 = "0.10" +sha3 = "0.9" +sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808" } +siphasher = "0.1.1" +smallvec = "1.6.1" +sp-runtime-interface = { version = "6.0.0", default-features = false, features = ["disable_target_static_assertions"] } +sp-trie = { version = "6.0", default-features = false } +sql-builder = "3.1.1" +syn = "1.0" +sysinfo = "0.28" +tempfile = "3.4.0" +# using the same version as cosmrs +tendermint-rpc = { version = "0.35", default-features = false } +testcontainers = "0.15.0" +tiny-bip39 = "0.8.0" +thiserror = "1.0.40" +time = "0.3.20" +timed-map = { version = "1.4", features = ["rustc-hash", "serde", "wasm"] } +tokio = { version = "1.20", default-features = false } +tokio-rustls = { version = "0.24", default-features = false } +tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "8fc7e2f", defautl-features = false, features = ["rustls-tls-native-roots"]} +tonic = { version = "0.10", default-features = false } +tonic-build = { version = "0.10", default-features = false, features = ["prost"] } +tower-service = "0.3" +trie-db = { version = "0.23.1", default-features = false } +trie-root = "0.16.0" +url = { version = "2.2.2", features = ["serde"] } +uint = "0.9.3" +uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +void = "1.0" +wagyu-zcash-parameters = { version = "0.2" } +wasm-bindgen = "0.2.86" +wasm-bindgen-futures = "0.4.21" +wasm-bindgen-test = "0.3.2" +wc_common = { git = "https://github.com/komodoplatform/walletconnectrust", tag = "k-0.1.3" } +webpki-roots = "0.25" +web-sys = {version = "0.3.55", default-features = false } +# we don't need the default web3 features at all since we added our own web3 transport using shared httparse.workspace = true instance. +# 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.20.0", default-features = false } +winapi = "0.3" +zbase32 = "0.1.2" +zcash_client_backend = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2" } +zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } +zcash_extras = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2" } +zcash_primitives = { git = "https://github.com/komodoplatform/librustzcash.git", tag = "k-1.4.2", features = ["transparent-inputs"] } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false } +x25519-dalek = { version = "2.0", features = ["static_secrets"] } +zeroize = { version = "1.5", features = ["zeroize_derive"] } [profile.release] debug = 0 diff --git a/README.md b/README.md index 441c23ba8c..6b67fc4086 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

@@ -37,6 +37,7 @@ release version + Ask DeepWiki chat on Discord @@ -48,13 +49,13 @@ ## What is the Komodo DeFi Framework? -The Komodo DeFi Framework is open-source [atomic-swap](https://komodoplatform.com/en/academy/atomic-swaps/) software for seamless, decentralized, peer to peer trading between almost every blockchain asset in existence. This software works with propagation of orderbooks and swap states through the [libp2p](https://libp2p.io/) protocol and uses [Hash Time Lock Contracts (HTLCs)](https://en.bitcoinwiki.org/wiki/Hashed_Timelock_Contracts) for ensuring that the two parties in a swap either mutually complete a trade, or funds return to thier original owner. +The Komodo DeFi Framework is open-source [atomic-swap](https://komodoplatform.com/en/docs/komodo-defi-framework/tutorials/#technical-comparisons) software for seamless, decentralized, peer to peer trading between almost every blockchain asset in existence. This software works with propagation of orderbooks and swap states through the [libp2p](https://libp2p.io/) protocol and uses [Hash Time Lock Contracts (HTLCs)](https://en.bitcoinwiki.org/wiki/Hashed_Timelock_Contracts) for ensuring that the two parties in a swap either mutually complete a trade, or funds return to thier original owner. There is no 3rd party intermediary, no proxy tokens, and at all times users remain in sole possession of their private keys. -A [well documented API](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) offers simple access to the underlying services using simple language agnostic JSON structured methods and parameters such that users can communicate with the core in a variety of methods such as [curl](https://developers.komodoplatform.com/basic-docs/atomicdex-api-legacy/buy.html) in CLI, or fully functioning [desktop and mobile applications](https://atomicdex.io/) like [Komodo Wallet Desktop](https://github.com/KomodoPlatform/komodo-wallet-desktop). +A [well documented API](https://komodoplatform.com/en/docs/komodo-defi-framework/tutorials/) offers simple access to the underlying services using simple language agnostic JSON structured methods and parameters such that users can communicate with the core in a variety of methods such as [curl](https://komodoplatform.com/en/docs/komodo-defi-framework/api/legacy/buy/) in CLI, or fully functioning [browser, desktop and mobile wallet apps](https://komodoplatform.com/en/downloads/) like [Komodo Wallet](https://github.com/KomodoPlatform/komodo-wallet). -For a curated list of Komodo DeFi Framework based projects and resources, check out [Awesome AtomicDEX](https://github.com/KomodoPlatform/awesome-atomicdex). +For a curated list of Komodo DeFi Framework based projects and resources, check out [Awesome KomoDeFi]( https://github.com/KomodoPlatform/awesome-komodefi). ## Features @@ -62,13 +63,13 @@ For a curated list of Komodo DeFi Framework based projects and resources, check - Perform blockchain transactions without a local native chain (e.g. via Electrum servers) - Query orderbooks for all pairs within the [supported coins](https://github.com/KomodoPlatform/coins/blob/master/coins) - Buy/sell from the orderbook, or create maker orders -- Configure automated ["makerbot" trading](https://developers.komodoplatform.com/basic-docs/atomicdex-api-20/start_simple_market_maker_bot.html) with periodic price updates and optional [telegram](https://telegram.org/) alerts +- Configure automated ["makerbot" trading](https://komodoplatform.com/en/docs/komodo-defi-framework/api/v20/swaps_and_orders/start_simple_market_maker_bot/) with periodic price updates and optional [telegram](https://telegram.org/) alerts ## Building from source ### On Host System: -[Pre-built release binaries](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html) are available for OSX, Linux or Windows. +[Pre-built release binaries](https://github.com/KomodoPlatform/komodo-defi-framework/releases) are available for Android, iOS, OSX, Linux, Windows and WASM. If you want to build from source, the following prerequisites are required: - [Rustup](https://rustup.rs/) @@ -80,7 +81,7 @@ If you want to build from source, the following prerequisites are required: To build, run `cargo build` (or `cargo build -vv` to get verbose build output). -For more detailed instructions, please refer to the [Installation Guide](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html). +For more detailed instructions, please refer to the [Installation Guide](https://komodoplatform.com/en/docs/komodo-defi-framework/setup/). ### From Container: @@ -100,6 +101,8 @@ docker run -v "$(pwd)":/app -w /app kdf-build-container cargo build Just like building it on your host system, you will now have the target directory containing the build files. +Alternatively, container images are available on [DockerHub](https://hub.docker.com/r/komodoofficial/komodo-defi-framework) + ## Building WASM binary Please refer to the [WASM Build Guide](./docs/WASM_BUILD.md). @@ -108,7 +111,7 @@ Please refer to the [WASM Build Guide](./docs/WASM_BUILD.md). Basic config is contained in two files, `MM2.json` and `coins` -The user configuration [MM2.json file](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/configure-mm2-json.html) contains rpc credentials, your mnemonic seed phrase, a `netid` (8762 is the current main network) and some extra [optional parameters](https://developers.komodoplatform.com/basic-docs/atomicdex/atomicdex-setup/get-started-atomicdex.html). +The user configuration `MM2.json` file contains rpc credentials, your mnemonic seed phrase, a `netid` (8762 is the current main network) and some extra [optional parameters](https://komodoplatform.com/en/docs/komodo-defi-framework/setup/configure-mm2-json/). For example: ```json @@ -116,7 +119,8 @@ For example: "gui": "core_readme", "netid": 8762, "rpc_password": "Ent3r_Un1Qu3_Pa$$w0rd", - "passphrase": "ENTER_UNIQUE_SEED_PHRASE_DONT_USE_THIS_CHANGE_IT_OR_FUNDS_NOT_SAFU" + "passphrase": "ENTER_UNIQUE_SEED_PHRASE_DONT_USE_THIS_CHANGE_IT_OR_FUNDS_NOT_SAFU", + "seednodes": ["example-seed-address1.com", "example-seed-address2.com", "example-seed-address3.com", "example-seed-address4.com"] } ``` @@ -167,7 +171,7 @@ curl --url "http://127.0.0.1:7783" --data '{ }' ``` -Refer to the [Komodo Developer Docs](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) for details of additional RPC methods and parameters +Refer to the [Komodo Developer Docs](https://komodoplatform.com/en/docs/komodo-defi-framework/api/) for details of additional RPC methods and parameters ## Project structure @@ -180,7 +184,7 @@ Refer to the [Komodo Developer Docs](https://developers.komodoplatform.com/basic - [Contribution guide](./docs/CONTRIBUTING.md) - [Setting up the environment to run the full tests suite](./docs/DEV_ENVIRONMENT.md) - [Git flow and general workflow](./docs/GIT_FLOW_AND_WORKING_PROCESS.md) -- [Komodo Developer Docs](https://developers.komodoplatform.com/basic-docs/atomicdex/introduction-to-atomicdex.html) +- [Komodo Developer Docs](https://komodoplatform.com/en/docs/komodo-defi-framework/) ## Disclaimer @@ -193,5 +197,5 @@ The current state can be considered as an alpha version. ## Help and troubleshooting -If you have any question/want to report a bug/suggest an improvement feel free to [open an issue](https://github.com/KomodoPlatform/komodo-defi-framework/issues/new/choose) or join the [Komodo Platform Discord](https://discord.gg/PGxVm2y) `dev-marketmaker` channel. +If you have any question/want to report a bug/suggest an improvement feel free to [open an issue](https://github.com/KomodoPlatform/komodo-defi-framework/issues/new/choose) or join the [Komodo Platform Discord](https://discord.gg/PGxVm2y) `dev-general` channel. diff --git a/docs/DEV_ENVIRONMENT.md b/docs/DEV_ENVIRONMENT.md index 5e4f6d1659..8782769079 100644 --- a/docs/DEV_ENVIRONMENT.md +++ b/docs/DEV_ENVIRONMENT.md @@ -66,16 +66,34 @@ CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack test --firefox --headless mm2src/mm2_main ``` Please note `CC` and `AR` must be specified in the same line as `wasm-pack test mm2src/mm2_main`. -#### Running specific WASM tests with Cargo
- - Install `wasm-bindgen-cli`:
- Make sure you have wasm-bindgen-cli installed with a version that matches the one specified in your Cargo.toml file. - You can install it using Cargo with the following command: - ``` - cargo install -f wasm-bindgen-cli --version - ``` - - Run - ``` - cargo test --target wasm32-unknown-unknown --package coins --lib utxo::utxo_block_header_storage::wasm::indexeddb_block_header_storage - ``` + +#### Running specific WASM tests + +There are two primary methods for running specific tests: + +* **Method 1: Using `wasm-pack` (Recommended for browser-based tests)** + + To filter tests, append `--` to the `wasm-pack test` command, followed by the name of the test you want to run. This will execute only the tests whose names contain the provided string. + + General Example: + ```shell + wasm-pack test --firefox --headless mm2src/mm2_main -- + ``` + + > **Note for macOS users:** You must prepend the `CC` and `AR` environment variables to the command if they weren't already exported, just as you would when running all tests. For example: `CC=... AR=... wasm-pack test ...` + +* **Method 2: Using `cargo test` (For non-browser tests)** + + This method uses the standard Cargo test runner with a wasm target and is useful for tests that do not require a browser environment. + + a. **Install `wasm-bindgen-cli`**: Make sure you have `wasm-bindgen-cli` installed with a version that matches the one specified in your `Cargo.toml` file. + ```shell + cargo install -f wasm-bindgen-cli --version + ``` + + b. **Run the test**: Append `--` to the `cargo test` command, followed by the test path. + ```shell + cargo test --target wasm32-unknown-unknown --package coins --lib -- utxo::utxo_block_header_storage::wasm::indexeddb_block_header_storage + ``` PS If you notice that this guide is outdated, please submit a PR. diff --git a/mm2src/adex_cli/Cargo.lock b/mm2src/adex_cli/Cargo.lock index 5d5eb5abeb..099a669565 100644 --- a/mm2src/adex_cli/Cargo.lock +++ b/mm2src/adex_cli/Cargo.lock @@ -336,12 +336,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base58" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" - [[package]] name = "base64" version = "0.21.7" @@ -1714,7 +1708,6 @@ dependencies = [ name = "keys" version = "0.1.0" dependencies = [ - "base58", "bech32", "bitcrypto", "derive_more", diff --git a/mm2src/adex_cli/Cargo.toml b/mm2src/adex_cli/Cargo.toml index cb477cacb0..cc05303755 100644 --- a/mm2src/adex_cli/Cargo.toml +++ b/mm2src/adex_cli/Cargo.toml @@ -7,33 +7,34 @@ description = "Provides a CLI interface and facilitates interoperating to komodo # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -anyhow = { version = "1.0", features = ["std"] } -async-trait = "0.1" -clap = { version = "4.2", features = ["derive"] } +anyhow = { workspace = true, features = ["std"] } +async-trait.workspace = true +clap.workspace = true common = { path = "../common" } -derive_more = "0.99" -directories = "5.0" -env_logger = "0.9.3" -http = "0.2" -hyper = { version = "0.14.26", features = ["client", "http2", "tcp"] } -hyper-rustls = "0.24" -gstuff = { version = "0.7" , features = [ "nightly" ]} -inquire = "0.6" -itertools = "0.10" -log = "0.4.21" +derive_more.workspace = true +directories.workspace = true +env_logger.workspace = true +http.workspace = true +hyper = { workspace = true, features = ["client", "http2", "tcp"] } +hyper-rustls.workspace = true +stuff.workspace = true +inquire.workspace = true +itertools.workspace = true +log.workspace = true mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc"} mm2_core = { path = "../mm2_core" } -passwords = "3.1" +passwords.workspace = true rpc = { path = "../mm2_bitcoin/rpc" } -rustls = { version = "0.21", features = [ "dangerous_configuration" ] } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -sysinfo = "0.28" -tiny-bip39 = "0.8.0" -tokio = { version = "1.20.0", features = [ "macros" ] } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +rustls = { workspace = true, features = [ "dangerous_configuration" ] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +sysinfo.workspace = true +tiny-bip39.workspace = true +tokio = { workspace = true, features = [ "macros" ] } +uuid.workspace = true [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.3", features = ["processthreadsapi", "winnt"] } +winapi = { workspace = true, features = ["processthreadsapi", "winnt"] } + diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 13aa9c2b72..3e49769c7e 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -13,60 +13,62 @@ enable-sia = [ default = [] run-docker-tests = [] for-tests = ["dep:mocktopus"] -new-db-arch = [] +new-db-arch = ["mm2_core/new-db-arch"] + +# Temporary feature for implementing IBC wrap/unwrap mechanism and will be removed +# once we consider it as stable. +ibc-routing-for-swaps = [] [lib] path = "lp_coins.rs" doctest = false [dependencies] -async-std = { version = "1.5", features = ["unstable"] } -async-trait = "0.1.52" -base64 = "0.21.2" -base58 = "0.2.0" -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -bitcoin_hashes = "0.11" +async-std = { workspace = true, features = ["unstable"] } +async-trait.workspace = true +base64.workspace = true +bip32.workspace = true +bitcoin_hashes.workspace = true bitcrypto = { path = "../mm2_bitcoin/crypto" } -blake2b_simd = { version = "0.5.10", optional = true } -byteorder = "1.3" -bytes = "0.4" -cfg-if = "1.0" +blake2b_simd = { workspace = true, optional = true } +bs58.workspace = true +byteorder.workspace = true +bytes.workspace = true +cfg-if.workspace = true chain = { path = "../mm2_bitcoin/chain" } -chrono = { version = "0.4.23", "features" = ["serde"] } +chrono = { workspace = true, "features" = ["serde"] } common = { path = "../common" } -compatible-time = { version = "1.1.0", package = "web-time" } -cosmrs = { version = "0.16", default-features = false } -crossbeam = "0.8" +compatible-time.workspace = true +cosmrs.workspace = true +crossbeam.workspace = true crypto = { path = "../crypto" } db_common = { path = "../db_common" } -derive_more = "0.99" -ed25519-dalek = { version = "1.0.1", features = ["serde"] } +derive_more.workspace = true +ed25519-dalek.workspace = true enum_derives = { path = "../derives/enum_derives" } -ethabi = { version = "17.0.0" } -ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -# Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. -#enum_dispatch = "0.1" -futures01 = { version = "0.1", package = "futures" } -futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } -futures-ticker = "0.0.3" -# using select macro requires the crate to be named futures, compilation failed with futures03 name -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } -group = "0.8.0" -gstuff = { version = "0.7", features = ["nightly"] } -hex = "0.4.2" -http = "0.2" -itertools = { version = "0.10", features = ["use_std"] } -jsonrpc-core = "18.0.0" +kdf_walletconnect = { path = "../kdf_walletconnect" } +ethabi.workspace = true +ethcore-transaction.workspace = true +ethereum-types.workspace = true +ethkey.workspace = true +futures01.workspace = true +futures-util.workspace = true +futures-ticker.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } +group.workspace = true +gstuff.workspace = true +hex.workspace = true +http.workspace = true +itertools = { workspace = true, features = ["use_std"] } +jsonrpc-core.workspace = true +jubjub.workspace = true keys = { path = "../mm2_bitcoin/keys" } -lazy_static = "1.4" -libc = "0.2" -nom = "6.1.2" +lazy_static.workspace = true +libc.workspace = true +nom.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } -mm2_git = { path = "../mm2_git" } mm2_io = { path = "../mm2_io" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } @@ -74,100 +76,101 @@ mm2_number = { path = "../mm2_number"} mm2_p2p = { path = "../mm2_p2p", default-features = false } mm2_rpc = { path = "../mm2_rpc" } mm2_state_machine = { path = "../mm2_state_machine" } -mocktopus = { version = "0.8.0", optional = true } -num-traits = "0.2" -parking_lot = { version = "0.12.0", features = ["nightly"] } +mocktopus = { workspace = true, optional = true } +num-traits.workspace = true +parking_lot = { workspace = true, features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } -prost = "0.12" -protobuf = "2.20" +prost.workspace = true +protobuf.workspace = true proxy_signature = { path = "../proxy_signature" } -rand = { version = "0.7", features = ["std", "small_rng"] } -regex = "1" -reqwest = { version = "0.11.9", default-features = false, features = ["json"], optional = true } -rlp = { version = "0.5" } -rmp-serde = "0.14.3" +rand = { workspace = true, features = ["std", "small_rng"] } +regex.workspace = true +reqwest = { workspace = true, optional = true } +rlp.workspace = true +rmp-serde.workspace = true rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } script = { path = "../mm2_bitcoin/script" } -secp256k1 = { version = "0.20" } +secp256k1.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_with = "1.14.0" +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_with.workspace = true serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808", optional = true } spv_validation = { path = "../mm2_bitcoin/spv_validation" } -sha2 = "0.10" -sha3 = "0.9" +sha2.workspace = true +sha3.workspace = true utxo_signer = { path = "utxo_signer" } # using the same version as cosmrs -tendermint-rpc = { version = "0.35", default-features = false } -tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", features = ["rustls-tls-native-roots"]} -url = { version = "2.2.2", features = ["serde"] } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +tendermint-rpc.workspace = true +tokio-tungstenite-wasm = { workspace = true, features = ["rustls-tls-native-roots"]} +url.workspace = true +uuid.workspace = true # 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.20.0", default-features = false } -zbase32 = "0.1.2" -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } -zcash_extras = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } -zcash_primitives = {features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } +web3 = { workspace = true, default-features = false } +zbase32.workspace = true +zcash_client_backend.workspace = true +zcash_extras.workspace = true +zcash_primitives.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -blake2b_simd = "0.5" -ff = "0.8" -futures-util = "0.3" -jubjub = "0.5.1" -js-sys = { version = "0.3.27" } +blake2b_simd.workspace = true +ff.workspace = true +futures-util.workspace = true +jubjub.workspace = true +js-sys.workspace = true mm2_db = { path = "../mm2_db" } mm2_metamask = { path = "../mm2_metamask" } mm2_test_helpers = { path = "../mm2_test_helpers" } -time = { version = "0.3.20", features = ["wasm-bindgen"] } -timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } -tonic = { version = "0.10", default-features = false, features = ["prost", "codegen", "gzip"] } -tower-service = "0.3" -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.2" } -web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover"] } +time = { workspace = true, features = ["wasm-bindgen"] } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } +tonic = { workspace = true, default-features = false, features = ["prost", "codegen", "gzip"] } +tower-service.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } +zcash_proofs = { workspace = true, features = ["local-prover"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -dirs = { version = "1" } -bitcoin = "0.29" -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } -# using webpki-tokio to avoid rejecting valid certificates -# got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features -hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -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.21", features = ["dangerous_configuration"] } -secp256k1v24 = { version = "0.24", package = "secp256k1" } -timed-map = { version = "1.3", features = ["rustc-hash"] } -tokio = { version = "1.20" } -tokio-rustls = { version = "0.24" } -tonic = { version = "0.10", features = ["tls", "tls-webpki-roots", "gzip"] } -webpki-roots = { version = "0.25" } -zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2" } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.2", default-features = false, features = ["local-prover", "multicore"] } +dirs.workspace = true +bitcoin.workspace = true +hyper = { workspace = true, features = ["client", "http2", "server", "tcp"] } +hyper-rustls = { workspace = true, default-features = false, features = ["http1", "http2", "webpki-tokio"] } +lightning.workspace = true +lightning-background-processor.workspace = true +lightning-invoice.workspace = true +lightning-net-tokio.workspace = true +rust-ini.workspace = true +rustls = { workspace = true, features = ["dangerous_configuration"] } +secp256k1v24.workspace = true +timed-map = { workspace = true, features = ["rustc-hash"] } +tokio.workspace = true +tokio-rustls.workspace = true +tonic = { workspace = true, features = ["codegen", "prost", "gzip", "tls", "tls-webpki-roots"] } +webpki-roots.workspace = true +zcash_client_sqlite.workspace = true +zcash_proofs = { workspace = true, features = ["local-prover", "multicore"] } [target.'cfg(windows)'.dependencies] -winapi = "0.3" +winapi.workspace = true [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } -mocktopus = { version = "0.8.0" } +mocktopus.workspace = true mm2_p2p = { path = "../mm2_p2p", features = ["application"] } +ff.workspace = true +jubjub.workspace = true +reqwest.workspace = true [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wagyu-zcash-parameters = { version = "0.2" } +wagyu-zcash-parameters.workspace = true [build-dependencies] -prost-build = { version = "0.12", default-features = false } -tonic-build = { version = "0.10", default-features = false, features = ["prost"] } +prost-build.workspace = true +tonic-build.workspace = true diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs index 3ec047ee80..dbcf02c343 100644 --- a/mm2src/coins/coin_balance.rs +++ b/mm2src/coins/coin_balance.rs @@ -506,6 +506,27 @@ pub mod common_impl { params.min_addresses_number.max(Some(path_to_address.address_id + 1)), ) .await?; + drop(new_account); + + if coin.is_trezor() { + let enabled_address = + hd_wallet + .get_enabled_address() + .await + .ok_or(EnableCoinBalanceError::NewAddressDerivingError( + NewAddressDerivingError::Internal( + "Couldn't find enabled address after it has already been enabled".to_string(), + ), + ))?; + coin.received_enabled_address_from_hw_wallet(enabled_address) + .await + .map_err(|e| { + EnableCoinBalanceError::NewAddressDerivingError(NewAddressDerivingError::Internal(format!( + "Coin rejected the enabled address derived from the hardware wallet: {}", + e + ))) + })?; + } // Todo: The enabled address should be indicated in the response. result.accounts.push(account_balance); return Ok(result); @@ -538,6 +559,27 @@ pub mod common_impl { .await?; result.accounts.push(account_balance); } + drop(accounts); + + if coin.is_trezor() { + let enabled_address = + hd_wallet + .get_enabled_address() + .await + .ok_or(EnableCoinBalanceError::NewAddressDerivingError( + NewAddressDerivingError::Internal( + "Couldn't find enabled address after it has already been enabled".to_string(), + ), + ))?; + coin.received_enabled_address_from_hw_wallet(enabled_address) + .await + .map_err(|e| { + EnableCoinBalanceError::NewAddressDerivingError(NewAddressDerivingError::Internal(format!( + "Coin rejected the enabled address derived from the hardware wallet: {}", + e + ))) + })?; + } Ok(result) } diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index 3e9bbc7349..86a2c0da00 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -108,3 +108,15 @@ pub enum MyAddressError { UnexpectedDerivationMethod(String), InternalError(String), } + +impl std::error::Error for MyAddressError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + // This error doesn't wrap another error, so we return None + None + } +} + +#[derive(Debug, Display)] +pub enum AddressFromPubkeyError { + InternalError(String), +} diff --git a/mm2src/coins/coins_tests.rs b/mm2src/coins/coins_tests.rs deleted file mode 100644 index bc7ab27897..0000000000 --- a/mm2src/coins/coins_tests.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::update_coins_config; - -#[test] -fn test_update_coin_config_success() { - let conf = json!([ - { - "coin": "RICK", - "asset": "RICK", - "fname": "RICK (TESTCOIN)", - "rpcport": 25435, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - }, - { - "coin": "MORTY", - "asset": "MORTY", - "fname": "MORTY (TESTCOIN)", - "rpcport": 16348, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - }, - { - "coin": "ETH", - "name": "ethereum", - "fname": "Ethereum", - "etomic": "0x0000000000000000000000000000000000000000", - "rpcport": 80, - "mm2": 1, - "required_confirmations": 3, - }, - { - "coin": "ARPA", - "name": "arpa-chain", - "fname": "ARPA Chain", - // ARPA coin contains the protocol already. This coin should be skipped. - "protocol": { - "type":"ERC20", - "protocol_data": { - "platform": "ETH", - "contract_address": "0xBA50933C268F567BDC86E1aC131BE072C6B0b71a" - } - }, - "rpcport": 80, - "mm2": 1, - "required_confirmations": 3, - }, - { - "coin": "JST", - "name": "JST", - "fname": "JST (TESTCOIN)", - "etomic": "0x996a8ae0304680f6a69b8a9d7c6e37d65ab5ab56", - "rpcport": 80, - "mm2": 1, - }, - ]); - let actual = update_coins_config(conf).unwrap(); - let expected = json!([ - { - "coin": "RICK", - "asset": "RICK", - "fname": "RICK (TESTCOIN)", - "rpcport": 25435, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - "protocol": { - "type": "UTXO" - }, - }, - { - "coin": "MORTY", - "asset": "MORTY", - "fname": "MORTY (TESTCOIN)", - "rpcport": 16348, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - "protocol": { - "type": "UTXO" - }, - }, - { - "coin": "ETH", - "name": "ethereum", - "fname": "Ethereum", - "rpcport": 80, - "mm2": 1, - "required_confirmations": 3, - "protocol": { - "type": "ETH" - }, - }, - { - "coin": "ARPA", - "name": "arpa-chain", - "fname": "ARPA Chain", - "protocol": { - "type": "ERC20", - "protocol_data": { - "platform": "ETH", - "contract_address": "0xBA50933C268F567BDC86E1aC131BE072C6B0b71a" - } - }, - "rpcport": 80, - "mm2": 1, - "required_confirmations": 3, - }, - { - "coin": "JST", - "name": "JST", - "fname": "JST (TESTCOIN)", - "rpcport": 80, - "mm2": 1, - "protocol": { - "type": "ERC20", - "protocol_data": { - "platform": "ETH", - "contract_address": "0x996a8ae0304680f6a69b8a9d7c6e37d65ab5ab56" - } - }, - }, - ]); - assert_eq!(actual, expected); -} - -#[test] -fn test_update_coin_config_error_not_array() { - let conf = json!({ - "coin": "RICK", - "asset": "RICK", - "fname": "RICK (TESTCOIN)", - "rpcport": 25435, - "txversion": 4, - "overwintered": 1, - "mm2": 1, - }); - let error = update_coins_config(conf).err().unwrap(); - assert!(error.contains("Coins config must be an array")); -} - -#[test] -fn test_update_coin_config_error_not_object() { - let conf = json!([["Ford", "BMW", "Fiat"]]); - let error = update_coins_config(conf).err().unwrap(); - assert!(error.contains("Expected object, found")); -} - -#[test] -fn test_update_coin_config_invalid_etomic() { - let conf = json!([ - { - "coin": "JST", - "name": "JST", - "fname": "JST (TESTCOIN)", - "etomic": 12345678, - "rpcport": 80, - "mm2": 1, - }, - ]); - let error = update_coins_config(conf).err().unwrap(); - assert!(error.contains("Expected etomic as string, found")); -} diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 7a82841687..91c3294c2d 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -20,6 +20,7 @@ // // Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved. // +use self::wallet_connect::{send_transaction_with_walletconnect, WcEthTxParams}; use super::eth::Action::{Call, Create}; use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT}; use super::*; @@ -58,6 +59,7 @@ use common::executor::{abortable_queue::AbortableQueue, AbortSettings, Abortable Timer}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; +use common::wait_until_sec; use common::{now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::privkey::key_pair_from_secret; use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; @@ -77,6 +79,7 @@ use futures::compat::Future01CompatExt; use futures::future::{join, join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; use futures01::Future; use http::Uri; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, BigUint, MmNumber}; @@ -100,9 +103,8 @@ use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallReques use web3::{self, Web3}; cfg_wasm32! { - use common::{now_ms, wait_until_ms}; use crypto::MetamaskArc; - use ethereum_types::{H264, H520}; + use ethereum_types::H520; use mm2_metamask::MetamaskError; use web3::types::TransactionRequest; } @@ -137,6 +139,7 @@ mod eth_rpc; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; #[cfg(any(test, target_arch = "wasm32"))] mod for_tests; pub(crate) mod nft_swap_v2; +pub mod wallet_connect; mod web3_transport; use web3_transport::{http_transport::HttpTransportNode, Web3Transport}; @@ -161,6 +164,8 @@ use erc20::get_token_decimals; pub(crate) mod eth_swap_v2; use eth_swap_v2::{extract_id_from_tx_data, EthPaymentType, PaymentMethod, SpendTxSearchParams}; +pub mod tron; + /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.137.5:8565) contract address: 0x83965C539899cC0F918552e5A26915de40ee8852 /// Ropsten: https://ropsten.etherscan.io/address/0x7bc1bbdd6a0a722fc9bffc49c921b685ecb84b94 @@ -762,6 +767,30 @@ struct SavedErc20Events { latest_block: U64, } +/// Specifies which blockchain the EthCoin operates on: EVM-compatible or TRON. +/// This distinction allows unified logic for EVM & TRON coins. +#[derive(Clone, Debug)] +pub enum ChainSpec { + Evm { chain_id: u64 }, + Tron { network: tron::Network }, +} + +impl ChainSpec { + pub fn chain_id(&self) -> Option { + match self { + ChainSpec::Evm { chain_id } => Some(*chain_id), + ChainSpec::Tron { .. } => None, + } + } + + pub fn kind(&self) -> &'static str { + match self { + ChainSpec::Evm { .. } => "EVM", + ChainSpec::Tron { .. } => "TRON", + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum EthCoinType { /// Ethereum itself or it's forks: ETC/others @@ -784,6 +813,11 @@ pub enum EthPrivKeyBuildPolicy { #[cfg(target_arch = "wasm32")] Metamask(MetamaskArc), Trezor, + WalletConnect { + address: Address, + public_key_uncompressed: H520, + session_topic: String, + }, } impl EthPrivKeyBuildPolicy { @@ -816,6 +850,8 @@ impl From for EthPrivKeyBuildPolicy { pub struct EthCoinImpl { ticker: String, pub coin_type: EthCoinType, + /// Specifies the underlying blockchain (EVM or TRON). + pub chain_spec: ChainSpec, pub(crate) priv_key_policy: EthPrivKeyPolicy, /// Either an Iguana address or a 'EthHDWallet' instance. /// Arc is used to use the same hd wallet from platform coin if we need to. @@ -836,7 +872,6 @@ pub struct EthCoinImpl { /// Coin needs access to the context in order to reuse the logging and shutdown facilities. /// Using a weak reference by default in order to avoid circular references and leaks. pub ctx: MmWeak, - chain_id: u64, /// The name of the coin with which Trezor wallet associates this asset. trezor_coin: Option, /// the block range used for eth_getLogs @@ -913,7 +948,7 @@ macro_rules! tx_type_from_pay_for_gas_option { impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] fn eth_traces_path(&self, ctx: &MmArc, my_address: Address) -> PathBuf { - ctx.dbdir() + ctx.address_dir(&my_address.display_address()) .join("TRANSACTIONS") .join(format!("{}_{:#02x}_trace.json", self.ticker, my_address)) } @@ -921,7 +956,8 @@ impl EthCoinImpl { /// Load saved ETH traces from local DB #[cfg(not(target_arch = "wasm32"))] fn load_saved_traces(&self, ctx: &MmArc, my_address: Address) -> Option { - let content = gstuff::slurp(&self.eth_traces_path(ctx, my_address)); + let path = self.eth_traces_path(ctx, my_address); + let content = gstuff::slurp(&path); if content.is_empty() { None } else { @@ -943,9 +979,8 @@ impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] fn store_eth_traces(&self, ctx: &MmArc, my_address: Address, traces: &SavedTraces) { let content = json::to_vec(traces).unwrap(); - let tmp_file = format!("{}.tmp", self.eth_traces_path(ctx, my_address).display()); - std::fs::write(&tmp_file, content).unwrap(); - std::fs::rename(tmp_file, self.eth_traces_path(ctx, my_address)).unwrap(); + let path = self.eth_traces_path(ctx, my_address); + mm2_io::fs::write(&path, &content, true).unwrap(); } /// Store ETH traces to local DB @@ -957,7 +992,7 @@ impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] fn erc20_events_path(&self, ctx: &MmArc, my_address: Address) -> PathBuf { - ctx.dbdir() + ctx.address_dir(&my_address.display_address()) .join("TRANSACTIONS") .join(format!("{}_{:#02x}_events.json", self.ticker, my_address)) } @@ -966,9 +1001,8 @@ impl EthCoinImpl { #[cfg(not(target_arch = "wasm32"))] fn store_erc20_events(&self, ctx: &MmArc, my_address: Address, events: &SavedErc20Events) { let content = json::to_vec(events).unwrap(); - let tmp_file = format!("{}.tmp", self.erc20_events_path(ctx, my_address).display()); - std::fs::write(&tmp_file, content).unwrap(); - std::fs::rename(tmp_file, self.erc20_events_path(ctx, my_address)).unwrap(); + let path = self.erc20_events_path(ctx, my_address); + mm2_io::fs::write(&path, &content, true).unwrap(); } /// Store ERC20 events to local DB @@ -981,7 +1015,8 @@ impl EthCoinImpl { /// Load saved ERC20 events from local DB #[cfg(not(target_arch = "wasm32"))] fn load_saved_erc20_events(&self, ctx: &MmArc, my_address: Address) -> Option { - let content = gstuff::slurp(&self.erc20_events_path(ctx, my_address)); + let path = self.erc20_events_path(ctx, my_address); + let content = gstuff::slurp(&path); if content.is_empty() { None } else { @@ -1043,7 +1078,7 @@ impl EthCoinImpl { } #[inline(always)] - pub fn chain_id(&self) -> u64 { self.chain_id } + pub fn chain_id(&self) -> Option { self.chain_spec.chain_id() } } async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> RawTransactionResult { @@ -1161,7 +1196,16 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit .build() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, Some(eth_coin.chain_id))?; + let chain_id = match eth_coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add support for Tron NFTs + ChainSpec::Tron { .. } => { + return MmError::err(WithdrawError::InternalError( + "Tron is not supported for withdraw_erc1155 yet".to_owned(), + )) + }, + }; + let signed = tx.sign(secret, Some(chain_id))?; let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; @@ -1252,7 +1296,16 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd .build() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, Some(eth_coin.chain_id))?; + let chain_id = match eth_coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add support for Tron NFTs + ChainSpec::Tron { .. } => { + return MmError::err(WithdrawError::InternalError( + "Tron is not supported for withdraw_erc721 yet".to_owned(), + )) + }, + }; + let signed = tx.sign(secret, Some(chain_id))?; let signed_bytes = rlp::encode(&signed); let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; @@ -1527,7 +1580,7 @@ impl SwapOps for EthCoin { activated_key: ref key_pair, .. } => key_pair_from_secret(key_pair.secret().as_fixed_bytes()).expect("valid key"), - EthPrivKeyPolicy::Trezor => todo!(), + EthPrivKeyPolicy::Trezor | EthPrivKeyPolicy::WalletConnect { .. } => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => todo!(), } @@ -1545,6 +1598,7 @@ impl SwapOps for EthCoin { .public_slice() .try_into() .expect("valid key length!"), + EthPrivKeyPolicy::WalletConnect { public_key, .. } => public_key.into(), EthPrivKeyPolicy::Trezor => todo!(), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => metamask_policy.public_key.0, @@ -2284,6 +2338,11 @@ impl MarketCoinOps for EthCoin { } } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let addr = addr_from_raw_pubkey(&pubkey.0).map_err(AddressFromPubkeyError::InternalError)?; + Ok(addr.display_address()) + } + async fn get_public_key(&self) -> Result> { match self.priv_key_policy { EthPrivKeyPolicy::Iguana(ref key_pair) @@ -2311,6 +2370,10 @@ impl MarketCoinOps for EthCoin { EthPrivKeyPolicy::Metamask(ref metamask_policy) => { Ok(format!("{:02x}", metamask_policy.public_key_uncompressed)) }, + EthPrivKeyPolicy::WalletConnect { + public_key_uncompressed, + .. + } => Ok(format!("{public_key_uncompressed:02x}")), } } @@ -2328,10 +2391,25 @@ impl MarketCoinOps for EthCoin { Some(keccak256(&stream.out()).take()) } - fn sign_message(&self, message: &str) -> SignatureResult { + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?; - let privkey = &self.priv_key_policy.activated_key_or_err()?.secret(); - let signature = sign(privkey, &H256::from(message_hash))?; + + let secret = if let Some(address) = address { + let path_to_coin = self.priv_key_policy.path_to_coin_or_err()?; + let derivation_path = address + .valid_derivation_path(path_to_coin) + .mm_err(|err| SignatureError::InvalidRequest(err.to_string()))?; + let privkey = self + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(&derivation_path)?; + ethkey::Secret::from_slice(privkey.as_slice()).ok_or(MmError::new(SignatureError::InternalError( + "failed to derive ethkey::Secret".to_string(), + )))? + } else { + self.priv_key_policy.activated_key_or_err()?.secret().clone() + }; + let signature = sign(&secret, &H256::from(message_hash))?; + Ok(format!("0x{}", signature)) } @@ -2578,6 +2656,7 @@ impl MarketCoinOps for EthCoin { EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' is not supported for MetaMask"), + EthPrivKeyPolicy::WalletConnect { .. } => ERR!("'display_priv_key' is not supported for WalletConnect"), } } @@ -2635,15 +2714,23 @@ async fn sign_transaction_with_keypair<'a>( if !coin.is_tx_type_supported(&tx_type) { return Err(TransactionErr::Plain("Eth transaction type not supported".into())); } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, action, value, data); let tx_builder = tx_builder_with_pay_for_gas_option(coin, tx_builder, pay_for_gas_option) .map_err(|e| TransactionErr::Plain(e.get_inner().to_string()))?; let tx = tx_builder.build()?; + let chain_id = match coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add Tron signing logic + ChainSpec::Tron { .. } => { + return Err(TransactionErr::Plain( + "Tron is not supported for sign_transaction_with_keypair yet".into(), + )) + }, + }; + let signed_tx = tx.sign(key_pair.secret(), Some(chain_id))?; - Ok(( - tx.sign(key_pair.secret(), Some(coin.chain_id))?, - web3_instances_with_latest_nonce, - )) + Ok((signed_tx, web3_instances_with_latest_nonce)) } /// Sign and send eth transaction with provided keypair, @@ -2717,7 +2804,7 @@ async fn sign_and_send_transaction_with_metamask( // It's important to return the transaction hex for the swap, // so wait up to 60 seconds for the transaction to appear on the RPC node. - let wait_rpc_timeout = 60_000; + let wait_rpc_timeout = 60; let check_every = 1.; // Please note that this method may take a long time @@ -2784,13 +2871,70 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw }) .map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format())) }, + EthPrivKeyPolicy::WalletConnect { .. } => { + // NOTE: doesn't work with wallets that doesn't support `eth_signTransaction`. + // e.g Metamask + let wc = { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key.") + }; + // Todo: Tron will have to be set with `ChainSpec::Evm` to work with walletconnect. + // This means setting the protocol as `ETH` in coin config and having a different coin for this mode. + let chain_id = coin.chain_spec.chain_id().ok_or(RawTransactionError::InvalidParam( + "WalletConnect needs chain_id to be set".to_owned(), + ))?; + let my_address = coin + .derivation_method + .single_addr_or_err() + .await + .mm_err(|e| RawTransactionError::InternalError(e.to_string()))?; + let address_lock = coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let pay_for_gas_option = if let Some(ref pay_for_gas) = args.pay_for_gas { + pay_for_gas.clone().try_into()? + } else { + // use legacy gas_price() if not set + info!(target: "sign-and-send", "get_gas_price…"); + let gas_price = coin.get_gas_price().await?; + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) + }; + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) + .compat() + .await + .map_to_mm(RawTransactionError::InvalidParam)?; + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + + info!(target: "sign-and-send", "WalletConnect signing and sending tx…"); + let (signed_tx, _) = coin + .wc_sign_tx(&wc, WcEthTxParams { + my_address, + gas_price: pay_for_gas_option.get_gas_price(), + action, + value, + gas: args.gas_limit, + data: &data, + nonce, + chain_id, + max_fee_per_gas, + max_priority_fee_per_gas, + }) + .await + .mm_err(|err| RawTransactionError::TransactionError(err.to_string()))?; + + Ok(RawTransactionRes { + tx_hex: signed_tx.tx_hex().into(), + }) + }, + EthPrivKeyPolicy::Trezor => MmError::err(RawTransactionError::InvalidParam( + "sign raw eth tx not implemented for Trezor".into(), + )), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => MmError::err(RawTransactionError::InvalidParam( "sign raw eth tx not implemented for Metamask".into(), )), - EthPrivKeyPolicy::Trezor => MmError::err(RawTransactionError::InvalidParam( - "sign raw eth tx not implemented for Trezor".into(), - )), } } @@ -3733,8 +3877,23 @@ impl EthCoin { .single_addr_or_err() .await .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + sign_and_send_transaction_with_keypair(&coin, key_pair, address, value, action, data, gas).await }, + EthPrivKeyPolicy::WalletConnect { .. } => { + let wc = { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key.") + }; + let address = coin + .derivation_method + .single_addr_or_err() + .await + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + send_transaction_with_walletconnect(coin, &wc, address, value, action, &data, gas).await + }, EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for swaps yet!"))), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { @@ -5368,15 +5527,14 @@ impl EthCoin { } /// Returns `None` if the transaction hasn't appeared on the RPC nodes at the specified time. - #[cfg(target_arch = "wasm32")] async fn wait_for_tx_appears_on_rpc( &self, tx_hash: H256, - wait_rpc_timeout_ms: u64, + wait_rpc_timeout_s: u64, check_every: f64, ) -> Web3RpcResult> { - let wait_until = wait_until_ms(wait_rpc_timeout_ms); - while now_ms() < wait_until { + let wait_until = wait_until_sec(wait_rpc_timeout_s); + while now_sec() < wait_until { let maybe_tx = self.transaction(TransactionId::Hash(tx_hash)).await?; if let Some(tx) = maybe_tx { let signed_tx = signed_tx_from_web3_tx(tx).map_to_mm(Web3RpcError::InvalidResponse)?; @@ -5386,10 +5544,10 @@ impl EthCoin { Timer::sleep(check_every).await; } - let timeout_s = wait_rpc_timeout_ms / 1000; warn!( - "Couldn't fetch the '{tx_hash:02x}' transaction hex as it hasn't appeared on the RPC node in {timeout_s}s" + "Couldn't fetch the '{tx_hash:02x}' transaction hex as it hasn't appeared on the RPC node in {wait_rpc_timeout_s}s" ); + Ok(None) } @@ -6314,6 +6472,27 @@ pub async fn eth_coin_from_conf_and_request( protocol: CoinProtocol, priv_key_policy: PrivKeyBuildPolicy, ) -> Result { + fn get_chain_id_from_platform(ctx: &MmArc, ticker: &str, platform: &str) -> Result { + let platform_conf = coin_conf(ctx, platform); + if platform_conf.is_null() { + return ERR!( + "Failed to activate ERC20 token '{}': the platform '{}' is not defined in the coins config.", + ticker, + platform + ); + } + let platform_protocol: CoinProtocol = json::from_value(platform_conf["protocol"].clone()) + .map_err(|e| ERRL!("Error parsing platform protocol for '{}': {}", platform, e))?; + match platform_protocol { + CoinProtocol::ETH { chain_id } => Ok(chain_id), + protocol => ERR!( + "Failed to activate ERC20 token '{}': the platform protocol '{:?}' must be ETH", + ticker, + protocol + ), + } + } + // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. let priv_key_policy = try_s!(EthPrivKeyBuildPolicy::try_from(priv_key_policy)); @@ -6402,8 +6581,8 @@ pub async fn eth_coin_from_conf_and_request( return ERR!("Failed to get client version for all urls"); } - let (coin_type, decimals) = match protocol { - CoinProtocol::ETH => (EthCoinType::Eth, ETH_DECIMALS), + let (coin_type, decimals, chain_id) = match protocol { + CoinProtocol::ETH { chain_id } => (EthCoinType::Eth, ETH_DECIMALS, chain_id), CoinProtocol::ERC20 { platform, contract_address, @@ -6422,9 +6601,13 @@ pub async fn eth_coin_from_conf_and_request( ), Some(d) => d as u8, }; - (EthCoinType::Erc20 { platform, token_addr }, decimals) + let chain_id = get_chain_id_from_platform(ctx, ticker, &platform)?; + (EthCoinType::Erc20 { platform, token_addr }, decimals, chain_id) + }, + CoinProtocol::NFT { platform } => { + let chain_id = get_chain_id_from_platform(ctx, ticker, &platform)?; + (EthCoinType::Nft { platform }, ETH_DECIMALS, chain_id) }, - CoinProtocol::NFT { platform } => (EthCoinType::Nft { platform }, ETH_DECIMALS), _ => return ERR!("Expect ETH, ERC20 or NFT protocol"), }; @@ -6444,10 +6627,6 @@ pub async fn eth_coin_from_conf_and_request( let sign_message_prefix: Option = json::from_value(conf["sign_message_prefix"].clone()).unwrap_or(None); - let chain_id = try_s!(conf["chain_id"] - .as_u64() - .ok_or_else(|| format!("chain_id is not set for {}", ticker))); - let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).unwrap_or(None); let initial_history_state = if req["tx_history"].as_bool().unwrap_or(false) { @@ -6480,6 +6659,8 @@ pub async fn eth_coin_from_conf_and_request( priv_key_policy: key_pair, derivation_method: Arc::new(derivation_method), coin_type, + // Tron is not supported for v1 activation + chain_spec: ChainSpec::Evm { chain_id }, sign_message_prefix, swap_contract_address, swap_v2_contracts: None, @@ -6493,7 +6674,6 @@ pub async fn eth_coin_from_conf_and_request( max_eth_tx_type, ctx: ctx.weak(), required_confirmations, - chain_id, trezor_coin, logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), address_nonce_locks, @@ -6782,6 +6962,8 @@ fn calc_total_fee(gas: U256, pay_for_gas_option: &PayForGasOption) -> NumConvers } } +// Todo: Tron have a different concept from gas (Energy, Bandwidth and Free Transaction), it should be added as a different function +// and this should be part of a trait abstracted over both types #[allow(clippy::result_large_err)] fn tx_builder_with_pay_for_gas_option( eth_coin: &EthCoin, @@ -6793,9 +6975,14 @@ fn tx_builder_with_pay_for_gas_option( PayForGasOption::Eip1559(Eip1559FeePerGas { max_priority_fee_per_gas, max_fee_per_gas, - }) => tx_builder - .with_priority_fee_per_gas(max_fee_per_gas, max_priority_fee_per_gas) - .with_chain_id(eth_coin.chain_id), + }) => { + let chain_id = eth_coin + .chain_id() + .ok_or_else(|| WithdrawError::InternalError("chain_id should be set for an EVM coin".to_string()))?; + tx_builder + .with_priority_fee_per_gas(max_fee_per_gas, max_priority_fee_per_gas) + .with_chain_id(chain_id) + }, }; Ok(tx_builder) } @@ -7279,6 +7466,15 @@ impl CommonSwapOpsV2 for EthCoin { .expect("slice with incorrect length"); Public::from_slice(&pubkey_bytes) }, + EthPrivKeyPolicy::WalletConnect { + public_key_uncompressed, + .. + } => { + let pubkey_bytes: [u8; 64] = public_key_uncompressed[1..65] + .try_into() + .expect("slice with incorrect length"); + Public::from_slice(&pubkey_bytes) + }, } } @@ -7299,6 +7495,7 @@ impl EthCoin { let coin = EthCoinImpl { ticker: self.ticker.clone(), coin_type: new_coin_type, + chain_spec: self.chain_spec.clone(), priv_key_policy: self.priv_key_policy.clone(), derivation_method: Arc::clone(&self.derivation_method), sign_message_prefix: self.sign_message_prefix.clone(), @@ -7315,7 +7512,6 @@ impl EthCoin { swap_txfee_policy: Mutex::new(self.swap_txfee_policy.lock().unwrap().clone()), max_eth_tx_type: self.max_eth_tx_type, ctx: self.ctx.clone(), - chain_id: self.chain_id, trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, address_nonce_locks: Arc::clone(&self.address_nonce_locks), diff --git a/mm2src/coins/eth/erc20.rs b/mm2src/coins/eth/erc20.rs index 75f7033fda..3ab3d89a03 100644 --- a/mm2src/coins/eth/erc20.rs +++ b/mm2src/coins/eth/erc20.rs @@ -1,6 +1,6 @@ use crate::eth::web3_transport::Web3Transport; use crate::eth::{EthCoin, ERC20_CONTRACT}; -use crate::{CoinsContext, MmCoinEnum}; +use crate::{CoinsContext, MarketCoinOps, MmCoinEnum}; use ethabi::Token; use ethereum_types::Address; use futures_util::TryFutureExt; @@ -90,16 +90,20 @@ pub fn get_erc20_ticker_by_contract_address(ctx: &MmArc, platform: &str, contrac }) } -/// Finds an enabled ERC20 token by its contract address and returns it as `MmCoinEnum`. -pub async fn get_enabled_erc20_by_contract( +/// Finds an enabled ERC20 token by contract address and platform coin ticker and returns it as `MmCoinEnum`. +pub async fn get_enabled_erc20_by_platform_and_contract( ctx: &MmArc, - contract_address: Address, + platform: &str, + contract_address: &Address, ) -> MmResult, String> { let cctx = CoinsContext::from_ctx(ctx)?; let coins = cctx.coins.lock().await; Ok(coins.values().find_map(|coin| match &coin.inner { - MmCoinEnum::EthCoin(eth_coin) if eth_coin.erc20_token_address() == Some(contract_address) => { + MmCoinEnum::EthCoin(eth_coin) + if eth_coin.platform_ticker() == platform + && eth_coin.erc20_token_address().as_ref() == Some(contract_address) => + { Some(coin.inner.clone()) }, _ => None, diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index 0cb7afe134..a991dab101 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -1,9 +1,10 @@ use super::EthCoin; use crate::{eth::{u256_to_big_decimal, Erc20TokenDetails}, + hd_wallet::AddrToString, BalanceError, CoinWithDerivationMethod}; use common::{executor::Timer, log, Future01CompatExt}; use mm2_err_handle::prelude::MmError; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use mm2_number::BigDecimal; use async_trait::async_trait; @@ -111,7 +112,7 @@ async fn fetch_balance( .await .map_err(|error| BalanceFetchError { ticker: token_ticker.clone(), - address: address.to_string(), + address: address.addr_to_string(), error, })?, coin.decimals, @@ -122,7 +123,7 @@ async fn fetch_balance( .await .map_err(|error| BalanceFetchError { ticker: token_ticker.clone(), - address: address.to_string(), + address: address.addr_to_string(), error, })?, info.decimals, @@ -131,13 +132,13 @@ async fn fetch_balance( let balance_as_big_decimal = u256_to_big_decimal(balance_as_u256, decimals).map_err(|e| BalanceFetchError { ticker: token_ticker.clone(), - address: address.to_string(), + address: address.addr_to_string(), error: e.into(), })?; Ok(BalanceData { ticker: token_ticker, - address: address.to_string(), + address: address.addr_to_string(), balance: balance_as_big_decimal, }) } @@ -146,7 +147,11 @@ async fn fetch_balance( impl EventStreamer for EthBalanceEventStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker) } + fn streamer_id(&self) -> StreamerId { + StreamerId::Balance { + coin: self.coin.ticker.to_string(), + } + } async fn handle( self, @@ -154,7 +159,7 @@ impl EventStreamer for EthBalanceEventStreamer { ready_tx: oneshot::Sender>, _: impl StreamHandlerInput, ) { - async fn start_polling(streamer_id: String, broadcaster: Broadcaster, coin: EthCoin, interval: f64) { + async fn start_polling(streamer_id: StreamerId, broadcaster: Broadcaster, coin: EthCoin, interval: f64) { async fn sleep_remaining_time(interval: f64, now: Instant) { // If the interval is x seconds, // our goal is to broadcast changed balances every x seconds. diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 2d2c43d08f..7cde52e740 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,7 +1,6 @@ use super::*; use crate::IguanaPrivKey; use common::block_on; -use futures_util::future; use mm2_core::mm_ctx::MmCtxBuilder; cfg_native!( @@ -10,6 +9,7 @@ cfg_native!( use common::{now_sec, block_on_f01}; use ethkey::{Generator, Random}; + use futures_util::future; use mm2_test_helpers::for_tests::{ETH_MAINNET_CHAIN_ID, ETH_MAINNET_NODES, ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, ETH_SEPOLIA_TOKEN_CONTRACT}; use mocktopus::mocking::*; @@ -26,6 +26,9 @@ cfg_native!( // old way to add some extra gas to the returned value from gas station (non-existent now), still used in tests const GAS_PRICE_PERCENT: u64 = 10; +const MATIC_CHAIN_ID: u64 = 137; + +const ETH: &str = "ETH"; fn check_sum(addr: &str, expected: &str) { let actual = checksum_address(addr); @@ -218,7 +221,7 @@ fn test_withdraw_impl_manual_fee() { let withdraw_req = WithdrawRequest { amount: 1.into(), to: "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), - coin: "ETH".to_string(), + coin: ETH.to_string(), fee: Some(WithdrawFee::EthGas { gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), @@ -230,7 +233,7 @@ fn test_withdraw_impl_manual_fee() { let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( EthTxFeeDetails { - coin: "ETH".into(), + coin: ETH.into(), gas_price: "0.000000001".parse().unwrap(), gas: gas_limit::ETH_MAX_TRADE_GAS, total_fee: "0.00015".parse().unwrap(), @@ -247,7 +250,7 @@ fn test_withdraw_impl_manual_fee() { fn test_withdraw_impl_fee_details() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str(ETH_SEPOLIA_TOKEN_CONTRACT).unwrap(), }, &["http://dummy.dummy"], @@ -276,7 +279,7 @@ fn test_withdraw_impl_fee_details() { let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( EthTxFeeDetails { - coin: "ETH".into(), + coin: ETH.into(), gas_price: "0.000000001".parse().unwrap(), gas: gas_limit::ETH_MAX_TRADE_GAS, total_fee: "0.00015".parse().unwrap(), @@ -313,7 +316,7 @@ fn get_sender_trade_preimage() { fn expected_fee(gas_price: u64, gas_limit: u64) -> TradeFee { let amount = u256_to_big_decimal((gas_limit * gas_price).into(), 18).expect("!u256_to_big_decimal"); TradeFee { - coin: "ETH".to_owned(), + coin: ETH.to_owned(), amount: amount.into(), paid_from_trading_vol: false, } @@ -381,7 +384,7 @@ fn get_erc20_sender_trade_preimage() { fn expected_trade_fee(gas_limit: u64, gas_price: u64) -> TradeFee { let amount = u256_to_big_decimal((gas_limit * gas_price).into(), 18).expect("!u256_to_big_decimal"); TradeFee { - coin: "ETH".to_owned(), + coin: ETH.to_owned(), amount: amount.into(), paid_from_trading_vol: false, } @@ -389,7 +392,7 @@ fn get_erc20_sender_trade_preimage() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::default(), }, &["http://dummy.dummy"], @@ -474,7 +477,7 @@ fn get_receiver_trade_preimage() { let amount = u256_to_big_decimal((gas_limit::ETH_RECEIVER_SPEND * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); let expected_fee = TradeFee { - coin: "ETH".to_owned(), + coin: ETH.to_owned(), amount: amount.into(), paid_from_trading_vol: false, }; @@ -497,7 +500,7 @@ fn test_get_fee_to_send_taker_fee() { // fee to send taker fee is `TRANSFER_GAS_LIMIT * gas_price` always. let amount = u256_to_big_decimal((TRANSFER_GAS_LIMIT * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); let expected_fee = TradeFee { - coin: "ETH".to_owned(), + coin: ETH.to_owned(), amount: amount.into(), paid_from_trading_vol: false, }; @@ -514,7 +517,7 @@ fn test_get_fee_to_send_taker_fee() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xaD22f63404f7305e4713CcBd4F296f34770513f4").unwrap(), }, &["http://dummy.dummy"], @@ -544,7 +547,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(40.into())))); let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xaD22f63404f7305e4713CcBd4F296f34770513f4").unwrap(), }, ETH_MAINNET_NODES, @@ -597,7 +600,7 @@ fn validate_dex_fee_invalid_sender_eth() { fn validate_dex_fee_invalid_sender_erc() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xa1d6df714f91debf4e0802a542e13067f31b8262").unwrap(), }, ETH_MAINNET_NODES, @@ -671,7 +674,7 @@ fn validate_dex_fee_eth_confirmed_before_min_block() { fn validate_dex_fee_erc_confirmed_before_min_block() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xa1d6df714f91debf4e0802a542e13067f31b8262").unwrap(), }, ETH_MAINNET_NODES, @@ -781,11 +784,13 @@ fn polygon_check_if_my_payment_sent() { "fname": "Polygon", "rpcport": 80, "mm2": 1, - "chain_id": 137, "avg_blocktime": 0.03, "required_confirmations": 3, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": MATIC_CHAIN_ID + } } }); @@ -802,7 +807,9 @@ fn polygon_check_if_my_payment_sent() { "MATIC", &conf, &request, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: MATIC_CHAIN_ID, + }, priv_key_policy, )) .unwrap(); @@ -863,7 +870,7 @@ fn test_sign_verify_message() { ); let message = "test"; - let signature = coin.sign_message(message).unwrap(); + let signature = coin.sign_message(message, None).unwrap(); assert_eq!(signature, "0xcdf11a9c4591fb7334daa4b21494a2590d3f7de41c7d2b333a5b61ca59da9b311b492374cc0ba4fbae53933260fa4b1c18f15d95b694629a7b0620eec77a938600"); let is_valid = coin @@ -877,7 +884,7 @@ fn test_sign_verify_message() { fn test_eth_extract_secret() { let key_pair = Random.generate().unwrap(); let coin_type = EthCoinType::Erc20 { - platform: "ETH".to_string(), + platform: ETH.to_string(), token_addr: Address::from_str("0xc0eb7aed740e1796992a08962c15661bdeb58003").unwrap(), }; let (_ctx, coin) = eth_coin_from_keypair(coin_type, &["http://dummy.dummy"], None, key_pair, ETH_SEPOLIA_CHAIN_ID); @@ -934,11 +941,13 @@ fn test_eth_validate_valid_and_invalid_pubkey() { "fname": "Polygon", "rpcport": 80, "mm2": 1, - "chain_id": 137, "avg_blocktime": 0.03, "required_confirmations": 3, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": MATIC_CHAIN_ID + } } }); @@ -959,7 +968,9 @@ fn test_eth_validate_valid_and_invalid_pubkey() { "MATIC", &conf, &request, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: MATIC_CHAIN_ID, + }, priv_key_policy, )) .unwrap(); @@ -975,6 +986,123 @@ fn test_eth_validate_valid_and_invalid_pubkey() { assert!(coin.validate_other_pubkey(&[1u8; 8]).is_err()); } +#[test] +fn test_get_enabled_erc20_by_contract_and_platform() { + use super::erc20::get_enabled_erc20_by_platform_and_contract; + use crate::rpc_command::get_enabled_coins::{get_enabled_coins_rpc, GetEnabledCoinsRequest}; + const BNB_TOKEN: &str = "1INCH-BEP20"; + const ETH_TOKEN: &str = "1INCH-ERC20"; + + let conf = json!({ + "coins": [{ + "coin": "BNB", + "name": "binancesmartchain", + "fname": "Binance Coin", + "avg_blocktime": 3, + "rpcport": 80, + "mm2": 1, + "use_access_list": true, + "max_eth_tx_type": 2, + "required_confirmations": 3, + "protocol": { + "type": "ETH", + "protocol_data": { + "chain_id": 56 + } + }, + "derivation_path": "m/44'/60'", + "trezor_coin": "Binance Smart Chain", + "links": { + "homepage": "https://www.binance.org" + } + },{ + "coin": BNB_TOKEN, + "name": "1inch_bep20", + "fname": "1Inch", + "rpcport": 80, + "mm2": 1, + "avg_blocktime": 3, + "required_confirmations": 3, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "BNB", + "contract_address": "0x111111111117dC0aa78b770fA6A738034120C302" + } + }, + "derivation_path": "m/44'/60'", + "use_access_list": true, + "max_eth_tx_type": 2, + "gas_limit": { + "eth_send_erc20": 60000, + "erc20_payment": 110000, + "erc20_receiver_spend": 85000, + "erc20_sender_refund": 85000 + } + },{ + "coin": "ETH", + "name": "ethereum", + "fname": "Ethereum", + "rpcport": 80, + "mm2": 1, + "sign_message_prefix": "Ethereum Signed Message:\n", + "required_confirmations": 3, + "avg_blocktime": 15, + "protocol": { + "type": "ETH", + "protocol_data": { + "chain_id": 1 + } + }, + "derivation_path": "m/44'/60'" + },{ + "coin": ETH_TOKEN, + "name": "1inch_erc20", + "fname": "1Inch", + "rpcport": 80, + "mm2": 1, + "avg_blocktime": 15, + "required_confirmations": 3, + "decimals": 18, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "ETH", + "contract_address": "0x111111111117dC0aa78b770fA6A738034120C302" + } + }, + "derivation_path": "m/44'/60'" + }] + }); + + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase( + ctx.clone(), + "spice describe gravity federal blast come thank unfair canal monkey style afraid", + ) + .unwrap(); + + let req_bnb_token = json!({ + "urls":["https://bsc-dataseed1.binance.org","https://bsc-dataseed1.defibit.io"], + "swap_contract_address":"0x9130b257d37a52e52f21054c4da3450c72f595ce", + }); + block_on(lp_coininit(&ctx, BNB_TOKEN, &req_bnb_token)).unwrap(); + + let req_eth_token = json!({ + "urls":["https://ethereum-rpc.publicnode.com", "https://eth.drpc.org"], + "swap_contract_address":"0x9130b257d37a52e52f21054c4da3450c72f595ce", + }); + block_on(lp_coininit(&ctx, ETH_TOKEN, &req_eth_token)).unwrap(); + + let coins = block_on(get_enabled_coins_rpc(ctx.clone(), GetEnabledCoinsRequest)).unwrap(); + assert_eq!(coins.coins.len(), 2); + + let contract_address = Address::from_str("0x111111111117dC0aa78b770fA6A738034120C302").unwrap(); + let res = block_on(get_enabled_erc20_by_platform_and_contract(&ctx, ETH, &contract_address)).unwrap(); + assert!(res.is_some()); + assert_eq!(res.unwrap().platform_ticker(), ETH); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_fee_history() { @@ -996,11 +1124,12 @@ fn test_gas_limit_conf() { "coin": "ETH", "name": "ethereum", "fname": "Ethereum", - "chain_id": 1337, "protocol":{ - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID + } }, - "chain_id": 1, "rpcport": 80, "mm2": 1, "gas_limit": { @@ -1018,7 +1147,7 @@ fn test_gas_limit_conf() { "urls":ETH_SEPOLIA_NODES, "swap_contract_address":ETH_SEPOLIA_SWAP_CONTRACT }); - let coin = block_on(lp_coininit(&ctx, "ETH", &req)).unwrap(); + let coin = block_on(lp_coininit(&ctx, ETH, &req)).unwrap(); let eth_coin = match coin { MmCoinEnum::EthCoin(eth_coin) => eth_coin, _ => panic!("not eth coin"), diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 287b3fb79a..367e88321d 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::lp_coininit; use crypto::CryptoCtx; use mm2_core::mm_ctx::MmCtxBuilder; -use mm2_test_helpers::for_tests::{ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; +use mm2_test_helpers::for_tests::{ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use wasm_bindgen_test::*; use web_sys::console; @@ -20,11 +20,12 @@ async fn init_eth_coin_helper() -> Result<(MmArc, MmCoinEnum), String> { "coin": "ETH", "name": "ethereum", "fname": "Ethereum", - "chain_id": 1337, "protocol":{ - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID, + } }, - "chain_id": 1, "rpcport": 80, "mm2": 1, "max_eth_tx_type": 2 diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs index b3de177a89..c9822619d6 100644 --- a/mm2src/coins/eth/eth_withdraw.rs +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -1,9 +1,10 @@ -use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, EthCoinType, EthDerivationMethod, +use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, ChainSpec, EthCoinType, EthDerivationMethod, EthPrivKeyPolicy, Public, WithdrawError, WithdrawRequest, WithdrawResult, ERC20_CONTRACT, H160, H256}; +use crate::eth::wallet_connect::WcEthTxParams; use crate::eth::{calc_total_fee, get_eth_gas_details_from_withdraw_fee, tx_builder_with_pay_for_gas_option, tx_type_from_pay_for_gas_option, Action, Address, EthTxFeeDetails, KeyPair, PayForGasOption, SignedEthTx, TransactionWrapper, UnSignedEthTxBuilder}; -use crate::hd_wallet::{HDCoinWithdrawOps, HDWalletOps, WithdrawFrom, WithdrawSenderAddress}; +use crate::hd_wallet::{HDAddressSelector, HDCoinWithdrawOps, HDWalletOps, WithdrawSenderAddress}; use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionData, TransactionDetails}; @@ -16,6 +17,7 @@ use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProces use crypto::{CryptoCtx, HwRpcError}; use ethabi::Token; use futures::compat::Future01CompatExt; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::map_mm_error::MapMmError; use mm2_err_handle::mm_error::MmResult; @@ -89,10 +91,12 @@ where /// Gets the derivation path for the address from which the withdrawal is made using the `from` parameter. #[allow(clippy::result_large_err)] - fn get_from_derivation_path(&self, from: &WithdrawFrom) -> Result> { + fn get_from_derivation_path(&self, from: &HDAddressSelector) -> Result> { let coin = self.coin(); let path_to_coin = &coin.deref().derivation_method.hd_wallet_or_err()?.derivation_path; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; + let path_to_address = from + .to_address_path(path_to_coin.coin_type()) + .mm_err(|err| WithdrawError::UnexpectedFromAddress(err.to_string()))?; let derivation_path = path_to_address.to_derivation_path(path_to_coin)?; Ok(derivation_path) } @@ -128,7 +132,16 @@ where match coin.priv_key_policy { EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { let key_pair = self.get_key_pair(req)?; - let signed = unsigned_tx.sign(key_pair.secret(), Some(coin.chain_id))?; + let chain_id = match coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Tron have different transaction signing algorithm, we should probably have a trait abstracting both + ChainSpec::Tron { .. } => { + return MmError::err(WithdrawError::InternalError( + "Tron is not supported for withdraw yet".to_owned(), + )) + }, + }; + let signed = unsigned_tx.sign(key_pair.secret(), Some(chain_id))?; let bytes = rlp::encode(&signed); Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) @@ -139,6 +152,9 @@ where let bytes = rlp::encode(&signed); Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) }, + EthPrivKeyPolicy::WalletConnect { .. } => { + MmError::err(WithdrawError::InternalError("invalid policy".to_owned())) + }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => MmError::err(WithdrawError::InternalError("invalid policy".to_owned())), } @@ -162,7 +178,7 @@ where } // Wait for 10 seconds for the transaction to appear on the RPC node. - let wait_rpc_timeout = 10_000; + let wait_rpc_timeout = 10; let check_every = 1.; // Please note that this method may take a long time @@ -178,7 +194,10 @@ where .unwrap_or_default(); Ok((tx_hash, tx_hex)) }, - EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } | EthPrivKeyPolicy::Trezor => { + EthPrivKeyPolicy::Iguana(_) + | EthPrivKeyPolicy::HDWallet { .. } + | EthPrivKeyPolicy::Trezor + | EthPrivKeyPolicy::WalletConnect { .. } => { MmError::err(WithdrawError::InternalError("invalid policy".to_owned())) }, } @@ -292,6 +311,51 @@ where }; self.send_withdraw_tx(&req, tx_to_send).await? }, + EthPrivKeyPolicy::WalletConnect { .. } => { + let ctx = MmArc::from_weak(&coin.ctx).expect("No context"); + let wc = WalletConnectCtx::from_ctx(&ctx) + .expect("TODO: handle error when enable kdf initialization without key."); + // Todo: Tron will have to be set with `ChainSpec::Evm` to work with walletconnect. + // This means setting the protocol as `ETH` in coin config and having a different coin for this mode. + let chain_id = coin.chain_spec.chain_id().ok_or(WithdrawError::UnsupportedError( + "WalletConnect needs chain_id to be set".to_owned(), + ))?; + let gas_price = pay_for_gas_option.get_gas_price(); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + let params = WcEthTxParams { + gas, + nonce, + data: &data, + my_address, + action: Action::Call(call_addr), + value: eth_value, + gas_price, + chain_id, + max_fee_per_gas, + max_priority_fee_per_gas, + }; + + let (tx, bytes) = if req.broadcast { + self.coin() + .wc_send_tx(&wc, params) + .await + .mm_err(|err| WithdrawError::SigningError(err.to_string()))? + } else { + self.coin() + .wc_sign_tx(&wc, params) + .await + .mm_err(|err| WithdrawError::SigningError(err.to_string()))? + }; + + (tx.tx_hash(), bytes) + }, }; self.on_finishing()?; @@ -374,8 +438,17 @@ impl EthWithdraw for InitEthWithdraw { let sign_processor = TrezorRpcTaskProcessor::new(self.task_handle.clone(), trezor_statuses); let sign_processor = Arc::new(sign_processor); let mut trezor_session = hw_ctx.trezor(sign_processor).await?; + let chain_id = match coin.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add support for Tron signing with Trezor + ChainSpec::Tron { .. } => { + return MmError::err(WithdrawError::InternalError( + "Tron is not supported for withdraw yet".to_owned(), + )) + }, + }; let unverified_tx = trezor_session - .sign_eth_tx(derivation_path, unsigned_tx, coin.chain_id) + .sign_eth_tx(derivation_path, unsigned_tx, chain_id) .await?; Ok(SignedEthTx::new(unverified_tx).map_to_mm(|err| WithdrawError::InternalError(err.to_string()))?) } diff --git a/mm2src/coins/eth/fee_estimation/eth_fee_events.rs b/mm2src/coins/eth/fee_estimation/eth_fee_events.rs index 0af1f13579..1c79e7da40 100644 --- a/mm2src/coins/eth/fee_estimation/eth_fee_events.rs +++ b/mm2src/coins/eth/fee_estimation/eth_fee_events.rs @@ -1,7 +1,7 @@ use super::ser::FeePerGasEstimated; use crate::eth::EthCoin; use common::executor::Timer; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use compatible_time::Instant; @@ -52,7 +52,11 @@ impl EthFeeEventStreamer { impl EventStreamer for EthFeeEventStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { format!("FEE_ESTIMATION:{}", self.coin.ticker) } + fn streamer_id(&self) -> StreamerId { + StreamerId::FeeEstimation { + coin: self.coin.ticker.to_string(), + } + } async fn handle( self, diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs index 781748832c..9652ba0a80 100644 --- a/mm2src/coins/eth/for_tests.rs +++ b/mm2src/coins/eth/for_tests.rs @@ -55,6 +55,7 @@ pub(crate) fn eth_coin_from_keypair( let eth_coin = EthCoin(Arc::new(EthCoinImpl { coin_type, + chain_spec: ChainSpec::Evm { chain_id }, decimals: 18, history_sync_state: Mutex::new(HistorySyncState::NotEnabled), sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), @@ -69,7 +70,6 @@ pub(crate) fn eth_coin_from_keypair( ctx: ctx.weak(), required_confirmations: 1.into(), swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), - chain_id, trezor_coin: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, address_nonce_locks: Arc::new(AsyncMutex::new(new_nonce_lock())), diff --git a/mm2src/coins/eth/tron.rs b/mm2src/coins/eth/tron.rs new file mode 100644 index 0000000000..c07307c13d --- /dev/null +++ b/mm2src/coins/eth/tron.rs @@ -0,0 +1,24 @@ +//! Minimal Tron placeholders for EthCoin integration. +//! These types will be expanded with full TRON logic in later steps. + +mod address; +pub use address::Address as TronAddress; + +/// Represents TRON chain/network. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum Network { + Mainnet, + Shasta, + Nile, + // TODO: Add more networks as needed. +} + +/// Placeholder for a TRON client. +#[derive(Clone, Debug)] +pub struct TronClient; + +/// Placeholder for TRON fee params. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TronFeeParams { + // TODO: Add TRON-specific fields in future steps. +} diff --git a/mm2src/coins/eth/tron/address.rs b/mm2src/coins/eth/tron/address.rs new file mode 100644 index 0000000000..0d4a45e07c --- /dev/null +++ b/mm2src/coins/eth/tron/address.rs @@ -0,0 +1,167 @@ +//! TRON address handling (base58, hex, validation, serde). + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::convert::{TryFrom, TryInto}; +use std::fmt; +use std::str::FromStr; + +pub const ADDRESS_PREFIX: u8 = 0x41; +pub const ADDRESS_BASE58_PREFIX: char = 'T'; +pub const ADDRESS_HEX_LEN: usize = 42; +pub const ADDRESS_BYTES_LEN: usize = 21; +pub const ADDRESS_BASE58_LEN: usize = 34; + +/// TRON mainnet or testnet address (21 bytes, 0x41 prefix + 20-bytes). +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Address { + pub inner: [u8; ADDRESS_BYTES_LEN], +} + +impl Address { + /// Construct from raw 21 bytes (must be 0x41-prefixed). + pub fn from_bytes(bytes: [u8; ADDRESS_BYTES_LEN]) -> Result { + if bytes[0] != ADDRESS_PREFIX { + return Err("TRON address must start with 0x41".into()); + } + Ok(Self { inner: bytes }) + } + + /// Construct from base58 string (with checksum). + pub fn from_base58(s: &str) -> Result { + let data = bs58::decode(s) + .with_check(None) + .into_vec() + .map_err(|e| format!("Invalid base58check address: {}", e))?; + + // SAFETY: Accessing `data[0]` is safe here because we first check that + // `data.len() == ADDRESS_BYTES_LEN`, guaranteeing the slice is not empty + // and has at least one element. + if data.len() != ADDRESS_BYTES_LEN || data[0] != ADDRESS_PREFIX { + return Err(format!( + "Invalid address: expected {} bytes with prefix 0x{:x}", + ADDRESS_BYTES_LEN, ADDRESS_PREFIX + )); + } + + let inner = data + .try_into() + .map_err(|_| "Failed to convert address bytes to array".to_string())?; + + Ok(Self { inner }) + } + + /// Construct from hex string, with or without `0x` prefix. + pub fn from_hex(s: &str) -> Result { + let s = s.strip_prefix("0x").unwrap_or(s); + let data = hex::decode(s).map_err(|e| format!("Invalid hex address: {}", e))?; + + // SAFETY: Accessing `data[0]` is safe here because we first check that + // `data.len() == ADDRESS_BYTES_LEN`, guaranteeing the slice is not empty + // and has at least one element. + if data.len() != ADDRESS_BYTES_LEN || data[0] != ADDRESS_PREFIX { + return Err(format!( + "Invalid address: expected {} bytes with prefix 0x{:x}", + ADDRESS_BYTES_LEN, ADDRESS_PREFIX + )); + } + + let inner = data + .try_into() + .map_err(|_| "Failed to convert address bytes to array".to_string())?; + + Ok(Self { inner }) + } + + /// Show as base58 string (canonical user format). + pub fn to_base58(&self) -> String { bs58::encode(self.inner).with_check().into_string() } + + /// Show as hex string, lowercase (canonical hex format). + pub fn to_hex(&self) -> String { hex::encode(self.inner) } + + /// Return the 21 bytes (0x41 + 20). + pub fn as_bytes(&self) -> &[u8] { &self.inner } +} + +impl TryFrom<[u8; ADDRESS_BYTES_LEN]> for Address { + type Error = String; + + fn try_from(bytes: [u8; ADDRESS_BYTES_LEN]) -> Result { Self::from_bytes(bytes) } +} + +impl AsRef<[u8]> for Address { + fn as_ref(&self) -> &[u8] { &self.inner } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_base58()) } +} + +impl fmt::Debug for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Address({} / 0x{})", self.to_base58(), self.to_hex()) + } +} + +impl Serialize for Address { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_base58()) + } +} + +impl<'de> Deserialize<'de> for Address { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = <&str>::deserialize(deserializer)?; + Address::from_str(s).map_err(serde::de::Error::custom) + } +} + +impl FromStr for Address { + type Err = String; + + fn from_str(s: &str) -> Result { + // Check for Base58 format + if s.len() == ADDRESS_BASE58_LEN && s.starts_with(ADDRESS_BASE58_PREFIX) { + return Self::from_base58(s); + } + + // Check for hex format (with or without 0x prefix) + if (s.len() == ADDRESS_HEX_LEN && s.starts_with("41")) + || (s.len() == ADDRESS_HEX_LEN + 2 && s.starts_with("0x41")) + { + return Self::from_hex(s); + } + + Err(format!( + "Invalid TRON address '{}': must be Base58 (34 chars starting with 'T') or hex (42 chars without 0x, 44 chars with 0x prefix)", + s + )) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_tron_address_base58_and_hex() { + let base58 = "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL"; + let hex = "418840e6c55b9ada326d211d818c34a994aeced808"; + let addr1 = Address::from_str(base58).unwrap(); + let addr2 = Address::from_str(hex).unwrap(); + assert_eq!(addr1, addr2); + assert_eq!(addr1.to_hex(), hex); + assert_eq!(addr2.to_base58(), base58); + } + + #[test] + fn test_invalid_tron_address() { + assert!(Address::from_str("foo").is_err()); + assert!(Address::from_str("0xdeadbeef").is_err()); + } +} diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 4e04bfebed..9bcd2d6c57 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::eth::erc20::{get_enabled_erc20_by_contract, get_token_decimals}; +use crate::eth::erc20::{get_enabled_erc20_by_platform_and_contract, get_token_decimals}; use crate::eth::web3_transport::http_transport::HttpTransport; use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage, HDWalletStorageError, DEFAULT_GAP_LIMIT}; @@ -7,10 +7,13 @@ use crate::nft::get_nfts_for_activation; use crate::nft::nft_errors::{GetNftInfoError, ParseChainTypeError}; use crate::nft::nft_structs::Chain; #[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; + use common::executor::AbortedError; use compatible_time::Instant; use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError}; use enum_derives::EnumFromTrait; +use ethereum_types::H264; +use kdf_walletconnect::error::WalletConnectError; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; @@ -21,7 +24,9 @@ use std::sync::atomic::Ordering; use url::Url; use web3_transport::websocket_transport::WebsocketTransport; -#[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] +#[derive( + Clone, Debug, Deserialize, Display, EnumFromTrait, EnumFromStringify, PartialEq, Serialize, SerializeErrorType, +)] #[serde(tag = "error_type", content = "error_data")] pub enum EthActivationV2Error { InvalidPayload(String), @@ -30,6 +35,11 @@ pub enum EthActivationV2Error { InvalidPathToAddress(String), #[display(fmt = "`chain_id` should be set for evm coins or tokens")] ChainIdNotSet, + #[display(fmt = "{} chains don't support {}", chain, feature)] + UnsupportedChain { + chain: String, + feature: String, + }, #[display(fmt = "Platform coin {} activation failed. {}", ticker, error)] ActivationFailed { ticker: String, @@ -65,6 +75,9 @@ pub enum EthActivationV2Error { InvalidHardwareWalletCall, #[display(fmt = "Custom token error: {}", _0)] CustomTokenError(CustomTokenError), + // TODO: Map WalletConnectError to distinct error categories (transport, invalid payload) after refactoring. + #[from_stringify("WalletConnectError")] + WalletConnectError(String), } impl From for EthActivationV2Error { @@ -155,12 +168,16 @@ impl From for EthActivationV2Error { /// An alternative to `crate::PrivKeyActivationPolicy`, typical only for ETH coin. #[derive(Clone, Deserialize, Default)] +#[serde(tag = "type", content = "params")] pub enum EthPrivKeyActivationPolicy { #[default] ContextPrivKey, Trezor, #[cfg(target_arch = "wasm32")] Metamask, + WalletConnect { + session_topic: String, + }, } impl EthPrivKeyActivationPolicy { @@ -397,7 +414,7 @@ impl EthCoin { // Todo: when custom token config storage is added, this might not be needed // `is_custom` was added to avoid this unnecessary check for non-custom tokens if is_custom { - match get_enabled_erc20_by_contract(&ctx, protocol.token_addr).await { + match get_enabled_erc20_by_platform_and_contract(&ctx, &protocol.platform, &protocol.token_addr).await { Ok(Some(token)) => { return MmError::err(EthTokenActivationError::CustomTokenError( CustomTokenError::TokenWithSameContractAlreadyActivated { @@ -454,6 +471,7 @@ impl EthCoin { // storage ticker will be the platform coin ticker derivation_method: self.derivation_method.clone(), coin_type, + chain_spec: self.chain_spec.clone(), sign_message_prefix: self.sign_message_prefix.clone(), swap_contract_address: self.swap_contract_address, swap_v2_contracts: self.swap_v2_contracts, @@ -467,7 +485,6 @@ impl EthCoin { max_eth_tx_type, ctx: self.ctx.clone(), required_confirmations, - chain_id: self.chain_id, trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, address_nonce_locks: self.address_nonce_locks.clone(), @@ -540,6 +557,7 @@ impl EthCoin { let global_nft = EthCoinImpl { ticker, coin_type, + chain_spec: self.chain_spec.clone(), priv_key_policy: self.priv_key_policy.clone(), derivation_method: self.derivation_method.clone(), sign_message_prefix: self.sign_message_prefix.clone(), @@ -554,7 +572,6 @@ impl EthCoin { max_eth_tx_type, required_confirmations, ctx: self.ctx.clone(), - chain_id: self.chain_id, trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, address_nonce_locks: self.address_nonce_locks.clone(), @@ -576,6 +593,7 @@ pub async fn eth_coin_from_conf_and_request_v2( conf: &Json, req: EthActivationV2Request, priv_key_build_policy: EthPrivKeyBuildPolicy, + chain_spec: ChainSpec, ) -> MmResult { if req.swap_contract_address == Address::default() { return Err(EthActivationV2Error::InvalidSwapContractAddr( @@ -620,14 +638,19 @@ pub async fn eth_coin_from_conf_and_request_v2( ) .await?; - let chain_id = conf["chain_id"].as_u64().ok_or(EthActivationV2Error::ChainIdNotSet)?; let web3_instances = match (req.rpc_mode, &priv_key_policy) { (EthRpcMode::Default, EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. }) - | (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) => { + | (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) + | (EthRpcMode::Default, EthPrivKeyPolicy::WalletConnect { .. }) => { build_web3_instances(ctx, ticker.to_string(), req.nodes.clone()).await? }, #[cfg(target_arch = "wasm32")] (EthRpcMode::Metamask, EthPrivKeyPolicy::Metamask(_)) => { + // Metamask doesn't support native Tron + let chain_id = chain_spec.chain_id().ok_or(EthActivationV2Error::UnsupportedChain { + chain: chain_spec.kind().to_string(), + feature: "Metamask".to_string(), + })?; build_metamask_transport(ctx, ticker.to_string(), chain_id).await? }, #[cfg(target_arch = "wasm32")] @@ -675,6 +698,7 @@ pub async fn eth_coin_from_conf_and_request_v2( priv_key_policy, derivation_method: Arc::new(derivation_method), coin_type, + chain_spec, sign_message_prefix, swap_contract_address: req.swap_contract_address, swap_v2_contracts: req.swap_v2_contracts, @@ -688,7 +712,6 @@ pub async fn eth_coin_from_conf_and_request_v2( max_eth_tx_type, ctx: ctx.weak(), required_confirmations, - chain_id, trezor_coin, logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), address_nonce_locks, @@ -802,6 +825,21 @@ pub(crate) async fn build_address_and_priv_key_policy( DerivationMethod::SingleAddress(address), )) }, + EthPrivKeyBuildPolicy::WalletConnect { + address, + public_key_uncompressed, + session_topic, + } => { + let public_key = compress_public_key(public_key_uncompressed)?; + Ok(( + EthPrivKeyPolicy::WalletConnect { + public_key, + public_key_uncompressed, + session_topic, + }, + DerivationMethod::SingleAddress(address), + )) + }, } } @@ -818,7 +856,7 @@ async fn build_web3_instances( eth_nodes.as_mut_slice().shuffle(&mut rng); drop_mutability!(eth_nodes); - let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); + let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker); let mut web3_instances = Vec::with_capacity(eth_nodes.len()); for eth_node in eth_nodes { @@ -973,10 +1011,10 @@ async fn check_metamask_supports_chain_id( } } -#[cfg(target_arch = "wasm32")] fn compress_public_key(uncompressed: H520) -> MmResult { let public_key = PublicKey::from_slice(uncompressed.as_bytes()) .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; let compressed = public_key.serialize(); + Ok(H264::from(compressed)) } diff --git a/mm2src/coins/eth/wallet_connect.rs b/mm2src/coins/eth/wallet_connect.rs new file mode 100644 index 0000000000..2bc241342e --- /dev/null +++ b/mm2src/coins/eth/wallet_connect.rs @@ -0,0 +1,298 @@ +/// https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc +use super::{ChainSpec, EthCoin, EthPrivKeyPolicy}; + +use crate::common::Future01CompatExt; +use crate::hd_wallet::AddrToString; +use crate::Eip1559Ops; +use crate::{BytesJson, MarketCoinOps, TransactionErr}; + +use common::log::info; +use common::u256_to_hex; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethcore_transaction::{Action, SignedTransaction}; +use ethereum_types::H256; +use ethereum_types::{Address, Public, H160, H520, U256}; +use ethkey::{public_to_address, Message, Signature}; +use kdf_walletconnect::chain::{WcChainId, WcRequestMethods}; +use kdf_walletconnect::error::WalletConnectError; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; +use mm2_err_handle::prelude::*; +use secp256k1::recovery::{RecoverableSignature, RecoveryId}; +use secp256k1::{PublicKey, Secp256k1}; +use std::iter::FromIterator; +use std::str::FromStr; +use web3::signing::hash_message; + +// Wait for 60 seconds for the transaction to appear on the RPC node. +const WAIT_RPC_TIMEOUT_SECS: u64 = 60; + +#[derive(Display, Debug, EnumFromStringify)] +pub enum EthWalletConnectError { + UnsupportedChainId(WcChainId), + InvalidSignature(String), + AccountMisMatch(String), + #[from_stringify("rlp::DecoderError", "hex::FromHexError")] + TxDecodingFailed(String), + #[from_stringify("ethkey::Error")] + InternalError(String), + InvalidTxData(String), + SessionError(String), + WalletConnectError(WalletConnectError), +} + +impl From for EthWalletConnectError { + fn from(value: WalletConnectError) -> Self { Self::WalletConnectError(value) } +} + +/// Eth Params required for constructing WalletConnect transaction. +pub struct WcEthTxParams<'a> { + pub(crate) gas: U256, + pub(crate) nonce: U256, + pub(crate) data: &'a [u8], + pub(crate) my_address: H160, + pub(crate) action: Action, + pub(crate) value: U256, + pub(crate) gas_price: Option, + pub(crate) chain_id: u64, + pub(crate) max_fee_per_gas: Option, + pub(crate) max_priority_fee_per_gas: Option, +} + +impl<'a> WcEthTxParams<'a> { + /// Construct WalletConnect transaction json from from `WcEthTxParams` + fn prepare_wc_tx_format(&self) -> MmResult { + let mut tx_object = serde_json::Map::from_iter([ + ("chainId".to_string(), json!(self.chain_id)), + ("nonce".to_string(), json!(u256_to_hex(self.nonce))), + ("from".to_string(), json!(self.my_address.addr_to_string())), + ("gasLimit".to_string(), json!(u256_to_hex(self.gas))), + ("value".to_string(), json!(u256_to_hex(self.value))), + ("data".to_string(), json!(format!("0x{}", hex::encode(self.data)))), + ]); + + if let Some(gas_price) = self.gas_price { + tx_object.insert("gasPrice".to_string(), json!(u256_to_hex(gas_price))); + } + + if let Some(max_fee_per_gas) = self.max_fee_per_gas { + tx_object.insert("maxFeePerGas".to_string(), json!(u256_to_hex(max_fee_per_gas))); + } + + if let Some(max_priority_fee_per_gas) = self.max_priority_fee_per_gas { + tx_object.insert( + "maxPriorityFeePerGas".to_string(), + json!(u256_to_hex(max_priority_fee_per_gas)), + ); + } + + if let Action::Call(addr) = self.action { + tx_object.insert("to".to_string(), json!(format!("0x{}", hex::encode(addr.as_bytes())))); + } + + Ok(json!(vec![serde_json::Value::Object(tx_object)])) + } +} + +#[async_trait::async_trait] +impl WalletConnectOps for EthCoin { + type Error = MmError; + type Params<'a> = WcEthTxParams<'a>; + type SignTxData = (SignedTransaction, BytesJson); + type SendTxData = (SignedTransaction, BytesJson); + + async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { + let session_topic = self.session_topic()?; + let chain_id = match self.chain_spec { + ChainSpec::Evm { chain_id } => chain_id, + // Todo: Add Tron signing logic + ChainSpec::Tron { .. } => { + return Err(MmError::new(EthWalletConnectError::InternalError( + "Tron is not supported for this action yet".into(), + ))) + }, + }; + let chain_id = WcChainId::new_eip155(chain_id.to_string()); + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + Ok(chain_id) + } + + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let bytes = { + let chain_id = self.wc_chain_id(wc).await?; + let tx_json = params.prepare_wc_tx_format()?; + let session_topic = self.session_topic()?; + let tx_hex: String = wc + .send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSignTransaction, tx_json) + .await?; + // if tx_hex.len() < 4 { + // return MmError::err(EthWalletConnectError::TxDecodingFailed( + // "invalid transaction hex returned from wallet".to_string(), + // )); + // } + // // First 4 bytes from WalletConnect represents Protoc info + let normalized_tx_hex = tx_hex.strip_prefix("0x").unwrap_or(&tx_hex); + hex::decode(normalized_tx_hex)? + }; + + let unverified = rlp::decode(&bytes)?; + let signed = SignedTransaction::new(unverified)?; + let bytes = rlp::encode(&signed); + + Ok((signed, BytesJson::from(bytes.to_vec()))) + } + + async fn wc_send_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let tx_hash: String = { + let chain_id = self.wc_chain_id(wc).await?; + let tx_json = params.prepare_wc_tx_format()?; + let session_topic = self.session_topic()?; + wc.send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::EthSendTransaction, tx_json) + .await? + }; + + let tx_hash = tx_hash.strip_prefix("0x").unwrap_or(&tx_hash); + let maybe_signed_tx = { + self.wait_for_tx_appears_on_rpc(H256::from_slice(&hex::decode(tx_hash)?), WAIT_RPC_TIMEOUT_SECS, 1.) + .await + .mm_err(|err| EthWalletConnectError::InternalError(err.to_string()))? + }; + let signed_tx = maybe_signed_tx.ok_or(MmError::new(EthWalletConnectError::InternalError(format!( + "Waited too long until the transaction {tx_hash:?} appear on the RPC node" + ))))?; + let tx_hex = BytesJson::from(rlp::encode(&signed_tx).to_vec()); + + Ok((signed_tx, tx_hex)) + } + + fn session_topic(&self) -> Result<&str, Self::Error> { + if let EthPrivKeyPolicy::WalletConnect { ref session_topic, .. } = &self.priv_key_policy { + return Ok(session_topic); + } + + MmError::err(EthWalletConnectError::SessionError(format!( + "{} is not activated via WalletConnect", + self.ticker() + ))) + } +} + +pub async fn eth_request_wc_personal_sign( + wc: &WalletConnectCtx, + session_topic: &str, + chain_id: u64, +) -> MmResult<(H520, Address), EthWalletConnectError> { + let chain_id = WcChainId::new_eip155(chain_id.to_string()); + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + let (account_str, _) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?; + let message = "Authenticate with KDF"; + let params = { + let message_hex = format!("0x{}", hex::encode(message)); + json!(&[&message_hex, &account_str]) + }; + let data = wc + .send_session_request_and_wait::(session_topic, &chain_id, WcRequestMethods::PersonalSign, params) + .await?; + + Ok(extract_pubkey_from_signature(&data, message, &account_str) + .mm_err(|err| WalletConnectError::SessionError(err.to_string()))?) +} + +fn extract_pubkey_from_signature( + signature_str: &str, + message: &str, + account: &str, +) -> MmResult<(H520, Address), EthWalletConnectError> { + let account = + H160::from_str(&account[2..]).map_to_mm(|err| EthWalletConnectError::InternalError(err.to_string()))?; + let uncompressed: H520 = { + let message_hash = hash_message(message); + let signature = Signature::from_str(&signature_str[2..]) + .map_to_mm(|err| EthWalletConnectError::InvalidSignature(err.to_string()))?; + let pubkey = recover(&signature, &message_hash).map_to_mm(|err| { + EthWalletConnectError::InvalidSignature(format!( + "Couldn't recover public key from the signature: '{signature:?}, error: {err:?}'" + )) + })?; + pubkey.serialize_uncompressed().into() + }; + + let mut public = Public::default(); + public.as_mut().copy_from_slice(&uncompressed[1..65]); + + let recovered_address = public_to_address(&public); + if account != recovered_address { + return MmError::err(EthWalletConnectError::AccountMisMatch(format!( + "Recovered address '{recovered_address:?}' should be the same as '{account:?}'" + ))); + } + + Ok((uncompressed, recovered_address)) +} + +pub(crate) fn recover(signature: &Signature, message: &Message) -> Result { + let recovery_id = { + let recovery_id = signature[64] + .checked_sub(27) + .ok_or_else(|| ethkey::Error::InvalidSignature)?; + RecoveryId::from_i32(recovery_id as i32)? + }; + let sig = RecoverableSignature::from_compact(&signature[0..64], recovery_id)?; + let pubkey = Secp256k1::new().recover(&secp256k1::Message::from_slice(&message[..])?, &sig)?; + + Ok(pubkey) +} + +/// Sign and send eth transaction with WalletConnect, +/// This fn is primarily for swap transactions so it uses swap tx fee policy +pub(crate) async fn send_transaction_with_walletconnect( + coin: EthCoin, + wc: &WalletConnectCtx, + my_address: Address, + value: U256, + action: Action, + data: &[u8], + gas: U256, +) -> Result { + info!("target: WalletConnect: sign-and-send, get_gas_price…"); + // Todo: Tron will have to use ETH protocol for walletconnect, it will be a different coin than the native one in coins config. + let chain_id = coin + .chain_spec + .chain_id() + .ok_or(TransactionErr::Plain("Tron is not supported for this action!".into()))?; + let pay_for_gas_option = try_tx_s!( + coin.get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await + ); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + let (nonce, _) = try_tx_s!(coin.clone().get_addr_nonce(my_address).compat().await); + + let params = WcEthTxParams { + gas, + nonce, + data, + my_address, + action, + value, + gas_price: pay_for_gas_option.get_gas_price(), + chain_id, + max_fee_per_gas, + max_priority_fee_per_gas, + }; + // Please note that this method may take a long time + // due to `eth_sendTransaction` requests. + info!("target: WalletConnect: sign-and-send, signing and sending tx"); + let (signed_tx, _) = try_tx_s!(coin.wc_send_tx(wc, params).await); + + Ok(signed_tx) +} diff --git a/mm2src/coins/hd_wallet/coin_ops.rs b/mm2src/coins/hd_wallet/coin_ops.rs index 27b92d0aa6..7bf6a7392b 100644 --- a/mm2src/coins/hd_wallet/coin_ops.rs +++ b/mm2src/coins/hd_wallet/coin_ops.rs @@ -1,7 +1,7 @@ use super::{inner_impl, AccountUpdatingError, AddressDerivingError, DisplayAddress, ExtendedPublicKeyOps, HDAccountOps, HDCoinExtendedPubkey, HDCoinHDAccount, HDCoinHDAddress, HDConfirmAddress, HDWalletOps, NewAddressDeriveConfirmError, NewAddressDerivingError}; -use crate::hd_wallet::{HDAddressOps, HDWalletStorageOps, TrezorCoinError}; +use crate::hd_wallet::{errors::SettingEnabledAddressError, HDAddressOps, HDWalletStorageOps, TrezorCoinError}; use async_trait::async_trait; use bip32::{ChildNumber, DerivationPath}; use crypto::Bip44Chain; @@ -231,4 +231,14 @@ pub trait HDWalletCoinOps { /// Returns the Trezor coin name for this coin. fn trezor_coin(&self) -> MmResult; + + /// Informs the coin of the enabled address provided/derived by the hardware wallet. + async fn received_enabled_address_from_hw_wallet( + &self, + _enabled_address: HDCoinHDAddress, + ) -> MmResult<(), SettingEnabledAddressError> { + // By default, the default implementation is doing nothing. + // Different coins can use this hook to perform additional actions if needed. + Ok(()) + } } diff --git a/mm2src/coins/hd_wallet/errors.rs b/mm2src/coins/hd_wallet/errors.rs index 8b517bc609..89ab2002de 100644 --- a/mm2src/coins/hd_wallet/errors.rs +++ b/mm2src/coins/hd_wallet/errors.rs @@ -242,3 +242,8 @@ impl From for NewAddressDeriveConfirmError { NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::Internal(e.to_string())) } } + +#[derive(Display)] +pub enum SettingEnabledAddressError { + Internal(String), +} diff --git a/mm2src/coins/hd_wallet/mod.rs b/mm2src/coins/hd_wallet/mod.rs index 666293cb60..5691f98c2c 100644 --- a/mm2src/coins/hd_wallet/mod.rs +++ b/mm2src/coins/hd_wallet/mod.rs @@ -30,7 +30,7 @@ pub use confirm_address::{HDConfirmAddress, HDConfirmAddressError}; mod errors; pub use errors::{AccountUpdatingError, AddressDerivingError, HDExtractPubkeyError, HDWithdrawError, InvalidBip44ChainError, NewAccountCreationError, NewAddressDeriveConfirmError, - NewAddressDerivingError, TrezorCoinError}; + NewAddressDerivingError, SettingEnabledAddressError, TrezorCoinError}; mod pubkey; pub use pubkey::{ExtendedPublicKeyOps, ExtractExtendedPubkey, HDXPubExtractor, RpcTaskXPubExtractor}; @@ -47,7 +47,7 @@ mod wallet_ops; pub use wallet_ops::HDWalletOps; mod withdraw_ops; -pub use withdraw_ops::{HDCoinWithdrawOps, WithdrawFrom, WithdrawSenderAddress}; +pub use withdraw_ops::{HDCoinWithdrawOps, WithdrawSenderAddress}; pub(crate) type HDAccountsMap = BTreeMap; pub(crate) type HDAccountsMutex = AsyncMutex>; @@ -485,6 +485,63 @@ impl HDPathAccountToAddressId { Ok(account_der_path) } } +/// Represents how a hierarchical deterministic (HD) address is selected. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum HDAddressSelector { + /// Specifies the HD address using its structured account, chain, and address ID. + AddressId(HDPathAccountToAddressId), + /// Specifies the HD address directly using a BIP-44,84 and other compliant derivation path. + /// + /// IMPORTANT: Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, + /// `serde::Deserialize` returns "data did not match any variant of untagged enum HDAddressSelector". + /// It's better to show the user an informative error. + DerivationPath { derivation_path: String }, +} + +impl HDAddressSelector { + pub fn to_address_path(&self, expected_coin_type: u32) -> MmResult { + match self { + HDAddressSelector::AddressId(address_id) => Ok(*address_id), + HDAddressSelector::DerivationPath { derivation_path } => { + let derivation_path = StandardHDPath::from_str(derivation_path).map_to_mm(StandardHDPathError::from)?; + let coin_type = derivation_path.coin_type(); + + if coin_type != expected_coin_type { + return MmError::err(StandardHDPathError::InvalidCoinType { + expected: expected_coin_type, + found: coin_type, + }); + } + + Ok(HDPathAccountToAddressId::from(derivation_path)) + }, + } + } + + pub fn valid_derivation_path(self, path_to_coin: &HDPathToCoin) -> MmResult { + match self { + HDAddressSelector::AddressId(id) => id + .to_derivation_path(path_to_coin) + .mm_err(StandardHDPathError::Bip32Error), + HDAddressSelector::DerivationPath { derivation_path } => { + let standard_hd_path = StandardHDPath::from_str(&derivation_path) + .map_to_mm(|_| StandardHDPathError::Bip32Error(Bip32Error::Decode))?; + let rpc_path_to_coin = standard_hd_path.path_to_coin(); + + // validate rpc path_to_coin against activated coin. + if &rpc_path_to_coin != path_to_coin { + return MmError::err(StandardHDPathError::InvalidPathToCoin { + expected: rpc_path_to_coin.to_string(), + found: path_to_coin.to_string(), + }); + }; + + Ok(standard_hd_path.to_derivation_path()) + }, + } + } +} pub(crate) mod inner_impl { use super::*; diff --git a/mm2src/coins/hd_wallet/pubkey.rs b/mm2src/coins/hd_wallet/pubkey.rs index 7babb12bd5..64b24e2abd 100644 --- a/mm2src/coins/hd_wallet/pubkey.rs +++ b/mm2src/coins/hd_wallet/pubkey.rs @@ -130,7 +130,7 @@ where let trezor_message_type = match coin_protocol { CoinProtocol::UTXO => TrezorMessageType::Bitcoin, CoinProtocol::QTUM => TrezorMessageType::Bitcoin, - CoinProtocol::ETH | CoinProtocol::ERC20 { .. } => TrezorMessageType::Ethereum, + CoinProtocol::ETH { .. } | CoinProtocol::ERC20 { .. } => TrezorMessageType::Ethereum, _ => return Err(MmError::new(HDExtractPubkeyError::CoinDoesntSupportTrezor)), }; Ok(RpcTaskXPubExtractor::Trezor { diff --git a/mm2src/coins/hd_wallet/withdraw_ops.rs b/mm2src/coins/hd_wallet/withdraw_ops.rs index 5b7ddf48cf..3b3d6ac7d9 100644 --- a/mm2src/coins/hd_wallet/withdraw_ops.rs +++ b/mm2src/coins/hd_wallet/withdraw_ops.rs @@ -1,51 +1,12 @@ use super::{DisplayAddress, HDPathAccountToAddressId, HDWalletOps, HDWithdrawError}; -use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps}; +use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDAddressSelector, HDCoinAddress, HDWalletCoinOps}; use async_trait::async_trait; use bip32::DerivationPath; -use crypto::{StandardHDPath, StandardHDPathError}; use mm2_err_handle::prelude::*; -use std::str::FromStr; type HDCoinPubKey = <<<::HDWallet as HDWalletOps>::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Pubkey; -/// Represents the source of the funds for a withdrawal operation. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum WithdrawFrom { - /// The address id of the sender address which is specified by the account id, chain, and address id. - AddressId(HDPathAccountToAddressId), - /// The derivation path of the sender address in the BIP-44 format. - /// - /// IMPORTANT: Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, - /// `serde::Deserialize` returns "data did not match any variant of untagged enum WithdrawFrom". - /// It's better to show the user an informative error. - DerivationPath { derivation_path: String }, -} - -impl WithdrawFrom { - #[allow(clippy::result_large_err)] - pub fn to_address_path(&self, expected_coin_type: u32) -> MmResult { - match self { - WithdrawFrom::AddressId(address_id) => Ok(*address_id), - WithdrawFrom::DerivationPath { derivation_path } => { - let derivation_path = StandardHDPath::from_str(derivation_path) - .map_to_mm(StandardHDPathError::from) - .mm_err(|e| HDWithdrawError::UnexpectedFromAddress(e.to_string()))?; - let coin_type = derivation_path.coin_type(); - if coin_type != expected_coin_type { - let error = format!( - "Derivation path '{}' must have '{}' coin type", - derivation_path, expected_coin_type - ); - return MmError::err(HDWithdrawError::UnexpectedFromAddress(error)); - } - Ok(HDPathAccountToAddressId::from(derivation_path)) - }, - } - } -} - /// Contains the details of the sender address for a withdraw operation. pub struct WithdrawSenderAddress { pub(crate) address: Address, @@ -61,13 +22,15 @@ pub trait HDCoinWithdrawOps: HDWalletCoinOps { async fn get_withdraw_hd_sender( &self, hd_wallet: &Self::HDWallet, - from: &WithdrawFrom, + from: &HDAddressSelector, ) -> MmResult, HDCoinPubKey>, HDWithdrawError> { let HDPathAccountToAddressId { account_id, chain, address_id, - } = from.to_address_path(hd_wallet.coin_type())?; + } = from + .to_address_path(hd_wallet.coin_type()) + .mm_err(|err| HDWithdrawError::UnexpectedFromAddress(err.to_string()))?; let hd_account = hd_wallet .get_account(account_id) diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 949c39a857..d9c48ecbf3 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -10,7 +10,8 @@ mod ln_sql; pub mod ln_storage; pub mod ln_utils; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::HDAddressSelector; use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_cltv_expiry_delta, PaymentError}; use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; @@ -65,7 +66,7 @@ use mm2_err_handle::prelude::*; use mm2_net::ip_addr::myipaddr; use mm2_number::{BigDecimal, MmNumber}; use parking_lot::Mutex as PaMutex; -use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; use script::TransactionInputSigner; use secp256k1v24::PublicKey; use serde::Deserialize; @@ -942,6 +943,12 @@ impl MarketCoinOps for LightningCoin { fn my_address(&self) -> MmResult { Ok(self.my_node_id()) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + PublicKey::from_slice(&pubkey.0) + .map(|pubkey| pubkey.to_string()) + .map_to_mm(|e| AddressFromPubkeyError::InternalError(format!("Couldn't parse bytes into secp pubkey: {e}"))) + } + async fn get_public_key(&self) -> Result> { Ok(self.my_node_id()) } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { @@ -950,7 +957,12 @@ impl MarketCoinOps for LightningCoin { Some(dhash256(prefixed_message.as_bytes()).take()) } - fn sign_message(&self, message: &str) -> SignatureResult { + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + if address.is_some() { + return MmError::err(SignatureError::InvalidRequest( + "functionality not supported for Lightning yet.".into(), + )); + } let message_hash = self.sign_message_hash(message).ok_or(SignatureError::PrefixNotFound)?; let secret_key = self .keys_manager diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index e7d57cb217..0d289aa8d0 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -568,7 +568,7 @@ impl FeeEstimator for Platform { ConfirmationTarget::Normal => self.confirmations_targets.normal, ConfirmationTarget::HighPriority => self.confirmations_targets.high_priority, }; - let fee_per_kb = tokio::task::block_in_place(move || { + let fee_rate = tokio::task::block_in_place(move || { block_on_f01(self.rpc_client().estimate_fee_sat( platform_coin.decimals(), // Todo: when implementing Native client detect_fee_method should be used for Native and @@ -582,16 +582,16 @@ impl FeeEstimator for Platform { // Set default fee to last known fee for the corresponding confirmation target match confirmation_target { - ConfirmationTarget::Background => self.latest_fees.set_background_fees(fee_per_kb), - ConfirmationTarget::Normal => self.latest_fees.set_normal_fees(fee_per_kb), - ConfirmationTarget::HighPriority => self.latest_fees.set_high_priority_fees(fee_per_kb), + ConfirmationTarget::Background => self.latest_fees.set_background_fees(fee_rate), + ConfirmationTarget::Normal => self.latest_fees.set_normal_fees(fee_rate), + ConfirmationTarget::HighPriority => self.latest_fees.set_high_priority_fees(fee_rate), }; // Must be no smaller than 253 (ie 1 satoshi-per-byte rounded up to ensure later round-downs don’t put us below 1 satoshi-per-byte). // https://docs.rs/lightning/0.0.101/lightning/chain/chaininterface/trait.FeeEstimator.html#tymethod.get_est_sat_per_1000_weight // This has changed in rust-lightning v0.0.110 as LDK currently wraps get_est_sat_per_1000_weight to ensure that the value returned is // no smaller than 253. https://github.com/lightningdevkit/rust-lightning/pull/1552 - (fee_per_kb as f64 / 4.0).ceil() as u32 + (fee_rate as f64 / 4.0).ceil() as u32 } } diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 79868908fa..68f3c7f7ab 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -22,7 +22,7 @@ use mm2_core::mm_ctx::MmArc; use std::collections::hash_map::Entry; use std::fs::File; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; pub const PAYMENT_RETRY_ATTEMPTS: usize = 5; @@ -54,13 +54,15 @@ impl From for RpcBestBlock { } #[inline] -fn ln_data_dir(ctx: &MmArc, ticker: &str) -> PathBuf { ctx.dbdir().join("LIGHTNING").join(ticker) } +fn ln_data_dir(ctx: &MmArc, platform_coin_address: &str, ticker: &str) -> PathBuf { + ctx.address_dir(platform_coin_address).join("LIGHTNING").join(ticker) +} #[inline] -fn ln_data_backup_dir(ctx: &MmArc, path: Option, ticker: &str) -> Option { +fn ln_data_backup_dir(path: Option, platform_coin_address: &str, ticker: &str) -> Option { path.map(|p| { PathBuf::from(&p) - .join(hex::encode(ctx.rmd160().as_slice())) + .join(platform_coin_address) .join("LIGHTNING") .join(ticker) }) @@ -68,11 +70,12 @@ fn ln_data_backup_dir(ctx: &MmArc, path: Option, ticker: &str) -> Option pub async fn init_persister( ctx: &MmArc, + platform_coin_address: &str, ticker: String, backup_path: Option, ) -> EnableLightningResult> { - let ln_data_dir = ln_data_dir(ctx, &ticker); - let ln_data_backup_dir = ln_data_backup_dir(ctx, backup_path, &ticker); + let ln_data_dir = ln_data_dir(ctx, platform_coin_address, &ticker); + let ln_data_backup_dir = ln_data_backup_dir(backup_path, platform_coin_address, &ticker); let persister = Arc::new(LightningFilesystemPersister::new(ln_data_dir, ln_data_backup_dir)); let is_initialized = persister.is_fs_initialized().await?; @@ -83,16 +86,15 @@ pub async fn init_persister( Ok(persister) } -pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult { - let db = SqliteLightningDB::new( - ticker, - ctx.sqlite_connection - .get() - .ok_or(MmError::new(EnableLightningError::DbError( - "sqlite_connection is not initialized".into(), - )))? - .clone(), - )?; +pub async fn init_db( + ctx: &MmArc, + platform_coin_address: &str, + ticker: String, +) -> EnableLightningResult { + let conn = ctx + .address_db(platform_coin_address) + .map_err(|e| EnableLightningError::IOError(e.to_string()))?; + let db = SqliteLightningDB::new(ticker, Arc::new(Mutex::new(conn)))?; if !db.is_db_initialized().await? { db.init_db().await?; diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 4de2ab31fc..c0414335f5 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -43,7 +43,6 @@ #[macro_use] extern crate ser_error_derive; use async_trait::async_trait; -use base58::FromBase58Error; use bip32::ExtendedPrivateKey; use common::custom_futures::timeout::TimeoutError; use common::executor::{abortable_queue::WeakSpawner, AbortedError, SpawnFuture}; @@ -54,7 +53,7 @@ use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoC Secp256k1ExtendedPublicKey, Secp256k1Secret, WithHwRpcError}; use derive_more::Display; use enum_derives::{EnumFromStringify, EnumFromTrait}; -use ethereum_types::{H256, U256}; +use ethereum_types::{H256, H264, H520, U256}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::{FutureExt, TryFutureExt}; @@ -72,7 +71,8 @@ use mm2_rpc::data::legacy::{EnabledCoin, GetEnabledResponse, Mm2RpcResult}; #[cfg(any(test, feature = "for-tests"))] use mocktopus::macros::*; use parking_lot::Mutex as PaMutex; -use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json, H264 as H264Json}; +use rpc_command::tendermint::ibc::ChannelId; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{self as json, Value as Json}; use std::array::TryFromSliceError; @@ -102,7 +102,7 @@ cfg_native! { } cfg_wasm32! { - use ethereum_types::{H264 as EthH264, H520 as EthH520}; + use ethereum_types::{H264 as EthH264}; use hd_wallet::HDWalletDb; use mm2_db::indexed_db::{ConstructibleDb, DbLocked, SharedDb}; use tx_history_storage::wasm::{clear_tx_history, load_tx_history, save_tx_history, TxHistoryDb}; @@ -214,13 +214,10 @@ pub mod lp_price; pub mod watcher_common; pub mod coin_errors; -use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentResult}; +use coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentResult}; use crypto::secret_hash_algo::SecretHashAlgo; -#[doc(hidden)] -#[cfg(test)] -pub mod coins_tests; - pub mod eth; use eth::erc20::get_erc20_ticker_by_contract_address; use eth::eth_swap_v2::{PrepareTxDataError, ValidatePaymentV2Err}; @@ -228,9 +225,9 @@ use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetail GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx}; pub mod hd_wallet; -use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, - HDCoinHDAccount, HDExtractPubkeyError, HDPathAccountToAddressId, HDWalletAddress, HDWalletCoinOps, - HDWalletOps, HDWithdrawError, HDXPubExtractor, WithdrawFrom, WithdrawSenderAddress}; +use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, + HDAddressSelector, HDCoinAddress, HDCoinHDAccount, HDExtractPubkeyError, HDPathAccountToAddressId, + HDWalletAddress, HDWalletCoinOps, HDWalletOps, HDWithdrawError, HDXPubExtractor, WithdrawSenderAddress}; #[cfg(not(target_arch = "wasm32"))] pub mod lightning; #[cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))] @@ -2080,11 +2077,13 @@ pub trait MarketCoinOps { fn my_address(&self) -> MmResult; + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult; + async fn get_public_key(&self) -> Result>; fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]>; - fn sign_message(&self, _message: &str) -> SignatureResult; + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult; fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult; @@ -2143,8 +2142,8 @@ pub trait MarketCoinOps { /// Is privacy coin like zcash or pirate fn is_privacy(&self) -> bool { false } - /// Is KMD coin - fn is_kmd(&self) -> bool { false } + /// Returns `true` for coins (like KMD) that should use direct DEX fee burning via OP_RETURN. + fn should_burn_directly(&self) -> bool { false } /// Should burn part of dex fee coin fn should_burn_dex_fee(&self) -> bool; @@ -2211,7 +2210,7 @@ pub trait GetWithdrawSenderAddress { #[derive(Clone, Default, Deserialize)] pub struct WithdrawRequest { coin: String, - from: Option, + from: Option, to: String, #[serde(default)] amount: BigDecimal, @@ -2220,9 +2219,8 @@ pub struct WithdrawRequest { fee: Option, memo: Option, /// Tendermint specific field used for manually providing the IBC channel IDs. - ibc_source_channel: Option, - /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask **only**. - #[cfg(target_arch = "wasm32")] + ibc_source_channel: Option, + /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask/WalletConnect(Some wallets e.g Metamask) **only**. #[serde(default)] broadcast: bool, } @@ -2295,10 +2293,11 @@ pub enum ValidatorsInfoDetails { Cosmos(rpc_command::tendermint::staking::ValidatorsQuery), } -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] pub struct SignatureRequest { coin: String, message: String, + address: Option, } #[derive(Serialize, Deserialize)] @@ -3179,15 +3178,10 @@ pub enum WithdrawError { }, #[display(fmt = "Signing error {}", _0)] SigningError(String), - #[display(fmt = "Eth transaction type not supported")] + #[display(fmt = "Transaction type not supported")] TxTypeNotSupported, - #[display(fmt = "'chain_registry_name' was not found in coins configuration for '{}'", _0)] - RegistryNameIsMissing(String), - #[display( - fmt = "IBC channel could not found for '{}' address. Consider providing it manually with 'ibc_source_channel' in the request.", - _0 - )] - IBCChannelCouldNotFound(String), + #[display(fmt = "Tendermint IBC error: {}", _0)] + IBCError(tendermint::IBCError), } impl HttpStatusCode for WithdrawError { @@ -3216,8 +3210,7 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::NoChainIdSet { .. } | WithdrawError::TxTypeNotSupported | WithdrawError::SigningError(_) - | WithdrawError::RegistryNameIsMissing(_) - | WithdrawError::IBCChannelCouldNotFound(_) + | WithdrawError::IBCError(_) | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] @@ -3414,17 +3407,14 @@ impl HttpStatusCode for VerificationError { } } -impl From for VerificationError { - fn from(e: FromBase58Error) -> Self { - match e { - FromBase58Error::InvalidBase58Character(c, _) => { - VerificationError::AddressDecodingError(format!("Invalid Base58 Character: {}", c)) - }, - FromBase58Error::InvalidBase58Length => { - VerificationError::AddressDecodingError(String::from("Invalid Base58 Length")) - }, - } - } +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] +pub enum OrderCreationPreCheckError { + #[display(fmt = "'{ticker}' is a wallet only asset and can't be used in orders.")] + IsWalletOnly { ticker: String }, + #[display(fmt = "Pre-Check failed due to this reason: {reason}")] + PreCheckFailed { reason: String }, + #[display(fmt = "Internal error: {reason}")] + InternalError { reason: String }, } /// NB: Implementations are expected to follow the pImpl idiom, providing cheap reference-counted cloning and garbage collection. @@ -3538,6 +3528,31 @@ pub trait MmCoin: SwapOps + WatcherOps + MarketCoinOps + Send + Sync + 'static { stage: FeeApproxStage, ) -> TradePreimageResult; + /// TODO: It's weird that we implement this function on this trait. + /// + /// Move this into the `SwapOps` trait when possible (this function requires `MmCoins` + /// trait to be implemented, but it's currently not possible to do `SwapOps: MmCoins` + /// as `MmCoins` is already `MmCoins: SwapOps`. + async fn pre_check_for_order_creation( + &self, + ctx: &MmArc, + rel_coin: &MmCoinEnum, + ) -> MmResult<(), OrderCreationPreCheckError> { + if self.wallet_only(ctx) { + return MmError::err(OrderCreationPreCheckError::IsWalletOnly { + ticker: self.ticker().to_owned(), + }); + } + + if rel_coin.wallet_only(ctx) { + return MmError::err(OrderCreationPreCheckError::IsWalletOnly { + ticker: rel_coin.ticker().to_owned(), + }); + } + + Ok(()) + } + /// required transaction confirmations number to ensure double-spend safety fn required_confirmations(&self) -> u64; @@ -3826,7 +3841,7 @@ impl DexFee { let dex_fee = trade_amount * &rate; let min_tx_amount = MmNumber::from(taker_coin.min_tx_amount()); - if taker_coin.is_kmd() { + if taker_coin.should_burn_directly() { // use a special dex fee option for kmd return Self::calc_dex_fee_for_op_return(dex_fee, min_tx_amount); } @@ -4194,13 +4209,26 @@ pub enum PrivKeyPolicy { /// with the Metamask extension, especially within web-based contexts. #[cfg(target_arch = "wasm32")] Metamask(EthMetamaskPolicy), + /// WalletConnect private key policy. + /// + /// This variant represents the key management details for connections + /// established via WalletConnect. It includes both compressed and uncompressed + /// public keys. + /// - `public_key`: Compressed public key, represented as [H264]. + /// - `public_key_uncompressed`: Uncompressed public key, represented as [H520]. + /// - `session_topic`: WalletConnect session that was used to activate this coin. + WalletConnect { + public_key: H264, + public_key_uncompressed: H520, + session_topic: String, + }, } #[cfg(target_arch = "wasm32")] #[derive(Clone, Debug)] pub struct EthMetamaskPolicy { pub(crate) public_key: EthH264, - pub(crate) public_key_uncompressed: EthH520, + pub(crate) public_key_uncompressed: H520, } impl From for PrivKeyPolicy { @@ -4215,7 +4243,7 @@ impl PrivKeyPolicy { activated_key: activated_key_pair, .. } => Some(activated_key_pair), - PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::WalletConnect { .. } | PrivKeyPolicy::Trezor => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -4235,7 +4263,7 @@ impl PrivKeyPolicy { PrivKeyPolicy::HDWallet { bip39_secp_priv_key, .. } => Some(bip39_secp_priv_key), - PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -4257,8 +4285,7 @@ impl PrivKeyPolicy { path_to_coin: derivation_path, .. } => Some(derivation_path), - PrivKeyPolicy::Trezor => None, - PrivKeyPolicy::Iguana(_) => None, + PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor | PrivKeyPolicy::WalletConnect { .. } => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } @@ -4515,11 +4542,19 @@ pub enum CoinProtocol { platform: String, contract_address: String, }, - ETH, + // Todo: Document this + /// # Breaking Changes + ETH { + chain_id: u64, + }, ERC20 { platform: String, contract_address: String, }, + TRX { + network: eth::tron::Network, + }, + // Todo: Add TRC20, Do we need to support TRC10? SLPTOKEN { platform: String, token_id: H256Json, @@ -4578,7 +4613,8 @@ impl CoinProtocol { CoinProtocol::LIGHTNING { platform, .. } => Some(platform), CoinProtocol::UTXO | CoinProtocol::QTUM - | CoinProtocol::ETH + | CoinProtocol::ETH { .. } + | CoinProtocol::TRX { .. } | CoinProtocol::BCH { .. } | CoinProtocol::TENDERMINT(_) | CoinProtocol::ZHTLC(_) => None, @@ -4596,7 +4632,8 @@ impl CoinProtocol { CoinProtocol::SLPTOKEN { .. } | CoinProtocol::UTXO | CoinProtocol::QTUM - | CoinProtocol::ETH + | CoinProtocol::ETH { .. } + | CoinProtocol::TRX { .. } | CoinProtocol::BCH { .. } | CoinProtocol::TENDERMINT(_) | CoinProtocol::TENDERMINTTOKEN(_) @@ -4859,7 +4896,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result { + CoinProtocol::ETH { .. } | CoinProtocol::ERC20 { .. } => { try_s!(eth_coin_from_conf_and_request(ctx, ticker, &coins_en, req, protocol, priv_key_policy).await).into() }, CoinProtocol::QRC20 { @@ -4917,6 +4954,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result return ERR!("TENDERMINTTOKEN protocol is not supported by lp_coininit"), CoinProtocol::ZHTLC { .. } => return ERR!("ZHTLC protocol is not supported by lp_coininit"), CoinProtocol::NFT { .. } => return ERR!("NFT protocol is not supported by lp_coininit"), + CoinProtocol::TRX { .. } => return ERR!("TRX 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(feature = "enable-sia")] @@ -5112,8 +5150,14 @@ pub async fn get_raw_transaction(ctx: MmArc, req: RawTransactionRequest) -> RawT } pub async fn sign_message(ctx: MmArc, req: SignatureRequest) -> SignatureResult { + if req.address.is_some() && !ctx.enable_hd() { + return MmError::err(SignatureError::InvalidRequest( + "You need to enable kdf with enable_hd to sign messages with a specific account/address".to_string(), + )); + }; let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - let signature = coin.sign_message(&req.message)?; + let signature = coin.sign_message(&req.message, req.address)?; + Ok(SignatureResponse { signature }) } @@ -5474,47 +5518,6 @@ pub async fn register_balance_update_handler( coins_ctx.balance_update_handlers.lock().await.push(handler); } -pub fn update_coins_config(mut config: Json) -> Result { - let coins = match config.as_array_mut() { - Some(c) => c, - _ => return ERR!("Coins config must be an array"), - }; - - for coin in coins { - // the coin_as_str is used only to be formatted - let coin_as_str = format!("{}", coin); - let coin = try_s!(coin - .as_object_mut() - .ok_or(ERRL!("Expected object, found {:?}", coin_as_str))); - if coin.contains_key("protocol") { - // the coin is up-to-date - continue; - } - let protocol = match coin.remove("etomic") { - Some(etomic) => { - let etomic = etomic - .as_str() - .ok_or(ERRL!("Expected etomic as string, found {:?}", etomic))?; - if etomic == "0x0000000000000000000000000000000000000000" { - CoinProtocol::ETH - } else { - let contract_address = etomic.to_owned(); - CoinProtocol::ERC20 { - platform: "ETH".into(), - contract_address, - } - } - }, - _ => CoinProtocol::UTXO, - }; - - let protocol = json::to_value(protocol).map_err(|e| ERRL!("Error {:?} on process {:?}", e, coin_as_str))?; - coin.insert("protocol".into(), protocol); - } - - Ok(config) -} - #[derive(Deserialize)] struct ConvertUtxoAddressReq { address: String, @@ -5550,7 +5553,11 @@ pub fn address_by_coin_conf_and_pubkey_str( ) -> Result { let protocol: CoinProtocol = try_s!(json::from_value(conf["protocol"].clone())); match protocol { - CoinProtocol::ERC20 { .. } | CoinProtocol::ETH | CoinProtocol::NFT { .. } => eth::addr_from_pubkey_str(pubkey), + CoinProtocol::ERC20 { .. } | CoinProtocol::ETH { .. } | CoinProtocol::NFT { .. } => { + eth::addr_from_pubkey_str(pubkey) + }, + // Todo: implement TRX address generation + CoinProtocol::TRX { .. } => ERR!("TRX address generation is not implemented yet"), CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) }, @@ -5828,7 +5835,7 @@ pub async fn get_my_address(ctx: MmArc, req: MyAddressReq) -> MmResult get_eth_address(&ctx, &conf, ticker, &req.path_to_address).await?, + CoinProtocol::ETH { .. } => get_eth_address(&ctx, &conf, ticker, &req.path_to_address).await?, _ => { return MmError::err(GetMyAddressError::CoinIsNotSupported(format!( "{} doesn't support get_my_address", @@ -6184,7 +6191,7 @@ mod tests { pub mod for_tests { use crate::rpc_command::init_withdraw::WithdrawStatusRequest; use crate::rpc_command::init_withdraw::{init_withdraw, withdraw_status}; - use crate::{TransactionDetails, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawRequest}; + use crate::{HDAddressSelector, TransactionDetails, WithdrawError, WithdrawFee, WithdrawRequest}; use common::executor::Timer; use common::{now_ms, wait_until_ms}; use mm2_core::mm_ctx::MmArc; @@ -6206,7 +6213,7 @@ pub mod for_tests { client_id: 0, inner: WithdrawRequest { amount: BigDecimal::from_str(amount).unwrap(), - from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath { + from: from_derivation_path.map(|from_derivation_path| HDAddressSelector::DerivationPath { derivation_path: from_derivation_path.to_owned(), }), to: to.to_owned(), diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index d35de25ddc..9bd5877ff5 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -187,6 +187,9 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T self.from_addresses.insert(address); } + /// TODO: This implementation is messy. We should do all the calculations before storing them + /// to the database. We shouldn’t need these on-demand calculations in this module; it's better + /// to remove this function entirely but some coins like UTXOs still depend on it. pub fn build(self) -> TransactionDetails { let (block_height, timestamp) = match self.block_height_and_time { Some(height_with_time) => (height_with_time.height, height_with_time.timestamp), @@ -210,15 +213,8 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T bytes_for_hash.extend_from_slice(&token_id.0); sha256(&bytes_for_hash).to_vec().into() }, - TransactionType::TendermintIBCTransfer { token_id } - | TransactionType::CustomTendermintMsg { token_id, .. } => { - if let Some(token_id) = token_id { - let mut bytes_for_hash = tx_hash.0.clone(); - bytes_for_hash.extend_from_slice(&token_id.0); - sha256(&bytes_for_hash).to_vec().into() - } else { - tx_hash.clone() - } + TransactionType::TendermintIBCTransfer { .. } | TransactionType::CustomTendermintMsg { .. } => { + unreachable!("Tendermint never invokes this function.") }, TransactionType::StakingDelegation | TransactionType::RemoveDelegation diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 3049a56ec2..3d6f8660b3 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1,5 +1,6 @@ -use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal, TryToAddress}; +use crate::hd_wallet::HDAddressSelector; use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc20RpcOps, TopicFilter, TxReceipt, ViewContractCallType}; use crate::utxo::qtum::QtumBasedCoin; @@ -11,11 +12,10 @@ use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoi UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_utxo_inputs_signed_by_pub, UtxoTxBuilder}; -use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, - GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, - UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, - UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, - UTXO_LOCK}; +use crate::utxo::{qtum, ActualFeeRate, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, GetUtxoListOps, + HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, UnsupportedAddr, + UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, + UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, Eip1559Ops, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, @@ -45,7 +45,8 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; -use rpc::v1::types::{Bytes as BytesJson, ToTxHash, Transaction as RpcTransaction, H160 as H160Json, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, ToTxHash, Transaction as RpcTransaction, H160 as H160Json, H256 as H256Json, + H264 as H264Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use script_pubkey::generate_contract_call_script_pubkey; use serde_json::{self as json, Value as Json}; @@ -489,8 +490,8 @@ impl Qrc20Coin { /// `gas_fee` should be calculated by: gas_limit * gas_price * (count of contract calls), /// or should be sum of gas fee of all contract calls. pub async fn get_qrc20_tx_fee(&self, gas_fee: u64) -> Result { - match try_s!(self.get_tx_fee().await) { - ActualTxFee::Dynamic(amount) | ActualTxFee::FixedPerKb(amount) => Ok(amount + gas_fee), + match try_s!(self.get_fee_rate().await) { + ActualFeeRate::Dynamic(amount) | ActualFeeRate::FixedPerKb(amount) => Ok(amount + gas_fee), } } @@ -545,10 +546,9 @@ impl Qrc20Coin { self.utxo.conf.fork_id, )?; - let miner_fee = data.fee_amount + data.unused_change; Ok(GenerateQrc20TxResult { signed, - miner_fee, + miner_fee: data.fee_amount, gas_fee, }) } @@ -609,17 +609,13 @@ impl UtxoTxBroadcastOps for Qrc20Coin { #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for Qrc20Coin { /// Get only QTUM transaction fee. - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: ScriptBytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] @@ -847,7 +843,8 @@ impl SwapOps for Qrc20Coin { }, }; let fee_tx_hash = fee_tx.hash().reversed().into(); - let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(fee_tx, validate_fee_args.expected_sender)?; + let inputs_signed_by_pub = + check_all_utxo_inputs_signed_by_pub(self, fee_tx, validate_fee_args.expected_sender).await?; if !inputs_signed_by_pub { return MmError::err(ValidatePaymentError::WrongPaymentTx( "The dex fee was sent from wrong address".to_string(), @@ -1030,6 +1027,11 @@ impl MarketCoinOps for Qrc20Coin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = Public::Compressed((*pubkey).into()); + Ok(UtxoCommonOps::address_from_pubkey(self, &pubkey).to_string()) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; Ok(pubkey.to_string()) @@ -1039,8 +1041,8 @@ impl MarketCoinOps for Qrc20Coin { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index caf5546de2..f4dd5a7121 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -59,8 +59,8 @@ pub fn qrc20_coin_for_test(priv_key: [u8; 32], fallback_swap: Option<&str>) -> ( (ctx, coin) } -fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualTxFee) { - let actual_tx_fee = block_on(coin.get_tx_fee()).unwrap(); +fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualFeeRate) { + let actual_tx_fee = block_on(coin.get_fee_rate()).unwrap(); assert_eq!(actual_tx_fee, expected_tx_fee); } @@ -429,6 +429,7 @@ fn test_validate_fee() { } #[test] +#[ignore] fn test_wait_for_tx_spend_malicious() { // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG let priv_key = [ @@ -712,7 +713,7 @@ fn test_get_trade_fee() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let actual_trade_fee = block_on_f01(coin.get_trade_fee()).unwrap(); let expected_trade_fee_amount = big_decimal_from_sat( @@ -739,7 +740,7 @@ fn test_sender_trade_preimage_zero_allowance() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let allowance = block_on(coin.allowance(coin.swap_contract_address)).expect("!allowance"); assert_eq!(allowance, 0.into()); @@ -775,7 +776,7 @@ fn test_sender_trade_preimage_with_allowance() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let allowance = block_on(coin.allowance(coin.swap_contract_address)).expect("!allowance"); assert_eq!(allowance, 300_000_000.into()); @@ -886,7 +887,7 @@ fn test_receiver_trade_preimage() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let actual = block_on_f01(coin.get_receiver_trade_fee(FeeApproxStage::WithoutApprox)).expect("!get_receiver_trade_fee"); @@ -911,7 +912,7 @@ fn test_taker_fee_tx_fee() { ]; let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); // check if the coin's tx fee is expected - check_tx_fee(&coin, ActualTxFee::FixedPerKb(EXPECTED_TX_FEE as u64)); + check_tx_fee(&coin, ActualFeeRate::FixedPerKb(EXPECTED_TX_FEE as u64)); let expected_balance = CoinBalance { spendable: BigDecimal::from(5u32), unspendable: BigDecimal::from(0u32), diff --git a/mm2src/coins/rpc_command/get_enabled_coins.rs b/mm2src/coins/rpc_command/get_enabled_coins.rs index 543f6160eb..d746d298df 100644 --- a/mm2src/coins/rpc_command/get_enabled_coins.rs +++ b/mm2src/coins/rpc_command/get_enabled_coins.rs @@ -5,7 +5,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; use mm2_err_handle::prelude::*; -#[derive(Serialize, Display, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum GetEnabledCoinsError { #[display(fmt = "Internal error: {}", _0)] @@ -23,17 +23,17 @@ impl HttpStatusCode for GetEnabledCoinsError { #[derive(Deserialize)] pub struct GetEnabledCoinsRequest; -#[derive(Serialize)] +#[derive(Debug, Serialize)] pub struct GetEnabledCoinsResponse { - coins: Vec, + pub coins: Vec, } -#[derive(Serialize)] +#[derive(Debug, Serialize)] pub struct EnabledCoinV2 { ticker: String, } -pub async fn get_enabled_coins( +pub async fn get_enabled_coins_rpc( ctx: MmArc, _req: GetEnabledCoinsRequest, ) -> MmResult { diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index c323ffe78a..774ab3bc77 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -318,7 +318,12 @@ impl RpcTask for InitCreateAccountTask { self.task_state.clone(), task_handle, eth.is_trezor(), - CoinProtocol::ETH, + // Todo: add support for Tron by checking eth.chain_spec + CoinProtocol::ETH { + chain_id: eth.chain_id().ok_or_else(|| { + CreateAccountRpcError::Internal("chain_id should be available for an EVM coin".to_string()) + })?, + }, ) .await?, )), diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index fdb5b9caa9..095ee1c4ee 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -174,7 +174,7 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes .with_fee_policy(fee_policy); let fee = platform_coin - .get_tx_fee() + .get_fee_rate() .await .map_err(|e| OpenChannelError::RpcError(e.to_string()))?; tx_builder = tx_builder.with_fee(fee); diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index c401853b2d..a47a9bf25a 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -7,5 +7,6 @@ pub mod init_account_balance; 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; + +#[cfg(not(target_arch = "wasm32"))] pub mod lightning; diff --git a/mm2src/coins/rpc_command/tendermint/ibc.rs b/mm2src/coins/rpc_command/tendermint/ibc.rs new file mode 100644 index 0000000000..48df82c44a --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/ibc.rs @@ -0,0 +1,12 @@ +use std::fmt; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, Hash)] +pub struct ChannelId(u16); + +impl fmt::Display for ChannelId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "channel-{}", self.0) } +} + +impl ChannelId { + pub fn new(id: u16) -> Self { Self(id) } +} diff --git a/mm2src/coins/rpc_command/tendermint/ibc_chains.rs b/mm2src/coins/rpc_command/tendermint/ibc_chains.rs deleted file mode 100644 index 67ed93e9fa..0000000000 --- a/mm2src/coins/rpc_command/tendermint/ibc_chains.rs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 4edcd0cd55..0000000000 --- a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs +++ /dev/null @@ -1,105 +0,0 @@ -use common::HttpStatusCode; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::MmError; - -use crate::{coin_conf, tendermint::get_ibc_transfer_channels}; - -pub type IBCTransferChannelsResult = Result>; - -#[derive(Clone, Deserialize)] -pub struct IBCTransferChannelsRequest { - pub(crate) source_coin: String, - pub(crate) destination_coin: 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 = "'chain_registry_name' was not found in coins configuration for '{}' prefix. Either update the coins configuration or use 'ibc_source_channel' in the request.", - _0 - )] - RegistryNameIsMissing(String), - #[display(fmt = "Could not find '{}' registry source.", _0)] - RegistrySourceCouldNotFound(String), - #[display(fmt = "Transport error: {}", _0)] - Transport(String), - #[display(fmt = "Could not found channel for '{}'.", _0)] - CouldNotFindChannel(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::CouldNotFindChannel(_) - | IBCTransferChannelsRequestError::RegistryNameIsMissing(_) - | 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 source_coin_conf = coin_conf(&ctx, &req.source_coin); - let source_registry_name = source_coin_conf - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("protocol_data") - .unwrap_or(&serde_json::Value::Null) - .get("chain_registry_name") - .map(|t| t.as_str().unwrap_or_default().to_owned()); - - let Some(source_registry_name) = source_registry_name else { - return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing(req.source_coin)); - }; - - let destination_coin_conf = coin_conf(&ctx, &req.destination_coin); - let destination_registry_name = destination_coin_conf - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("protocol_data") - .unwrap_or(&serde_json::Value::Null) - .get("chain_registry_name") - .map(|t| t.as_str().unwrap_or_default().to_owned()); - - let Some(destination_registry_name) = destination_registry_name else { - return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing( - req.destination_coin, - )); - }; - - get_ibc_transfer_channels(source_registry_name, destination_registry_name).await -} diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs index 9a3d714bd3..d0f2c82685 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1,13 +1,2 @@ -mod ibc_chains; -mod ibc_transfer_channels; +pub mod ibc; pub mod staking; - -pub use ibc_chains::*; -pub use ibc_transfer_channels::*; - -// 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 = "nucl"; -pub(crate) const CHAIN_REGISTRY_IBC_DIR_NAME: &str = "_IBC"; diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs index 190477b7bd..519c18cc38 100644 --- a/mm2src/coins/rpc_command/tendermint/staking.rs +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -3,7 +3,8 @@ use cosmrs::staking::{Commission, Description, Validator}; use mm2_err_handle::prelude::MmError; use mm2_number::BigDecimal; -use crate::{hd_wallet::WithdrawFrom, tendermint::TendermintCoinRpcError, MmCoinEnum, StakingInfoError, WithdrawFee}; +use crate::{hd_wallet::HDAddressSelector, tendermint::TendermintCoinRpcError, MmCoinEnum, StakingInfoError, + WithdrawFee}; /// Represents current status of the validator. #[derive(Debug, Default, Deserialize)] @@ -47,7 +48,8 @@ impl From for StakingInfoError { match e { TendermintCoinRpcError::InvalidResponse(e) | TendermintCoinRpcError::PerformError(e) - | TendermintCoinRpcError::RpcClientError(e) => StakingInfoError::Transport(e), + | TendermintCoinRpcError::RpcClientError(e) + | TendermintCoinRpcError::NotFound(e) => StakingInfoError::Transport(e), TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => StakingInfoError::Internal(e), TendermintCoinRpcError::UnexpectedAccountType { .. } => StakingInfoError::Internal( "RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal." @@ -130,7 +132,7 @@ pub async fn validators_rpc( pub struct DelegationPayload { pub validator_address: String, pub fee: Option, - pub withdraw_from: Option, + pub withdraw_from: Option, #[serde(default)] pub memo: String, #[serde(default)] diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index bb5ec12353..c0b82f313b 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -1,12 +1,15 @@ use super::{BalanceError, CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TransactionEnum}; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, - DexFee, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, PrivKeyPolicy, - RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, - ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidatePaymentResult, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawFut, WithdrawRequest}; +use crate::hd_wallet::HDAddressSelector; +use crate::{coin_errors::MyAddressError, AddressFromPubkeyError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, + ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, + PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, + SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, TradePreimageFut, + TradePreimageResult, TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, + ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, + ValidatePaymentResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawFut, + WithdrawRequest}; +use crate::{SignatureError, VerificationError}; use async_trait::async_trait; use common::executor::AbortedError; pub use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signature}; @@ -16,7 +19,7 @@ use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, BigInt, MmNumber}; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; use serde_json::Value as Json; use std::ops::Deref; use std::sync::Arc; @@ -307,19 +310,37 @@ impl MarketCoinOps for SiaCoin { ) .into()); }, + PrivKeyPolicy::WalletConnect { .. } => { + return Err(MyAddressError::UnexpectedDerivationMethod( + "WalletConnect not yet supported. Must use iguana seed.".to_string(), + ) + .into()) + }, }; let address = SpendPolicy::PublicKey(key_pair.public).address(); Ok(address.to_string()) } - async fn get_public_key(&self) -> Result> { unimplemented!() } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = PublicKey::from_bytes(&pubkey.0[..32]).map_err(|e| { + AddressFromPubkeyError::InternalError(format!("Couldn't parse bytes into ed25519 pubkey: {e:?}")) + })?; + let address = SpendPolicy::PublicKey(pubkey).address(); + Ok(address.to_string()) + } - fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } + async fn get_public_key(&self) -> Result> { + MmError::err(UnexpectedDerivationMethod::InternalError("Not implemented".into())) + } + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { None } - fn sign_message(&self, _message: &str) -> SignatureResult { unimplemented!() } + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { + MmError::err(SignatureError::InternalError("Not implemented".into())) + } fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult { - unimplemented!() + MmError::err(VerificationError::InternalError("Not implemented".into())) } fn my_balance(&self) -> BalanceFut { diff --git a/mm2src/coins/tendermint/htlc/mod.rs b/mm2src/coins/tendermint/htlc/mod.rs index 5675c915f2..77b38b8208 100644 --- a/mm2src/coins/tendermint/htlc/mod.rs +++ b/mm2src/coins/tendermint/htlc/mod.rs @@ -33,8 +33,8 @@ impl FromStr for HtlcType { fn from_str(s: &str) -> Result { match s { - "iaa" => Ok(HtlcType::Iris), - "nuc" => Ok(HtlcType::Nucleus), + super::IRIS_PREFIX => Ok(HtlcType::Iris), + super::NUCLEUS_PREFIX => Ok(HtlcType::Nucleus), unsupported => Err(io::Error::new( io::ErrorKind::Unsupported, format!("Account type '{unsupported}' is not supported for HTLCs"), diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index ac88f1bac9..a1b2cc1edd 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -10,11 +10,13 @@ pub mod tendermint_balance_events; mod tendermint_coin; mod tendermint_token; pub mod tendermint_tx_history_v2; +pub mod wallet_connect; pub use cosmrs::tendermint::PublicKey as TendermintPublicKey; pub use cosmrs::AccountId; pub use tendermint_coin::*; pub use tendermint_token::*; +pub use wallet_connect::*; pub(crate) const BCH_COIN_PROTOCOL_TYPE: &str = "BCH"; pub(crate) const BCH_TOKEN_PROTOCOL_TYPE: &str = "SLPTOKEN"; diff --git a/mm2src/coins/tendermint/tendermint_balance_events.rs b/mm2src/coins/tendermint/tendermint_balance_events.rs index eed451b5dd..3304e35e97 100644 --- a/mm2src/coins/tendermint/tendermint_balance_events.rs +++ b/mm2src/coins/tendermint/tendermint_balance_events.rs @@ -3,7 +3,7 @@ use common::{http_uri_to_ws_address, log, PROXY_REQUEST_EXPIRATION_SEC}; use futures::channel::oneshot; use futures_util::{SinkExt, StreamExt}; use jsonrpc_core::{Id as RpcId, Params as RpcParams, Value as RpcValue, Version as RpcVersion}; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use mm2_number::BigDecimal; use proxy_signature::RawMessage; use std::collections::{HashMap, HashSet}; @@ -23,7 +23,11 @@ impl TendermintBalanceEventStreamer { impl EventStreamer for TendermintBalanceEventStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } + fn streamer_id(&self) -> StreamerId { + StreamerId::Balance { + coin: self.coin.ticker().to_string(), + } + } async fn handle( self, diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 3f5f8a33de..b442f94ab2 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -3,17 +3,13 @@ use super::htlc::{ClaimHtlcMsg, ClaimHtlcProto, CreateHtlcMsg, CreateHtlcProto, QueryHtlcResponse, TendermintHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, HTLC_STATE_REFUNDED}; use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; -use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; -use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; -use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; +use super::rpc::*; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::hd_wallet::{HDAddressSelector, HDPathAccountToAddressId}; +use crate::rpc_command::tendermint::ibc::ChannelId; use crate::rpc_command::tendermint::staking::{ClaimRewardsPayload, Delegation, DelegationPayload, DelegationsQueryResponse, Undelegation, UndelegationEntry, UndelegationsQueryResponse, ValidatorStatus}; -use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, - IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, - IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, - 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, @@ -54,6 +50,8 @@ use cosmrs::proto::cosmos::staking::v1beta1::{QueryDelegationRequest, QueryDeleg QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; +use cosmrs::proto::ibc; +use cosmrs::proto::ibc::core::channel::v1::{QueryChannelRequest, QueryChannelResponse}; use cosmrs::proto::prost::{DecodeError, Message}; use cosmrs::staking::{MsgDelegate, MsgUndelegate, QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; @@ -70,10 +68,10 @@ use futures::{FutureExt, TryFutureExt}; use futures01::Future; use hex::FromHexError; use itertools::Itertools; +use kdf_walletconnect::{WalletConnectCtx, WalletConnectOps}; use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; -use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; use mm2_number::bigdecimal::ParseBigDecimalError; use mm2_number::MmNumber; use mm2_p2p::p2p_ctx::P2PContext; @@ -81,7 +79,7 @@ use num_traits::Zero; use parking_lot::Mutex as PaMutex; use primitives::hash::H256; use regex::Regex; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; @@ -106,6 +104,10 @@ const ABCI_DELEGATION_PATH: &str = "/cosmos.staking.v1beta1.Query/Delegation"; const ABCI_DELEGATOR_DELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorDelegations"; const ABCI_DELEGATOR_UNDELEGATIONS_PATH: &str = "/cosmos.staking.v1beta1.Query/DelegatorUnbondingDelegations"; const ABCI_DELEGATION_REWARDS_PATH: &str = "/cosmos.distribution.v1beta1.Query/DelegationRewards"; +const ABCI_IBC_CHANNEL_QUERY_PATH: &str = "/ibc.core.channel.v1.Query/Channel"; + +#[cfg(feature = "ibc-routing-for-swaps")] +const DEFAULT_MIN_BALANCE_FOR_IBC_ROUTING: f32 = 2.0; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -126,6 +128,9 @@ const MIN_TIME_LOCK: i64 = 50; const ACCOUNT_SEQUENCE_ERR: &str = "account sequence mismatch"; +pub(crate) const IRIS_PREFIX: &str = "iaa"; +pub(crate) const NUCLEUS_PREFIX: &str = "nuc"; + lazy_static! { static ref SEQUENCE_PARSER_REGEX: Regex = Regex::new(r"expected (\d+)").unwrap(); } @@ -195,11 +200,15 @@ pub struct TendermintFeeDetails { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct TendermintProtocolInfo { pub decimals: u8, - denom: String, + pub(crate) denom: Denom, + min_balance_for_ibc_routing: Option, pub account_prefix: String, - chain_id: String, + pub chain_id: ChainId, gas_price: Option, - chain_registry_name: Option, + /// Key represents the account prefix of the target chain and + /// the value is the channel ID used for sending transactions. + #[serde(default)] + ibc_channels: HashMap, } #[derive(Clone)] @@ -287,14 +296,17 @@ impl TendermintActivationPolicy { PublicKey::from_raw_secp256k1(&activated_key.public_key.to_bytes()) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")) }, - PrivKeyPolicy::Trezor => Err(io::Error::new( io::ErrorKind::Unsupported, "Trezor is not supported yet!", )), - + PrivKeyPolicy::WalletConnect { public_key, .. } => PublicKey::from_raw_secp256k1(public_key.as_bytes()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")), #[cfg(target_arch = "wasm32")] - PrivKeyPolicy::Metamask(_) => unreachable!(), + PrivKeyPolicy::Metamask(_) => Err(io::Error::new( + io::ErrorKind::Unsupported, + "Metamask is not supported yet!", + )), }, Self::PublicKey(account_public_key) => Ok(*account_public_key), } @@ -373,27 +385,35 @@ impl RpcCommonOps for TendermintCoin { } } +#[derive(PartialEq)] +pub enum TendermintWalletConnectionType { + Wc(String), + WcLedger(String), + KeplrLedger, + Keplr, + Native, +} + +impl Default for TendermintWalletConnectionType { + fn default() -> Self { Self::Native } +} + pub struct TendermintCoinImpl { ticker: String, /// As seconds avg_blocktime: u8, /// My address pub account_id: AccountId, - pub(super) account_prefix: String, pub activation_policy: TendermintActivationPolicy, - pub(crate) decimals: u8, - pub(super) denom: Denom, - chain_id: ChainId, - gas_price: Option, pub 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, - pub(crate) chain_registry_name: Option, pub ctx: MmWeak, - pub(crate) is_keplr_from_ledger: bool, + pub(crate) wallet_type: TendermintWalletConnectionType, + pub(crate) protocol_info: TendermintProtocolInfo, } #[derive(Clone)] @@ -419,7 +439,6 @@ pub enum TendermintInitErrorKind { EmptyRpcUrls, RpcClientInitError(String), InvalidChainId(String), - InvalidDenom(String), InvalidProtocolData(String), InvalidPathToAddress(String), #[display(fmt = "'derivation_path' field is not found in config")] @@ -439,6 +458,10 @@ pub enum TendermintInitErrorKind { #[display(fmt = "avg_blocktime must be in-between '0' and '255'.")] AvgBlockTimeInvalid, BalanceStreamInitError(String), + #[display(fmt = "Watcher features can not be used with pubkey-only activation policy.")] + CantUseWatchersWithPubkeyPolicy, + #[display(fmt = "Unable to fetch account for chain: {_0}")] + UnableToFetchChainAccount(String), } /// TODO: Rename this into `ClientRpcError` because this is very @@ -455,6 +478,31 @@ pub enum TendermintCoinRpcError { UnexpectedAccountType { prefix: String, }, + NotFound(String), +} + +#[derive(Clone, Debug, Display, PartialEq, Serialize)] +pub enum IBCError { + #[display( + fmt = "IBC channel could not be found in coins file for '{}' address prefix. Provide it manually by including `ibc_source_channel` in the request.", + address_prefix + )] + IBCChannelCouldNotBeFound { address_prefix: String }, + #[display( + fmt = "IBC channel '{}' is not healthy. Provide a healthy one manually by including `ibc_source_channel` in the request.", + channel_id + )] + IBCChannelNotHealthy { channel_id: ChannelId }, + #[display(fmt = "IBC channel '{}' is not present on the target node.", channel_id)] + IBCChannelMissingOnNode { channel_id: ChannelId }, + #[display(fmt = "Transport error: {reason}")] + Transport { reason: String }, + #[display(fmt = "Internal error: {reason}")] + InternalError { reason: String }, +} + +impl From for WithdrawError { + fn from(err: IBCError) -> Self { WithdrawError::IBCError(err) } } impl From for TendermintCoinRpcError { @@ -478,9 +526,9 @@ impl From for BalanceError { match err { TendermintCoinRpcError::InvalidResponse(e) => BalanceError::InvalidResponse(e), TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e), - TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { - BalanceError::Transport(e) - }, + TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) + | TendermintCoinRpcError::NotFound(e) => BalanceError::Transport(e), TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs")) @@ -494,9 +542,9 @@ impl From for ValidatePaymentError { match err { TendermintCoinRpcError::InvalidResponse(e) => ValidatePaymentError::InvalidRpcResponse(e), TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e), - TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { - ValidatePaymentError::Transport(e) - }, + TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) + | TendermintCoinRpcError::NotFound(e) => ValidatePaymentError::Transport(e), TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs")) @@ -606,7 +654,7 @@ impl From for SearchForSwapTxSpendErr { #[async_trait] impl TendermintCommons for TendermintCoin { - fn platform_denom(&self) -> &Denom { &self.denom } + fn platform_denom(&self) -> &Denom { &self.protocol_info.denom } fn set_history_sync_state(&self, new_state: HistorySyncState) { *self.history_sync_state.lock().unwrap() = new_state; @@ -621,7 +669,7 @@ impl TendermintCommons for TendermintCoin { } fn denom_to_ticker(&self, denom: &str) -> Option { - if self.denom.as_ref() == denom { + if self.protocol_info.denom.as_ref() == denom { return Some(self.ticker.clone()); } @@ -637,9 +685,9 @@ impl TendermintCommons for TendermintCoin { async fn get_all_balances(&self) -> MmResult { let platform_balance_denom = self - .account_balance_for_denom(&self.account_id, self.denom.to_string()) + .account_balance_for_denom(&self.account_id, self.protocol_info.denom.to_string()) .await?; - let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); + let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.protocol_info.decimals); let ibc_assets_info = self.tokens_info.lock().clone(); let mut requests = Vec::with_capacity(ibc_assets_info.len()); @@ -679,7 +727,7 @@ impl TendermintCoin { nodes: Vec, tx_history: bool, activation_policy: TendermintActivationPolicy, - is_keplr_from_ledger: bool, + wallet_type: TendermintWalletConnectionType, ) -> MmResult { if nodes.is_empty() { return MmError::err(TendermintInitError { @@ -702,16 +750,6 @@ impl TendermintCoin { let client_impl = TendermintRpcClientImpl { rpc_clients }; - let chain_id = ChainId::try_from(protocol_info.chain_id).map_to_mm(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::InvalidChainId(e.to_string()), - })?; - - let denom = Denom::from_str(&protocol_info.denom).map_to_mm(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::InvalidDenom(e.to_string()), - })?; - let history_sync_state = if tx_history { HistorySyncState::NotStarted } else { @@ -731,52 +769,99 @@ impl TendermintCoin { Ok(TendermintCoin(Arc::new(TendermintCoinImpl { ticker, account_id, - account_prefix: protocol_info.account_prefix, activation_policy, - decimals: protocol_info.decimals, - denom, - chain_id, - gas_price: protocol_info.gas_price, avg_blocktime: conf.avg_blocktime, 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, + protocol_info, ctx: ctx.weak(), - is_keplr_from_ledger, + wallet_type, }))) } - /// Extracts corresponding IBC channel ID for `AccountId` from https://github.com/KomodoPlatform/chain-registry/tree/nucl. - pub(crate) async fn detect_channel_id_for_ibc_transfer( + /// Finds the IBC channel by querying the given channel ID and port ID + /// and returns its information. + async fn query_ibc_channel( &self, - to_address: &AccountId, - ) -> Result> { - let ctx = MmArc::from_weak(&self.ctx).ok_or_else(|| WithdrawError::InternalError("No context".to_owned()))?; - - let source_registry_name = self - .chain_registry_name - .clone() - .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; + channel_id: ChannelId, + port_id: &str, + ) -> Result { + let payload = QueryChannelRequest { + channel_id: channel_id.to_string(), + port_id: port_id.to_string(), + } + .encode_to_vec(); - let destination_registry_name = chain_registry_name_from_account_prefix(&ctx, to_address.prefix()) - .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; + let request = AbciRequest::new( + Some(ABCI_IBC_CHANNEL_QUERY_PATH.to_string()), + payload, + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ); - let channels = get_ibc_transfer_channels(source_registry_name, destination_registry_name) + let response = self + .rpc_client() + .await + .map_err(|e| IBCError::Transport { reason: e.to_string() })? + .perform(request) .await - .map_err(|_| WithdrawError::IBCChannelCouldNotFound(to_address.to_string()))?; + .map_err(|e| IBCError::Transport { reason: e.to_string() })?; - Ok(channels - .ibc_transfer_channels - .last() - .ok_or_else(|| WithdrawError::InternalError("channel list can not be empty".to_owned()))? - .channel_id - .clone()) + let response = QueryChannelResponse::decode(response.response.value.as_slice()) + .map_err(|e| IBCError::InternalError { reason: e.to_string() })?; + + response.channel.ok_or(IBCError::IBCChannelMissingOnNode { channel_id }) + } + + /// Looks for a healthy IBC channel on a network that supports HTLC transactions. + /// Right now it first tries to find a channel on IRIS network, if none is found, then falls + /// back to NUCLEUS network. + pub async fn get_healthy_ibc_channel_to_htlc_chain(&self) -> Result> { + let channel_id = if let Ok(channel_id) = self.get_healthy_ibc_channel_for_address_prefix(IRIS_PREFIX).await { + channel_id + } else { + self.get_healthy_ibc_channel_for_address_prefix(NUCLEUS_PREFIX).await? + }; + + Ok(channel_id) + } + + /// Returns a **healthy** IBC channel ID for the given target address. + pub async fn get_healthy_ibc_channel_for_address_prefix( + &self, + address_prefix: &str, + ) -> Result> { + // ref: https://github.com/cosmos/ibc-go/blob/7f34724b982581435441e0bb70598c3e3a77f061/proto/ibc/core/channel/v1/channel.proto#L51-L68 + const STATE_OPEN: i32 = 3; + + let channel_id = *self.protocol_info.ibc_channels.get(address_prefix).ok_or_else(|| { + IBCError::IBCChannelCouldNotBeFound { + address_prefix: address_prefix.to_owned(), + } + })?; + + let channel = self.query_ibc_channel(channel_id, "transfer").await?; + + // TODO: Extend the validation logic to also include: + // + // - Checking the time of the last update on the channel + // - Verifying the total amount transferred since the channel was created + // - Check the channel creation time + if channel.state != STATE_OPEN { + return MmError::err(IBCError::IBCChannelNotHealthy { channel_id }); + } + + Ok(channel_id) + } + + pub fn supports_htlc(&self) -> bool { + matches!(self.protocol_info.account_prefix.as_str(), NUCLEUS_PREFIX | IRIS_PREFIX) } #[inline(always)] - fn gas_price(&self) -> f64 { self.gas_price.unwrap_or(DEFAULT_GAS_PRICE) } + fn gas_price(&self) -> f64 { self.protocol_info.gas_price.unwrap_or(DEFAULT_GAS_PRICE) } #[allow(unused)] async fn get_latest_block(&self) -> MmResult { @@ -820,7 +905,7 @@ impl TendermintCoin { memo: &str, ) -> cosmrs::Result> { let fee_amount = Coin { - denom: self.denom.clone(), + denom: self.protocol_info.denom.clone(), amount: 0_u64.into(), }; @@ -829,7 +914,12 @@ impl TendermintCoin { let signkey = SigningKey::from_slice(priv_key.as_slice())?; let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let auth_info = SignerInfo::single_direct(Some(signkey.public_key()), account_info.sequence).auth_info(fee); - let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; + let sign_doc = SignDoc::new( + &tx_body, + &auth_info, + &self.protocol_info.chain_id, + account_info.account_number, + )?; sign_doc.sign(&signkey)?.to_bytes() } @@ -884,6 +974,14 @@ impl TendermintCoin { ) }, TendermintActivationPolicy::PublicKey(_) => { + if self.is_wallet_connect() { + return try_tx_s!( + self.seq_safe_send_raw_tx_bytes(tx_payload, fee, timeout_height, memo) + .timeout(expiration) + .await + ); + }; + try_tx_s!( self.send_unsigned_tx_externally(tx_payload, fee, timeout_height, memo, expiration) .timeout(expiration) @@ -893,6 +991,38 @@ impl TendermintCoin { } } + async fn get_tx_raw( + &self, + account_info: &BaseAccount, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: &str, + ) -> Result { + if self.is_wallet_connect() { + let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); + let wc = try_tx_s!(WalletConnectCtx::from_ctx(&ctx).map_err(|e| e.to_string())); + let SerializedUnsignedTx { tx_json, .. } = if self.is_ledger_connection() { + try_tx_s!(self.any_to_legacy_amino_json(account_info, tx_payload, fee, timeout_height, memo)) + } else { + try_tx_s!(self.any_to_serialized_sign_doc(account_info, tx_payload, fee, timeout_height, memo)) + }; + + return Ok(try_tx_s!(self.wc_sign_tx(&wc, tx_json).await.map_err(|err| err.to_string())).into()); + } + + let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( + try_tx_s!(self.activation_policy.activated_key_or_err()), + account_info, + tx_payload, + fee, + timeout_height, + memo, + )); + + Ok(tx_raw) + } + async fn seq_safe_send_raw_tx_bytes( &self, tx_payload: Any, @@ -901,31 +1031,29 @@ impl TendermintCoin { memo: &str, ) -> Result<(String, Raw), TransactionErr> { let mut account_info = try_tx_s!(self.account_info(&self.account_id).await); - let (tx_id, tx_raw) = loop { - let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( - try_tx_s!(self.activation_policy.activated_key_or_err()), - &account_info, - tx_payload.clone(), - fee.clone(), - timeout_height, - memo, - )); + loop { + let tx_raw = try_tx_s!( + self.get_tx_raw(&account_info, tx_payload.clone(), fee.clone(), timeout_height, memo,) + .await + ); - match self.send_raw_tx_bytes(&try_tx_s!(tx_raw.to_bytes())).compat().await { - Ok(tx_id) => break (tx_id, tx_raw), + // Attempt to send the transaction bytes + match self.send_raw_tx_bytes(try_tx_s!(&tx_raw.to_bytes())).compat().await { + Ok(tx_id) => { + return Ok((tx_id, tx_raw)); + }, Err(e) => { + // Handle sequence number mismatch and retry if e.contains(ACCOUNT_SEQUENCE_ERR) { account_info.sequence = try_tx_s!(parse_expected_sequence_number(&e)); - debug!("Got wrong account sequence, trying again."); + debug!("Account sequence mismatch, retrying..."); continue; } - return Err(crate::TransactionErr::Plain(ERRL!("{}", e))); + return Err(TransactionErr::Plain(ERRL!("Transaction failed: {}", e))); }, - }; - }; - - Ok((tx_id, tx_raw)) + } + } } async fn send_unsigned_tx_externally( @@ -944,32 +1072,32 @@ impl TendermintCoin { let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); let account_info = try_tx_s!(self.account_info(&self.account_id).await); - let SerializedUnsignedTx { tx_json, body_bytes } = if self.is_keplr_from_ledger { + let SerializedUnsignedTx { tx_json, body_bytes } = if self.is_ledger_connection() { try_tx_s!(self.any_to_legacy_amino_json(&account_info, tx_payload, fee, timeout_height, memo)) } else { try_tx_s!(self.any_to_serialized_sign_doc(&account_info, tx_payload, fee, timeout_height, memo)) }; let data: TxHashData = try_tx_s!(ctx - .ask_for_data(&format!("TX_HASH:{}", self.ticker()), tx_json, timeout) + .ask_for_data(&format!("TX_HASH:{}", self.ticker()), tx_json.clone(), timeout) .await .map_err(|e| ERRL!("{}", e))); let tx = try_tx_s!(self.request_tx(data.hash.clone()).await.map_err(|e| ERRL!("{}", e))); - let tx_raw_inner = TxRaw { + let tx_raw = TxRaw { body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), signatures: tx.signatures, }; - if body_bytes != tx_raw_inner.body_bytes { + if body_bytes != tx_raw.body_bytes { return Err(crate::TransactionErr::Plain(ERRL!( "Unsigned transaction don't match with the externally provided transaction." ))); } - Ok((data.hash, Raw::from(tx_raw_inner))) + Ok((data.hash, Raw::from(tx_raw))) } #[allow(deprecated)] @@ -1135,10 +1263,10 @@ impl TendermintCoin { .account .or_mm_err(|| TendermintCoinRpcError::InvalidResponse("Account is None".into()))?; - let account_prefix = self.account_prefix.clone(); + let account_prefix = self.protocol_info.account_prefix.clone(); let base_account = match BaseAccount::decode(account.value.as_slice()) { Ok(account) => account, - Err(err) if account_prefix.as_str() == "iaa" => { + Err(err) if account_prefix.as_str() == IRIS_PREFIX => { let ethermint_account = EthermintAccount::decode(account.value.as_slice())?; ethermint_account @@ -1181,7 +1309,7 @@ impl TendermintCoin { pub(super) fn extract_account_id_and_private_key( &self, - withdraw_from: Option, + withdraw_from: Option, ) -> Result<(AccountId, Option), io::Error> { if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { return Ok((self.account_id.clone(), None)); @@ -1205,7 +1333,7 @@ impl TendermintCoin { .hd_wallet_derived_priv_key_or_err(&path_to_address) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &self.account_prefix) + let account_id = account_id_from_privkey(priv_key.as_slice(), &self.protocol_info.account_prefix) .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?; Ok((account_id, Some(priv_key))) }, @@ -1220,7 +1348,7 @@ impl TendermintCoin { } } - pub(super) fn any_to_transaction_data( + pub(super) async fn any_to_transaction_data( &self, maybe_priv_key: Option, message: Any, @@ -1234,19 +1362,34 @@ impl TendermintCoin { let tx_bytes = tx_raw.to_bytes()?; let hash = sha256(&tx_bytes); - Ok(TransactionData::new_signed( + return Ok(TransactionData::new_signed( tx_bytes.into(), hex::encode_upper(hash.as_slice()), - )) + )); + }; + + let SerializedUnsignedTx { tx_json, .. } = if self.is_ledger_connection() { + self.any_to_legacy_amino_json(account_info, message, fee, timeout_height, memo) } else { - let SerializedUnsignedTx { tx_json, .. } = if self.is_keplr_from_ledger { - self.any_to_legacy_amino_json(account_info, message, fee, timeout_height, memo) - } else { - self.any_to_serialized_sign_doc(account_info, message, fee, timeout_height, memo) - }?; + self.any_to_serialized_sign_doc(account_info, message, fee, timeout_height, memo) + }?; - Ok(TransactionData::Unsigned(tx_json)) - } + if self.is_wallet_connect() { + let ctx = MmArc::from_weak(&self.ctx) + .ok_or(MyAddressError::InternalError(ERRL!("ctx must be initialized already")))?; + let wallet_connect = WalletConnectCtx::from_ctx(&ctx)?; + + let tx_raw: Raw = self.wc_sign_tx(&wallet_connect, tx_json).await?.into(); + let tx_bytes = tx_raw.to_bytes()?; + let hash = sha256(&tx_bytes); + + return Ok(TransactionData::new_signed( + tx_bytes.into(), + hex::encode_upper(hash.as_slice()), + )); + }; + + Ok(TransactionData::Unsigned(tx_json)) } fn gen_create_htlc_tx( @@ -1260,10 +1403,10 @@ impl TendermintCoin { let amount = vec![Coin { denom, amount }]; let timestamp = 0_u64; - let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { TxMarshalingErr::NotSupported(format!( "Account type '{}' is not supported for HTLCs", - self.account_prefix + self.protocol_info.account_prefix )) })?; @@ -1288,10 +1431,10 @@ impl TendermintCoin { } fn gen_claim_htlc_tx(&self, htlc_id: String, secret: &[u8]) -> MmResult { - let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { TxMarshalingErr::NotSupported(format!( "Account type '{}' is not supported for HTLCs", - self.account_prefix + self.protocol_info.account_prefix )) })?; @@ -1317,7 +1460,12 @@ impl TendermintCoin { let signkey = SigningKey::from_slice(priv_key.as_slice())?; let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let auth_info = SignerInfo::single_direct(Some(signkey.public_key()), account_info.sequence).auth_info(fee); - let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; + let sign_doc = SignDoc::new( + &tx_body, + &auth_info, + &self.protocol_info.chain_id, + account_info.account_number, + )?; sign_doc.sign(&signkey) } @@ -1332,16 +1480,40 @@ impl TendermintCoin { let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let pubkey = self.activation_policy.public_key()?.into(); let auth_info = SignerInfo::single_direct(Some(pubkey), account_info.sequence).auth_info(fee); - let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; - - let tx_json = json!({ - "sign_doc": { - "body_bytes": sign_doc.body_bytes, - "auth_info_bytes": sign_doc.auth_info_bytes, - "chain_id": sign_doc.chain_id, - "account_number": sign_doc.account_number, - } - }); + let sign_doc = SignDoc::new( + &tx_body, + &auth_info, + &self.protocol_info.chain_id, + account_info.account_number, + )?; + + let tx_json = if self.is_wallet_connect() { + let ctx = MmArc::from_weak(&self.ctx).expect("No context"); + let wc = WalletConnectCtx::from_ctx(&ctx).expect("should never fail in this block"); + let session_topic = self + .session_topic() + .expect("session_topic can't be None inside this block"); + let encode = |data| wc.encode(session_topic, data); + + json!({ + "signerAddress": self.my_address()?, + "signDoc": { + "accountNumber": sign_doc.account_number.to_string(), + "chainId": sign_doc.chain_id, + "bodyBytes": encode(&sign_doc.body_bytes), + "authInfoBytes": encode(&sign_doc.auth_info_bytes) + } + }) + } else { + json!({ + "sign_doc": { + "body_bytes": &sign_doc.body_bytes, + "auth_info_bytes": sign_doc.auth_info_bytes, + "chain_id": sign_doc.chain_id, + "account_number": sign_doc.account_number, + } + }) + }; Ok(SerializedUnsignedTx { tx_json, @@ -1349,7 +1521,7 @@ impl TendermintCoin { }) } - /// This should only be used for Keplr from Ledger! + /// This should only be used for Keplr/WalletConnect from Ledger! /// When using Keplr from Ledger, they don't accept `SING_MODE_DIRECT` transactions. /// /// Visit https://docs.cosmos.network/main/build/architecture/adr-050-sign-mode-textual#context for more context. @@ -1377,8 +1549,6 @@ impl TendermintCoin { let msg_send = MsgSend::from_any(&tx_payload)?; let timeout_height = u32::try_from(timeout_height)?; - let original_tx_type_url = tx_payload.type_url.clone(); - let body_bytes = tx::Body::new(vec![tx_payload], memo, timeout_height).into_bytes()?; let amount: Vec = msg_send .amount @@ -1415,20 +1585,45 @@ impl TendermintCoin { }) .collect(); - let tx_json = serde_json::json!({ - "legacy_amino_json": { - "account_number": account_info.account_number.to_string(), - "chain_id": self.chain_id.to_string(), - "fee": { - "amount": fee_amount, - "gas": fee.gas_limit.to_string() + let sign_doc = json!({ + "account_number": account_info.account_number.to_string(), + "chain_id": self.protocol_info.chain_id.to_string(), + "fee": { + "amount": fee_amount, + "gas": fee.gas_limit.to_string() }, - "memo": memo, - "msgs": [msg], - "sequence": account_info.sequence.to_string(), - }, - "original_tx_type_url": original_tx_type_url, + "memo": memo, + "msgs": [msg], + "sequence": account_info.sequence.to_string() }); + let (tx_json, body_bytes) = match self.wallet_type { + TendermintWalletConnectionType::WcLedger(_) => { + let signer_address = self + .my_address() + .map_err(|e| ErrorReport::new(io::Error::new(io::ErrorKind::Other, e.to_string())))?; + let body_bytes = tx::Body::new(vec![tx_payload], memo, timeout_height).into_bytes()?; + let json = serde_json::json!({ + "signerAddress": signer_address, + "signDoc": sign_doc, + }); + (json, body_bytes) + }, + TendermintWalletConnectionType::KeplrLedger => { + let original_tx_type_url = tx_payload.type_url.clone(); + let body_bytes = tx::Body::new(vec![tx_payload], memo, timeout_height).into_bytes()?; + let json = serde_json::json!({ + "legacy_amino_json": sign_doc, + "original_tx_type_url": original_tx_type_url, + }); + (json, body_bytes) + }, + _ => { + return Err(ErrorReport::new(io::Error::new( + io::ErrorKind::InvalidInput, + "Only WalletConnect activated with Ledger can call this function", + ))) + }, + }; Ok(SerializedUnsignedTx { tx_json, body_bytes }) } @@ -1461,7 +1656,10 @@ impl TendermintCoin { }]; let pubkey_hash = dhash160(other_pub); - let to_address = try_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); + let to_address = try_fus!(AccountId::new( + &self.protocol_info.account_prefix, + pubkey_hash.as_slice() + )); let htlc_id = self.calculate_htlc_id(&self.account_id, &to_address, &amount, secret_hash); @@ -1505,7 +1703,7 @@ impl TendermintCoin { let deserialized_tx = try_s!(cosmrs::Tx::from_bytes(&tx.tx)); let msg = try_s!(deserialized_tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc = try_s!(CreateHtlcProto::decode( - try_s!(HtlcType::from_str(&coin.account_prefix)), + try_s!(HtlcType::from_str(&coin.protocol_info.account_prefix)), msg.value.as_slice() )); @@ -1537,7 +1735,10 @@ impl TendermintCoin { decimals: u8, ) -> TransactionFut { let pubkey_hash = dhash160(other_pub); - let to = try_tx_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); + let to = try_tx_fus!(AccountId::new( + &self.protocol_info.account_prefix, + pubkey_hash.as_slice() + )); let amount_as_u64 = try_tx_fus!(sat_from_big_decimal(&amount, decimals)); let amount = cosmrs::Amount::from(amount_as_u64); @@ -1593,8 +1794,14 @@ impl TendermintCoin { let from_address = self.account_id.clone(); let dex_pubkey_hash = dhash160(self.dex_pubkey()); let burn_pubkey_hash = dhash160(self.burn_pubkey()); - let dex_address = try_tx_fus!(AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice())); - let burn_address = try_tx_fus!(AccountId::new(&self.account_prefix, burn_pubkey_hash.as_slice())); + let dex_address = try_tx_fus!(AccountId::new( + &self.protocol_info.account_prefix, + dex_pubkey_hash.as_slice() + )); + let burn_address = try_tx_fus!(AccountId::new( + &self.protocol_info.account_prefix, + burn_pubkey_hash.as_slice() + )); let fee_amount_as_u64 = try_tx_fus!(dex_fee.fee_amount_as_u64(decimals)); let fee_amount = vec![Coin { @@ -1695,8 +1902,11 @@ impl TendermintCoin { .to_string(); let sender_pubkey_hash = dhash160(expected_sender); - let expected_sender_address = try_f!(AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice()) - .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))); + let expected_sender_address = try_f!(AccountId::new( + &self.protocol_info.account_prefix, + sender_pubkey_hash.as_slice() + ) + .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))); let coin = self.clone(); let dex_fee = dex_fee.clone(); @@ -1764,10 +1974,10 @@ impl TendermintCoin { "Payment tx must have exactly one message".into(), )); } - let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { ValidatePaymentError::InvalidParameter(format!( "Account type '{}' is not supported for HTLCs", - self.account_prefix + self.protocol_info.account_prefix )) })?; @@ -1777,7 +1987,7 @@ impl TendermintCoin { .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; let sender_pubkey_hash = dhash160(&input.other_pub); - let sender = AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice()) + let sender = AccountId::new(&self.protocol_info.account_prefix, sender_pubkey_hash.as_slice()) .map_to_mm(|e| ValidatePaymentError::InvalidParameter(e.to_string()))?; let amount = sat_from_big_decimal(&input.amount, decimals)?; @@ -1844,7 +2054,7 @@ impl TendermintCoin { } let dex_pubkey_hash = dhash160(self.dex_pubkey()); - let expected_dex_address = AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice()) + let expected_dex_address = AccountId::new(&self.protocol_info.account_prefix, dex_pubkey_hash.as_slice()) .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; let fee_amount_as_u64 = dex_fee.fee_amount_as_u64(decimals)?; @@ -1896,11 +2106,11 @@ impl TendermintCoin { } let dex_pubkey_hash = dhash160(self.dex_pubkey()); - let expected_dex_address = AccountId::new(&self.account_prefix, dex_pubkey_hash.as_slice()) + let expected_dex_address = AccountId::new(&self.protocol_info.account_prefix, dex_pubkey_hash.as_slice()) .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; let burn_pubkey_hash = dhash160(self.burn_pubkey()); - let expected_burn_address = AccountId::new(&self.account_prefix, burn_pubkey_hash.as_slice()) + let expected_burn_address = AccountId::new(&self.protocol_info.account_prefix, burn_pubkey_hash.as_slice()) .map_to_mm(|r| ValidatePaymentError::InvalidParameter(r.to_string()))?; let fee_amount_as_u64 = dex_fee.fee_amount_as_u64(decimals)?; @@ -1988,7 +2198,7 @@ impl TendermintCoin { common::os_rng(&mut sec).map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; drop_mutability!(sec); - let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) + let to_address = account_id_from_pubkey_hex(&self.protocol_info.account_prefix, DEX_FEE_ADDR_PUBKEY) .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&amount, decimals)?; @@ -2022,7 +2232,7 @@ impl TendermintCoin { ) .await?; - let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, self.decimals); + let fee_amount = big_decimal_from_sat_unsigned(fee_uamount, self.protocol_info.decimals); Ok(TradeFee { coin: ticker, @@ -2038,7 +2248,7 @@ impl TendermintCoin { decimals: u8, dex_fee_amount: DexFee, ) -> TradePreimageResult { - let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) + let to_address = account_id_from_pubkey_hex(&self.protocol_info.account_prefix, DEX_FEE_ADDR_PUBKEY) .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&dex_fee_amount.fee_amount().into(), decimals)?; @@ -2147,10 +2357,11 @@ impl TendermintCoin { } pub(crate) async fn query_htlc(&self, id: String) -> MmResult { - let htlc_type = - HtlcType::from_str(&self.account_prefix).map_err(|_| TendermintCoinRpcError::UnexpectedAccountType { - prefix: self.account_prefix.clone(), - })?; + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { + TendermintCoinRpcError::UnexpectedAccountType { + prefix: self.protocol_info.account_prefix.clone(), + } + })?; let request = QueryHtlcRequestProto { id }; let response = self @@ -2184,10 +2395,11 @@ impl TendermintCoin { .first() .or_mm_err(|| SearchForSwapTxSpendErr::TxMessagesEmpty)?; - let htlc_type = - HtlcType::from_str(&self.account_prefix).map_err(|_| SearchForSwapTxSpendErr::UnexpectedAccountType { - prefix: self.account_prefix.clone(), - })?; + let htlc_type = HtlcType::from_str(&self.protocol_info.account_prefix).map_err(|_| { + SearchForSwapTxSpendErr::UnexpectedAccountType { + prefix: self.protocol_info.account_prefix.clone(), + } + })?; let htlc_proto = CreateHtlcProto::decode(htlc_type, first_message.value.as_slice())?; let htlc = CreateHtlcMsg::try_from(htlc_proto)?; @@ -2255,8 +2467,8 @@ impl TendermintCoin { } pub(crate) fn active_ticker_and_decimals_from_denom(&self, denom: &str) -> Option<(String, u8)> { - if self.denom.as_ref() == denom { - return Some((self.ticker.clone(), self.decimals)); + if self.protocol_info.denom.as_ref() == denom { + return Some((self.ticker.clone(), self.protocol_info.decimals)); } let tokens = self.tokens_info.lock(); @@ -2268,6 +2480,22 @@ impl TendermintCoin { None } + #[inline] + pub fn is_ledger_connection(&self) -> bool { + matches!( + self.wallet_type, + TendermintWalletConnectionType::WcLedger(_) | TendermintWalletConnectionType::KeplrLedger + ) + } + + #[inline] + pub fn is_wallet_connect(&self) -> bool { + matches!( + self.wallet_type, + TendermintWalletConnectionType::WcLedger(_) | TendermintWalletConnectionType::Wc(_) + ) + } + pub(crate) async fn validators_list( &self, filter_status: ValidatorStatus, @@ -2350,7 +2578,7 @@ impl TendermintCoin { return Err(not_sufficient(total)); } - let amount_u64 = sat_from_big_decimal(&request_amount, coin.decimals) + let amount_u64 = sat_from_big_decimal(&request_amount, coin.protocol_info.decimals) .map_err(|e| DelegationError::InternalError(e.to_string()))?; Ok((amount_u64, total)) @@ -2364,13 +2592,13 @@ impl TendermintCoin { .map_err(|e| DelegationError::InternalError(e.to_string()))?; let (balance_u64, balance_dec) = self - .get_balance_as_unsigned_and_decimal(&delegator_address, &self.denom, self.decimals()) + .get_balance_as_unsigned_and_decimal(&delegator_address, &self.protocol_info.denom, self.decimals()) .await?; let amount_u64 = if req.max { balance_u64 } else { - sat_from_big_decimal(&req.amount, self.decimals) + sat_from_big_decimal(&req.amount, self.protocol_info.decimals) .map_err(|e| DelegationError::InternalError(e.to_string()))? }; @@ -2378,7 +2606,7 @@ impl TendermintCoin { let msg_for_fee_prediction = generate_message( delegator_address.clone(), validator_address.clone(), - self.denom.clone(), + self.protocol_info.denom.clone(), amount_u64.into(), ) .map_err(|e| DelegationError::InternalError(e.to_string()))?; @@ -2409,7 +2637,7 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas( Coin { - denom: self.denom.clone(), + denom: self.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }, gas_limit, @@ -2428,7 +2656,7 @@ impl TendermintCoin { let msg_for_actual_tx = generate_message( delegator_address.clone(), validator_address.clone(), - self.denom.clone(), + self.protocol_info.denom.clone(), amount_u64.into(), ) .map_err(|e| DelegationError::InternalError(e.to_string()))?; @@ -2444,12 +2672,10 @@ impl TendermintCoin { timeout_height, &req.memo, ) + .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), None); Ok(TransactionDetails { tx, @@ -2509,14 +2735,14 @@ impl TendermintCoin { }); }; - sat_from_big_decimal(&req.amount, self.decimals) + sat_from_big_decimal(&req.amount, self.protocol_info.decimals) .map_err(|e| DelegationError::InternalError(e.to_string()))? }; let undelegate_msg = generate_message( delegator_address.clone(), validator_address.clone(), - self.denom.clone(), + self.protocol_info.denom.clone(), uamount_to_undelegate.into(), ) .map_err(|e| DelegationError::InternalError(e.to_string()))?; @@ -2557,7 +2783,7 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas( Coin { - denom: self.denom.clone(), + denom: self.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }, gas_limit, @@ -2574,12 +2800,10 @@ impl TendermintCoin { timeout_height, &req.memo, ) + .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().map_or_else(Vec::new, |h| h.to_vec()); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), None); Ok(TransactionDetails { tx, @@ -2684,9 +2908,9 @@ impl TendermintCoin { match decoded_response .rewards .iter() - .find(|t| t.denom == self.denom.to_string()) + .find(|t| t.denom == self.protocol_info.denom.to_string()) { - Some(dec_coin) => extract_big_decimal_from_dec_coin(dec_coin, self.decimals as u32) + Some(dec_coin) => extract_big_decimal_from_dec_coin(dec_coin, self.protocol_info.decimals as u32) .map_to_mm(|e| DelegationError::InternalError(e.to_string())), None => MmError::err(DelegationError::NothingToClaim { coin: self.ticker.clone(), @@ -2763,7 +2987,7 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas( Coin { - denom: self.denom.clone(), + denom: self.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }, gas_limit, @@ -2773,12 +2997,10 @@ impl TendermintCoin { let tx = self .any_to_transaction_data(maybe_priv_key, msg, &account_info, fee, timeout_height, &req.memo) + .await .map_to_mm(|e| DelegationError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().map_or_else(Vec::new, |h| h.to_vec()); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), None); Ok(TransactionDetails { tx, @@ -2955,51 +3177,38 @@ fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult IBCChainRegistriesResult { - fn map_metadata_to_chain_registry_name(metadata: &FileMetadata) -> Result> { - let split_filename_by_dash: Vec<&str> = metadata.name.split('-').collect(); - let chain_registry_name = split_filename_by_dash - .first() - .or_mm_err(|| { - IBCChainsRequestError::InternalError(format!( - "Could not read chain registry name from '{}'", - metadata.name - )) - })? - .to_string(); - - Ok(chain_registry_name) - } - - let git_controller: GitController = GitController::new(GITHUB_API_URI); - - let metadata_list = git_controller - .client - .get_file_metadata_list( - CHAIN_REGISTRY_REPO_OWNER, - CHAIN_REGISTRY_REPO_NAME, - CHAIN_REGISTRY_BRANCH, - CHAIN_REGISTRY_IBC_DIR_NAME, - ) - .await - .map_err(|e| IBCChainsRequestError::Transport(format!("{:?}", e)))?; - - let chain_list: Result, MmError> = - metadata_list.iter().map(map_metadata_to_chain_registry_name).collect(); - - let mut distinct_chain_list = chain_list?; - distinct_chain_list.dedup(); - - Ok(IBCChainRegistriesResponse { - chain_registry_list: distinct_chain_list, - }) -} - #[async_trait] #[allow(unused_variables)] impl MmCoin for TendermintCoin { fn is_asset_chain(&self) -> bool { false } + #[cfg(feature = "ibc-routing-for-swaps")] + fn wallet_only(&self, ctx: &MmArc) -> bool { + // Keplr with Ledger does not support some transactions like HTLC due to + // the transaction format they use. As HTLC is part of our swap system's DNA, + // treat any Tendermint asset as wallet-only. + // + // TODO: Once `SIGN_MODE_DIRECT` is supported, we can remove this. + if self.is_ledger_connection() { + common::log::info!("Using Keplr with Ledger: operating in wallet only mode."); + return true; + } + + let coin_conf = crate::coin_conf(ctx, self.ticker()); + let wallet_only_conf = coin_conf + .get("wallet_only") + .unwrap_or(&json!(false)) + .as_bool() + .unwrap_or(false); + + if wallet_only_conf { + warn!("`wallet_only` option cannot be set to true for Tendermint assets. This setting will be ignored."); + } + + false + } + + #[cfg(not(feature = "ibc-routing-for-swaps"))] fn wallet_only(&self, ctx: &MmArc) -> bool { let coin_conf = crate::coin_conf(ctx, self.ticker()); // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only @@ -3008,7 +3217,7 @@ impl MmCoin for TendermintCoin { } let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); - wallet_only_conf || self.is_keplr_from_ledger + wallet_only_conf || self.is_ledger_connection() } fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } @@ -3019,24 +3228,28 @@ impl MmCoin for TendermintCoin { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); + let is_ibc_transfer = + to_address.prefix() != coin.protocol_info.account_prefix || req.ibc_source_channel.is_some(); let (account_id, maybe_priv_key) = coin .extract_account_id_and_private_key(req.from) .map_err(|e| WithdrawError::InternalError(e.to_string()))?; let (balance_denom, balance_dec) = coin - .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &coin.protocol_info.denom, coin.decimals()) .await?; let (amount_denom, amount_dec) = if req.max { let amount_denom = balance_denom; - (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) + ( + amount_denom, + big_decimal_from_sat_unsigned(amount_denom, coin.decimals()), + ) } else { - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) + (sat_from_big_decimal(&req.amount, coin.decimals())?, req.amount.clone()) }; - if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { + if !coin.is_tx_amount_enough(coin.decimals(), &amount_dec) { return MmError::err(WithdrawError::AmountTooLow { amount: amount_dec, threshold: coin.min_tx_amount(), @@ -3052,7 +3265,10 @@ impl MmCoin for TendermintCoin { let channel_id = if is_ibc_transfer { match &req.ibc_source_channel { Some(_) => req.ibc_source_channel, - None => Some(coin.detect_channel_id_for_ibc_transfer(&to_address).await?), + None => Some( + coin.get_healthy_ibc_channel_for_address_prefix(to_address.prefix()) + .await?, + ), } } else { None @@ -3061,9 +3277,9 @@ impl MmCoin for TendermintCoin { let msg_payload = create_withdraw_msg_as_any( account_id.clone(), to_address.clone(), - &coin.denom, + &coin.protocol_info.denom, amount_denom, - channel_id.clone(), + channel_id, ) .await?; @@ -3094,13 +3310,15 @@ impl MmCoin for TendermintCoin { ) .await?; - let fee_amount_u64 = if coin.is_keplr_from_ledger { + let fee_amount_u64 = if coin.is_ledger_connection() { // When using `SIGN_MODE_LEGACY_AMINO_JSON`, Keplr ignores the fee we calculated // and calculates another one which is usually double what we calculate. // To make sure the transaction doesn't fail on the Keplr side (because if Keplr // calculates a higher fee than us, the withdrawal might fail), we use three times // the actual fee. fee_amount_u64 * 3 + } else if is_ibc_transfer { + fee_amount_u64 * 3 / 2 } else { fee_amount_u64 }; @@ -3108,7 +3326,7 @@ impl MmCoin for TendermintCoin { let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); let fee_amount = Coin { - denom: coin.denom.clone(), + denom: coin.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }; @@ -3134,13 +3352,13 @@ impl MmCoin for TendermintCoin { }); } - (sat_from_big_decimal(&req.amount, coin.decimals)?, total) + (sat_from_big_decimal(&req.amount, coin.decimals())?, total) }; let msg_payload = create_withdraw_msg_as_any( account_id.clone(), to_address.clone(), - &coin.denom, + &coin.protocol_info.denom, amount_denom, channel_id, ) @@ -3150,12 +3368,10 @@ impl MmCoin for TendermintCoin { let tx = coin .any_to_transaction_data(maybe_priv_key, msg_payload, &account_info, fee, timeout_height, &memo) + .await .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), None); Ok(TransactionDetails { tx, @@ -3215,7 +3431,7 @@ impl MmCoin for TendermintCoin { Box::new(fut.boxed().compat()) } - fn decimals(&self) -> u8 { self.decimals } + fn decimals(&self) -> u8 { self.protocol_info.decimals } fn convert_to_address(&self, from: &str, to_address_format: Json) -> Result { // TODO @@ -3255,8 +3471,129 @@ impl MmCoin for TendermintCoin { let amount = match value { TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, }; - self.get_sender_trade_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, amount) + self.get_sender_trade_fee_for_denom( + self.ticker.clone(), + self.protocol_info.denom.clone(), + self.protocol_info.decimals, + amount, + ) + .await + } + + /// Overrides the default `pre_check_for_order_creation` implementation with + /// additional IBC-related logic on top of the default behavior. + #[cfg(feature = "ibc-routing-for-swaps")] + async fn pre_check_for_order_creation( + &self, + ctx: &MmArc, + rel_coin: &crate::MmCoinEnum, + ) -> MmResult<(), crate::OrderCreationPreCheckError> { + use crate::{lp_coinfind, MmCoinEnum, OrderCreationPreCheckError}; + + /// Looks for a Tendermint platform coin by the given ticker. + /// + /// Returns `Ok(Some(...))` if the coin exists and is a Tendermint platform coin, + /// `Ok(None)` if it's not active, or an error if somethings goes wrong or the ticker + /// isn't belongs to a Tendermint platform coin. + async fn find_tendermint_platform_coin( + ctx: &MmArc, + ticker: &str, + ) -> Result, MmError> { + match lp_coinfind(ctx, ticker).await { + Ok(Some(MmCoinEnum::Tendermint(coin))) => Ok(Some(coin)), + Ok(Some(other)) => MmError::err(OrderCreationPreCheckError::InternalError { + reason: format!( + "Expected a Tendermint coin for '{}', but found '{}'.", + ticker, + other.ticker() + ), + }), + Ok(None) => Ok(None), + Err(reason) => MmError::err(OrderCreationPreCheckError::PreCheckFailed { reason }), + } + } + + /// Picks an HTLC coin (IRIS or NUCLEUS) based on which IBC channel is configured + /// and is healthy. + async fn get_htlc_coin( + coin: &TendermintCoin, + ctx: &MmArc, + ) -> Result, MmError> { + const IRIS_TICKER: &str = "IRIS"; + const NUCLEUS_TICKER: &str = "NUCLEUS"; + + if coin + .get_healthy_ibc_channel_for_address_prefix(IRIS_PREFIX) + .await + .is_ok() + { + return find_tendermint_platform_coin(ctx, IRIS_TICKER).await; + } + + if coin + .get_healthy_ibc_channel_for_address_prefix(NUCLEUS_PREFIX) + .await + .is_ok() + { + return find_tendermint_platform_coin(ctx, NUCLEUS_TICKER).await; + } + + MmError::err(OrderCreationPreCheckError::PreCheckFailed { + reason: format!("No healthy IBC channel found for {}.", coin.ticker()), + }) + } + + if self.wallet_only(ctx) { + return MmError::err(OrderCreationPreCheckError::IsWalletOnly { + ticker: self.ticker().to_owned(), + }); + } + + if rel_coin.wallet_only(ctx) { + return MmError::err(OrderCreationPreCheckError::IsWalletOnly { + ticker: rel_coin.ticker().to_owned(), + }); + } + + if self.supports_htlc() { + return Ok(()); + } + + // If `self` is not an HTLC-supported coin, we need to check a few things when creating the order: + // - Is there an HTLC coin enabled? + // - Does that HTLC network have an IBC channel configured to `self` network? + // - Does that HTLC coin have enough balance to handle IBC routing? + + let Some(htlc_coin) = get_htlc_coin(self, ctx).await? else { + return MmError::err(OrderCreationPreCheckError::PreCheckFailed { + reason: "No HTLC coin is currently enabled. Please enable either Iris or Nucleus.".into(), + }); + }; + + let my_balance = htlc_coin + .my_balance() + .compat() .await + .map_err(|e| OrderCreationPreCheckError::InternalError { reason: e.to_string() })? + .spendable; + + let min_balance_for_ibc_routing = htlc_coin + .protocol_info + .min_balance_for_ibc_routing + .unwrap_or(DEFAULT_MIN_BALANCE_FOR_IBC_ROUTING); + let min_balance_for_ibc_routing = BigDecimal::try_from(min_balance_for_ibc_routing) + .map_err(|e| OrderCreationPreCheckError::InternalError { reason: e.to_string() })?; + + if min_balance_for_ibc_routing > my_balance { + let htlc_ticker = htlc_coin.ticker(); + let self_ticker = self.ticker(); + let reason = format!( + "Insufficient balance on HTLC coin ({htlc_ticker}) for making orders with {self_ticker}. Minimum required expected balance {min_balance_for_ibc_routing}, current balance {my_balance}.", + ); + return MmError::err(OrderCreationPreCheckError::PreCheckFailed { reason }); + } + + Ok(()) } fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { @@ -3266,8 +3603,8 @@ impl MmCoin for TendermintCoin { // Since create and claim htlc fees are almost same, we can simply simulate create htlc tx. coin.get_sender_trade_fee_for_denom( coin.ticker.clone(), - coin.denom.clone(), - coin.decimals, + coin.protocol_info.denom.clone(), + coin.decimals(), coin.min_tx_amount(), ) .await @@ -3280,8 +3617,13 @@ impl MmCoin for TendermintCoin { dex_fee_amount: DexFee, _stage: FeeApproxStage, ) -> TradePreimageResult { - self.get_fee_to_send_taker_fee_for_denom(self.ticker.clone(), self.denom.clone(), self.decimals, dex_fee_amount) - .await + self.get_fee_to_send_taker_fee_for_denom( + self.ticker.clone(), + self.protocol_info.denom.clone(), + self.protocol_info.decimals, + dex_fee_amount, + ) + .await } fn required_confirmations(&self) -> u64 { 0 } @@ -3323,6 +3665,12 @@ impl MarketCoinOps for TendermintCoin { fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let address = account_id_from_raw_pubkey(&self.protocol_info.account_prefix, &pubkey.0) + .map_err(|e| AddressFromPubkeyError::InternalError(e.to_string()))?; + Ok(address.to_string()) + } + async fn get_public_key(&self) -> Result> { let key = SigningKey::from_slice(self.activation_policy.activated_key_or_err()?.as_slice()) .expect("privkey validity is checked on coin creation"); @@ -3334,7 +3682,7 @@ impl MarketCoinOps for TendermintCoin { None } - fn sign_message(&self, _message: &str) -> SignatureResult { + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { // TODO MmError::err(SignatureError::InternalError("Not implemented".into())) } @@ -3348,10 +3696,10 @@ impl MarketCoinOps for TendermintCoin { let coin = self.clone(); let fut = async move { let balance_denom = coin - .account_balance_for_denom(&coin.account_id, coin.denom.to_string()) + .account_balance_for_denom(&coin.account_id, coin.protocol_info.denom.to_string()) .await?; Ok(CoinBalance { - spendable: big_decimal_from_sat_unsigned(balance_denom, coin.decimals), + spendable: big_decimal_from_sat_unsigned(balance_denom, coin.decimals()), unspendable: BigDecimal::default(), }) }; @@ -3369,7 +3717,7 @@ impl MarketCoinOps for TendermintCoin { self.send_raw_tx_bytes(&tx_bytes) } - /// Consider using `seq_safe_raw_tx_bytes` instead. + /// Consider using `seq_safe_send_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 @@ -3450,7 +3798,7 @@ impl MarketCoinOps for TendermintCoin { let tx = try_tx_s!(cosmrs::Tx::from_bytes(args.tx_bytes)); let first_message = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto = try_tx_s!(CreateHtlcProto::decode( - try_tx_s!(HtlcType::from_str(&self.account_prefix)), + try_tx_s!(HtlcType::from_str(&self.protocol_info.account_prefix)), first_message.value.as_slice() )); let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); @@ -3509,7 +3857,7 @@ impl MarketCoinOps for TendermintCoin { } #[inline] - fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(MIN_TX_SATOSHIS, self.decimals) } + fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat(MIN_TX_SATOSHIS, self.protocol_info.decimals) } #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } @@ -3529,9 +3877,15 @@ impl MarketCoinOps for TendermintCoin { #[allow(unused_variables)] impl SwapOps for TendermintCoin { async fn send_taker_fee(&self, dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionResult { - self.send_taker_fee_for_denom(&dex_fee, self.denom.clone(), self.decimals, uuid, expire_at) - .compat() - .await + self.send_taker_fee_for_denom( + &dex_fee, + self.protocol_info.denom.clone(), + self.protocol_info.decimals, + uuid, + expire_at, + ) + .compat() + .await } async fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionResult { @@ -3540,8 +3894,8 @@ impl SwapOps for TendermintCoin { maker_payment_args.other_pubkey, maker_payment_args.secret_hash, maker_payment_args.amount, - self.denom.clone(), - self.decimals, + self.protocol_info.denom.clone(), + self.protocol_info.decimals, ) .compat() .await @@ -3553,8 +3907,8 @@ impl SwapOps for TendermintCoin { taker_payment_args.other_pubkey, taker_payment_args.secret_hash, taker_payment_args.amount, - self.denom.clone(), - self.decimals, + self.protocol_info.denom.clone(), + self.protocol_info.decimals, ) .compat() .await @@ -3573,7 +3927,7 @@ impl SwapOps for TendermintCoin { let msg = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto = try_tx_s!(CreateHtlcProto::decode( - try_tx_s!(HtlcType::from_str(&self.account_prefix)), + try_tx_s!(HtlcType::from_str(&self.protocol_info.account_prefix)), msg.value.as_slice() )); let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); @@ -3629,7 +3983,7 @@ impl SwapOps for TendermintCoin { let msg = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto = try_tx_s!(CreateHtlcProto::decode( - try_tx_s!(HtlcType::from_str(&self.account_prefix)), + try_tx_s!(HtlcType::from_str(&self.protocol_info.account_prefix)), msg.value.as_slice() )); let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); @@ -3694,21 +4048,21 @@ impl SwapOps for TendermintCoin { validate_fee_args.fee_tx, validate_fee_args.expected_sender, validate_fee_args.dex_fee, - self.decimals, + self.protocol_info.decimals, validate_fee_args.uuid, - self.denom.to_string(), + self.protocol_info.denom.to_string(), ) .compat() .await } async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { - self.validate_payment_for_denom(input, self.denom.clone(), self.decimals) + self.validate_payment_for_denom(input, self.protocol_info.denom.clone(), self.protocol_info.decimals) .await } async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { - self.validate_payment_for_denom(input, self.denom.clone(), self.decimals) + self.validate_payment_for_denom(input, self.protocol_info.denom.clone(), self.protocol_info.decimals) .await } @@ -3717,8 +4071,8 @@ impl SwapOps for TendermintCoin { if_my_payment_sent_args: CheckIfMyPaymentSentArgs<'_>, ) -> Result, String> { self.check_if_my_payment_sent_for_denom( - self.decimals, - self.denom.clone(), + self.protocol_info.decimals, + self.protocol_info.denom.clone(), if_my_payment_sent_args.other_pub, if_my_payment_sent_args.secret_hash, if_my_payment_sent_args.amount, @@ -3751,7 +4105,7 @@ impl SwapOps for TendermintCoin { let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); let htlc_proto = try_s!(ClaimHtlcProto::decode( - try_s!(HtlcType::from_str(&self.account_prefix)), + try_s!(HtlcType::from_str(&self.protocol_info.account_prefix)), msg.value.as_slice() )); let htlc = try_s!(ClaimHtlcMsg::try_from(htlc_proto)); @@ -3858,54 +4212,15 @@ pub fn tendermint_priv_key_policy( } } -pub(crate) fn chain_registry_name_from_account_prefix(ctx: &MmArc, prefix: &str) -> Option { - let Some(coins) = ctx.conf["coins"].as_array() else { - return None; - }; - - for coin in coins { - let protocol = coin - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("type") - .unwrap_or(&serde_json::Value::Null) - .as_str(); - - if protocol != Some(TENDERMINT_COIN_PROTOCOL_TYPE) { - continue; - } - - let coin_account_prefix = coin - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("protocol_data") - .unwrap_or(&serde_json::Value::Null) - .get("account_prefix") - .map(|t| t.as_str().unwrap_or_default()); - - if coin_account_prefix == Some(prefix) { - return coin - .get("protocol") - .unwrap_or(&serde_json::Value::Null) - .get("protocol_data") - .unwrap_or(&serde_json::Value::Null) - .get("chain_registry_name") - .map(|t| t.as_str().unwrap_or_default().to_owned()); - } - } - - None -} - pub(crate) async fn create_withdraw_msg_as_any( sender: AccountId, receiver: AccountId, denom: &Denom, amount: u64, - ibc_source_channel: Option, + ibc_source_channel: Option, ) -> Result> { if let Some(channel_id) = ibc_source_channel { - MsgTransfer::new_with_default_timeout(channel_id, sender, receiver, Coin { + MsgTransfer::new_with_default_timeout(channel_id.to_string(), sender, receiver, Coin { denom: denom.clone(), amount: amount.into(), }) @@ -3924,86 +4239,6 @@ pub(crate) async fn create_withdraw_msg_as_any( .map_to_mm(|e| WithdrawError::InternalError(e.to_string())) } -pub async fn get_ibc_transfer_channels( - source_registry_name: String, - destination_registry_name: String, -) -> IBCTransferChannelsResult { - #[derive(Deserialize)] - struct ChainRegistry { - channels: Vec, - } - - #[derive(Deserialize)] - struct ChannelInfo { - channel_id: String, - port_id: String, - } - - #[derive(Deserialize)] - struct IbcChannel { - #[allow(dead_code)] - chain_1: ChannelInfo, - chain_2: ChannelInfo, - ordering: String, - version: String, - tags: Option, - } - - let source_filename = format!("{}-{}.json", source_registry_name, destination_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_2.port_id == *IBC_OUT_SOURCE_PORT); - - let result: Vec = registry_object - .channels - .iter() - .map(|ch| IBCTransferChannel { - channel_id: ch.chain_2.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(); - - if result.is_empty() { - return MmError::err(IBCTransferChannelsRequestError::CouldNotFindChannel( - destination_registry_name, - )); - } - - Ok(IBCTransferChannelsResponse { - ibc_transfer_channels: result, - }) -} - fn extract_big_decimal_from_dec_coin(dec_coin: &DecCoin, decimals: u32) -> Result { let raw = BigDecimal::from_str(&dec_coin.amount)?; // `DecCoin` represents decimal numbers as integer-like strings where the last 18 digits are the decimal part. @@ -4025,8 +4260,17 @@ fn parse_expected_sequence_number(e: &str) -> MmResult) -> BytesJson { + let mut bytes = bytes.to_vec(); + + if let Some(token_id) = token_id { + bytes.extend_from_slice(&token_id); + } + sha256(&bytes).to_vec().into() +} + #[cfg(test)] -pub mod tendermint_coin_tests { +pub mod tendermint_falsecoin_tests { use super::*; use crate::DexFeeBurnDestination; @@ -4076,33 +4320,39 @@ pub mod tendermint_coin_tests { fn get_iris_usdc_ibc_protocol() -> TendermintProtocolInfo { TendermintProtocolInfo { decimals: 6, - denom: String::from("ibc/5C465997B4F582F602CD64E12031C6A6E18CAF1E6EDC9B5D808822DC0B5F850C"), - account_prefix: String::from("iaa"), - chain_id: String::from("nyancat-9"), + denom: Denom::from_str("ibc/5C465997B4F582F602CD64E12031C6A6E18CAF1E6EDC9B5D808822DC0B5F850C").unwrap(), + min_balance_for_ibc_routing: None, + account_prefix: String::from(IRIS_PREFIX), + chain_id: ChainId::from_str("nyancat-9").unwrap(), gas_price: None, - chain_registry_name: None, + ibc_channels: HashMap::new(), } } fn get_iris_protocol() -> TendermintProtocolInfo { + let mut ibc_channels = HashMap::new(); + ibc_channels.insert("cosmos".into(), ChannelId::new(0)); + TendermintProtocolInfo { decimals: 6, - denom: String::from("unyan"), - account_prefix: String::from("iaa"), - chain_id: String::from("nyancat-9"), + denom: Denom::from_str("unyan").unwrap(), + min_balance_for_ibc_routing: None, + account_prefix: String::from(IRIS_PREFIX), + chain_id: ChainId::from_str("nyancat-9").unwrap(), gas_price: None, - chain_registry_name: None, + ibc_channels, } } fn get_iris_ibc_nucleus_protocol() -> TendermintProtocolInfo { TendermintProtocolInfo { decimals: 6, - denom: String::from("ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C"), - account_prefix: String::from("nuc"), - chain_id: String::from("nucleus-testnet"), + denom: Denom::from_str("ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C").unwrap(), + min_balance_for_ibc_routing: None, + account_prefix: String::from(NUCLEUS_PREFIX), + chain_id: ChainId::from_str("nucleus-testnet").unwrap(), gas_price: None, - chain_registry_name: None, + ibc_channels: HashMap::new(), } } @@ -4151,14 +4401,14 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); // << BEGIN HTLC CREATION let to: AccountId = IRIS_TESTNET_HTLC_PAIR2_ADDRESS.parse().unwrap(); let amount = 1; - let amount_dec = big_decimal_from_sat_unsigned(amount, coin.decimals); + let amount_dec = big_decimal_from_sat_unsigned(amount, coin.decimals()); let mut sec = [0u8; 32]; common::os_rng(&mut sec).unwrap(); @@ -4168,7 +4418,7 @@ pub mod tendermint_coin_tests { let create_htlc_tx = coin .gen_create_htlc_tx( - coin.denom.clone(), + coin.protocol_info.denom.clone(), &to, amount.into(), sha256(&sec).as_slice(), @@ -4269,7 +4519,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -4323,7 +4573,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -4394,7 +4644,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -4587,7 +4837,7 @@ pub mod tendermint_coin_tests { nucleus_nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -4650,7 +4900,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -4733,7 +4983,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -4809,7 +5059,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -4881,7 +5131,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -4936,7 +5186,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -5055,7 +5305,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -5105,7 +5355,7 @@ pub mod tendermint_coin_tests { nodes, false, activation_policy, - false, + Default::default(), )) .unwrap(); @@ -5133,4 +5383,41 @@ pub mod tendermint_coin_tests { assert_eq!(expected_list, actual_list); } + + #[test] + fn test_get_ibc_channel_for_target_address() { + let nodes = vec![RpcNode::for_test(IRIS_TESTNET_RPC_URL)]; + let protocol_conf = get_iris_protocol(); + let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); + let conf = TendermintConf { + avg_blocktime: AVG_BLOCKTIME, + derivation_path: None, + }; + + let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + + let coin = block_on(TendermintCoin::init( + &ctx, + "IRIS-TEST".to_string(), + conf, + protocol_conf, + nodes, + false, + activation_policy, + Default::default(), + )) + .unwrap(); + + let expected_channel = ChannelId::new(0); + let expected_channel_str = "channel-0"; + + let actual_channel = block_on(coin.get_healthy_ibc_channel_for_address_prefix("cosmos")).unwrap(); + let actual_channel_str = actual_channel.to_string(); + + assert_eq!(expected_channel, actual_channel); + assert_eq!(expected_channel_str, actual_channel_str); + } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 589220b555..728541e9c3 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -3,7 +3,8 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{create_withdraw_msg_as_any, TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; -use crate::coin_errors::ValidatePaymentResult; +use crate::coin_errors::{AddressFromPubkeyError, ValidatePaymentResult}; +use crate::hd_wallet::HDAddressSelector; 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, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, @@ -29,7 +30,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::MmNumber; use primitives::hash::H256; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; use serde_json::Value as Json; use std::ops::Deref; use std::str::FromStr; @@ -58,7 +59,7 @@ impl Deref for TendermintToken { pub struct TendermintTokenProtocolInfo { pub platform: String, pub decimals: u8, - pub denom: String, + pub denom: Denom, } #[derive(Clone, Deserialize)] @@ -66,7 +67,6 @@ pub struct TendermintTokenActivationParams {} pub enum TendermintTokenInitError { Internal(String), - InvalidDenom(String), MyAddressError(String), CouldNotFetchBalance(String), } @@ -84,9 +84,8 @@ impl TendermintToken { ticker: String, platform_coin: TendermintCoin, decimals: u8, - denom: String, + denom: Denom, ) -> MmResult { - let denom = Denom::from_str(&denom).map_to_mm(|e| TendermintTokenInitError::InvalidDenom(e.to_string()))?; let token_impl = TendermintTokenImpl { abortable_system: platform_coin.abortable_system.create_subsystem()?, ticker, @@ -269,13 +268,19 @@ impl MarketCoinOps for TendermintToken { fn my_address(&self) -> MmResult { self.platform_coin.my_address() } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + self.platform_coin.address_from_pubkey(pubkey) + } + async fn get_public_key(&self) -> Result> { self.platform_coin.get_public_key().await } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { self.platform_coin.sign_message_hash(message) } - fn sign_message(&self, message: &str) -> SignatureResult { self.platform_coin.sign_message(message) } + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + self.platform_coin.sign_message(message, address) + } fn verify_message(&self, signature: &str, message: &str, address: &str) -> VerificationResult { self.platform_coin.verify_message(signature, message, address) @@ -358,16 +363,7 @@ impl MarketCoinOps for TendermintToken { impl MmCoin for TendermintToken { fn is_asset_chain(&self) -> bool { false } - fn wallet_only(&self, ctx: &MmArc) -> bool { - let coin_conf = crate::coin_conf(ctx, self.ticker()); - // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only - if coin_conf.is_null() { - return true; - } - let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); - - wallet_only_conf || self.platform_coin.is_keplr_from_ledger - } + fn wallet_only(&self, ctx: &MmArc) -> bool { self.platform_coin.wallet_only(ctx) } fn spawner(&self) -> WeakSpawner { self.abortable_system.weak_spawner() } @@ -378,14 +374,15 @@ impl MmCoin for TendermintToken { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some(); + let is_ibc_transfer = + to_address.prefix() != platform.protocol_info.account_prefix || req.ibc_source_channel.is_some(); let (account_id, maybe_priv_key) = platform .extract_account_id_and_private_key(req.from) .map_err(|e| WithdrawError::InternalError(e.to_string()))?; let (base_denom_balance, base_denom_balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) + .get_balance_as_unsigned_and_decimal(&account_id, &platform.protocol_info.denom, token.decimals()) .await?; let (balance_denom, balance_dec) = platform @@ -430,7 +427,11 @@ impl MmCoin for TendermintToken { let channel_id = if is_ibc_transfer { match &req.ibc_source_channel { Some(_) => req.ibc_source_channel, - None => Some(platform.detect_channel_id_for_ibc_transfer(&to_address).await?), + None => Some( + platform + .get_healthy_ibc_channel_for_address_prefix(to_address.prefix()) + .await?, + ), } } else { None @@ -441,7 +442,7 @@ impl MmCoin for TendermintToken { to_address.clone(), &token.denom, amount_denom, - channel_id.clone(), + channel_id, ) .await?; @@ -482,7 +483,7 @@ impl MmCoin for TendermintToken { } let fee_amount = Coin { - denom: platform.denom.clone(), + denom: platform.protocol_info.denom.clone(), amount: fee_amount_u64.into(), }; @@ -492,12 +493,11 @@ impl MmCoin for TendermintToken { let tx = platform .any_to_transaction_data(maybe_priv_key, msg_payload, &account_info, fee, timeout_height, &memo) + .await .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let internal_id = { - let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); - sha256(&hex_vec).to_vec().into() - }; + let internal_id = + super::tendermint_tx_internal_id(tx.tx_hash().unwrap_or_default().as_bytes(), Some(token.token_id())); Ok(TransactionDetails { tx, diff --git a/mm2src/coins/tendermint/wallet_connect.rs b/mm2src/coins/tendermint/wallet_connect.rs new file mode 100644 index 0000000000..d3242d4f77 --- /dev/null +++ b/mm2src/coins/tendermint/wallet_connect.rs @@ -0,0 +1,306 @@ +/// https://docs.reown.com/advanced/multichain/rpc-reference/cosmos-rpc +use base64::engine::general_purpose; +use base64::Engine; +use cosmrs::proto::cosmos::tx::v1beta1::TxRaw; +use kdf_walletconnect::chain::WcChainId; +use kdf_walletconnect::error::WalletConnectError; +use kdf_walletconnect::WalletConnectOps; +use kdf_walletconnect::{chain::WcRequestMethods, WalletConnectCtx}; +use mm2_err_handle::prelude::*; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::str::FromStr; + +use super::{CosmosTransaction, TendermintCoin, TendermintWalletConnectionType}; +use crate::MarketCoinOps; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct CosmosTxSignedData { + pub(crate) signature: CosmosTxSignature, + pub(crate) signed: CosmosSignData, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct CosmosTxSignature { + pub(crate) pub_key: CosmosTxPublicKey, + pub(crate) signature: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub(crate) struct CosmosTxPublicKey { + #[serde(rename = "type")] + pub(crate) key_type: String, + pub(crate) value: String, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CosmosSignData { + pub(crate) chain_id: String, + pub(crate) account_number: String, + #[serde(deserialize_with = "deserialize_vec_field")] + pub(crate) auth_info_bytes: Vec, + #[serde(deserialize_with = "deserialize_vec_field")] + pub(crate) body_bytes: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum CosmosAccountAlgo { + #[serde(rename = "secp256k1")] + Secp256k1, + #[serde(rename = "tendermint/PubKeySecp256k1")] + TendermintSecp256k1, +} + +impl FromStr for CosmosAccountAlgo { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "secp256k1" => Ok(Self::Secp256k1), + "tendermint/PubKeySecp256k1" => Ok(Self::TendermintSecp256k1), + _ => Err(format!("Unknown pubkey type: {s}")), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CosmosAccount { + pub address: String, + #[serde(deserialize_with = "deserialize_vec_field")] + pub pubkey: Vec, + pub algo: CosmosAccountAlgo, + #[serde(default)] + pub is_ledger: Option, +} + +#[async_trait::async_trait] +impl WalletConnectOps for TendermintCoin { + type Error = MmError; + type Params<'a> = serde_json::Value; + type SignTxData = TxRaw; + type SendTxData = CosmosTransaction; + + async fn wc_chain_id(&self, wc: &WalletConnectCtx) -> Result { + let chain_id = WcChainId::new_cosmos(self.protocol_info.chain_id.to_string()); + let session_topic = self.session_topic()?; + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + Ok(chain_id) + } + + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result { + let chain_id = self.wc_chain_id(wc).await?; + let session_topic = self.session_topic()?; + let method = if wc.is_ledger_connection(session_topic) { + WcRequestMethods::CosmosSignAmino + } else { + WcRequestMethods::CosmosSignDirect + }; + let data: CosmosTxSignedData = wc + .send_session_request_and_wait(session_topic, &chain_id, method, params) + .await?; + let signature = general_purpose::STANDARD + .decode(data.signature.signature) + .map_to_mm(|err| WalletConnectError::PayloadError(err.to_string()))?; + + Ok(TxRaw { + body_bytes: data.signed.body_bytes, + auth_info_bytes: data.signed.auth_info_bytes, + signatures: vec![signature], + }) + } + + async fn wc_send_tx<'a>( + &self, + _ctx: &WalletConnectCtx, + _params: Self::Params<'a>, + ) -> Result { + todo!() + } + + fn session_topic(&self) -> Result<&str, Self::Error> { + match self.wallet_type { + TendermintWalletConnectionType::WcLedger(ref session_topic) + | TendermintWalletConnectionType::Wc(ref session_topic) => Ok(session_topic), + _ => MmError::err(WalletConnectError::SessionError(format!( + "{} is not activated via WalletConnect", + self.ticker() + ))), + } + } +} + +pub async fn cosmos_get_accounts_impl( + wc: &WalletConnectCtx, + session_topic: &str, + chain_id: &str, +) -> MmResult { + let chain_id = WcChainId::new_cosmos(chain_id.to_string()); + wc.validate_update_active_chain_id(session_topic, &chain_id).await?; + + let (account, properties) = wc.get_account_and_properties_for_chain_id(session_topic, &chain_id)?; + + // Check if session has session_properties and return wallet account; + if let Some(props) = properties { + if let Some(keys) = &props.keys { + if let Some(key) = keys.iter().next() { + let pubkey = decode_data(&key.pub_key).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding pubkey payload: {err:?}")) + })?; + let address = decode_data(&key.address).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding address payload: {err:?}")) + })?; + let address = hex::encode(address); + let algo = CosmosAccountAlgo::from_str(&key.algo).map_to_mm(|err| { + WalletConnectError::PayloadError(format!("error decoding algo payload: {err:?}")) + })?; + + return Ok(CosmosAccount { + address, + pubkey, + algo, + is_ledger: Some(key.is_nano_ledger), + }); + } + } + } + + let params = serde_json::to_value(&account).unwrap(); + let accounts: Vec = wc + .send_session_request_and_wait(session_topic, &chain_id, WcRequestMethods::CosmosGetAccounts, params) + .await?; + + accounts.first().cloned().or_mm_err(|| { + WalletConnectError::NoAccountFound("Expected atleast an account from connected wallet".to_string()) + }) +} + +fn deserialize_vec_field<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + + match value { + Value::Object(map) => map + .iter() + .map(|(_, value)| { + value + .as_u64() + .ok_or_else(|| serde::de::Error::custom("Invalid byte value")) + .and_then(|n| { + if n <= 255 { + Ok(n as u8) + } else { + Err(serde::de::Error::custom("Invalid byte value")) + } + }) + }) + .collect(), + Value::Array(arr) => arr + .into_iter() + .map(|v| { + v.as_u64() + .ok_or_else(|| serde::de::Error::custom("Invalid byte value")) + .map(|n| n as u8) + }) + .collect(), + Value::String(data) => { + let data = decode_data(&data).map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(data) + }, + _ => Err(serde::de::Error::custom("Pubkey must be an string, object or array")), + } +} + +fn decode_data(encoded: &str) -> Result, &'static str> { + if encoded.chars().all(|c| c.is_ascii_hexdigit()) && encoded.len() % 2 == 0 { + hex::decode(encoded).map_err(|_| "Invalid hex encoding") + } else if encoded.contains('=') || encoded.contains('/') || encoded.contains('+') || encoded.len() % 4 == 0 { + general_purpose::STANDARD + .decode(encoded) + .map_err(|_| "Invalid base64 encoding") + } else { + Err("Unknown encoding format") + } +} + +#[cfg(test)] +mod test_cosmos_walletconnect { + use serde_json::json; + + use super::{decode_data, CosmosSignData, CosmosTxPublicKey, CosmosTxSignature, CosmosTxSignedData}; + + #[test] + fn test_decode_base64() { + // "Hello world" in base64 + let base64_data = "SGVsbG8gd29ybGQ="; + let expected = b"Hello world".to_vec(); + let result = decode_data(base64_data); + assert_eq!(result.unwrap(), expected, "Base64 decoding failed"); + } + + #[test] + fn test_decode_hex() { + // "Hello world" in hex + let hex_data = "48656c6c6f20776f726c64"; + let expected = b"Hello world".to_vec(); + let result = decode_data(hex_data); + assert_eq!(result.unwrap(), expected, "Hex decoding failed"); + } + + #[test] + fn test_deserialize_sign_message_response() { + let json = json!({ + "signature": { + "signature": "eGrmDGKTmycxJO56yTQORDzTFjBEBgyBmHc8ey6FbHh9WytzgsJilYBywz5uludhyKePZdRwznamg841fXw50Q==", + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "AjqZ1rq/EsPAb4SA6l0qjpVMHzqXotYXz23D5kOceYYu" + } + }, + "signed": { + "chainId": "cosmoshub-4", + "authInfoBytes": "0a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21023a99d6babf12c3c06f8480ea5d2a8e954c1f3a97a2d617cf6dc3e6439c79862e12040a020801180212140a0e0a057561746f6d1205313837353010c8d007", + "bodyBytes": "0a8e010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126e0a2d636f736d6f7331376c386432737973646e3667683636786d366664666b6575333634703836326a68396c6e6667122d636f736d6f7331376c386432737973646e3667683636786d366664666b6575333634703836326a68396c6e66671a0e0a057561746f6d12053430303030189780e00a", + "accountNumber": "2934714" + } + }); + let expected_tx = CosmosTxSignedData { + signature: CosmosTxSignature { + pub_key: CosmosTxPublicKey { + key_type: "tendermint/PubKeySecp256k1".to_owned(), + value: "AjqZ1rq/EsPAb4SA6l0qjpVMHzqXotYXz23D5kOceYYu".to_owned(), + }, + signature: "eGrmDGKTmycxJO56yTQORDzTFjBEBgyBmHc8ey6FbHh9WytzgsJilYBywz5uludhyKePZdRwznamg841fXw50Q==" + .to_owned(), + }, + signed: CosmosSignData { + chain_id: "cosmoshub-4".to_owned(), + account_number: "2934714".to_owned(), + auth_info_bytes: vec![ + 10, 80, 10, 70, 10, 31, 47, 99, 111, 115, 109, 111, 115, 46, 99, 114, 121, 112, 116, 111, 46, 115, + 101, 99, 112, 50, 53, 54, 107, 49, 46, 80, 117, 98, 75, 101, 121, 18, 35, 10, 33, 2, 58, 153, 214, + 186, 191, 18, 195, 192, 111, 132, 128, 234, 93, 42, 142, 149, 76, 31, 58, 151, 162, 214, 23, 207, + 109, 195, 230, 67, 156, 121, 134, 46, 18, 4, 10, 2, 8, 1, 24, 2, 18, 20, 10, 14, 10, 5, 117, 97, + 116, 111, 109, 18, 5, 49, 56, 55, 53, 48, 16, 200, 208, 7, + ], + body_bytes: vec![ + 10, 142, 1, 10, 28, 47, 99, 111, 115, 109, 111, 115, 46, 98, 97, 110, 107, 46, 118, 49, 98, 101, + 116, 97, 49, 46, 77, 115, 103, 83, 101, 110, 100, 18, 110, 10, 45, 99, 111, 115, 109, 111, 115, 49, + 55, 108, 56, 100, 50, 115, 121, 115, 100, 110, 54, 103, 104, 54, 54, 120, 109, 54, 102, 100, 102, + 107, 101, 117, 51, 54, 52, 112, 56, 54, 50, 106, 104, 57, 108, 110, 102, 103, 18, 45, 99, 111, 115, + 109, 111, 115, 49, 55, 108, 56, 100, 50, 115, 121, 115, 100, 110, 54, 103, 104, 54, 54, 120, 109, + 54, 102, 100, 102, 107, 101, 117, 51, 54, 52, 112, 56, 54, 50, 106, 104, 57, 108, 110, 102, 103, + 26, 14, 10, 5, 117, 97, 116, 111, 109, 18, 5, 52, 48, 48, 48, 48, 24, 151, 128, 224, 10, + ], + }, + }; + + let actual_tx = serde_json::from_value::(json).unwrap(); + assert_eq!(expected_tx, actual_tx); + } +} diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index ce2511b5b0..13c8beb7a5 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -3,8 +3,8 @@ use super::{CoinBalance, CommonSwapOpsV2, FindPaymentSpendError, FundingTxSpend, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee, TransactionEnum, TransactionFut}; -use crate::coin_errors::ValidatePaymentResult; -use crate::hd_wallet::AddrToString; +use crate::coin_errors::{AddressFromPubkeyError, ValidatePaymentResult}; +use crate::hd_wallet::{AddrToString, HDAddressSelector}; use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MmCoinEnum, NegotiateSwapContractAddrErr, ParseCoinAssocTypes, PaymentInstructionArgs, @@ -28,7 +28,7 @@ use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(any(test, feature = "for-tests"))] use mocktopus::macros::*; -use rpc::v1::types::Bytes as BytesJson; +use rpc::v1::types::{Bytes as BytesJson, H264 as H264Json}; use serde_json::Value as Json; use std::fmt::{Display, Formatter}; use std::ops::Deref; @@ -65,11 +65,15 @@ impl MarketCoinOps for TestCoin { fn my_address(&self) -> MmResult { unimplemented!() } + fn address_from_pubkey(&self, _pubkey: &H264Json) -> MmResult { unimplemented!() } + async fn get_public_key(&self) -> Result> { unimplemented!() } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } - fn sign_message(&self, _message: &str) -> SignatureResult { unimplemented!() } + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { + unimplemented!() + } fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult { unimplemented!() @@ -109,7 +113,7 @@ impl MarketCoinOps for TestCoin { fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } - fn is_kmd(&self) -> bool { &self.ticker == "KMD" } + fn should_burn_directly(&self) -> bool { &self.ticker == "KMD" } fn should_burn_dex_fee(&self) -> bool { false } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 35e7281d4e..53e9bc7cdb 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -260,29 +260,61 @@ pub struct AdditionalTxData { pub received_by_me: u64, pub spent_by_me: u64, pub fee_amount: u64, - pub unused_change: u64, pub kmd_rewards: Option, } /// The fee set from coins config #[derive(Debug)] -pub enum TxFee { +pub enum FeeRate { /// Tell the coin that it should request the fee from daemon RPC and calculate it relying on tx size Dynamic(EstimateFeeMethod), /// Tell the coin that it has fixed tx fee per kb. FixedPerKb(u64), } -/// The actual "runtime" fee that is received from RPC in case of dynamic calculation +/// The actual "runtime" tx fee rate (per kb) that is received from RPC in case of dynamic calculation +/// or fixed tx fee rate #[derive(Copy, Clone, Debug, PartialEq)] -pub enum ActualTxFee { +pub enum ActualFeeRate { /// fee amount per Kbyte received from coin RPC Dynamic(u64), - /// Use specified amount per each 1 kb of transaction and also per each output less than amount. + /// Use specified fee amount per each 1 kb of transaction and also per each output less than the fee amount. /// Used by DOGE, but more coins might support it too. FixedPerKb(u64), } +impl ActualFeeRate { + fn get_tx_fee(&self, tx_size: u64) -> u64 { + match self { + ActualFeeRate::Dynamic(fee_rate) => (fee_rate * tx_size) / KILO_BYTE, + // return fee_rate here as swap spend transaction size is always less than 1 kb + ActualFeeRate::FixedPerKb(fee_rate) => { + let tx_size_kb = if tx_size % KILO_BYTE == 0 { + tx_size / KILO_BYTE + } else { + tx_size / KILO_BYTE + 1 + }; + fee_rate * tx_size_kb + }, + } + } + + /// Return extra tx fee for the change output as p2pkh + fn get_tx_fee_for_change(&self, tx_size: u64) -> u64 { + match self { + ActualFeeRate::Dynamic(fee_rate) => (*fee_rate * P2PKH_OUTPUT_LEN) / KILO_BYTE, + ActualFeeRate::FixedPerKb(fee_rate) => { + // take into account the change output if tx_size_kb(tx with change) > tx_size_kb(tx without change) + if tx_size % KILO_BYTE + P2PKH_OUTPUT_LEN > KILO_BYTE { + *fee_rate + } else { + 0 + } + }, + } + } +} + /// Fee policy applied on transaction creation pub enum FeePolicy { /// Send the exact amount specified in output(s), fee is added to spent input amount @@ -577,7 +609,7 @@ pub struct UtxoCoinFields { /// Emercoin has 6 /// Bitcoin Diamond has 7 pub decimals: u8, - pub tx_fee: TxFee, + pub tx_fee: FeeRate, /// Minimum transaction value at which the value is not less than fee pub dust_amount: u64, /// RPC client @@ -592,6 +624,7 @@ pub struct UtxoCoinFields { /// The cache of recently send transactions used to track the spent UTXOs and replace them with new outputs /// The daemon needs some time to update the listunspent list for address which makes it return already spent UTXOs /// This cache helps to prevent UTXO reuse in such cases + // TODO: change the type of `recently_spent_outpoints` to `AsyncMutex>` to better support HD wallets. pub recently_spent_outpoints: AsyncMutex, pub tx_hash_algo: TxHashAlgo, /// The flag determines whether to use mature unspent outputs *only* to generate transactions. @@ -839,18 +872,15 @@ pub trait UtxoTxBroadcastOps { #[async_trait] #[cfg_attr(test, mockable)] pub trait UtxoTxGenerationOps { - async fn get_tx_fee(&self) -> UtxoRpcResult; + async fn get_fee_rate(&self) -> UtxoRpcResult; /// Calculates interest if the coin is KMD /// Adds the value to existing output to my_script_pub or creates additional interest output /// returns transaction and data as is if the coin is not KMD - async fn calc_interest_if_required( - &self, - mut unsigned: TransactionInputSigner, - mut data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)>; + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult; + + /// Returns `true` if this coin supports Komodo-style interest accrual; otherwise, returns `false`. + fn supports_interest(&self) -> bool; } /// The UTXO address balance scanner. @@ -1747,7 +1777,6 @@ where { let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.activated_key_or_err()); - let mut builder = UtxoTxBuilder::new(coin) .await .add_available_inputs(unspents) diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index e38a59ce27..062f15fc66 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1,8 +1,9 @@ use super::*; use crate::coin_balance::{EnableCoinBalanceError, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; -use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDExtractPubkeyError, HDXPubExtractor, - TrezorCoinError, WithdrawSenderAddress}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, + HDExtractPubkeyError, HDXPubExtractor, SettingEnabledAddressError, TrezorCoinError, + WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; @@ -34,6 +35,7 @@ use keys::CashAddress; pub use keys::NetworkPrefix as CashAddrPrefix; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; +use rpc::v1::types::H264 as H264Json; use serde_json::{self as json, Value as Json}; use serialization::{deserialize, CoinVariant}; use std::sync::MutexGuard; @@ -700,17 +702,13 @@ impl UtxoTxBroadcastOps for BchCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for BchCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] @@ -1138,6 +1136,11 @@ impl MarketCoinOps for BchCoin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = Public::Compressed((*pubkey).into()); + Ok(UtxoCommonOps::address_from_pubkey(self, &pubkey).to_string()) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) @@ -1147,8 +1150,8 @@ impl MarketCoinOps for BchCoin { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { @@ -1386,6 +1389,15 @@ impl HDWalletCoinOps for BchCoin { } fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } + + async fn received_enabled_address_from_hw_wallet( + &self, + enabled_address: UtxoHDAddress, + ) -> MmResult<(), SettingEnabledAddressError> { + utxo_common::received_enabled_address_from_hw_wallet(self, enabled_address.address) + .await + .mm_err(SettingEnabledAddressError::Internal) + } } impl HDCoinWithdrawOps for BchCoin {} @@ -1703,7 +1715,7 @@ mod bch_tests { #[test] fn test_sign_message() { let (_ctx, coin) = tbch_coin_for_test(); - let signature = coin.sign_message("test").unwrap(); + let signature = coin.sign_message("test", None).unwrap(); assert_eq!( signature, "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=" diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 82da94209b..7a29ab81c5 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -2,9 +2,10 @@ use super::utxo_common::utxo_prepare_addresses_for_balance_stream_if_enabled; use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; -use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, - HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, + HDExtractPubkeyError, HDXPubExtractor, SettingEnabledAddressError, TrezorCoinError, + WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, @@ -40,6 +41,7 @@ use futures::{FutureExt, TryFutureExt}; use keys::AddressHashEnum; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; +use rpc::v1::types::H264 as H264Json; use serde::Serialize; use serialization::CoinVariant; use utxo_signer::UtxoSignerOps; @@ -313,17 +315,13 @@ impl UtxoTxBroadcastOps for QtumCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for QtumCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] @@ -761,6 +759,11 @@ impl MarketCoinOps for QtumCoin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = Public::Compressed((*pubkey).into()); + Ok(UtxoCommonOps::address_from_pubkey(self, &pubkey).to_string()) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) @@ -770,8 +773,8 @@ impl MarketCoinOps for QtumCoin { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { @@ -1034,6 +1037,15 @@ impl HDWalletCoinOps for QtumCoin { } fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } + + async fn received_enabled_address_from_hw_wallet( + &self, + enabled_address: UtxoHDAddress, + ) -> MmResult<(), SettingEnabledAddressError> { + utxo_common::received_enabled_address_from_hw_wallet(self, enabled_address.address) + .await + .mm_err(SettingEnabledAddressError::Internal) + } } impl HDCoinWithdrawOps for QtumCoin {} diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index b83289d97c..3723109720 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -294,10 +294,9 @@ impl QtumCoin { let signed = sign_tx(unsigned, key_pair, utxo.conf.signature_version, utxo.conf.fork_id)?; - let miner_fee = data.fee_amount + data.unused_change; let generated_tx = GenerateQrc20TxResult { signed, - miner_fee, + miner_fee: data.fee_amount, gas_fee, }; diff --git a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs index e7c5311db2..3cf3996f09 100644 --- a/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs +++ b/mm2src/coins/utxo/rpc_clients/electrum_rpc/client.rs @@ -806,8 +806,12 @@ impl UtxoRpcClientOps for ElectrumClient { // If the plain pubkey is available, fetch the UTXOs found in P2PK outputs as well (if any). if let Some(pubkey) = address.pubkey() { - let p2pk_output_script = output_script_p2pk(pubkey); - output_scripts.push(p2pk_output_script); + // We don't want to show P2PK outputs along with segwit ones (P2WPKH). + // Allow listing the P2PK outputs only if the address is not segwit (i.e. show P2PK outputs along with P2PKH). + if !address.addr_format().is_segwit() { + let p2pk_output_script = output_script_p2pk(pubkey); + output_scripts.push(p2pk_output_script); + } } let this = self.clone(); @@ -937,8 +941,11 @@ impl UtxoRpcClientOps for ElectrumClient { // If the plain pubkey is available, fetch the balance found in P2PK output as well (if any). if let Some(pubkey) = address.pubkey() { - let p2pk_output_script = output_script_p2pk(pubkey); - hashes.push(hex::encode(electrum_script_hash(&p2pk_output_script))); + // Show the balance in P2PK outputs only for the non-segwit legacy addresses (P2PKH). + if !address.addr_format().is_segwit() { + let p2pk_output_script = output_script_p2pk(pubkey); + hashes.push(hex::encode(electrum_script_hash(&p2pk_output_script))); + } } let this = self.clone(); diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 1ca37e3eba..5a537e42dc 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -3,16 +3,17 @@ //! Tracking issue: https://github.com/KomodoPlatform/atomicDEX-API/issues/701 //! More info about the protocol and implementation guides can be found at https://slp.dev/ -use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::hd_wallet::HDAddressSelector; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::bch::BchCoin; use crate::utxo::bchd_grpc::{check_slp_transaction, validate_slp_utxos, ValidateSlpUtxosErr}; use crate::utxo::rpc_clients::{UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcResult}; use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_script, UtxoTxBuilder}; -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::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualFeeRate, BroadcastTxErr, FeePolicy, + GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, + UtxoTxBroadcastOps, UtxoTxGenerationOps}; use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DerivationMethod, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, @@ -44,7 +45,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; use primitives::hash::H256; -use rpc::v1::types::{Bytes as BytesJson, ToTxHash, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, ToTxHash, H256 as H256Json, H264 as H264Json}; use script::bytes::Bytes; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; @@ -1073,19 +1074,13 @@ impl UtxoTxBroadcastOps for SlpToken { #[async_trait] impl UtxoTxGenerationOps for SlpToken { - async fn get_tx_fee(&self) -> UtxoRpcResult { self.platform_coin.get_tx_fee().await } + async fn get_fee_rate(&self) -> UtxoRpcResult { self.platform_coin.get_fee_rate().await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - self.platform_coin - .calc_interest_if_required(unsigned, data, my_script_pub, dust) - .await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + self.platform_coin.calc_interest_if_required(unsigned).await } + + fn supports_interest(&self) -> bool { self.platform_coin.supports_interest() } } #[async_trait] @@ -1108,6 +1103,11 @@ impl MarketCoinOps for SlpToken { slp_address.encode().map_to_mm(MyAddressError::InternalError) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + // TODO: We have two `address_from_pubkey`s, one in MarketCoinOps and one in UtxoCommonOps. We should give them different names. + MarketCoinOps::address_from_pubkey(&self.platform_coin, pubkey) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.platform_coin.as_ref())?; Ok(pubkey.to_string()) @@ -1117,8 +1117,8 @@ impl MarketCoinOps for SlpToken { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature: &str, message: &str, address: &str) -> VerificationResult { @@ -1128,7 +1128,7 @@ impl MarketCoinOps for SlpToken { let signature = CompactSignature::try_from(STANDARD.decode(signature)?) .map_to_mm(|err| VerificationError::SignatureDecodingError(err.to_string()))?; let pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; - let address_from_pubkey = self.platform_coin.address_from_pubkey(&pubkey); + let address_from_pubkey = UtxoCommonOps::address_from_pubkey(&self.platform_coin, &pubkey); let slp_address = self .platform_coin .slp_address(&address_from_pubkey) @@ -1541,11 +1541,11 @@ impl MmCoin for SlpToken { match req.fee { Some(WithdrawFee::UtxoFixed { amount }) => { let fixed = sat_from_big_decimal(&amount, platform_decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::FixedPerKb(fixed)) + tx_builder = tx_builder.with_fee(ActualFeeRate::FixedPerKb(fixed)) }, Some(WithdrawFee::UtxoPerKbyte { amount }) => { let dynamic = sat_from_big_decimal(&amount, platform_decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + tx_builder = tx_builder.with_fee(ActualFeeRate::Dynamic(dynamic)); }, Some(fee_policy) => { let error = format!( @@ -2167,7 +2167,7 @@ mod slp_tests { let (_ctx, bch) = tbch_coin_for_test(); let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch, 0).unwrap(); - let signature = fusd.sign_message("test").unwrap(); + let signature = fusd.sign_message("test", None).unwrap(); assert_eq!( signature, "ILuePKMsycXwJiNDOT7Zb7TfIlUW7Iq+5ylKd15AK72vGVYXbnf7Gj9Lk9MFV+6Ub955j7MiAkp0wQjvuIoRPPA=" diff --git a/mm2src/coins/utxo/tx_history_events.rs b/mm2src/coins/utxo/tx_history_events.rs index c336e6fbb0..e0404228a9 100644 --- a/mm2src/coins/utxo/tx_history_events.rs +++ b/mm2src/coins/utxo/tx_history_events.rs @@ -1,5 +1,5 @@ use crate::TransactionDetails; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -14,14 +14,14 @@ impl TxHistoryEventStreamer { pub fn new(coin: String) -> Self { Self { coin } } #[inline(always)] - pub fn derive_streamer_id(coin: &str) -> String { format!("TX_HISTORY:{coin}") } + pub fn derive_streamer_id(coin: &str) -> StreamerId { StreamerId::TxHistory { coin: coin.to_string() } } } #[async_trait] impl EventStreamer for TxHistoryEventStreamer { type DataInType = Vec; - fn streamer_id(&self) -> String { Self::derive_streamer_id(&self.coin) } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id(&self.coin) } async fn handle( self, diff --git a/mm2src/coins/utxo/utxo_balance_events.rs b/mm2src/coins/utxo/utxo_balance_events.rs index 8fdde86ab7..9451675cdb 100644 --- a/mm2src/coins/utxo/utxo_balance_events.rs +++ b/mm2src/coins/utxo/utxo_balance_events.rs @@ -12,7 +12,7 @@ use common::log; use futures::channel::oneshot; use futures::StreamExt; use keys::Address; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use std::collections::{HashMap, HashSet}; macro_rules! try_or_continue { @@ -40,14 +40,18 @@ impl UtxoBalanceEventStreamer { } } - pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } + pub fn derive_streamer_id(coin: &str) -> StreamerId { StreamerId::Balance { coin: coin.to_string() } } } #[async_trait] impl EventStreamer for UtxoBalanceEventStreamer { type DataInType = ScripthashNotification; - fn streamer_id(&self) -> String { format!("BALANCE:{}", self.coin.ticker()) } + fn streamer_id(&self) -> StreamerId { + StreamerId::Balance { + coin: self.coin.ticker().to_string(), + } + } async fn handle( self, diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index b916afc232..05513dd93b 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -5,7 +5,7 @@ use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientSettings, ElectrumC use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError}; -use crate::utxo::{output_script, ElectrumBuilderArgs, RecentlySpentOutPoints, TxFee, UtxoCoinConf, UtxoCoinFields, +use crate::utxo::{output_script, ElectrumBuilderArgs, FeeRate, RecentlySpentOutPoints, UtxoCoinConf, UtxoCoinFields, UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, UTXO_DUST_AMOUNT}; use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, @@ -19,7 +19,6 @@ use derive_more::Display; use futures::channel::mpsc::{channel, Receiver as AsyncReceiver}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; -use keys::bytes::Bytes; pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressScriptType, KeyPair, Private, Public, Secret}; use mm2_core::mm_ctx::MmArc; @@ -298,11 +297,6 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { } let hd_wallet_rmd160 = self.trezor_wallet_rmd160()?; - // For now, use a default script pubkey. - // TODO change the type of `recently_spent_outpoints` to `AsyncMutex>` - let my_script_pubkey = Bytes::new(); - let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); - let address_format = self.address_format()?; let path_to_coin = conf .derivation_path @@ -327,6 +321,9 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { address_format, }; + // TODO: Creating a dummy output script for now. We better set it to the enabled address output script. + let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(Default::default())); + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. let abortable_system: AbortableQueue = self.ctx().abortable_system.create_subsystem()?; @@ -467,9 +464,9 @@ pub trait UtxoCoinBuilderCommonOps { Ok(self.conf()["decimals"].as_u64().unwrap_or(8) as u8) } - async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { + async fn tx_fee(&self, rpc_client: &UtxoRpcClientEnum) -> UtxoCoinBuildResult { let tx_fee = match self.conf()["txfee"].as_u64() { - None => TxFee::FixedPerKb(1000), + None => FeeRate::FixedPerKb(1000), Some(0) => { let fee_method = match &rpc_client { UtxoRpcClientEnum::Electrum(_) => EstimateFeeMethod::Standard, @@ -479,9 +476,9 @@ pub trait UtxoCoinBuilderCommonOps { .await .map_to_mm(UtxoCoinBuildError::ErrorDetectingFeeMethod)?, }; - TxFee::Dynamic(fee_method) + FeeRate::Dynamic(fee_method) }, - Some(fee) => TxFee::FixedPerKb(fee), + Some(fee) => FeeRate::FixedPerKb(fee), }; Ok(tx_fee) } @@ -681,7 +678,7 @@ pub trait UtxoCoinBuilderCommonOps { } #[cfg(not(target_arch = "wasm32"))] - fn tx_cache_path(&self) -> PathBuf { self.ctx().dbdir().join("TX_CACHE") } + fn tx_cache_path(&self) -> PathBuf { self.ctx().global_dir().join("TX_CACHE") } fn block_header_status_channel( &self, diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index db7ad1f65a..6d13af3c0d 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2,7 +2,7 @@ use super::*; use crate::coin_balance::{HDAddressBalance, HDWalletBalanceObject, HDWalletBalanceOps}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::eth::EthCoinType; -use crate::hd_wallet::{HDCoinAddress, HDCoinHDAccount, HDCoinWithdrawOps, TrezorCoinError}; +use crate::hd_wallet::{HDAddressSelector, HDCoinAddress, HDCoinHDAccount, HDCoinWithdrawOps, TrezorCoinError}; use crate::lp_price::get_base_price_in_rel; use crate::rpc_command::init_withdraw::WithdrawTaskHandleShared; use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentInfo, UnspentMap, UtxoRpcClientEnum, @@ -76,11 +76,10 @@ pub const DEFAULT_FEE_VOUT: usize = 0; pub const DEFAULT_SWAP_TX_SPEND_SIZE: u64 = 496; // TODO: checking with komodo-like tx size, included the burn output pub const DEFAULT_SWAP_VOUT: usize = 0; pub const DEFAULT_SWAP_VIN: usize = 0; -const MIN_BTC_TRADING_VOL: &str = "0.00777"; -macro_rules! true_or { +macro_rules! return_err_if { ($cond: expr, $etype: expr) => { - if !$cond { + if $cond { return Err(MmError::new($etype)); } }; @@ -95,18 +94,18 @@ lazy_static! { pub const HISTORY_TOO_LARGE_ERR_CODE: i64 = -1; -pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { +pub async fn get_fee_rate(coin: &UtxoCoinFields) -> UtxoRpcResult { let conf = &coin.conf; match &coin.tx_fee { - TxFee::Dynamic(method) => { - let fee = coin + FeeRate::Dynamic(method) => { + let fee_rate = coin .rpc_client .estimate_fee_sat(coin.decimals, method, &conf.estimate_fee_mode, conf.estimate_fee_blocks) .compat() .await?; - Ok(ActualTxFee::Dynamic(fee)) + Ok(ActualFeeRate::Dynamic(fee_rate)) }, - TxFee::FixedPerKb(satoshis) => Ok(ActualTxFee::FixedPerKb(*satoshis)), + FeeRate::FixedPerKb(satoshis) => Ok(ActualFeeRate::FixedPerKb(*satoshis)), } } @@ -139,6 +138,27 @@ where }) } +pub(crate) async fn received_enabled_address_from_hw_wallet( + coin: &Coin, + enabled_address: Address, +) -> MmResult<(), String> +where + Coin: AsRef, +{ + let my_script_pubkey = match output_script(&enabled_address) { + Ok(script) => script.to_bytes(), + Err(e) => { + return MmError::err(format!( + "Error generating the output_script for the enabled_address={}: {}", + enabled_address, e + )); + }, + }; + let mut recently_spent_outputs = coin.as_ref().recently_spent_outpoints.lock().await; + *recently_spent_outputs = RecentlySpentOutPoints::new(my_script_pubkey); + Ok(()) +} + pub async fn produce_hd_address_scanner(coin: &T) -> BalanceResult where T: AsRef, @@ -270,37 +290,22 @@ where pub fn derivation_method(coin: &UtxoCoinFields) -> &DerivationMethod { &coin.derivation_method } -/// returns the fee required to be paid for HTLC spend transaction +/// returns the tx fee required to be paid for HTLC spend transaction pub async fn get_htlc_spend_fee( coin: &T, tx_size: u64, stage: &FeeApproxStage, ) -> UtxoRpcResult { - let coin_fee = coin.get_tx_fee().await?; - let mut fee = match coin_fee { - // atomic swap payment spend transaction is slightly more than 300 bytes in average as of now - ActualTxFee::Dynamic(fee_per_kb) => { - let fee_per_kb = increase_dynamic_fee_by_stage(&coin, fee_per_kb, stage); - (fee_per_kb * tx_size) / KILO_BYTE - }, - // return satoshis here as swap spend transaction size is always less than 1 kb - ActualTxFee::FixedPerKb(satoshis) => { - let tx_size_kb = if tx_size % KILO_BYTE == 0 { - tx_size / KILO_BYTE - } else { - tx_size / KILO_BYTE + 1 - }; - satoshis * tx_size_kb + let fee_rate = coin.get_fee_rate().await?; + let fee_rate = match fee_rate { + ActualFeeRate::Dynamic(dynamic_fee_rate) => { + // increase dynamic fee for a chance if it grows in the swap + ActualFeeRate::Dynamic(increase_dynamic_fee_by_stage(coin, dynamic_fee_rate, stage)) }, + ActualFeeRate::FixedPerKb(_) => fee_rate, }; - if coin.as_ref().conf.force_min_relay_fee { - let relay_fee = coin.as_ref().rpc_client.get_relay_fee().compat().await?; - let relay_fee_sat = sat_from_big_decimal(&relay_fee, coin.as_ref().decimals)?; - if fee < relay_fee_sat { - fee = relay_fee_sat; - } - } - Ok(fee) + let min_relay_fee_rate = get_min_relay_rate(coin).await?; + Ok(get_tx_fee_with_relay_fee(&fee_rate, tx_size, min_relay_fee_rate)) } pub fn addresses_from_script(coin: &T, script: &Script) -> Result, String> { @@ -384,6 +389,9 @@ pub fn my_public_key(coin: &UtxoCoinFields) -> Result<&Public, MmError MmError::err(UnexpectedDerivationMethod::UnsupportedError( "`PrivKeyPolicy::Metamask` is not supported in this context".to_string(), )), + PrivKeyPolicy::WalletConnect { .. } => MmError::err(UnexpectedDerivationMethod::UnsupportedError( + "`PrivKeyPolicy::WalletConnect` is not supported in this context".to_string(), + )), } } @@ -469,18 +477,21 @@ pub fn output_script_checked(coin: &UtxoCoinFields, addr: &Address) -> MmResult< pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { coin: &'a T, from: Option
, + /// The required inputs that *must* be added in the resulting tx + required_inputs: Vec, /// The available inputs that *can* be included in the resulting tx available_inputs: Vec, + outputs: Vec, fee_policy: FeePolicy, - fee: Option, + fee: Option, gas_fee: Option, tx: TransactionInputSigner, - change: u64, sum_inputs: u64, - sum_outputs_value: u64, - tx_fee: u64, - min_relay_fee: Option, + sum_outputs: u64, + tx_fee_needed: u64, + min_relay_fee_rate: Option, dust: Option, + interest: u64, } impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { @@ -489,16 +500,18 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { tx: coin.as_ref().transaction_preimage(), coin, from: coin.as_ref().derivation_method.single_addr().await, + required_inputs: vec![], available_inputs: vec![], + outputs: vec![], fee_policy: FeePolicy::SendExact, fee: None, gas_fee: None, - change: 0, sum_inputs: 0, - sum_outputs_value: 0, - tx_fee: 0, - min_relay_fee: None, + sum_outputs: 0, + tx_fee_needed: 0, + min_relay_fee_rate: None, dust: None, + interest: 0, } } @@ -513,14 +526,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } pub fn add_required_inputs(mut self, inputs: impl IntoIterator) -> Self { - self.tx - .inputs - .extend(inputs.into_iter().map(|input| UnsignedTransactionInput { - previous_output: input.outpoint, - prev_script: input.script, - sequence: SEQUENCE_FINAL, - amount: input.value, - })); + self.required_inputs.extend(inputs); self } @@ -532,7 +538,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } pub fn add_outputs(mut self, outputs: impl IntoIterator) -> Self { - self.tx.outputs.extend(outputs); + self.outputs.extend(outputs); self } @@ -541,7 +547,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { self } - pub fn with_fee(mut self, fee: ActualTxFee) -> Self { + pub fn with_fee(mut self, fee: ActualFeeRate) -> Self { self.fee = Some(fee); self } @@ -554,75 +560,136 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { self } - /// Recalculates fee and checks whether transaction is complete (inputs collected cover the outputs) - fn update_fee_and_check_completeness( - &mut self, - from_addr_format: &UtxoAddressFormat, - actual_tx_fee: &ActualTxFee, - ) -> bool { - self.tx_fee = match &actual_tx_fee { - ActualTxFee::Dynamic(f) => { - let transaction = UtxoTx::from(self.tx.clone()); - let v_size = tx_size_in_v_bytes(from_addr_format, &transaction); - (f * v_size as u64) / KILO_BYTE - }, - ActualTxFee::FixedPerKb(f) => { - let transaction = UtxoTx::from(self.tx.clone()); - let v_size = tx_size_in_v_bytes(from_addr_format, &transaction) as u64; - let v_size_kb = if v_size % KILO_BYTE == 0 { - v_size / KILO_BYTE - } else { - v_size / KILO_BYTE + 1 - }; - f * v_size_kb + fn required_amount(&self) -> u64 { + let mut sum_output = self + .outputs + .iter() + .fold(0u64, |required, output| required + output.value); + match self.fee_policy { + FeePolicy::SendExact => { + sum_output += self.total_tx_fee_needed(); }, + FeePolicy::DeductFromOutput(_) => {}, }; + sum_output + } + fn add_tx_inputs(&mut self, amount: u64) -> u64 { + self.tx.inputs.clear(); + let mut total = 0u64; + for utxo in &self.required_inputs { + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script.clone(), + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + total += utxo.value; + } + for utxo in &self.available_inputs { + if total >= amount { + break; + } + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script.clone(), + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + total += utxo.value; + } + total + } + + fn add_tx_outputs(&mut self) -> u64 { + self.tx.outputs.clear(); + let mut total = 0u64; + for output in self.outputs.clone() { + total += output.value; + self.tx.outputs.push(output); + } + total + } + + fn make_kmd_rewards_data(coin: &T, interest: u64) -> Option { + let rewards_amount = big_decimal_from_sat_unsigned(interest, coin.as_ref().decimals); + if coin.supports_interest() { + Some(KmdRewardsDetails::claimed_by_me(rewards_amount)) + } else { + None + } + } + + /// Adds change output. + /// Returns change value and dust change + fn add_change(&mut self, change_script_pubkey: &Bytes) -> u64 { + let sum_output_with_fee = self.sum_outputs + self.total_tx_fee_needed(); + if self.sum_inputs < sum_output_with_fee { + return 0u64; + } + let change = self.sum_inputs + self.interest - sum_output_with_fee; + if change < self.dust() { + return 0u64; + }; + self.tx.outputs.push({ + TransactionOutput { + value: change, + script_pubkey: change_script_pubkey.clone(), + } + }); + change + } + + /// Recalculates tx fee for tx size. + /// If needed, checks if tx fee is not less than min relay tx fee + fn update_tx_fee(&mut self, from_addr_format: &UtxoAddressFormat, fee_rate: &ActualFeeRate) { + let transaction = UtxoTx::from(self.tx.clone()); + let v_size = tx_size_in_v_bytes(from_addr_format, &transaction) as u64; + self.tx_fee_needed = get_tx_fee_with_relay_fee(fee_rate, v_size, self.min_relay_fee_rate); + } + + /// Deduct tx fee from output if requested by fee_policy + fn deduct_txfee_from_output(&mut self) -> MmResult { match self.fee_policy { - FeePolicy::SendExact => { - let mut outputs_plus_fee = self.sum_outputs_value + self.tx_fee; - if self.sum_inputs >= outputs_plus_fee { - self.change = self.sum_inputs - outputs_plus_fee; - if self.change > self.dust() { - // there will be change output - if let ActualTxFee::Dynamic(ref f) = actual_tx_fee { - self.tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; - outputs_plus_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; - } - } - if let Some(min_relay) = self.min_relay_fee { - if self.tx_fee < min_relay { - let fee_diff = min_relay - self.tx_fee; - outputs_plus_fee += fee_diff; - self.tx_fee += fee_diff; - } - } - self.sum_inputs >= outputs_plus_fee - } else { - false - } - }, - FeePolicy::DeductFromOutput(_) => { - if self.sum_inputs >= self.sum_outputs_value { - self.change = self.sum_inputs - self.sum_outputs_value; - if self.change > self.dust() { - if let ActualTxFee::Dynamic(ref f) = actual_tx_fee { - self.tx_fee += (f * P2PKH_OUTPUT_LEN) / KILO_BYTE; - } - } - if let Some(min_relay) = self.min_relay_fee { - if self.tx_fee < min_relay { - self.tx_fee = min_relay; - } - } - true - } else { - false - } + FeePolicy::SendExact => Ok(0), + FeePolicy::DeductFromOutput(i) => { + let tx_fee = self.total_tx_fee_needed(); + let min_output = tx_fee + self.dust(); + let val = self.tx.outputs[i].value; + return_err_if!(val < min_output, GenerateTxError::DeductFeeFromOutputFailed { + output_idx: i, + output_value: val, + required: min_output, + }); + self.tx.outputs[i].value -= tx_fee; + Ok(tx_fee) }, } } + fn validate_not_dust(&self) -> MmResult<(), GenerateTxError> { + for output in self.outputs.iter() { + let script: Script = output.script_pubkey.clone().into(); + if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { + return_err_if!(output.value < self.dust(), GenerateTxError::OutputValueLessThanDust { + value: output.value, + dust: self.dust() + }); + } + } + Ok(()) + } + + fn sum_received_by_me(&self, change_script_pubkey: &Bytes) -> u64 { + self.tx.outputs.iter().fold(0u64, |received_by_me, output| { + if &output.script_pubkey == change_script_pubkey { + received_by_me + output.value + } else { + received_by_me + } + }) + } + fn dust(&self) -> u64 { match self.dust { Some(dust) => dust, @@ -630,143 +697,98 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } } + fn total_tx_fee_needed(&self) -> u64 { self.tx_fee_needed + self.gas_fee.unwrap_or(0u64) } + + fn tx_fee_fact(&self) -> MmResult { + (self.sum_inputs + self.interest) + .checked_sub(self.gas_fee.unwrap_or_default()) + .or_mm_err(|| GenerateTxError::Internal("gas_fee underflow".to_owned()))? + .checked_sub(self.sum_outputs) + .or_mm_err(|| GenerateTxError::Internal("sum_outputs underflow".to_owned())) + } + /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. /// sends the change (inputs amount - outputs amount) to the [`UtxoTxBuilder::from`] address. /// Also returns additional transaction data pub async fn build(mut self) -> GenerateTxResult { let coin = self.coin; - let dust: u64 = self.dust(); let from = self .from .clone() .or_mm_err(|| GenerateTxError::Internal("'from' address is not specified".to_owned()))?; let change_script_pubkey = output_script(&from).map(|script| script.to_bytes())?; - let actual_tx_fee = match self.fee { + let actual_fee_rate = match self.fee { Some(fee) => fee, - None => coin.get_tx_fee().await?, + None => coin.get_fee_rate().await?, }; - true_or!(!self.tx.outputs.is_empty(), GenerateTxError::EmptyOutputs); - - let mut received_by_me = 0; - for output in self.tx.outputs.iter() { - let script: Script = output.script_pubkey.clone().into(); - if script.opcodes().next() != Some(Ok(Opcode::OP_RETURN)) { - true_or!(output.value >= dust, GenerateTxError::OutputValueLessThanDust { - value: output.value, - dust - }); - } - self.sum_outputs_value += output.value; - if output.script_pubkey == change_script_pubkey { - received_by_me += output.value; - } - } + return_err_if!(self.outputs.is_empty(), GenerateTxError::EmptyOutputs); - if let Some(gas_fee) = self.gas_fee { - self.sum_outputs_value += gas_fee; - } + self.validate_not_dust()?; - true_or!( - !self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), + return_err_if!( + self.available_inputs.is_empty() && self.tx.inputs.is_empty(), GenerateTxError::EmptyUtxoSet { - required: self.sum_outputs_value + required: self.required_amount() } ); - self.min_relay_fee = if coin.as_ref().conf.force_min_relay_fee { - let fee_dec = coin.as_ref().rpc_client.get_relay_fee().compat().await?; - let min_relay_fee = sat_from_big_decimal(&fee_dec, coin.as_ref().decimals)?; - Some(min_relay_fee) - } else { - None - }; - - // The function `update_fee_and_check_completeness` checks if the total value of the current inputs - // (added using add_required_inputs or directly) is enough to cover the transaction outputs and fees. - // If it returns `true`, it indicates that no additional inputs are needed from the available inputs, - // and we can skip the loop that adds these additional inputs. - if !self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { - for utxo in self.available_inputs.clone() { - self.tx.inputs.push(UnsignedTransactionInput { - previous_output: utxo.outpoint, - prev_script: utxo.script, - sequence: SEQUENCE_FINAL, - amount: utxo.value, - }); - self.sum_inputs += utxo.value; + self.min_relay_fee_rate = get_min_relay_rate(coin).await?; - if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { - break; - } + let mut one_time_fee_update = false; + loop { + let required_amount_0 = self.required_amount(); + self.sum_inputs = self.add_tx_inputs(required_amount_0); + self.sum_outputs = self.add_tx_outputs(); + self.interest = coin.calc_interest_if_required(&mut self.tx).await?; + + // try once tx_fee without the change output (if maybe txfee fits between total inputs and outputs) + if !one_time_fee_update { + self.update_tx_fee(from.addr_format(), &actual_fee_rate); + one_time_fee_update = true; } - } - - match self.fee_policy { - FeePolicy::SendExact => self.sum_outputs_value += self.tx_fee, - FeePolicy::DeductFromOutput(i) => { - let min_output = self.tx_fee + dust; - let val = self.tx.outputs[i].value; - true_or!(val >= min_output, GenerateTxError::DeductFeeFromOutputFailed { - output_idx: i, - output_value: val, - required: min_output, - }); - self.tx.outputs[i].value -= self.tx_fee; - if self.tx.outputs[i].script_pubkey == change_script_pubkey { - received_by_me -= self.tx_fee; - } - }, - }; - true_or!( - self.sum_inputs >= self.sum_outputs_value, - GenerateTxError::NotEnoughUtxos { + return_err_if!(self.sum_inputs < required_amount_0, GenerateTxError::NotEnoughUtxos { sum_utxos: self.sum_inputs, - required: self.sum_outputs_value - } - ); - - let change = self.sum_inputs - self.sum_outputs_value; - let unused_change = if change > dust { - self.tx.outputs.push({ - TransactionOutput { - value: change, - script_pubkey: change_script_pubkey.clone(), - } + required: self.required_amount(), // send updated required amount, with txfee }); - received_by_me += change; - 0 - } else { - change - }; + + self.sum_outputs = self + .sum_outputs + .checked_sub(self.deduct_txfee_from_output()?) + .or_mm_err(|| GenerateTxError::Internal("sum_outputs underflow".to_owned()))?; + let change = self.add_change(&change_script_pubkey); + self.sum_outputs += change; + self.update_tx_fee(from.addr_format(), &actual_fee_rate); // recalculate txfee with the change output, if added + if self.sum_inputs + self.interest >= self.sum_outputs + self.total_tx_fee_needed() { + break; + } + } let data = AdditionalTxData { - fee_amount: self.tx_fee, - received_by_me, + fee_amount: self.tx_fee_fact()?, // we return only txfee here (w/o gas_fee) + received_by_me: self.sum_received_by_me(&change_script_pubkey), spent_by_me: self.sum_inputs, - unused_change, // will be changed if the ticker is KMD - kmd_rewards: None, + kmd_rewards: Self::make_kmd_rewards_data(coin, self.interest), }; - Ok(coin - .calc_interest_if_required(self.tx, data, change_script_pubkey, dust) - .await?) + Ok((self.tx, data)) } /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. /// Adds or updates inputs with UnspentInfo /// Does not do any checks or add any outputs pub async fn build_unchecked(mut self) -> Result> { + self.sum_outputs = 0u64; for output in self.tx.outputs.iter() { - self.sum_outputs_value += output.value; + self.sum_outputs += output.value; } - true_or!( - !self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), + return_err_if!( + self.available_inputs.is_empty() && self.tx.inputs.is_empty(), GenerateTxError::EmptyUtxoSet { - required: self.sum_outputs_value + required: self.sum_outputs } ); @@ -803,61 +825,62 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { /// returns transaction and data as is if the coin is not KMD pub async fn calc_interest_if_required( coin: &T, - mut unsigned: TransactionInputSigner, - mut data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, -) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - if coin.as_ref().conf.ticker != "KMD" { - return Ok((unsigned, data)); + unsigned: &mut TransactionInputSigner, +) -> UtxoRpcResult { + if !coin.supports_interest() { + return Ok(0); } unsigned.lock_time = coin.get_current_mtp().await?; let mut interest = 0; + let prev_hashes = unsigned + .inputs + .iter() + .map(|input| input.previous_output.hash.reversed().into()) + .collect::>(); + let prev_txns = get_verbose_transactions_from_cache_or_rpc(coin.as_ref(), prev_hashes).await?; for input in unsigned.inputs.iter() { let prev_hash = input.previous_output.hash.reversed().into(); - let tx = coin - .as_ref() - .rpc_client - .get_verbose_transaction(&prev_hash) - .compat() - .await?; + let tx = prev_txns + .get(&prev_hash) + .ok_or(MmError::new(UtxoRpcError::Internal("previous tx not found".to_owned())))? + .to_inner(); if let Ok(output_interest) = kmd_interest(tx.height, input.amount, tx.locktime as u64, unsigned.lock_time as u64) { interest += output_interest; }; } - if interest > 0 { - data.received_by_me += interest; - let mut output_to_me = unsigned - .outputs - .iter_mut() - .find(|out| out.script_pubkey == my_script_pub); - // add calculated interest to existing output to my address - // or create the new one if it's not found - match output_to_me { - Some(ref mut output) => output.value += interest, - None => { - let maybe_change_output_value = interest + data.unused_change; - if maybe_change_output_value > dust { - let change_output = TransactionOutput { - script_pubkey: my_script_pub, - value: maybe_change_output_value, - }; - unsigned.outputs.push(change_output); - data.unused_change = 0; - } else { - data.unused_change += interest; - } - }, - }; - } else { + if interest == 0 { // if interest is zero attempt to set the lowest possible lock_time to claim it later unsigned.lock_time = now_sec_u32() - 3600 + 777 * 2; } - let rewards_amount = big_decimal_from_sat_unsigned(interest, coin.as_ref().decimals); - data.kmd_rewards = Some(KmdRewardsDetails::claimed_by_me(rewards_amount)); - Ok((unsigned, data)) + Ok(interest) +} + +pub fn is_kmd(coin: &T) -> bool { &coin.as_ref().conf.ticker == "KMD" } + +/// Helper to get min relay fee rate and convert to sat +async fn get_min_relay_rate + UtxoTxGenerationOps>(coin: &T) -> UtxoRpcResult> { + if coin.as_ref().conf.force_min_relay_fee { + let fee_dec = coin.as_ref().rpc_client.get_relay_fee().compat().await?; + let min_relay_fee_rate = sat_from_big_decimal(&fee_dec, coin.as_ref().decimals)?; + Ok(Some(min_relay_fee_rate)) + } else { + Ok(None) + } +} + +/// Helper to get tx fee if min relay rate is known +fn get_tx_fee_with_relay_fee(fee_rate: &ActualFeeRate, tx_size: u64, min_relay_fee_rate: Option) -> u64 { + let tx_fee = fee_rate.get_tx_fee(tx_size); + if let Some(min_relay_fee_rate) = min_relay_fee_rate { + let min_relay_dynamic_fee_rate = ActualFeeRate::Dynamic(min_relay_fee_rate); + let min_relay_tx_fee = min_relay_dynamic_fee_rate.get_tx_fee(tx_size); + if tx_fee < min_relay_tx_fee { + return min_relay_tx_fee; + } + } + tx_fee } pub struct P2SHSpendingTxInput<'a> { @@ -2019,19 +2042,100 @@ pub async fn send_maker_refunds_payment( refund_htlc_payment(coin, args).await.map(|tx| tx.into()) } +/// Sets the amount of the input at the given index to the value of the corresponding output in the previous transaction. +/// +/// This invokes the RPC client to fetch the previous transaction and extract the output value. +pub async fn set_index_amount_from_prev_tx( + rpc_client: &UtxoRpcClientEnum, + signer: &mut TransactionInputSigner, + idx: usize, +) -> Result<(), String> { + let inputs_len = signer.inputs.len(); + let input = signer.inputs.get_mut(idx).ok_or_else(|| { + format!( + "Input index {} out of bounds for transaction with {} inputs", + idx, inputs_len + ) + })?; + let prev_output_tx_hash = input.previous_output.hash.reversed().into(); + let prev_output_index = input.previous_output.index as usize; + let prev_tx_hex = rpc_client + .get_transaction_bytes(&prev_output_tx_hash) + .compat() + .await + .map_err(|e| format!("Failed to get prev tx hex: {e}"))?; + let prev_tx: UtxoTx = deserialize(prev_tx_hex.0.as_slice()) + .map_err(|e| format!("Failed to deserialize prev tx {}: {}", prev_output_tx_hash, e))?; + let prev_output = prev_tx.outputs.get(prev_output_index).ok_or_else(|| { + format!( + "Prev tx output index {} out of bounds for tx {}", + input.previous_output.index, + prev_tx.hash() + ) + })?; + input.amount = prev_output.value; + Ok(()) +} + +/// Verifies that the script that spends a P2PK is signed by the expected pubkey. +fn verify_p2pk_input_pubkey( + script: &Script, + expected_pubkey: &Public, + unsigned_tx: &TransactionInputSigner, + index: usize, + signature_version: SignatureVersion, + fork_id: u32, +) -> Result { + // Extract the signature from the scriptSig. + let signature = script.extract_signature()?; + // Validate the signature. + try_s!(SecpSignature::from_der(&signature[..signature.len() - 1])); + let signature = signature.into(); + // Make sure we have no more instructions. P2PK scriptSigs consist of a single instruction only containing the signature. + if script.get_instruction(1).is_some() { + return ERR!("Unexpected instruction at position 2 of script {:?}", script); + }; + // Get the scriptPub for this input. We need it to get the transaction sig_hash to sign (but actually "to verify" in this case). + let pubkey = expected_pubkey + .to_secp256k1_pubkey() + .map_err(|e| ERRL!("Error converting plain pubkey to secp256k1 pubkey: {}", e))?; + // P2PK scriptPub has two valid possible formats depending on whether the public key is written in compressed or uncompressed form. + let possible_pubkey_scripts = [ + Builder::build_p2pk(&Public::Compressed(pubkey.serialize().into())), + Builder::build_p2pk(&Public::Normal(pubkey.serialize_uncompressed().into())), + ]; + for pubkey_script in possible_pubkey_scripts { + // Get the transaction hash that has been signed in the scriptSig. + let hash = match signature_hash_to_sign( + unsigned_tx, + index, + &pubkey_script, + signature_version, + SIGHASH_ALL, + fork_id, + ) { + Ok(hash) => hash, + Err(e) => return ERR!("Error calculating signature hash: {}", e), + }; + // Verify that the signature is valid for the transaction hash with respect to the expected public key. + return match expected_pubkey.verify(&hash, &signature) { + Ok(true) => Ok(true), + // The signature is invalid for this pubkey, try the other possible pubkey script. + Ok(false) => continue, + Err(e) => ERR!("Error verifying signature: {}", e), + }; + } + + // Both possible pubkey scripts failed to verify the signature. + Ok(false) +} + /// Extracts pubkey from script sig fn pubkey_from_script_sig(script: &Script) -> Result { - match script.get_instruction(0) { - Some(Ok(instruction)) => match instruction.opcode { - Opcode::OP_PUSHBYTES_70 | Opcode::OP_PUSHBYTES_71 | Opcode::OP_PUSHBYTES_72 => match instruction.data { - Some(bytes) => try_s!(SecpSignature::from_der(&bytes[..bytes.len() - 1])), - None => return ERR!("No data at instruction 0 of script {:?}", script), - }, - _ => return ERR!("Unexpected opcode {:?}", instruction.opcode), - }, - Some(Err(e)) => return ERR!("Error {} on getting instruction 0 of script {:?}", e, script), - None => return ERR!("None instruction 0 of script {:?}", script), - }; + // Extract the signature from the scriptSig. + let signature = script.extract_signature()?; + // Validate the signature. + try_s!(SecpSignature::from_der(&signature[..signature.len() - 1])); let pubkey = match script.get_instruction(1) { Some(Ok(instruction)) => match instruction.opcode { @@ -2087,18 +2191,65 @@ where } } -pub fn check_all_utxo_inputs_signed_by_pub( +/// This function is used to check that all inputs are signed/owned by the expected pubkey. +/// +/// It's used to verify that all the inputs of the taker-sent dex fee are signed/owned by the taker's pubkey. +/// It's used also by watcher to verify that all the taker payment inputs are signed/owned by the taker's pubkey. +/// The `expected_pub` should be the taker's pubkey in compressed (33-byte) format. +pub async fn check_all_utxo_inputs_signed_by_pub( + coin: &T, tx: &UtxoTx, expected_pub: &[u8], ) -> Result> { - for input in &tx.inputs { + let expected_pub = + H264::from_slice(expected_pub).map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; + let mut unsigned_tx: Option = None; + + for (idx, input) in tx.inputs.iter().enumerate() { + let script = Script::from(input.script_sig.clone()); + + // This handles the case where the input is a P2PK input. + if !input.has_witness() && script.does_script_spend_p2pk() { + let unsigned_tx = unsigned_tx.get_or_insert_with(|| tx.clone().into()); + // If the transaction is overwintered, we need to set the consensus branch id and the input's amount. + // This is needed for the sighash calculation. + if unsigned_tx.overwintered { + set_index_amount_from_prev_tx(&coin.as_ref().rpc_client, unsigned_tx, idx) + .await + .map_err(|e| { + ValidatePaymentError::TxDeserializationError(format!( + "Failed to set index amount for input {}: {}", + idx, e + )) + })?; + unsigned_tx.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; + } + // Verfiy that the P2PK input's scriptSig corresponds to the expected public key. + let successful_verification = verify_p2pk_input_pubkey( + &script, + &Public::Compressed(expected_pub), + unsigned_tx, + idx, + coin.as_ref().conf.signature_version, + coin.as_ref().conf.fork_id, + ) + .map_to_mm(ValidatePaymentError::TxDeserializationError)?; + if successful_verification { + // No pubkey extraction for P2PK inputs. Continue. + continue; + } + return Ok(false); + } + let pubkey = if input.has_witness() { + // Extract the pubkey from a P2WPKH scriptSig. pubkey_from_witness_script(&input.script_witness).map_to_mm(ValidatePaymentError::TxDeserializationError)? } else { - let script: Script = input.script_sig.clone().into(); + // Extract the pubkey from a P2PKH scriptSig. pubkey_from_script_sig(&script).map_to_mm(ValidatePaymentError::TxDeserializationError)? }; - if *pubkey != expected_pub { + + if pubkey != expected_pub { return Ok(false); } } @@ -2146,7 +2297,8 @@ pub fn watcher_validate_taker_fee( }; let taker_fee_tx: UtxoTx = deserialize(tx_from_rpc.hex.0.as_slice())?; - let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(&taker_fee_tx, &sender_pubkey)?; + let inputs_signed_by_pub = + check_all_utxo_inputs_signed_by_pub(&coin, &taker_fee_tx, &sender_pubkey).await?; if !inputs_signed_by_pub { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Taker fee does not belong to the verified public key", @@ -2278,17 +2430,18 @@ pub fn validate_fee( ) -> ValidatePaymentFut<()> { let dex_address = try_f!(dex_address(&coin).map_to_mm(ValidatePaymentError::InternalError)); let burn_address = try_f!(burn_address(&coin).map_to_mm(ValidatePaymentError::InternalError)); - let inputs_signed_by_pub = try_f!(check_all_utxo_inputs_signed_by_pub(&tx, sender_pubkey)); - if !inputs_signed_by_pub { - return Box::new(futures01::future::err( - ValidatePaymentError::WrongPaymentTx(format!( - "{INVALID_SENDER_ERR_LOG}: Taker payment does not belong to the verified public key" - )) - .into(), - )); - } + let sender_pubkey = sender_pubkey.to_vec(); let fut = async move { + match check_all_utxo_inputs_signed_by_pub(&coin, &tx, &sender_pubkey).await { + Ok(true) => {}, + Ok(false) => { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "{INVALID_SENDER_ERR_LOG}: Taker payment does not belong to the verified public key" + ))) + }, + Err(e) => return Err(e), + }; let tx_from_rpc = coin .as_ref() .rpc_client @@ -2394,7 +2547,8 @@ pub fn watcher_validate_taker_payment( let coin = coin.clone(); let fut = async move { - let inputs_signed_by_pub = check_all_utxo_inputs_signed_by_pub(&taker_payment_tx, &input.taker_pub)?; + let inputs_signed_by_pub = + check_all_utxo_inputs_signed_by_pub(&coin, &taker_payment_tx, &input.taker_pub).await?; if !inputs_signed_by_pub { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{INVALID_SENDER_ERR_LOG}: Taker payment does not belong to the verified public key" @@ -2755,10 +2909,32 @@ pub fn sign_message_hash(coin: &UtxoCoinFields, message: &str) -> Option<[u8; 32 Some(dhash256(&stream.out()).take()) } -pub fn sign_message(coin: &UtxoCoinFields, message: &str) -> SignatureResult { +pub fn sign_message( + coin: &UtxoCoinFields, + message: &str, + account: Option, +) -> SignatureResult { let message_hash = sign_message_hash(coin, message).ok_or(SignatureError::PrefixNotFound)?; - let private_key = coin.priv_key_policy.activated_key_or_err()?.private(); - let signature = private_key.sign_compact(&H256::from(message_hash))?; + + let private = if let Some(account) = account { + let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; + let derivation_path = account + .valid_derivation_path(path_to_coin) + .mm_err(|err| SignatureError::InvalidRequest(err.to_string()))?; + let privkey = coin + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(&derivation_path)?; + Private { + prefix: coin.conf.wif_prefix, + secret: privkey, + compressed: true, + checksum_type: coin.conf.checksum_type, + } + } else { + *coin.priv_key_policy.activated_key_or_err()?.private() + }; + + let signature = private.sign_compact(&H256::from(message_hash))?; Ok(STANDARD.encode(&*signature)) } @@ -3087,6 +3263,7 @@ pub fn display_priv_key(coin: &UtxoCoinFields) -> Result { PrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support Metamask"), + PrivKeyPolicy::WalletConnect { .. } => ERR!("'display_priv_key' doesn't support WalletConnect"), } } @@ -3095,9 +3272,6 @@ pub fn min_tx_amount(coin: &UtxoCoinFields) -> BigDecimal { } pub fn min_trading_vol(coin: &UtxoCoinFields) -> MmNumber { - if coin.conf.ticker == "BTC" { - return MmNumber::from(MIN_BTC_TRADING_VOL); - } let dust_multiplier = MmNumber::from(10); dust_multiplier * min_tx_amount(coin).into() } @@ -3900,7 +4074,7 @@ pub async fn calc_interest_of_tx( tx: &UtxoTx, input_transactions: &mut HistoryUtxoTxMap, ) -> UtxoRpcResult { - if coin.as_ref().conf.ticker != "KMD" { + if !coin.supports_interest() { let error = format!("Expected KMD ticker, found {}", coin.as_ref().conf.ticker); return MmError::err(UtxoRpcError::Internal(error)); } @@ -3935,10 +4109,10 @@ pub fn get_trade_fee(coin: T) -> Box f, - ActualTxFee::FixedPerKb(f) => f, + ActualFeeRate::Dynamic(f) => f, + ActualFeeRate::FixedPerKb(f) => f, }; Ok(TradeFee { coin: ticker, @@ -3973,28 +4147,27 @@ where T: UtxoCommonOps + GetUtxoListOps, { let decimals = coin.as_ref().decimals; - let tx_fee = coin.get_tx_fee().await?; + let fee_rate = coin.get_fee_rate().await?; // [`FeePolicy::DeductFromOutput`] is used if the value is [`TradePreimageValue::UpperBound`] only let is_amount_upper_bound = matches!(fee_policy, FeePolicy::DeductFromOutput(_)); let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; - match tx_fee { + match fee_rate { // if it's a dynamic fee, we should generate a swap transaction to get an actual trade fee - ActualTxFee::Dynamic(fee) => { - // take into account that the dynamic tx fee may increase during the swap - let dynamic_fee = coin.increase_dynamic_fee_by_stage(fee, stage); + ActualFeeRate::Dynamic(fee_rate) => { + // take into account that the dynamic tx fee rate may increase during the swap + let dynamic_fee_rate = coin.increase_dynamic_fee_by_stage(fee_rate, stage); let outputs_count = outputs.len(); let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; - let actual_tx_fee = ActualTxFee::Dynamic(dynamic_fee); - + let actual_fee_rate = ActualFeeRate::Dynamic(dynamic_fee_rate); let mut tx_builder = UtxoTxBuilder::new(coin) .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy) - .with_fee(actual_tx_fee); + .with_fee(actual_fee_rate); if let Some(gas) = gas_fee { tx_builder = tx_builder.with_gas_fee(gas); } @@ -4002,26 +4175,26 @@ where TradePreimageError::from_generate_tx_error(e, ticker.to_owned(), decimals, is_amount_upper_bound) })?; + // We need to add extra tx fee for the absent change output for e.g. to ensure max_taker_vol is calculated correctly + // (If we do not do this then in a swap the change output may appear and we may not have sufficient balance to pay taker fee) let total_fee = if tx.outputs.len() == outputs_count { // take into account the change output - data.fee_amount + (dynamic_fee * P2PKH_OUTPUT_LEN) / KILO_BYTE + data.fee_amount + actual_fee_rate.get_tx_fee_for_change(0) } else { // the change output is included already data.fee_amount }; - Ok(big_decimal_from_sat(total_fee as i64, decimals)) }, - ActualTxFee::FixedPerKb(fee) => { + ActualFeeRate::FixedPerKb(_fee) => { let outputs_count = outputs.len(); let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; - let mut tx_builder = UtxoTxBuilder::new(coin) .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy) - .with_fee(tx_fee); + .with_fee(fee_rate); if let Some(gas) = gas_fee { tx_builder = tx_builder.with_gas_fee(gas); } @@ -4029,20 +4202,17 @@ where TradePreimageError::from_generate_tx_error(e, ticker.to_string(), decimals, is_amount_upper_bound) })?; + // We need to add extra tx fee for the absent change output for e.g. to ensure max_taker_vol is calculated correctly + // (If we do not do this then in a swap the change output may appear and we may not have sufficient balance to pay taker fee) let total_fee = if tx.outputs.len() == outputs_count { - // take into account the change output if tx_size_kb(tx with change) > tx_size_kb(tx without change) let tx = UtxoTx::from(tx); let tx_bytes = serialize(&tx); - if tx_bytes.len() as u64 % KILO_BYTE + P2PKH_OUTPUT_LEN > KILO_BYTE { - data.fee_amount + fee - } else { - data.fee_amount - } + // take into account the change output + data.fee_amount + fee_rate.get_tx_fee_for_change(tx_bytes.len() as u64) } else { // the change output is included already data.fee_amount }; - Ok(big_decimal_from_sat(total_fee as i64, decimals)) }, } @@ -4882,9 +5052,10 @@ pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) -> activated_key: activated_key_pair, .. } => activated_key_pair, - PrivKeyPolicy::Trezor => todo!(), + PrivKeyPolicy::Trezor => panic!("`PrivKeyPolicy::Trezor` is not supported for UTXO coins"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => panic!("`PrivKeyPolicy::Metamask` is not supported for UTXO coins"), + PrivKeyPolicy::WalletConnect { .. } => panic!("`PrivKeyPolicy::WalletConnect` is not supported for UTXO coins"), } } @@ -5292,6 +5463,52 @@ fn test_pubkey_from_script_sig() { pubkey_from_script_sig(&script_sig_err).unwrap_err(); } +#[test] +fn test_verify_p2pk_input_pubkey() { + // 65-byte (uncompressed) pubkey example. + // https://mempool.space/tx/1db6251a9afce7025a2061a19e63c700dffc3bec368bd1883decfac353357a9d + let tx: UtxoTx = "0100000001740443e82e526cef440ed590d1c43a67f509424134542de092e5ae68721575d60100000049483045022078e86c021003cca23842d4b2862dfdb68d2478a98c08c10dcdffa060e55c72be022100f6a41da12cdc2e350045f4c97feeab76a7c0ab937bd8a9e507293ce6d37c9cc201ffffffff0200f2052a010000001976a91431891996d28cc0214faa3760a765b40846bd035888ac00ba1dd2050000004341049464205950188c29d377eebca6535e0f3699ce4069ecd77ffebfbd0bcf95e3c134cb7d2742d800a12df41413a09ef87a80516353a2f0a280547bb5512dc03da8ac00000000".into(); + let script_sig = tx.inputs[0].script_sig.clone().into(); + let expected_pub = Public::Normal("049464205950188c29d377eebca6535e0f3699ce4069ecd77ffebfbd0bcf95e3c134cb7d2742d800a12df41413a09ef87a80516353a2f0a280547bb5512dc03da8".into()); + let unsigned_tx: TransactionInputSigner = tx.into(); + let successful_verification = + verify_p2pk_input_pubkey(&script_sig, &expected_pub, &unsigned_tx, 0, SignatureVersion::Base, 0).unwrap(); + assert!(successful_verification); + + // 33-byte (compressed) pubkey example. + // https://kmdexplorer.io/tx/07ceb50f9eedc3b820e48dc1e5250f6625115afe4ace3089bfcc66b34f5d4344 + let tx: UtxoTx = "0400008085202f89013683897bf3bfb1e217663aa9591bd73c9eb105f8c8471e88dbe7152ca7627a19050000004948304502210087100bf4a665ebab3cc6d3472068905bdc6c6def37e432597e78e2ccc4da017a02205b5f0800cabe84bc49b5eb0997926b48dfee3b8ca5a31623ae9506272f8a5cd501ffffffff0288130000000000002321020e46e79a2a8d12b9b5d12c7a91adb4e454edfae43c0a0cb805427d2ac7613fd9ac0000000000000000226a20976bd7ad5596ac3521fd90295e753b1096e4eb90ad9ded1170b2ed81f810df5fc0dbf36752ea42000000000000000000000000".into(); + let script_sig = tx.inputs[0].script_sig.clone().into(); + let expected_pub = Public::Compressed("02f9a7b49282885cd03969f1f5478287497bc8edfceee9eac676053c107c5fcdaf".into()); + let mut unsigned_tx: TransactionInputSigner = tx.into(); + // For overwintered transactions, the amount must be set, as wel as the consensus branch id. + unsigned_tx.inputs[0].amount = 10000; + unsigned_tx.consensus_branch_id = 0x76b8_09bb; + let successful_verification = + verify_p2pk_input_pubkey(&script_sig, &expected_pub, &unsigned_tx, 0, SignatureVersion::Base, 0).unwrap(); + assert!(successful_verification); +} + +#[test] +fn test_check_all_utxo_inputs_signed_by_pub_overwintered() { + use super::utxo_tests::electrum_client_for_test; + use common::block_on; + + // We need a running electrum client for this test to test the functionality of fetching a tx from the network, parsing it, and using its input amount for sig_hash calculations. + let client = UtxoRpcClientEnum::Electrum(electrum_client_for_test(&[ + "electrum3.cipig.net:10001", + "electrum1.cipig.net:10001", + "electrum2.cipig.net:10001", + ])); + let mut fields = utxo_coin_fields_for_test(client, None, false); + fields.conf.ticker = "KMD".to_owned(); + let coin = utxo_coin_from_fields(fields); + + let tx: UtxoTx = "0400008085202f89013683897bf3bfb1e217663aa9591bd73c9eb105f8c8471e88dbe7152ca7627a19050000004948304502210087100bf4a665ebab3cc6d3472068905bdc6c6def37e432597e78e2ccc4da017a02205b5f0800cabe84bc49b5eb0997926b48dfee3b8ca5a31623ae9506272f8a5cd501ffffffff0288130000000000002321020e46e79a2a8d12b9b5d12c7a91adb4e454edfae43c0a0cb805427d2ac7613fd9ac0000000000000000226a20976bd7ad5596ac3521fd90295e753b1096e4eb90ad9ded1170b2ed81f810df5fc0dbf36752ea42000000000000000000000000".into(); + let expected_pub = Public::Compressed("02f9a7b49282885cd03969f1f5478287497bc8edfceee9eac676053c107c5fcdaf".into()); + assert!(block_on(check_all_utxo_inputs_signed_by_pub(&coin, &tx, &expected_pub)).unwrap()); +} + #[test] fn test_tx_v_size() { // Multiple legacy inputs with P2SH and P2PKH output diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index d432207d8e..4203f4d8ba 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -136,7 +136,7 @@ pub(super) fn utxo_coin_fields_for_test( }, decimals: TEST_COIN_DECIMALS, dust_amount: UTXO_DUST_AMOUNT, - tx_fee: TxFee::FixedPerKb(1000), + tx_fee: FeeRate::FixedPerKb(1000), rpc_client, priv_key_policy, derivation_method, diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 99a97d846f..dae16222de 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -2,9 +2,10 @@ use super::utxo_common::utxo_prepare_addresses_for_balance_stream_if_enabled; use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; -use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, - HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::{ExtractExtendedPubkey, HDAddressSelector, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, + HDExtractPubkeyError, HDXPubExtractor, SettingEnabledAddressError, TrezorCoinError, + WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, @@ -43,6 +44,7 @@ use futures::{FutureExt, TryFutureExt}; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; #[cfg(test)] use mocktopus::macros::*; +use rpc::v1::types::H264 as H264Json; use script::Opcode; use utxo_signer::UtxoSignerOps; @@ -110,17 +112,13 @@ impl UtxoTxBroadcastOps for UtxoStandardCoin { #[async_trait] #[cfg_attr(test, mockable)] impl UtxoTxGenerationOps for UtxoStandardCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] @@ -852,12 +850,17 @@ impl MarketCoinOps for UtxoStandardCoin { fn my_address(&self) -> MmResult { utxo_common::my_address(self) } + fn address_from_pubkey(&self, pubkey: &H264Json) -> MmResult { + let pubkey = Public::Compressed((*pubkey).into()); + Ok(UtxoCommonOps::address_from_pubkey(self, &pubkey).to_string()) + } + fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { utxo_common::sign_message_hash(self.as_ref(), message) } - fn sign_message(&self, message: &str) -> SignatureResult { - utxo_common::sign_message(self.as_ref(), message) + fn sign_message(&self, message: &str, address: Option) -> SignatureResult { + utxo_common::sign_message(self.as_ref(), message, address) } fn verify_message(&self, signature_base64: &str, message: &str, address: &str) -> VerificationResult { @@ -915,7 +918,7 @@ impl MarketCoinOps for UtxoStandardCoin { fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } - fn is_kmd(&self) -> bool { &self.utxo_arc.conf.ticker == "KMD" } + fn should_burn_directly(&self) -> bool { &self.utxo_arc.conf.ticker == "KMD" } fn should_burn_dex_fee(&self) -> bool { utxo_common::should_burn_dex_fee() } @@ -1121,6 +1124,15 @@ impl HDWalletCoinOps for UtxoStandardCoin { } fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } + + async fn received_enabled_address_from_hw_wallet( + &self, + enabled_address: UtxoHDAddress, + ) -> MmResult<(), SettingEnabledAddressError> { + utxo_common::received_enabled_address_from_hw_wallet(self, enabled_address.address) + .await + .mm_err(SettingEnabledAddressError::Internal) + } } impl HDCoinWithdrawOps for UtxoStandardCoin {} diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 7b9d39d38b..7d8d760452 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -12,10 +12,10 @@ use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, S ScanAddressesResponse}; use crate::utxo::qtum::{qtum_coin_with_priv_key, QtumCoin, QtumDelegationOps, QtumDelegationRequest}; #[cfg(not(target_arch = "wasm32"))] -use crate::utxo::rpc_clients::{BlockHashOrHeight, NativeUnspent}; +use crate::utxo::rpc_clients::{BlockHashOrHeight, ElectrumClientSettings, NativeUnspent}; use crate::utxo::rpc_clients::{ElectrumBalance, ElectrumBlockHeader, ElectrumClient, ElectrumClientImpl, - ElectrumClientSettings, GetAddressInfoRes, ListSinceBlockRes, NativeClient, - NativeClientImpl, NetworkInfo, UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; + GetAddressInfoRes, ListSinceBlockRes, NativeClient, NativeClientImpl, NetworkInfo, + UtxoRpcClientOps, ValidateAddressRes, VerboseBlock}; use crate::utxo::spv::SimplePaymentVerification; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::utxo_block_header_storage::{BlockHeaderStorage, SqliteBlockHeadersStorage}; @@ -43,6 +43,7 @@ use futures::future::{join_all, Either, FutureExt, TryFutureExt}; use hex::FromHex; use keys::prefixes::*; use mm2_core::mm_ctx::MmCtxBuilder; +#[cfg(not(target_arch = "wasm32"))] use mm2_event_stream::StreamingManager; use mm2_number::bigdecimal::{BigDecimal, Signed}; use mm2_number::MmNumber; @@ -50,6 +51,7 @@ use mm2_test_helpers::electrums::doc_electrums; use mm2_test_helpers::for_tests::{electrum_servers_rpc, mm_ctx_with_custom_db, DOC_ELECTRUM_ADDRS, MARTY_ELECTRUM_ADDRS, T_BCH_ELECTRUMS}; use mocktopus::mocking::*; +use rand::{rngs::ThreadRng, Rng}; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant, CompactInteger, Reader}; use spv_validation::conf::{BlockHeaderValidationParams, SPVBlockHeader}; @@ -227,8 +229,7 @@ fn test_generate_transaction() { // so no extra outputs should appear in generated transaction assert_eq!(generated.0.outputs.len(), 1); - assert_eq!(generated.1.fee_amount, 1000); - assert_eq!(generated.1.unused_change, 999); + assert_eq!(generated.1.fee_amount, 1000 + 999); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 100000); @@ -255,7 +256,6 @@ fn test_generate_transaction() { assert_eq!(generated.0.outputs.len(), 1); assert_eq!(generated.1.fee_amount, 1000); - assert_eq!(generated.1.unused_change, 0); assert_eq!(generated.1.received_by_me, 99000); assert_eq!(generated.1.spent_by_me, 100000); assert_eq!(generated.0.outputs[0].value, 99000); @@ -710,9 +710,9 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { ..Default::default() }; let tx_details = block_on_f01(coin.withdraw(withdraw_req)).unwrap(); - // The resulting transaction size might be 210 or 211 bytes depending on signature size - // MM2 always expects the worst case during fee calculation - // 0.1 * 211 / 1000 = 0.0211 + // The resulting transaction size might be 210 or 211 bytes (no change output) depending on signature size + // MM2 always expects the worst case during fee calculation: + // tx_fee = 0.1 * 211 / 1000 = 0.0211 let expected_fee = Some( UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -895,10 +895,10 @@ fn test_withdraw_kmd_rewards_impl( }); UtxoStandardCoin::get_current_mtp .mock_safe(move |_fields| MockResult::Return(Box::pin(futures::future::ok(current_mtp)))); - NativeClient::get_verbose_transaction.mock_safe(move |_coin, txid| { - let expected: H256Json = <[u8; 32]>::from_hex(tx_hash).unwrap().into(); - assert_eq!(*txid, expected); - MockResult::Return(Box::new(futures01::future::ok(verbose.clone()))) + NativeClient::get_verbose_transactions.mock_safe(move |_coin, txids| { + let expected = <[u8; 32]>::from_hex(tx_hash).unwrap().into(); + assert_eq!(txids, &[expected]); + MockResult::Return(Box::new(futures01::future::ok([verbose.clone()].into()))) }); let client = NativeClient(Arc::new(NativeClientImpl::default())); @@ -1077,7 +1077,7 @@ fn test_electrum_rpc_client_error() { // use the static string instead because the actual error message cannot be obtain // by serde_json serialization - let expected = r#"method: "blockchain.transaction.get", params: [String("0000000000000000000000000000000000000000000000000000000000000000"), Bool(true)] }, error: Response(electrum1.cipig.net:10060, Object({"code": Number(2), "message": String("daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})")})) }"#; + let expected = r#"method: "blockchain.transaction.get", params: [String("0000000000000000000000000000000000000000000000000000000000000000"), Bool(true)] }, error: Response(electrum1.cipig.net:10060, Object {"code": Number(2), "message": String("daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})")}) }"#; let actual = format!("{}", err); assert!(actual.contains(expected)); @@ -1197,19 +1197,162 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower() { let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) - .with_fee(ActualTxFee::Dynamic(100)); + .with_fee(ActualFeeRate::Dynamic(100)); let generated = block_on(builder.build()).unwrap(); - assert_eq!(generated.0.outputs.len(), 1); + assert_eq!(generated.0.outputs.len(), 2); // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay - assert_eq!(generated.1.fee_amount, 100000000); - assert_eq!(generated.1.unused_change, 0); - assert_eq!(generated.1.received_by_me, 0); + assert_eq!(generated.1.fee_amount, 22000000); + assert_eq!(generated.1.received_by_me, 78000000); assert_eq!(generated.1.spent_by_me, 1000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); } +/// Test the transaction builder calculations (with random generated values) +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_generate_transaction_random_values() { + let client = NativeClientImpl::default(); + let mut rng = rand::thread_rng(); + + // tx_size for zcash, no shielded + let est_tx_size = |n_inputs: usize, n_outputs: usize| { + 4 + 4 + + 1 + + (n_inputs as u64) * (1 + 1 + 72 + 1 + 33 + 32 + 4 + 4) + + 1 + + (n_outputs as u64) * (1 + 25 + 8) + + 4 + + 4 + + 8 + + 1 + + 1 + + 1 + }; + + let make_random_vec_u64 = |rng: &mut ThreadRng, max_size: usize, max_value: u64| { + let vsize = rng.gen_range(1, max_size); + let mut v = vec![]; + for _i in 0..vsize { + v.push(rng.gen_range(0, max_value)) + } + v + }; + + NativeClient::get_relay_fee + .mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok("0.0".parse().unwrap())))); + let client = UtxoRpcClientEnum::Native(NativeClient(Arc::new(client))); + let mut coin = utxo_coin_fields_for_test(client, None, false); + coin.conf.force_min_relay_fee = false; + let coin = utxo_coin_from_fields(coin); + + for _i in 0..9999 { + let input_vals = make_random_vec_u64(&mut rng, 100, 100_000); + let output_vals = make_random_vec_u64(&mut rng, 100, 100_000); + let dust = rng.gen_range(0, 1000); + let fee_rate = rng.gen_range(0, 1000); + + let mut total_inputs = 0_u64; + let mut unspents = vec![]; + for val in &input_vals { + unspents.push(UnspentInfo { + value: *val, + outpoint: OutPoint::default(), + height: Default::default(), + script: Vec::new().into(), + }); + total_inputs += *val; + } + + let mut has_dust_output = false; + let mut outputs = vec![]; + let mut total_outputs = 0_u64; + for val in &output_vals { + outputs.push(TransactionOutput { + script_pubkey: "76a914124b0846223ef78130b8e544b9afc3b09988238688ac".into(), + value: *val, + }); + if *val < dust { + has_dust_output = true; + } + total_outputs += *val; + } + + let builder = block_on(UtxoTxBuilder::new(&coin)) + .add_available_inputs(unspents) + .add_outputs(outputs.clone()) + .with_dust(dust) + .with_fee(ActualFeeRate::Dynamic(fee_rate)); + + let result = block_on(builder.build()); + if has_dust_output { + let is_err_dust = matches!( + result.unwrap_err().get_inner(), + GenerateTxError::OutputValueLessThanDust { value: _, dust: _ } + ); + assert!(is_err_dust); + continue; + } + if let Err(ref err) = result { + let tx_size_max = est_tx_size(input_vals.len(), output_vals.len() + 1); + let tx_fee_max = fee_rate * tx_size_max / 1000; + if matches!(err.get_inner(), GenerateTxError::NotEnoughUtxos { + sum_utxos: _, + required: _ + }) { + assert!(total_inputs < total_outputs + tx_fee_max); + continue; + } + panic!("unexpected utxo builder err"); + } + + let generated = result.unwrap(); + + // generated transaction has no change output but dust + assert!(generated.0.outputs.len() >= output_vals.len() && generated.0.outputs.len() <= output_vals.len() + 1); + let fact_inputs = generated.0.inputs.iter().fold(0u64, |acc, input| acc + input.amount); + // total w/o change: + let fact_outputs_no_change = generated + .0 + .outputs + .iter() + .take(output_vals.len()) + .fold(0u64, |acc, output| acc + output.value); + + assert_eq!(generated.1.spent_by_me, fact_inputs); + + assert_eq!(total_outputs, fact_outputs_no_change); + + assert_eq!( + generated.1.spent_by_me, + generated.1.fee_amount + generated.1.received_by_me + total_outputs + ); + + let tx_size = est_tx_size(generated.0.inputs.len(), generated.0.outputs.len()); + let estimated_txfee = fee_rate * tx_size / 1000; + //println!("generated.1.fee_amount={} estimated_txfee={} received_by_me={} output_vals.len={} generated.0.outputs.len={} dust={}, fee_rate={}", + // generated.1.fee_amount, estimated_txfee, generated.1.received_by_me, output_vals.len(), generated.0.outputs.len(), dust, fee_rate); + + const CHANGE_OUTPUT_SIZE: u64 = 1 + 25 + 8; + let max_overpay = dust + fee_rate * CHANGE_OUTPUT_SIZE / 1000; // could be slight overpay due to dust change removed from tx + if generated.1.fee_amount > estimated_txfee { + println!( + "overpay detected: generated.1.fee_amount={} estimated_txfee={}", + generated.1.fee_amount, estimated_txfee + ); + } + assert!(generated.1.fee_amount >= estimated_txfee && generated.1.fee_amount <= estimated_txfee + max_overpay); + + let received_by_me = if generated.0.outputs.len() > output_vals.len() { + generated.0.outputs.last().unwrap().value + } else { + 0u64 + }; + assert_eq!(generated.1.received_by_me, received_by_me); + } +} + #[test] #[cfg(not(target_arch = "wasm32"))] // https://github.com/KomodoPlatform/atomicDEX-API/issues/1037 @@ -1241,16 +1384,15 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower_and_ded .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(FeePolicy::DeductFromOutput(0)) - .with_fee(ActualTxFee::Dynamic(100)); + .with_fee(ActualFeeRate::Dynamic(100)); let generated = block_on(tx_builder.build()).unwrap(); assert_eq!(generated.0.outputs.len(), 1); - // `output (= 10.0) - fee_amount (= 1.0)` - assert_eq!(generated.0.outputs[0].value, 900000000); + // `output (= 10.0) - tx_fee (= 186 byte * 100000000 / 1000)` + assert_eq!(generated.0.outputs[0].value, 981400000); - // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay - assert_eq!(generated.1.fee_amount, 100000000); - assert_eq!(generated.1.unused_change, 0); + // generated transaction fee must be equal to relay fee if calculated dynamic fee is lower than relay fee + assert_eq!(generated.1.fee_amount, 18600000); assert_eq!(generated.1.received_by_me, 0); assert_eq!(generated.1.spent_by_me, 1000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); @@ -1289,7 +1431,7 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) - .with_fee(ActualTxFee::Dynamic(1000)); + .with_fee(ActualFeeRate::Dynamic(1000)); let generated = block_on(builder.build()).unwrap(); @@ -1298,7 +1440,6 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { // resulting signed transaction size would be 3032 bytes so fee is 3032 sat assert_eq!(generated.1.fee_amount, 3032); - assert_eq!(generated.1.unused_change, 0); assert_eq!(generated.1.received_by_me, 999996968); assert_eq!(generated.1.spent_by_me, 20000000000); assert!(unsafe { GET_RELAY_FEE_CALLED }); @@ -2633,7 +2774,7 @@ fn test_get_sender_trade_fee_dynamic_tx_fee() { Some("bob passphrase max taker vol with dynamic trade fee"), false, ); - coin_fields.tx_fee = TxFee::Dynamic(EstimateFeeMethod::Standard); + coin_fields.tx_fee = FeeRate::Dynamic(EstimateFeeMethod::Standard); let coin = utxo_coin_from_fields(coin_fields); let my_balance = block_on_f01(coin.my_spendable_balance()).expect("!my_balance"); let expected_balance = BigDecimal::from_str("2.22222").expect("!BigDecimal::from_str"); @@ -3673,6 +3814,365 @@ fn test_split_qtum() { log!("Res = {:?}", res); } +#[test] +fn test_raven_low_tx_fee_okay() { + let config = json!({ + "coin": "RVN", + "name": "raven", + "fname": "RavenCoin", + "sign_message_prefix": "Raven Signed Message:\n", + "rpcport": 8766, + "pubtype": 60, + "p2shtype": 122, + "wiftype": 128, + "segwit": true, + "txfee": 1000000, + "mm2": 1, + "required_confirmations": 3, + "avg_blocktime": 60, + "protocol": { + "type": "UTXO" + }, + "derivation_path": "m/44'/175'", + "trezor_coin": "Ravencoin", + "links": { + "github": "https://github.com/RavenProject/Ravencoin", + "homepage": "https://ravencoin.org" + } + }); + let request = json!({ + "method": "electrum", + "coin": "RVN", + "servers": [{"url": "electrum1.cipig.net:10060"},{"url": "electrum2.cipig.net:10060"},{"url": "electrum3.cipig.net:10060"}], + }); + let ctx = MmCtxBuilder::default().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&request).unwrap(); + + let priv_key = Secp256k1Secret::from([1; 32]); + let raven = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RVN", &config, ¶ms, priv_key, + )) + .unwrap(); + + let unspents = vec![ + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("be3f13e94d4c58293c2fbee40dd70714c3f833a10ab05b6a328b558bb72c38a7").unwrap(), + index: 2, + }, + value: 10618039482, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("2f2eb00dad863079fc20f0c5356bb72e18f3346c126cc3f2e3654360af930f85").unwrap(), + index: 0, + }, + value: 15105673480, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("4a806e97f1fa33439d58ce5fad32c5be1e1f1a59d742050a42f237b33f2196ab").unwrap(), + index: 0, + }, + value: 15376032861, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c0f855886343247051bb42b39f75ff35690ad0fb67a08dba5e9f8b680f6fecf3").unwrap(), + index: 0, + }, + value: 29999000000, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("0e75a62d6bb49c6312a5a1f3635d4bfc39c3d1549a35dc07b253ec1b1dd3b835").unwrap(), + index: 0, + }, + value: 31916552049, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("921554ccd2e50729b521422d3ad22ae00b5721f888e35fca8d2c8ee7a7506490").unwrap(), + index: 0, + }, + value: 33542311009, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9df4256f2e3d0a65745402e7233f309767a2a629755cb3841ff0f47ce90553be").unwrap(), + index: 0, + }, + value: 35133858231, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("bf3e69728fa9a41ab06da0e595da63bc0fbe055c04f0e7125c320b3255067a3b").unwrap(), + index: 0, + }, + value: 46177879500, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c62efa3598fec9332746d0657b7bd2a1974efe637da549ddeb84c952535e214b").unwrap(), + index: 2, + }, + value: 155455117689, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9b676bc6a81e4e801a37b48f11f3834c0b1fd49ff420e104563e0895f0517946").unwrap(), + index: 2, + }, + value: 251289432230, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("210525a94adc033a745bfae158d931464a720b60bd708d00415fa38d7aa1bed1").unwrap(), + index: 0, + }, + value: 260317094896, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("d78d731e8dfc9fc1591da45da7622b13a3e395a73fd3178e6b832cd30436ed14").unwrap(), + index: 0, + }, + value: 460964136766, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("02143bce641ef1f70354085dfdff6f1031db019df561aa09b06835fbcf41b8a4").unwrap(), + index: 0, + }, + value: 515274184960, + height: None, + script: Vec::new().into(), + }, + ]; + let outputs = vec![ + TransactionOutput { + value: 1742160278745, + script_pubkey: "a9147484c59a11d053535314d5a1047005952f7fdf1e87".into(), + }, + TransactionOutput { + value: 0, + script_pubkey: "6a140e7d2af72dc4363283f4b50e1cfe6775a1ad81c1".into(), + }, + TransactionOutput { + value: 119006034408, + script_pubkey: "76a914124b0846223ef78130b8e544b9afc3b09988238688ac".into(), + }, + ]; + let builder = block_on(UtxoTxBuilder::new(&raven)) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); + let expected_fee = 3000000; + assert_eq!(expected_fee, data.fee_amount); +} + +/// Test to validate fix for https://github.com/KomodoPlatform/komodo-defi-framework/issues/2313 +#[test] +fn test_raven_low_tx_fee_error() { + let config = json!({ + "coin": "RVN", + "name": "raven", + "fname": "RavenCoin", + "sign_message_prefix": "Raven Signed Message:\n", + "rpcport": 8766, + "pubtype": 60, + "p2shtype": 122, + "wiftype": 128, + "segwit": true, + "txfee": 1000000, + "mm2": 1, + "required_confirmations": 3, + "avg_blocktime": 60, + "protocol": { + "type": "UTXO" + }, + "derivation_path": "m/44'/175'", + "trezor_coin": "Ravencoin", + "links": { + "github": "https://github.com/RavenProject/Ravencoin", + "homepage": "https://ravencoin.org" + } + }); + let request = json!({ + "method": "electrum", + "coin": "RVN", + "servers": [{"url": "electrum1.cipig.net:10060"},{"url": "electrum2.cipig.net:10060"},{"url": "electrum3.cipig.net:10060"}], + }); + let ctx = MmCtxBuilder::default().into_mm_arc(); + let params = UtxoActivationParams::from_legacy_req(&request).unwrap(); + + let priv_key = Secp256k1Secret::from([1; 32]); + let raven = block_on(utxo_standard_coin_with_priv_key( + &ctx, "RVN", &config, ¶ms, priv_key, + )) + .unwrap(); + + let unspents = vec![ + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("fde4ef4f23edc53085460559702783f7128d4b9bacd6898ffae2234576e7feb9").unwrap(), + index: 2, + }, + value: 11014394719, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("2f2eb00dad863079fc20f0c5356bb72e18f3346c126cc3f2e3654360af930f85").unwrap(), + index: 0, + }, + value: 15105673480, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("4a806e97f1fa33439d58ce5fad32c5be1e1f1a59d742050a42f237b33f2196ab").unwrap(), + index: 0, + }, + value: 15376032861, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c0f855886343247051bb42b39f75ff35690ad0fb67a08dba5e9f8b680f6fecf3").unwrap(), + index: 0, + }, + value: 29999000000, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("0e75a62d6bb49c6312a5a1f3635d4bfc39c3d1549a35dc07b253ec1b1dd3b835").unwrap(), + index: 0, + }, + value: 31916552049, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("921554ccd2e50729b521422d3ad22ae00b5721f888e35fca8d2c8ee7a7506490").unwrap(), + index: 0, + }, + value: 33542311009, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9df4256f2e3d0a65745402e7233f309767a2a629755cb3841ff0f47ce90553be").unwrap(), + index: 0, + }, + value: 35133858231, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("bf3e69728fa9a41ab06da0e595da63bc0fbe055c04f0e7125c320b3255067a3b").unwrap(), + index: 0, + }, + value: 46177879500, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("c62efa3598fec9332746d0657b7bd2a1974efe637da549ddeb84c952535e214b").unwrap(), + index: 2, + }, + value: 155455117689, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("9b676bc6a81e4e801a37b48f11f3834c0b1fd49ff420e104563e0895f0517946").unwrap(), + index: 2, + }, + value: 251289432230, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("210525a94adc033a745bfae158d931464a720b60bd708d00415fa38d7aa1bed1").unwrap(), + index: 0, + }, + value: 260317094896, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("d78d731e8dfc9fc1591da45da7622b13a3e395a73fd3178e6b832cd30436ed14").unwrap(), + index: 0, + }, + value: 460964136766, + height: None, + script: Vec::new().into(), + }, + UnspentInfo { + outpoint: OutPoint { + hash: H256::from_str("02143bce641ef1f70354085dfdff6f1031db019df561aa09b06835fbcf41b8a4").unwrap(), + index: 0, + }, + value: 515274184960, + height: None, + script: Vec::new().into(), + }, + ]; + let outputs = vec![ + TransactionOutput { + value: 1752628943415, + script_pubkey: "a91417ad3c3cd6e32aede379ac0efa42e310ba30b81d87".into(), + }, + TransactionOutput { + value: 0, + script_pubkey: "6a145786f27ae947255c21e47a3d3fe0d4e132f34e6c".into(), + }, + ]; + let builder = block_on(UtxoTxBuilder::new(&raven)) + .add_available_inputs(unspents) + .add_outputs(outputs); + let (_, data) = block_on(builder.build()).unwrap(); + let expected_fee = 3000000; + assert_eq!(expected_fee, data.fee_amount); +} + /// `QtumCoin` hasn't to check UTXO maturity if `check_utxo_maturity` is `false`. /// https://github.com/KomodoPlatform/atomicDEX-API/issues/1181 #[test] @@ -4539,7 +5039,7 @@ fn test_sign_verify_message() { ); let message = "test"; - let signature = coin.sign_message(message).unwrap(); + let signature = coin.sign_message(message, None).unwrap(); assert_eq!( signature, "HzetbqVj9gnUOznon9bvE61qRlmjH5R+rNgkxu8uyce3UBbOu+2aGh7r/GGSVFGZjRnaYC60hdwtdirTKLb7bE4=" @@ -4560,7 +5060,7 @@ fn test_sign_verify_message_segwit() { ); let message = "test"; - let signature = coin.sign_message(message).unwrap(); + let signature = coin.sign_message(message, None).unwrap(); assert_eq!( signature, "HzetbqVj9gnUOznon9bvE61qRlmjH5R+rNgkxu8uyce3UBbOu+2aGh7r/GGSVFGZjRnaYC60hdwtdirTKLb7bE4=" diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index 29ef2f5963..8dd491babe 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,7 +1,7 @@ use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; -use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, - UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; +use crate::utxo::{output_script, sat_from_big_decimal, ActualFeeRate, Address, FeePolicy, GetUtxoListOps, + PrivKeyPolicy, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionData, TransactionDetails, UnexpectedDerivationMethod, WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; @@ -180,11 +180,11 @@ where match req.fee { Some(WithdrawFee::UtxoFixed { ref amount }) => { let fixed = sat_from_big_decimal(amount, decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::FixedPerKb(fixed)); + tx_builder = tx_builder.with_fee(ActualFeeRate::FixedPerKb(fixed)); }, Some(WithdrawFee::UtxoPerKbyte { ref amount }) => { - let dynamic = sat_from_big_decimal(amount, decimals)?; - tx_builder = tx_builder.with_fee(ActualTxFee::Dynamic(dynamic)); + let dynamic_fee_rate = sat_from_big_decimal(amount, decimals)?; + tx_builder = tx_builder.with_fee(ActualFeeRate::Dynamic(dynamic_fee_rate)); }, Some(ref fee_policy) => { let error = format!( @@ -206,10 +206,9 @@ where // Finish by generating `TransactionDetails` from the signed transaction. self.on_finishing()?; - let fee_amount = data.fee_amount + data.unused_change; let fee_details = UtxoFeeDetails { coin: Some(ticker.clone()), - amount: big_decimal_from_sat(fee_amount as i64, decimals), + amount: big_decimal_from_sat(data.fee_amount as i64, decimals), }; let tx_hex = match coin.addr_format() { UtxoAddressFormat::Segwit => serialize_with_flags(&signed, SERIALIZE_TRANSACTION_WITNESS).into(), @@ -372,6 +371,11 @@ where "`PrivKeyPolicy::Metamask` is not supported for UTXO coins!".to_string(), )) }, + PrivKeyPolicy::WalletConnect { .. } => { + return MmError::err(WithdrawError::UnsupportedError( + "`PrivKeyPolicy::WalletConnect` is not supported for UTXO coins!".to_string(), + )) + }, }; Ok(signed) diff --git a/mm2src/coins/utxo_signer/Cargo.toml b/mm2src/coins/utxo_signer/Cargo.toml index 7e44ad011e..c37b1c675b 100644 --- a/mm2src/coins/utxo_signer/Cargo.toml +++ b/mm2src/coins/utxo_signer/Cargo.toml @@ -7,13 +7,13 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true chain = { path = "../../mm2_bitcoin/chain" } common = { path = "../../common" } mm2_err_handle = { path = "../../mm2_err_handle" } crypto = { path = "../../crypto" } -derive_more = "0.99" -hex = "0.4.2" +derive_more.workspace = true +hex.workspace = true keys = { path = "../../mm2_bitcoin/keys" } primitives = { path = "../../mm2_bitcoin/primitives" } rpc = { path = "../../mm2_bitcoin/rpc" } diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 6a44bf2ddb..2d4d3bde42 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -7,25 +7,25 @@ mod z_coin_errors; mod z_htlc; mod z_rpc; mod z_tx_history; +#[cfg(all(test, not(target_arch = "wasm32")))] mod z_unit_tests; -use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; -use crate::hd_wallet::HDPathAccountToAddressId; +use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::{HDAddressSelector, HDPathAccountToAddressId}; use crate::my_tx_history_v2::{MyTxHistoryErrorV2, MyTxHistoryRequestV2, MyTxHistoryResponseV2}; use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::rpc_clients::{ElectrumConnectionSettings, UnspentInfo, UtxoRpcClientEnum, UtxoRpcError, UtxoRpcFut, UtxoRpcResult}; -use crate::utxo::utxo_builder::UtxoCoinBuildError; -use crate::utxo::utxo_builder::{UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, - UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; -use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat}; -use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script}; -use crate::utxo::{sat_from_big_decimal, utxo_common, ActualTxFee, AdditionalTxData, AddrFromStrError, Address, +use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, + UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, + UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_common::{addresses_from_script, big_decimal_from_sat, big_decimal_from_sat_unsigned, + payment_script}; +use crate::utxo::{sat_from_big_decimal, utxo_common, ActualFeeRate, AdditionalTxData, AddrFromStrError, Address, BroadcastTxErr, FeePolicy, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, - RecentlySpentOutPointsGuard, UtxoActivationParams, UtxoAddressFormat, UtxoArc, UtxoCoinFields, - UtxoCommonOps, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom}; -use crate::utxo::{UnsupportedAddr, UtxoFeeDetails}; -use crate::z_coin::storage::{BlockDbImpl, WalletDbShared}; - + RecentlySpentOutPointsGuard, UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoArc, + UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoRpcMode, UtxoTxBroadcastOps, UtxoTxGenerationOps, + VerboseTransactionFrom}; +use crate::z_coin::storage::{BlockDbImpl, LockedNotesStorage, WalletDbShared}; use crate::z_coin::z_tx_history::{fetch_tx_history_from_db, ZCoinTxHistoryItem}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, @@ -38,15 +38,18 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, Con ValidatePaymentError, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WeakSpawner, WithdrawError, WithdrawFut, WithdrawRequest}; +use crate::z_coin::storage::z_locked_notes::LockedNote; use async_trait::async_trait; use bitcrypto::dhash256; use chain::constants::SEQUENCE_FINAL; use chain::{Transaction as UtxoTx, TransactionOutput}; -use common::executor::{AbortableSystem, AbortedError}; +use common::executor::{AbortableSystem, AbortedError, SpawnFuture}; +use common::log::info; use common::{calc_total_pages, log}; use crypto::privkey::{key_pair_from_secret, secp_privkey_from_hash}; use crypto::HDPathToCoin; use crypto::{Bip32DerPathOps, GlobalHDAccountArc}; +use futures::channel::oneshot; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; @@ -57,17 +60,15 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; #[cfg(test)] use mocktopus::macros::*; -use primitives::bytes::Bytes; -use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; +use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json, H264 as H264Json}; use script::{Builder as ScriptBuilder, Opcode, Script, TransactionInputSigner}; use serde_json::Value as Json; use serialization::CoinVariant; use std::collections::{HashMap, HashSet}; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; use std::iter; use std::num::NonZeroU32; use std::num::TryFromIntError; -use std::path::PathBuf; use std::sync::Arc; pub use z_coin_errors::*; pub use z_htlc::z_send_dex_fee; @@ -80,8 +81,10 @@ use zcash_client_backend::wallet::{AccountId, SpendableNote}; use zcash_extras::WalletRead; use zcash_primitives::consensus::{BlockHeight, BranchId, NetworkUpgrade, Parameters, H0}; use zcash_primitives::memo::MemoBytes; +use zcash_primitives::sapling::keys::prf_expand; use zcash_primitives::sapling::keys::OutgoingViewingKey; use zcash_primitives::sapling::note_encryption::try_sapling_output_recovery; +use zcash_primitives::sapling::Rseed; use zcash_primitives::transaction::builder::Builder as ZTxBuilder; use zcash_primitives::transaction::components::{Amount, OutputDescription, TxOut}; use zcash_primitives::transaction::Transaction as ZTransaction; @@ -92,8 +95,7 @@ use zcash_proofs::prover::LocalTxProver; cfg_native!( use common::{async_blocking, sha256_digest}; - use zcash_client_sqlite::error::SqliteClientError as ZcashClientError; - use zcash_client_sqlite::wallet::get_balance; + use std::path::PathBuf; use zcash_proofs::default_params_folder; use z_rpc::init_native_client; ); @@ -101,7 +103,6 @@ cfg_native!( cfg_wasm32!( use crate::z_coin::storage::ZcashParamsWasmImpl; use common::executor::AbortOnDropHandle; - use futures::channel::oneshot; use rand::rngs::OsRng; use zcash_primitives::transaction::builder::TransactionMetadata; pub use z_coin_errors::ZCoinBalanceError; @@ -130,6 +131,8 @@ const DEX_FEE_OVK: OutgoingViewingKey = OutgoingViewingKey([7; 32]); const DEX_FEE_Z_ADDR: &str = "zs1rp6426e9r6jkq2nsanl66tkd34enewrmr0uvj0zelhkcwmsy0uvxz2fhm9eu9rl3ukxvgzy2v9f"; const DEX_BURN_Z_ADDR: &str = "zs1ntx28kyurgvsc7rxgkdhasz8p6wzv63nqpcayvnh7c4r6cs4wfkz8ztkwazjzdsxkgaq6erscyl"; cfg_native!( + #[cfg(test)] + const DOWNLOAD_URL: &str = "https://komodoplatform.com/downloads"; const SAPLING_OUTPUT_NAME: &str = "sapling-output.params"; const SAPLING_SPEND_NAME: &str = "sapling-spend.params"; const BLOCKS_TABLE: &str = "blocks"; @@ -208,6 +211,7 @@ pub struct ZCoinFields { light_wallet_db: WalletDbShared, consensus_params: ZcoinConsensusParams, sync_state_connector: AsyncMutex, + locked_notes_db: LockedNotesStorage, } impl Transaction for ZTransaction { @@ -263,6 +267,13 @@ pub struct ZcoinTxDetails { internal_id: i64, } +struct GenTxData<'a> { + tx: ZTransaction, + data: AdditionalTxData, + sync_guard: SaplingSyncGuard<'a>, + rseeds: Vec, +} + impl ZCoin { #[inline] pub fn utxo_rpc_client(&self) -> &UtxoRpcClientEnum { &self.utxo_arc.rpc_client } @@ -326,25 +337,7 @@ impl ZCoin { }) } - #[cfg(not(target_arch = "wasm32"))] - async fn my_balance_sat(&self) -> Result> { - let wallet_db = self.z_fields.light_wallet_db.clone(); - async_blocking(move || { - let db_guard = wallet_db.db.inner(); - let db_guard = db_guard.lock().unwrap(); - let balance = get_balance(&db_guard, AccountId::default())?.into(); - Ok(balance) - }) - .await - } - - #[cfg(target_arch = "wasm32")] - async fn my_balance_sat(&self) -> Result> { - let wallet_db = self.z_fields.light_wallet_db.clone(); - Ok(wallet_db.db.get_balance(AccountId::default()).await?.into()) - } - - async fn get_spendable_notes(&self) -> Result, MmError> { + async fn get_wallet_notes(&self) -> Result, MmError> { let wallet_db = self.z_fields.light_wallet_db.clone(); let db_guard = wallet_db.db; let latest_db_block = match db_guard @@ -363,17 +356,17 @@ impl ZCoin { } /// Returns spendable notes - async fn spendable_notes_ordered(&self) -> Result, MmError> { - let mut unspents = self.get_spendable_notes().await?; + async fn wallet_notes_ordered(&self) -> Result, MmError> { + let mut unspents = self.get_wallet_notes().await?; unspents.sort_unstable_by(|a, b| a.note_value.cmp(&b.note_value)); Ok(unspents) } async fn get_one_kbyte_tx_fee(&self) -> UtxoRpcResult { - let fee = self.get_tx_fee().await?; + let fee = self.get_fee_rate().await?; match fee { - ActualTxFee::Dynamic(fee) | ActualTxFee::FixedPerKb(fee) => { + ActualFeeRate::Dynamic(fee) | ActualFeeRate::FixedPerKb(fee) => { Ok(big_decimal_from_sat_unsigned(fee, self.decimals())) }, } @@ -384,27 +377,28 @@ impl ZCoin { &self, t_outputs: Vec, z_outputs: Vec, - ) -> Result<(ZTransaction, AdditionalTxData, SaplingSyncGuard<'_>), MmError> { + ) -> Result, MmError> { + // Wait for chain to sync before selecting spendable notes or waiting for locked_notes to become + // available. let sync_guard = self.wait_for_gen_tx_blockchain_sync().await?; - + drop(sync_guard); let tx_fee = self.get_one_kbyte_tx_fee().await?; let t_output_sat: u64 = t_outputs.iter().fold(0, |cur, out| cur + u64::from(out.value)); let z_output_sat: u64 = z_outputs.iter().fold(0, |cur, out| cur + u64::from(out.amount)); let total_output_sat = t_output_sat + z_output_sat; let total_output = big_decimal_from_sat_unsigned(total_output_sat, self.utxo_arc.decimals); let total_required = &total_output + &tx_fee; + let spendable_notes = wait_for_spendable_balance_spawner(self, &total_required).await?; + + // Recreate sync_guard + let sync_guard = self.wait_for_gen_tx_blockchain_sync().await?; - let spendable_notes = self - .spendable_notes_ordered() - .await - .map_err(|err| GenTxError::SpendableNotesError(err.to_string()))?; let mut total_input_amount = BigDecimal::from(0); let mut change = BigDecimal::from(0); - let mut received_by_me = 0u64; - let mut tx_builder = ZTxBuilder::new(self.consensus_params(), sync_guard.respawn_guard.current_block()); + let mut rseeds: Vec = vec![]; for spendable_note in spendable_notes { total_input_amount += big_decimal_from_sat_unsigned(spendable_note.note_value.into(), self.decimals()); @@ -423,6 +417,8 @@ impl ZCoin { .or_mm_err(|| GenTxError::FailedToGetMerklePath)?, )?; + rseeds.push(rseed_to_string(&spendable_note.rseed)); + if total_input_amount >= total_required { change = &total_input_amount - &total_required; break; @@ -445,19 +441,21 @@ impl ZCoin { tx_builder.add_sapling_output(z_out.viewing_key, z_out.to_addr, z_out.amount, z_out.memo)?; } + // add change to tx output + let change_sat = sat_from_big_decimal(&change, self.utxo_arc.decimals)?; if change > BigDecimal::from(0u8) { - let change_sat = sat_from_big_decimal(&change, self.utxo_arc.decimals)?; received_by_me += change_sat; + let change_amount = Amount::from_u64(change_sat).map_to_mm(|_| { + GenTxError::NumConversion(NumConversError(format!( + "Failed to get ZCash amount from {}", + change_sat + ))) + })?; tx_builder.add_sapling_output( Some(self.z_fields.evk.fvk.ovk), self.z_fields.my_z_addr.clone(), - Amount::from_u64(change_sat).map_to_mm(|_| { - GenTxError::NumConversion(NumConversError(format!( - "Failed to get ZCash amount from {}", - change_sat - ))) - })?, + change_amount, None, )?; } @@ -479,14 +477,19 @@ impl ZCoin { .await? .tx_result?; - let additional_data = AdditionalTxData { + let data = AdditionalTxData { received_by_me, spent_by_me: sat_from_big_decimal(&total_input_amount, self.decimals())?, fee_amount: sat_from_big_decimal(&tx_fee, self.decimals())?, - unused_change: 0, kmd_rewards: None, }; - Ok((tx, additional_data, sync_guard)) + + Ok(GenTxData { + tx, + data, + sync_guard, + rseeds, + }) } pub async fn send_outputs( @@ -494,7 +497,12 @@ impl ZCoin { t_outputs: Vec, z_outputs: Vec, ) -> Result> { - let (tx, _, mut sync_guard) = self.gen_tx(t_outputs, z_outputs).await?; + let GenTxData { + tx, + data, + rseeds, + mut sync_guard, + } = self.gen_tx(t_outputs, z_outputs).await?; let mut tx_bytes = Vec::with_capacity(1024); tx.write(&mut tx_bytes).expect("Write should not fail"); @@ -503,6 +511,25 @@ impl ZCoin { .compat() .await?; + // TODO: Execute updates to `locked_notes_db` and `wallet_db` in a single transaction. + // This will be possible with a newer librustzcash that supports both spent notes and unconfirmed change tracking. + // See: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331#pullrequestreview-2883773336 + for rseed in rseeds { + self.z_fields + .locked_notes_db + .insert_spent_note(tx.txid().to_string(), rseed) + .await + .mm_err(|err| SendOutputsErr::InternalError(err.to_string()))?; + } + + if data.received_by_me > 0 { + self.z_fields + .locked_notes_db + .insert_change_note(tx.txid().to_string(), data.received_by_me) + .await + .mm_err(|err| SendOutputsErr::InternalError(err.to_string()))?; + } + sync_guard.respawn_guard.watch_for_tx(tx.txid()); Ok(tx) } @@ -680,7 +707,7 @@ impl ZCoin { else { return Ok(false); }; - if &address == expected_address { + if &address != expected_address { return Ok(false); } if note.value != amount_sat { @@ -785,6 +812,8 @@ pub enum ZcoinRpcMode { /// Will use `sync_params` if no last synced block found. skip_sync_params: Option, }, + #[cfg(test)] + UnitTests, } #[derive(Clone, Deserialize)] @@ -828,10 +857,6 @@ pub async fn z_coin_from_conf_and_params( protocol_info: ZcoinProtocolInfo, priv_key_policy: PrivKeyBuildPolicy, ) -> Result> { - #[cfg(target_arch = "wasm32")] - let db_dir_path = PathBuf::new(); - #[cfg(not(target_arch = "wasm32"))] - let db_dir_path = ctx.dbdir(); let z_spending_key = None; let builder = ZCoinBuilder::new( ctx, @@ -839,10 +864,9 @@ pub async fn z_coin_from_conf_and_params( conf, params, priv_key_policy, - db_dir_path, z_spending_key, protocol_info, - ); + )?; builder.build().await } @@ -874,10 +898,9 @@ pub struct ZCoinBuilder<'a> { z_coin_params: &'a ZcoinActivationParams, utxo_params: UtxoActivationParams, priv_key_policy: PrivKeyBuildPolicy, - #[cfg_attr(target_arch = "wasm32", allow(unused))] - db_dir_path: PathBuf, - /// `Some` if `ZCoin` should be initialized with a forced spending key. - z_spending_key: Option, + z_spending_key: ExtendedSpendingKey, + my_z_addr: PaymentAddress, + my_z_addr_encoded: String, protocol_info: ZcoinProtocolInfo, } @@ -910,19 +933,6 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { let utxo = self.build_utxo_fields().await?; let utxo_arc = UtxoArc::new(utxo); - let z_spending_key = match self.z_spending_key { - Some(ref z_spending_key) => z_spending_key.clone(), - None => extended_spending_key_from_protocol_info_and_policy( - &self.protocol_info, - &self.priv_key_policy, - self.z_coin_params.account, - )?, - }; - - let (_, my_z_addr) = z_spending_key - .default_address() - .map_err(|_| MmError::new(ZCoinBuildError::GetAddressError))?; - let dex_fee_addr = decode_payment_address( self.protocol_info.consensus_params.hrp_sapling_payment_address(), DEX_FEE_Z_ADDR, @@ -938,17 +948,13 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { .expect("DEX_BURN_Z_ADDR is a valid z-address"); let z_tx_prover = self.z_tx_prover().await?; - let my_z_addr_encoded = encode_payment_address( - self.protocol_info.consensus_params.hrp_sapling_payment_address(), - &my_z_addr, - ); - let blocks_db = self.init_blocks_db().await?; + let locked_notes_db = LockedNotesStorage::new(self.ctx, self.my_z_addr_encoded.clone()).await?; let (sync_state_connector, light_wallet_db) = match &self.z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => { - init_native_client(&self, self.native_client()?, blocks_db, &z_spending_key).await? + init_native_client(&self, self.native_client()?, blocks_db, locked_notes_db.clone()).await? }, ZcoinRpcMode::Light { light_wallet_d_servers, @@ -962,23 +968,26 @@ impl<'a> UtxoCoinBuilder for ZCoinBuilder<'a> { blocks_db, sync_params, skip_sync_params.unwrap_or_default(), - &z_spending_key, + locked_notes_db.clone(), ) .await? }, + #[cfg(test)] + ZcoinRpcMode::UnitTests => z_unit_tests::create_test_sync_connector(&self).await, }; let z_fields = Arc::new(ZCoinFields { dex_fee_addr, dex_burn_addr, - my_z_addr, - my_z_addr_encoded, - evk: ExtendedFullViewingKey::from(&z_spending_key), - z_spending_key, + my_z_addr: self.my_z_addr, + my_z_addr_encoded: self.my_z_addr_encoded, + evk: ExtendedFullViewingKey::from(&self.z_spending_key), + z_spending_key: self.z_spending_key, z_tx_prover: Arc::new(z_tx_prover), light_wallet_db, consensus_params: self.protocol_info.consensus_params, sync_state_connector, + locked_notes_db, }); Ok(ZCoin { utxo_arc, z_fields }) @@ -993,10 +1002,9 @@ impl<'a> ZCoinBuilder<'a> { conf: &'a Json, z_coin_params: &'a ZcoinActivationParams, priv_key_policy: PrivKeyBuildPolicy, - db_dir_path: PathBuf, z_spending_key: Option, protocol_info: ZcoinProtocolInfo, - ) -> ZCoinBuilder<'a> { + ) -> MmResult, ZCoinBuildError> { let utxo_mode = match &z_coin_params.mode { #[cfg(not(target_arch = "wasm32"))] ZcoinRpcMode::Native => UtxoRpcMode::Native, @@ -1010,6 +1018,12 @@ impl<'a> ZCoinBuilder<'a> { min_connected: *min_connected, max_connected: *max_connected, }, + #[cfg(test)] + ZcoinRpcMode::UnitTests => UtxoRpcMode::Electrum { + servers: vec![], + min_connected: None, + max_connected: Some(1), + }, }; let utxo_params = UtxoActivationParams { mode: utxo_mode, @@ -1025,27 +1039,44 @@ impl<'a> ZCoinBuilder<'a> { // This is not used for Zcoin so we just provide a default value path_to_address: HDPathAccountToAddressId::default(), }; - ZCoinBuilder { + + let z_spending_key = match z_spending_key { + Some(ref z_spending_key) => z_spending_key.clone(), + None => extended_spending_key_from_protocol_info_and_policy( + &protocol_info, + &priv_key_policy, + z_coin_params.account, + )?, + }; + + let (_, my_z_addr) = z_spending_key + .default_address() + .map_to_mm(|_| ZCoinBuildError::GetAddressError)?; + + let my_z_addr_encoded = + encode_payment_address(protocol_info.consensus_params.hrp_sapling_payment_address(), &my_z_addr); + + Ok(ZCoinBuilder { ctx, ticker, conf, z_coin_params, utxo_params, priv_key_policy, - db_dir_path, z_spending_key, + my_z_addr, + my_z_addr_encoded, protocol_info, - } + }) } async fn init_blocks_db(&self) -> Result> { - let cache_db_path = self.db_dir_path.join(format!("{}_cache.db", self.ticker)); - let ctx = self.ctx.clone(); + let ctx = &self.ctx; let ticker = self.ticker.to_string(); - BlockDbImpl::new(&ctx, ticker, cache_db_path) - .map_err(|err| MmError::new(ZcoinClientInitError::ZcoinStorageError(err.to_string()))) + BlockDbImpl::new(ctx, ticker) .await + .mm_err(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string())) } #[cfg(not(target_arch = "wasm32"))] @@ -1055,6 +1086,9 @@ impl<'a> ZCoinBuilder<'a> { Some(file_path) => PathBuf::from(file_path), }; + #[cfg(test)] + z_unit_tests::download_parameters_for_tests(¶ms_dir).await; + async_blocking(move || { let (spend_path, output_path) = get_spend_output_paths(params_dir)?; let verification_successful = verify_checksum_zcash_params(&spend_path, &output_path)?; @@ -1104,7 +1138,6 @@ pub async fn z_coin_from_conf_and_params_with_docker( conf: &Json, params: &ZcoinActivationParams, priv_key_policy: PrivKeyBuildPolicy, - db_dir_path: PathBuf, protocol_info: ZcoinProtocolInfo, spending_key: &str, ) -> Result> { @@ -1120,10 +1153,9 @@ pub async fn z_coin_from_conf_and_params_with_docker( conf, params, priv_key_policy, - db_dir_path, Some(z_spending_key), protocol_info, - ); + )?; println!("ZOMBIE_wallet.db will be synch'ed with the chain, this may take a while for the first time."); println!("You may also run prepare_zombie_sapling_cache test to update ZOMBIE_wallet.db before running tests."); @@ -1136,6 +1168,11 @@ impl MarketCoinOps for ZCoin { fn my_address(&self) -> MmResult { Ok(self.z_fields.my_z_addr_encoded.clone()) } + fn address_from_pubkey(&self, _pubkey: &H264Json) -> MmResult { + // NOTE: We can't derive a z-address from pubkey, so we will just return our own z_address. + Ok(self.z_fields.my_z_addr_encoded.clone()) + } + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; Ok(pubkey.to_string()) @@ -1143,7 +1180,7 @@ impl MarketCoinOps for ZCoin { fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { None } - fn sign_message(&self, _message: &str) -> SignatureResult { + fn sign_message(&self, _message: &str, _address: Option) -> SignatureResult { MmError::err(SignatureError::InvalidRequest( "Message signing is not supported by the given coin type".to_string(), )) @@ -1155,15 +1192,62 @@ impl MarketCoinOps for ZCoin { )) } + /// Calculates the wallet balance, divided into spendable and unspendable portions. + /// Unspendable balance consists of notes that are locked in the wallet. + /// TODO: Track unconfirmed change outputs in a dedicated DB/table (similar to locked_notes_db). + /// - Include them in the unspendable portion of the balance until confirmed. + /// - This will improve spendable/unspendable accuracy. fn my_balance(&self) -> BalanceFut { let coin = self.clone(); let fut = async move { - let sat = coin - .my_balance_sat() + let locked_notes = coin + .z_fields + .locked_notes_db + .load_all_notes() .await .mm_err(|e| BalanceError::WalletStorageError(e.to_string()))?; - Ok(CoinBalance::new(big_decimal_from_sat_unsigned(sat, coin.decimals()))) + + // Locked (unconfirmed) spent notes are not counted as spendable. + let spent_rseeds: HashSet<_> = locked_notes + .iter() + .filter_map(|n| { + if let LockedNote::Spent { rseed, .. } = n { + Some(rseed.clone()) + } else { + None + } + }) + .collect(); + + // Locked (unconfirmed) change notes are counted as unspendable. + let unspendable_change_sat: u64 = locked_notes + .iter() + .filter_map(|n| { + if let LockedNote::Change { value, .. } = n { + Some(*value) + } else { + None + } + }) + .sum(); + + let wallet_notes = coin + .get_wallet_notes() + .await + .map_err(|err| BalanceError::WalletStorageError(err.to_string()))?; + + let spendable_amount = wallet_notes + .iter() + .filter(|n| !spent_rseeds.contains(&rseed_to_string(&n.rseed))) + .fold(Amount::zero(), |acc, n| acc + n.note_value); + + let spendable_sat = + u64::try_from(spendable_amount).map_to_mm(|err| BalanceError::Internal(err.to_string()))?; + let unspendable = big_decimal_from_sat_unsigned(unspendable_change_sat, coin.decimals()); + let spendable = big_decimal_from_sat_unsigned(spendable_sat, coin.decimals()); + Ok(CoinBalance { spendable, unspendable }) }; + Box::new(fut.boxed().compat()) } @@ -1414,7 +1498,7 @@ impl SwapOps for ZCoin { /// TODO: when all mm2 nodes upgrade to support the burn account then disable validation of the Standard option async fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentResult<()> { let z_tx = match validate_fee_args.fee_tx { - TransactionEnum::ZTransaction(t) => t.clone(), + TransactionEnum::ZTransaction(t) => t, fee_tx => { return MmError::err(ValidatePaymentError::InternalError(format!( "Invalid fee tx type. fee tx: {:?}", @@ -1432,7 +1516,7 @@ impl SwapOps for ZCoin { .get_verbose_transaction(&tx_hash.into()) .compat() .await - .map_err(|e| MmError::new(ValidatePaymentError::InvalidRpcResponse(e.into_inner().to_string())))?; + .mm_err(|e| ValidatePaymentError::InvalidRpcResponse(e.to_string()))?; let mut encoded = Vec::with_capacity(1024); z_tx.write(&mut encoded).expect("Writing should not fail"); @@ -1726,17 +1810,13 @@ impl MmCoin for ZCoin { #[async_trait] impl UtxoTxGenerationOps for ZCoin { - async fn get_tx_fee(&self) -> UtxoRpcResult { utxo_common::get_tx_fee(&self.utxo_arc).await } + async fn get_fee_rate(&self) -> UtxoRpcResult { utxo_common::get_fee_rate(&self.utxo_arc).await } - async fn calc_interest_if_required( - &self, - unsigned: TransactionInputSigner, - data: AdditionalTxData, - my_script_pub: Bytes, - dust: u64, - ) -> UtxoRpcResult<(TransactionInputSigner, AdditionalTxData)> { - utxo_common::calc_interest_if_required(self, unsigned, data, my_script_pub, dust).await + async fn calc_interest_if_required(&self, unsigned: &mut TransactionInputSigner) -> UtxoRpcResult { + utxo_common::calc_interest_if_required(self, unsigned).await } + + fn supports_interest(&self) -> bool { utxo_common::is_kmd(self) } } #[async_trait] @@ -1925,7 +2005,7 @@ impl InitWithdrawCoin for ZCoin { memo, }; - let (tx, data, _sync_guard) = self.gen_tx(vec![], vec![z_output]).await?; + let GenTxData { tx, data, .. } = self.gen_tx(vec![], vec![z_output]).await?; let mut tx_bytes = Vec::with_capacity(1024); tx.write(&mut tx_bytes) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; @@ -1934,9 +2014,10 @@ impl InitWithdrawCoin for ZCoin { let received_by_me = big_decimal_from_sat_unsigned(data.received_by_me, self.decimals()); let spent_by_me = big_decimal_from_sat_unsigned(data.spent_by_me, self.decimals()); + let tx_hash_hex = hex::encode(&tx_hash); Ok(TransactionDetails { - tx: TransactionData::new_signed(tx_bytes.into(), hex::encode(&tx_hash)), + tx: TransactionData::new_signed(tx_bytes.into(), tx_hash_hex), from: vec![self.z_fields.my_z_addr_encoded.clone()], to: vec![req.to], my_balance_change: &received_by_me - &spent_by_me, @@ -1958,6 +2039,108 @@ impl InitWithdrawCoin for ZCoin { } } +/// Waits until there are enough _unlocked_ Sapling notes to cover `total_required`. +/// TODO: Consider adding `wait_until` argument. +/// TODO: Integrate this into `light_wallet_db_sync_loop` instead of having a separate function. +/// Can be addressed when migrating to a newer librustzcash which supports spent note tracking. +/// See: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331#pullrequestreview-2883773336 +async fn wait_for_spendable_balance_impl( + selfi: ZCoin, + total_required: BigDecimal, +) -> Result, MmError> { + const MAX_RETRIES: usize = 40; + const RETRY_DELAY: f64 = 15.0; + + let mut retries = 0; + + loop { + let wallet_notes = selfi + .wallet_notes_ordered() + .await + .map_err(|e| GenTxError::SpendableNotesError(e.to_string()))?; + let wallet_notes_len = wallet_notes.len(); + + let locked_notes = selfi.z_fields.locked_notes_db.load_all_notes().await?; + + let unlocked_notes: Vec = if locked_notes.is_empty() { + wallet_notes + } else { + let unconfirmed_spent_rseeds: HashSet = locked_notes + .iter() + .filter_map(|n| { + if let LockedNote::Spent { rseed, .. } = n { + Some(rseed.clone()) + } else { + None + } + }) + .collect(); + + wallet_notes + .into_iter() + .filter(|note| !unconfirmed_spent_rseeds.contains(&rseed_to_string(¬e.rseed))) + .collect() + }; + let unlocked_notes_len = unlocked_notes.len(); + + let sum_available = unlocked_notes.iter().map(|n| n.note_value).sum::(); + let sum_available = u64::try_from(sum_available).map_to_mm(|err| GenTxError::Internal(err.to_string()))?; + let sum_available = big_decimal_from_sat_unsigned(sum_available, selfi.decimals()); + + // Reteurn InsufficientBalance error when all notes are unlocked but amount is insufficient. + if sum_available < total_required && unlocked_notes_len == wallet_notes_len { + return MmError::err(GenTxError::InsufficientBalance { + coin: selfi.ticker().to_string(), + available: sum_available, + required: total_required, + }); + } + + // Returns available notes when either sufficient funds exist or all notes are unlocked. + // Otherwise, waits for locked notes to become available up to MAX_RETRIES. + if sum_available >= total_required || unlocked_notes_len == wallet_notes_len { + return Ok(unlocked_notes.into_iter()); + } + + if retries >= MAX_RETRIES { + return MmError::err(GenTxError::Internal(format!( + "Locked notes did not become available after {} retries", + MAX_RETRIES + ))); + } + + info!( + "Locked notes present; retrying in {}s (attempt {}/{})", + RETRY_DELAY, + retries + 1, + MAX_RETRIES + ); + common::executor::Timer::sleep(RETRY_DELAY).await; + retries += 1; + } +} + +async fn wait_for_spendable_balance_spawner( + selfi: &ZCoin, + total_required: &BigDecimal, +) -> Result, MmError> { + let coin = selfi.clone(); + let required = total_required.clone(); + let (tx, rx) = oneshot::channel(); + + selfi.spawner().spawn(async move { + let result = wait_for_spendable_balance_impl(coin, required).await; + let _ = tx.send(result); + }); + + match rx.await { + Ok(res) => res, + Err(_) => MmError::err(GenTxError::Internal( + "wait_for_spendable_balance task was cancelled".into(), + )), + } +} + /// Interpret a string or hex-encoded memo, and return a Memo object. /// Inspired by https://github.com/adityapk00/zecwallet-light-cli/blob/v1.7.20/lib/src/lightwallet/utils.rs#L23 #[allow(clippy::result_large_err)] @@ -2021,52 +2204,12 @@ fn extended_spending_key_from_global_hd_account( Ok(spending_key) } -#[test] -fn derive_z_key_from_mm_seed() { - use crypto::privkey::key_pair_from_seed; - use zcash_client_backend::encoding::encode_extended_spending_key; - - let seed = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; - let secp_keypair = key_pair_from_seed(seed).unwrap(); - let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); - let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); - assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqqytwz2zjt587n63kyz6jawmflttqu5rxavvqx3lzfs0tdr0w7g5tgntxzf5erd3jtvva5s52qx0ms598r89vrmv30r69zehxy2r3vesghtqd6dfwdtnauzuj8u8eeqfx7qpglzu6z54uzque6nzzgnejkgq569ax4lmk0v95rfhxzxlq3zrrj2z2kqylx2jp8g68lqu6alczdxd59lzp4hlfuj3jp54fp06xsaaay0uyass992g507tdd7psua5w6q76dyq3"); - - let (_, address) = z_spending_key.default_address().unwrap(); - let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); - assert_eq!( - encoded_addr, - "zs182ht30wnnnr8jjhj2j9v5dkx3qsknnr5r00jfwk2nczdtqy7w0v836kyy840kv2r8xle5gcl549" - ); - - let seed = "also shoot benefit prefer juice shell elder veteran woman mimic image kidney"; - let secp_keypair = key_pair_from_seed(seed).unwrap(); - let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); - let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); - assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqq8jnhc9stsqwts6pu5ayzgy4szplvy03u227e50n3u8e6dwn5l0q5s3s8xfc03r5wmyh5s5dq536ufwn2k89ngdhnxy64sd989elwas6kr7ygztsdkw6k6xqyvhtu6e0dhm4mav8rus0fy8g0hgy9vt97cfjmus0m2m87p4qz5a00um7gwjwk494gul0uvt3gqyjujcclsqry72z57kr265jsajactgfn9m3vclqvx8fsdnwp4jwj57ffw560vvwks9g9hpu"); - - let (_, address) = z_spending_key.default_address().unwrap(); - let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); - assert_eq!( - encoded_addr, - "zs1funuwrjr2stlr6fnhkdh7fyz3p7n0p8rxase9jnezdhc286v5mhs6q3myw0phzvad5mvqgfxpam" - ); -} - -#[test] -fn test_interpret_memo_string() { - use std::str::FromStr; - use zcash_primitives::memo::Memo; +#[inline] +fn rseed_to_string(rseed: &Rseed) -> String { + const INPUT: [u8; 1] = [0x04]; - let actual = interpret_memo_string("68656c6c6f207a63617368").unwrap(); - let expected = Memo::from_str("68656c6c6f207a63617368").unwrap().encode(); - assert_eq!(actual, expected); - - let actual = interpret_memo_string("A custom memo").unwrap(); - let expected = Memo::from_str("A custom memo").unwrap().encode(); - assert_eq!(actual, expected); - - let actual = interpret_memo_string("0x68656c6c6f207a63617368").unwrap(); - let expected = MemoBytes::from_bytes(&hex::decode("68656c6c6f207a63617368").unwrap()).unwrap(); - assert_eq!(actual, expected); + match rseed { + Rseed::BeforeZip212(rcm) => rcm.to_string(), + Rseed::AfterZip212(rseed) => jubjub::Fr::from_bytes_wide(prf_expand(rseed, &INPUT).as_array()).to_string(), + } } diff --git a/mm2src/coins/z_coin/storage.rs b/mm2src/coins/z_coin/storage.rs index e2534281b7..e8b1b6e971 100644 --- a/mm2src/coins/z_coin/storage.rs +++ b/mm2src/coins/z_coin/storage.rs @@ -1,16 +1,19 @@ use crate::z_coin::{ValidateBlocksError, ZcoinConsensusParams, ZcoinStorageError}; use mm2_event_stream::StreamingManager; -pub mod blockdb; -pub use blockdb::*; +pub(crate) mod blockdb; +pub(crate) use blockdb::*; + +pub(crate) mod walletdb; +pub(crate) use walletdb::*; + +pub(crate) mod z_locked_notes; +pub(crate) use z_locked_notes::{LockedNotesStorage, LockedNotesStorageError}; -pub mod walletdb; #[cfg(target_arch = "wasm32")] mod z_params; #[cfg(target_arch = "wasm32")] pub(crate) use z_params::ZcashParamsWasmImpl; -pub use walletdb::*; - use mm2_err_handle::mm_error::MmResult; #[cfg(target_arch = "wasm32")] use walletdb::wasm::storage::DataConnStmtCacheWasm; @@ -118,6 +121,7 @@ pub async fn scan_cached_block( data: &DataConnStmtCacheWrapper, params: &ZcoinConsensusParams, block: &CompactBlock, + locked_notes_db: &LockedNotesStorage, last_height: &mut BlockHeight, ) -> Result>, ValidateBlocksError> { let mut data_guard = data.inner().clone(); @@ -159,7 +163,6 @@ pub async fn scan_cached_block( // To enforce that all roots match, // see -> https://github.com/KomodoPlatform/librustzcash/blob/e92443a7bbd1c5e92e00e6deb45b5a33af14cea4/zcash_client_backend/src/data_api/chain.rs#L304-L326 - let new_witnesses = data_guard .advance_by_block( &(PrunedBlock { @@ -186,5 +189,15 @@ pub async fn scan_cached_block( witnesses.extend(new_witnesses); *last_height = current_height; + // TODO: Execute updates to `locked_notes_db` and `wallet_db` in a single transaction. + // This will be possible with a newer librustzcash that supports both spent notes and unconfirmed change tracking. + // See: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2331#pullrequestreview-2883773336 + for tx in &txs { + locked_notes_db + .remove_notes_for_txid(tx.txid.to_string()) + .await + .map_err(|err| ValidateBlocksError::DbError(err.to_string()))?; + } + Ok(txs) } diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs index 826ed52bdd..b95af8c4cd 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_idb_storage.rs @@ -1,5 +1,5 @@ use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, - ZcoinConsensusParams, ZcoinStorageRes}; + LockedNotesStorage, ZcoinConsensusParams, ZcoinStorageRes}; use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; use crate::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; use crate::z_coin::z_coin_errors::ZcoinStorageError; @@ -10,7 +10,6 @@ use mm2_db::indexed_db::{BeBigUint, ConstructibleDb, DbIdentifier, DbInstance, D IndexedDbBuilder, InitDbResult, MultiIndex, OnUpgradeResult, TableSignature}; use mm2_err_handle::prelude::*; use protobuf::Message; -use std::path::PathBuf; use zcash_client_backend::proto::compact_formats::CompactBlock; use zcash_extras::WalletRead; use zcash_primitives::block::BlockHash; @@ -68,7 +67,7 @@ impl BlockDbInner { } impl BlockDbImpl { - pub async fn new(ctx: &MmArc, ticker: String, _path: PathBuf) -> ZcoinStorageRes { + pub async fn new(ctx: &MmArc, ticker: String) -> ZcoinStorageRes { Ok(Self { db: ConstructibleDb::new(ctx).into_shared(), ticker, @@ -221,6 +220,7 @@ impl BlockDbImpl { mode: BlockProcessingMode, validate_from: Option<(BlockHeight, BlockHash)>, limit: Option, + locked_notes_db: &LockedNotesStorage, ) -> ZcoinStorageRes<()> { let ticker = self.ticker.to_owned(); let mut from_height = match &mode { @@ -254,7 +254,7 @@ impl BlockDbImpl { validate_chain(block, &mut prev_height, &mut prev_hash).await?; }, BlockProcessingMode::Scan(data, streaming_manager) => { - let txs = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + let txs = scan_cached_block(data, ¶ms, &block, locked_notes_db, &mut from_height).await?; if !txs.is_empty() { // Stream out the new transactions. streaming_manager diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs index 44721b4364..6083751df6 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs @@ -1,5 +1,5 @@ use crate::z_coin::storage::{scan_cached_block, validate_chain, BlockDbImpl, BlockProcessingMode, CompactBlockRow, - ZcoinStorageRes}; + LockedNotesStorage, ZcoinStorageRes}; use crate::z_coin::tx_history_events::ZCoinTxHistoryEventStreamer; use crate::z_coin::z_balance_streaming::ZCoinBalanceEventStreamer; use crate::z_coin::z_coin_errors::ZcoinStorageError; @@ -12,7 +12,6 @@ use itertools::Itertools; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use protobuf::Message; -use std::path::PathBuf; use std::sync::{Arc, Mutex}; use zcash_client_backend::data_api::error::Error as ChainError; use zcash_client_backend::proto::compact_formats::CompactBlock; @@ -46,8 +45,10 @@ impl From> for ZcoinStorageError { impl BlockDbImpl { #[cfg(not(test))] - pub async fn new(_ctx: &MmArc, ticker: String, path: PathBuf) -> ZcoinStorageRes { + pub async fn new(ctx: &MmArc, ticker: String) -> ZcoinStorageRes { + let path = ctx.global_dir().join(format!("{}_cache.db", ticker)); async_blocking(move || { + mm2_io::fs::create_parents(&path).map_err(|err| ZcoinStorageError::IoError(err.to_string()))?; let conn = Connection::open(path).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; let conn = Arc::new(Mutex::new(conn)); let conn_lock = conn.lock().unwrap(); @@ -69,7 +70,7 @@ impl BlockDbImpl { } #[cfg(test)] - pub(crate) async fn new(ctx: &MmArc, ticker: String, _path: PathBuf) -> ZcoinStorageRes { + pub(crate) async fn new(ctx: &MmArc, ticker: String) -> ZcoinStorageRes { let ctx = ctx.clone(); async_blocking(move || { let conn = ctx @@ -191,6 +192,7 @@ impl BlockDbImpl { mode: BlockProcessingMode, validate_from: Option<(BlockHeight, BlockHash)>, limit: Option, + locked_notes_db: &LockedNotesStorage, ) -> ZcoinStorageRes<()> { let ticker = self.ticker.to_owned(); let mut from_height = match &mode { @@ -229,7 +231,7 @@ impl BlockDbImpl { validate_chain(block, &mut prev_height, &mut prev_hash).await?; }, BlockProcessingMode::Scan(data, streaming_manager) => { - let txs = scan_cached_block(data, ¶ms, &block, &mut from_height).await?; + let txs = scan_cached_block(data, ¶ms, &block, locked_notes_db, &mut from_height).await?; if !txs.is_empty() { // Stream out the new transactions. streaming_manager diff --git a/mm2src/coins/z_coin/storage/blockdb/mod.rs b/mm2src/coins/z_coin/storage/blockdb/mod.rs index bc41c4de00..e1b9c13d5b 100644 --- a/mm2src/coins/z_coin/storage/blockdb/mod.rs +++ b/mm2src/coins/z_coin/storage/blockdb/mod.rs @@ -25,7 +25,6 @@ pub struct BlockDbImpl { mod block_db_storage_tests { use crate::z_coin::storage::BlockDbImpl; use common::log::info; - use std::path::PathBuf; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; @@ -37,9 +36,7 @@ mod block_db_storage_tests { pub(crate) async fn test_insert_block_and_get_latest_block_impl() { let ctx = mm_ctx_with_custom_db(); - let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) - .await - .unwrap(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string()).await.unwrap(); // insert block for header in HEADERS.iter() { db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); @@ -52,9 +49,7 @@ mod block_db_storage_tests { pub(crate) async fn test_rewind_to_height_impl() { let ctx = mm_ctx_with_custom_db(); - let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) - .await - .unwrap(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string()).await.unwrap(); // insert block for header in HEADERS.iter() { db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); @@ -77,9 +72,7 @@ mod block_db_storage_tests { #[allow(unused)] pub(crate) async fn test_process_blocks_with_mode_impl() { let ctx = mm_ctx_with_custom_db(); - let db = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) - .await - .unwrap(); + let db = BlockDbImpl::new(&ctx, TICKER.to_string()).await.unwrap(); // insert block for header in HEADERS.iter() { let inserted_id = db.insert_block(header.0, hex::decode(header.1).unwrap()).await.unwrap(); diff --git a/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs b/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs index 3a957d375f..0408182ae0 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wallet_sql_storage.rs @@ -11,7 +11,7 @@ use zcash_extras::{WalletRead, WalletWrite}; use zcash_primitives::block::BlockHash; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::TxId; -use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; +use zcash_primitives::zip32::ExtendedFullViewingKey; /// `create_wallet_db` is responsible for creating a new Zcoin wallet database, initializing it /// with the provided parameters, and executing various initialization steps. These steps include checking and @@ -24,6 +24,9 @@ pub async fn create_wallet_db( evk: ExtendedFullViewingKey, continue_from_prev_sync: bool, ) -> Result, MmError> { + mm2_io::fs::create_parents_async(&wallet_db_path) + .await + .map_err(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string()))?; let db = async_blocking(move || { WalletDbAsync::for_path(wallet_db_path, consensus_params) .map_to_mm(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string())) @@ -81,16 +84,15 @@ impl<'a> WalletDbShared { pub async fn new( builder: &ZCoinBuilder<'a>, checkpoint_block: Option, - z_spending_key: &ExtendedSpendingKey, continue_from_prev_sync: bool, ) -> ZcoinStorageRes { let ticker = builder.ticker; let consensus_params = builder.protocol_info.consensus_params.clone(); let wallet_db = create_wallet_db( - builder.db_dir_path.join(format!("{ticker}_wallet.db")), + builder.ctx.wallet_dir().join(format!("{ticker}_wallet.db")), consensus_params, checkpoint_block, - ExtendedFullViewingKey::from(z_spending_key), + ExtendedFullViewingKey::from(&builder.z_spending_key), continue_from_prev_sync, ) .await diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs index c1ffdfb0a2..5fb428db4f 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/mod.rs @@ -68,6 +68,7 @@ fn to_spendable_note(note: SpendableNoteConstructor) -> MmResult LocalTxProver { let (spend_buf, output_buf) = wagyu_zcash_parameters::load_sapling_parameters(); @@ -206,6 +208,9 @@ mod wasm_test { async fn test_valid_chain_state() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -226,6 +231,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -247,6 +253,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -259,6 +266,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -271,6 +279,7 @@ mod wasm_test { BlockProcessingMode::Validate, max_height_hash, None, + &locked_notes_db, ) .await .unwrap(); @@ -292,6 +301,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -304,6 +314,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -315,6 +326,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -324,6 +336,9 @@ mod wasm_test { async fn invalid_chain_cache_disconnected() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -363,6 +378,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -374,6 +390,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -403,6 +420,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap_err(); @@ -418,6 +436,9 @@ mod wasm_test { async fn test_invalid_chain_reorg() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -457,6 +478,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -468,6 +490,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap(); @@ -497,6 +520,7 @@ mod wasm_test { BlockProcessingMode::Validate, walletdb.get_max_height_hash().await.unwrap(), None, + &locked_notes_db, ) .await .unwrap_err(); @@ -512,6 +536,9 @@ mod wasm_test { async fn test_data_db_rewinding() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -546,6 +573,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -576,6 +604,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -588,6 +617,9 @@ mod wasm_test { async fn test_scan_cached_blocks_requires_sequential_blocks() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -615,6 +647,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap(); @@ -633,6 +666,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await .unwrap_err(); @@ -656,7 +690,8 @@ mod wasm_test { consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, - None + None, + &locked_notes_db ) .await .is_ok()); @@ -671,6 +706,9 @@ mod wasm_test { async fn test_scan_cached_blokcs_finds_received_notes() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -700,7 +738,8 @@ mod wasm_test { consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, - None + None, + &locked_notes_db ) .await .is_ok()); @@ -721,7 +760,8 @@ mod wasm_test { consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, - None + None, + &locked_notes_db ) .await .is_ok()); @@ -734,6 +774,9 @@ mod wasm_test { async fn test_scan_cached_blocks_finds_change_notes() { // init blocks_db let ctx = mm_ctx_with_custom_db(); + let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()) + .await + .unwrap(); let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()) .await .unwrap(); @@ -763,7 +806,8 @@ mod wasm_test { consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, - None + None, + &locked_notes_db ) .await .is_ok()); @@ -794,6 +838,7 @@ mod wasm_test { BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, + &locked_notes_db, ) .await; assert!(scan.is_ok()); @@ -810,6 +855,7 @@ mod wasm_test { // async fn create_to_address_fails_on_unverified_notes() { // // init blocks_db // let ctx = mm_ctx_with_custom_db(); + // let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()).await.unwrap(); // let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()).await.unwrap(); // // // init walletdb. @@ -853,7 +899,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .is_ok()); // @@ -898,7 +944,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .is_ok()); // @@ -929,7 +975,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // assert!(blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .is_ok()); // @@ -1079,6 +1125,7 @@ mod wasm_test { // // // init blocks_db // let ctx = mm_ctx_with_custom_db(); + // let locked_notes_db = LockedNotesStorage::new(ctx.clone(), MY_ADDRESS.to_string()).await.unwrap(); // let blockdb = BlockDbImpl::new(&ctx, TICKER.to_string(), PathBuf::new()).await.unwrap(); // // // init walletdb. @@ -1099,7 +1146,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .unwrap(); // assert_eq!(walletdb.get_balance(AccountId(0)).await.unwrap(), value); @@ -1156,7 +1203,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .unwrap(); // @@ -1192,7 +1239,7 @@ mod wasm_test { // // Scan the cache // let scan = DataConnStmtCacheWrapper::new(DataConnStmtCacheWasm(walletdb.clone())); // blockdb - // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None) + // .process_blocks_with_mode(consensus_params.clone(), BlockProcessingMode::Scan(scan, StreamingManager::default()), None, None, &locked_notes_db) // .await // .unwrap(); // diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs index d9ac5ec322..78d8a6fbaa 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs @@ -33,7 +33,7 @@ use zcash_primitives::merkle_tree::{CommitmentTree, IncrementalWitness}; use zcash_primitives::sapling::{Node, Nullifier, PaymentAddress}; use zcash_primitives::transaction::components::Amount; use zcash_primitives::transaction::{Transaction, TxId}; -use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}; +use zcash_primitives::zip32::ExtendedFullViewingKey; const DB_NAME: &str = "wallet_db_cache"; const DB_VERSION: u32 = 1; @@ -54,7 +54,6 @@ impl<'a> WalletDbShared { pub async fn new( builder: &ZCoinBuilder<'a>, checkpoint_block: Option, - z_spending_key: &ExtendedSpendingKey, continue_from_prev_sync: bool, ) -> ZcoinStorageRes { let ticker = builder.ticker; @@ -62,7 +61,7 @@ impl<'a> WalletDbShared { let db = WalletIndexedDb::new(builder.ctx, ticker, consensus_params).await?; let extrema = db.block_height_extrema().await?; let get_evk = db.get_extended_full_viewing_keys().await?; - let evk = ExtendedFullViewingKey::from(z_spending_key); + let evk = ExtendedFullViewingKey::from(&builder.z_spending_key); let min_sync_height = extrema.map(|(min, _)| u32::from(min)); let init_block_height = checkpoint_block.clone().map(|block| block.height); @@ -1083,7 +1082,7 @@ impl WalletRead for WalletIndexedDb { let matching_tx = maybe_txs.iter().find(|(id_tx, _tx)| id_tx.to_bigint() == note.spent); if let Some((_, tx)) = matching_tx { - if tx.block.is_none() { + if tx.block.is_some() { nullifiers.push(( AccountId( note.account @@ -1096,18 +1095,6 @@ impl WalletRead for WalletIndexedDb { .unwrap(), )); } - } else { - nullifiers.push(( - AccountId( - note.account - .to_u32() - .ok_or_else(|| ZcoinStorageError::GetFromStorageError("Invalid amount".to_string()))?, - ), - Nullifier::from_slice(¬e.nf.clone().ok_or_else(|| { - ZcoinStorageError::GetFromStorageError("Error while putting tx_meta".to_string()) - })?) - .unwrap(), - )); } } diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs index 53471571d4..92ce152837 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/tables.rs @@ -141,11 +141,6 @@ impl WalletDbReceivedNotesTable { pub const TICKER_ACCOUNT_INDEX: &'static str = "ticker_account_index"; /// A **unique** index that consists of the following properties: /// * ticker - /// * note_id - /// * nf - pub const TICKER_NOTES_ID_NF_INDEX: &'static str = "ticker_note_id_nf_index"; - /// A **unique** index that consists of the following properties: - /// * ticker /// * tx /// * output_index pub const TICKER_TX_OUTPUT_INDEX: &'static str = "ticker_tx_output_index"; diff --git a/mm2src/coins/z_coin/storage/z_locked_notes/mod.rs b/mm2src/coins/z_coin/storage/z_locked_notes/mod.rs new file mode 100644 index 0000000000..fdbd9b8b76 --- /dev/null +++ b/mm2src/coins/z_coin/storage/z_locked_notes/mod.rs @@ -0,0 +1,155 @@ +use enum_derives::EnumFromStringify; + +cfg_native!( + pub(crate) mod sqlite; + + use db_common::async_sql_conn::{AsyncConnError, AsyncConnection}; + use futures::lock::Mutex; + use std::sync::Arc; +); + +cfg_wasm32!( + pub(crate) mod wasm; + + use self::wasm::LockedNoteDbInner; + use mm2_db::indexed_db::{DbTransactionError, InitDbError, SharedDb}; +); + +/// Represents a shielded note temporarily locked due to a pending transaction. +/// Locked notes are excluded from the spendable balance until confirmed or cleared. +#[derive(Debug, Clone)] +pub(crate) enum LockedNote { + /// A note being spent by a pending shielded transaction (`rseed` is the note's randomness). + Spent { rseed: String }, + + /// A pending change output from an unconfirmed shielded transaction (`value` is the expected amount). + Change { value: u64 }, +} + +/// A wrapper for the db connection to the change note cache database in native and browser. +#[derive(Clone)] +pub struct LockedNotesStorage { + #[cfg(not(target_arch = "wasm32"))] + pub db: Arc>, + #[cfg(target_arch = "wasm32")] + pub db: SharedDb, + #[allow(unused)] + address: String, +} + +#[derive(Clone, Debug, Display, Eq, PartialEq, EnumFromStringify)] +pub(crate) enum LockedNotesStorageError { + #[cfg(not(target_arch = "wasm32"))] + #[display(fmt = "Sqlite Error: {_0}")] + #[from_stringify("AsyncConnError", "db_common::sqlite::rusqlite::Error")] + SqliteError(String), + #[cfg(target_arch = "wasm32")] + #[display(fmt = "IndexedDb Error: {_0}")] + #[from_stringify("InitDbError", "DbTransactionError")] + IndexedDbError(String), +} + +#[cfg(any(test, target_arch = "wasm32"))] +pub(super) mod locked_notes_test { + use crate::z_coin::storage::z_locked_notes::{LockedNote, LockedNotesStorage}; + use common::cross_test; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; + + common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + const MY_ADDRESS: &str = "my_address"; + + cross_test!(test_insert_and_remove_note, { + let ctx = mm_ctx_with_custom_db(); + let db = LockedNotesStorage::new(&ctx, MY_ADDRESS.to_string()).await.unwrap(); + + // Insert a pending spent note + let spent_txid = "0x18b1acd8ceae8d71a2ae8b7e4a3e48ceb39dc237f0aa38c468425b88dc8d5f3e".to_string(); + let spent_rseed = "0xcfec34a81e67e85aa1ce1a6666f92f9bc5606f0795be555bb3c9f9ac089aa4f7".to_string(); + db.insert_spent_note(spent_txid.clone(), spent_rseed.clone()) + .await + .unwrap(); + + // Insert a pending change note + let change_txid = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string(); + let change_value = 123456; + db.insert_change_note(change_txid.clone(), change_value).await.unwrap(); + + // Remove by txid + db.remove_notes_for_txid(spent_txid.clone()).await.unwrap(); + db.remove_notes_for_txid(change_txid.clone()).await.unwrap(); + + let notes = db.load_all_notes().await.unwrap(); + assert!(notes.is_empty()); + + // Insert both again but using same txid + db.insert_spent_note(spent_txid.clone(), spent_rseed.clone()) + .await + .unwrap(); + db.insert_change_note(spent_txid.clone(), change_value).await.unwrap(); + + // Remove by txid (removes both input and output if same txid) + db.remove_notes_for_txid(spent_txid.clone()).await.unwrap(); + + let notes = db.load_all_notes().await.unwrap(); + assert!(notes.is_empty()); + }); + + cross_test!(test_load_all_notes, { + let ctx = mm_ctx_with_custom_db(); + let db = LockedNotesStorage::new(&ctx, MY_ADDRESS.to_string()).await.unwrap(); + + let spent_txid = "0x01".to_string(); + let spent_rseed = "0xcafe000000000000000000000000000000000000000000000000000000000000".to_string(); + let change_txid = "0x02".to_string(); + let change_value = 123456789; + + db.insert_spent_note(spent_txid.clone(), spent_rseed.clone()) + .await + .unwrap(); + db.insert_change_note(change_txid.clone(), change_value).await.unwrap(); + + let notes = db.load_all_notes().await.unwrap(); + + assert_eq!(notes.len(), 2); + + match ¬es[0] { + LockedNote::Spent { rseed } => { + assert_eq!(rseed, &spent_rseed); + }, + _ => panic!("First note should be a Spent note"), + } + match ¬es[1] { + LockedNote::Change { value } => { + assert_eq!(*value, change_value); + }, + _ => panic!("Second note should be a Change note"), + } + }); + + cross_test!(test_sum_changes, { + let ctx = mm_ctx_with_custom_db(); + let db = LockedNotesStorage::new(&ctx, MY_ADDRESS.to_string()).await.unwrap(); + + db.insert_change_note("txid1".to_string(), 1000).await.unwrap(); + db.insert_change_note("txid2".to_string(), 2000).await.unwrap(); + db.insert_spent_note("0xinputrseed".to_string(), "txid3".to_string()) + .await + .unwrap(); + + let notes = db.load_all_notes().await.unwrap(); + + // Only sum Output note values + let sum: u64 = notes + .iter() + .filter_map(|n| match n { + LockedNote::Change { value, .. } => Some(*value), + _ => None, + }) + .sum(); + assert_eq!(sum, 3000); + }); +} diff --git a/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs b/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs new file mode 100644 index 0000000000..1738c82025 --- /dev/null +++ b/mm2src/coins/z_coin/storage/z_locked_notes/sqlite.rs @@ -0,0 +1,158 @@ +use super::{LockedNote, LockedNotesStorage, LockedNotesStorageError}; +use db_common::async_sql_conn::{AsyncConnError, AsyncConnection}; +use db_common::sqlite::run_optimization_pragmas; +use db_common::sqlite::rusqlite::params; +use futures::lock::Mutex; +use itertools::Itertools; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use std::convert::TryInto; +use std::sync::Arc; + +const TABLE_NAME: &str = "locked_notes_cache"; + +async fn create_table(conn: Arc>) -> Result<(), AsyncConnError> { + let conn = conn.lock().await; + conn.call(move |conn| { + run_optimization_pragmas(conn)?; + conn.execute( + &format!( + "CREATE TABLE IF NOT EXISTS {TABLE_NAME} ( + variant TEXT NOT NULL, -- 'Spent' or 'Change' + txid VARCHAR NOT NULL, + rseed VARCHAR, -- only for Spent + value INTEGER, -- only for Change + UNIQUE (variant, txid, rseed, value) + )" + ), + [], + )?; + Ok(()) + }).await +} + +impl LockedNotesStorage { + #[cfg(not(any(test, feature = "run-docker-tests")))] + pub(crate) async fn new(ctx: &MmArc, address: String) -> MmResult { + let path = ctx.wallet_dir().join(format!("{}_locked_notes_cache.db", address)); + let db = AsyncConnection::open(path) + .await + .map_to_mm(|err| LockedNotesStorageError::SqliteError(err.to_string()))?; + let db = Arc::new(Mutex::new(db)); + + create_table(db.clone()).await?; + + Ok(Self { db, address }) + } + + #[cfg(any(test, feature = "run-docker-tests"))] + pub(crate) async fn new(ctx: &MmArc, address: String) -> MmResult { + #[cfg(feature = "run-docker-tests")] + let db = { + let path = ctx.wallet_dir().join(format!("{}_locked_notes_cache.db", address)); + mm2_io::fs::create_parents_async(&path) + .await + .map_err(|err| LockedNotesStorageError::SqliteError(err.to_string()))?; + Arc::new(Mutex::new( + AsyncConnection::open(path) + .await + .map_to_mm(|err| LockedNotesStorageError::SqliteError(err.to_string()))?, + )) + }; + #[cfg(all(test, not(feature = "run-docker-tests")))] + let db = { + let test_conn = Arc::new(Mutex::new(AsyncConnection::open_in_memory().await.unwrap())); + ctx.async_sqlite_connection.get().cloned().unwrap_or(test_conn) + }; + + create_table(db.clone()).await?; + + Ok(Self { db, address }) + } + + pub(crate) async fn insert_spent_note( + &self, + txid: String, + rseed: String, + ) -> MmResult<(), LockedNotesStorageError> { + let db = self.db.lock().await; + Ok(db.call(move |conn| { + conn.prepare(&format!( + "INSERT OR REPLACE INTO {TABLE_NAME} (variant, txid, rseed, value) VALUES (?, ?, ?, NULL)" + ))? + .execute(params!["Spent", txid, rseed])?; + Ok(()) + }).await?) + } + + pub(crate) async fn insert_change_note( + &self, + txid: String, + value: u64, + ) -> MmResult<(), LockedNotesStorageError> { + let db = self.db.lock().await; + Ok(db.call(move |conn| { + conn.prepare(&format!( + "INSERT OR REPLACE INTO {TABLE_NAME} (variant, txid, rseed, value) VALUES (?, ?, NULL, ?)" + ))? + .execute(params!["Change", txid, value as i64])?; + Ok(()) + }).await?) + } + + pub(crate) async fn remove_notes_for_txid(&self, txid: String) -> MmResult<(), LockedNotesStorageError> { + let db = self.db.lock().await; + Ok(db + .call(move |conn| { + conn.execute( + &format!("DELETE FROM {TABLE_NAME} WHERE txid=?"), + [&txid], + )?; + Ok(()) + }) + .await?) + } + + pub(crate) async fn load_all_notes(&self) -> MmResult, LockedNotesStorageError> { + let db = self.db.lock().await; + Ok(db.call(move |conn| { + let mut stmt = conn.prepare(&format!( + "SELECT variant, txid, rseed, value FROM {TABLE_NAME};" + ))?; + let rows = stmt.query_map(params![], |row| { + let variant: String = row.get(0)?; + let rseed: Option = row.get(2)?; + let value: Option = row.get(3)?; + + match variant.as_str() { + "Spent" => { + let rseed = rseed.ok_or_else(|| db_common::sqlite::rusqlite::Error::FromSqlConversionFailure( + 2, // Column index for "rseed" + db_common::sqlite::rusqlite::types::Type::Text, + "NULL value found for required rseed field".into() + ))?; + Ok(LockedNote::Spent { rseed }) + }, + "Change" => { + let i64_value = value.ok_or_else(|| db_common::sqlite::rusqlite::Error::FromSqlConversionFailure( + 3, // Column index for "value" + db_common::sqlite::rusqlite::types::Type::Integer, + "NULL value found for required value field".into() + ))?; + + let value = i64_value.try_into() + .map_err(|_| db_common::sqlite::rusqlite::Error::IntegralValueOutOfRange(3, i64_value))?; + + Ok(LockedNote::Change { value }) + }, + unexpected => Err(db_common::sqlite::rusqlite::Error::FromSqlConversionFailure( + 0, // Column index for "variant" + db_common::sqlite::rusqlite::types::Type::Text, + format!("Unexpected variant value: {}", unexpected).into() + )), + } + })?; + Ok(rows.flatten().collect_vec()) + }).await?) + } +} diff --git a/mm2src/coins/z_coin/storage/z_locked_notes/wasm.rs b/mm2src/coins/z_coin/storage/z_locked_notes/wasm.rs new file mode 100644 index 0000000000..dc72ca9d4e --- /dev/null +++ b/mm2src/coins/z_coin/storage/z_locked_notes/wasm.rs @@ -0,0 +1,158 @@ +use super::{LockedNote, LockedNotesStorage, LockedNotesStorageError}; + +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, IndexedDbBuilder, + InitDbResult, OnUpgradeResult, TableSignature, OnUpgradeError}; +use mm2_err_handle::prelude::*; + +const DB_NAME: &str = "z_change_note_storage"; +const DB_VERSION: u32 = 1; + +pub type LockedNotesDbInnerLocked<'a> = DbLocked<'a, LockedNoteDbInner>; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct LockedNoteTable { + address: String, + variant: String, // "Spent" or "Change" + txid: String, + rseed: Option, // Only for Spent + value: Option, // Only for Change +} + +impl TableSignature for LockedNoteTable { + const TABLE_NAME: &'static str = "change_notes"; + + fn on_upgrade_needed(upgrader: &DbUpgrader, mut old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + while old_version < new_version { + match old_version { + 0 => { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_index("address", false)?; + table.create_index("variant", false)?; + table.create_index("txid", false)?; + table.create_index("rseed", false)?; + table.create_index("value", false)?; + } + unsupported_version => { + return MmError::err(OnUpgradeError::UnsupportedVersion { + unsupported_version, + old_version, + new_version, + }); + } + } + old_version += 1; + } + Ok(()) + } +} + +pub struct LockedNoteDbInner(IndexedDb); + +#[async_trait::async_trait] +impl DbInstance for LockedNoteDbInner { + const DB_NAME: &'static str = DB_NAME; + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .build() + .await?; + + Ok(Self(inner)) + } +} + +impl LockedNoteDbInner { + pub fn get_inner(&self) -> &IndexedDb { &self.0 } +} + +impl LockedNotesStorage { + async fn lockdb(&self) -> MmResult { + Ok(self.db.get_or_initialize().await?) + } +} + +impl LockedNotesStorage { + pub(crate) async fn new(ctx: &MmArc, address: String) -> Result { + let db = ConstructibleDb::new(ctx).into_shared(); + Ok(Self { address, db }) + } + + pub(crate) async fn insert_spent_note( + &self, + txid: String, + rseed: String, + ) -> MmResult<(), LockedNotesStorageError> { + let db = self.lockdb().await?; + let address = self.address.clone(); + let transaction = db.get_inner().transaction().await?; + let change_note_table = transaction.table::().await?; + + let change_note = LockedNoteTable { + address, + variant: "Spent".to_owned(), + txid, + rseed: Some(rseed), + value: None, + }; + Ok(change_note_table + .add_item(&change_note) + .await + .map(|_| ())?) + } + + pub(crate) async fn insert_change_note( + &self, + txid: String, + value: u64, + ) -> MmResult<(), LockedNotesStorageError> { + let db = self.lockdb().await?; + let address = self.address.clone(); + let transaction = db.get_inner().transaction().await?; + let change_note_table = transaction.table::().await?; + + let change_note = LockedNoteTable { + address, + variant: "Change".to_owned(), + txid, + rseed: None, + value: Some(value), + }; + Ok(change_note_table + .add_item(&change_note) + .await + .map(|_| ())?) + } + + pub(crate) async fn remove_notes_for_txid(&self, txid: String) -> MmResult<(), LockedNotesStorageError> { + let db = self.lockdb().await?; + let transaction = db.get_inner().transaction().await?; + let change_note_table = transaction.table::().await?; + change_note_table.delete_items_by_index("txid", &txid).await?; + + Ok(()) + } + + pub(crate) async fn load_all_notes(&self) -> MmResult, LockedNotesStorageError> { + let db = self.lockdb().await?; + let transaction = db.get_inner().transaction().await?; + let change_note_table = transaction.table::().await?; + let records = change_note_table.get_items("address", &self.address).await?; + Ok(records + .into_iter() + .filter_map(|(_, n)| { + match n.variant.as_str() { + "Spent" => n.rseed.clone().map(|rseed| LockedNote::Spent { + rseed, + }), + "Change" => n.value.map(|value| LockedNote::Change { + value, + }), + _ => None, + } + }) + .collect()) + } +} diff --git a/mm2src/coins/z_coin/tx_history_events.rs b/mm2src/coins/z_coin/tx_history_events.rs index f374bc22b1..c09da5d732 100644 --- a/mm2src/coins/z_coin/tx_history_events.rs +++ b/mm2src/coins/z_coin/tx_history_events.rs @@ -4,7 +4,7 @@ use crate::utxo::rpc_clients::UtxoRpcError; use crate::MarketCoinOps; use common::log; use mm2_err_handle::prelude::MmError; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use rpc::v1::types::H256 as H256Json; use async_trait::async_trait; @@ -23,14 +23,14 @@ impl ZCoinTxHistoryEventStreamer { pub fn new(coin: ZCoin) -> Self { Self { coin } } #[inline(always)] - pub fn derive_streamer_id(coin: &str) -> String { format!("TX_HISTORY:{coin}") } + pub fn derive_streamer_id(coin: &str) -> StreamerId { StreamerId::TxHistory { coin: coin.to_string() } } } #[async_trait] impl EventStreamer for ZCoinTxHistoryEventStreamer { type DataInType = Vec>; - fn streamer_id(&self) -> String { Self::derive_streamer_id(self.coin.ticker()) } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id(self.coin.ticker()) } async fn handle( self, diff --git a/mm2src/coins/z_coin/tx_streaming_tests/native.rs b/mm2src/coins/z_coin/tx_streaming_tests/native.rs index f4bc2849dc..6cb7f9a97c 100644 --- a/mm2src/coins/z_coin/tx_streaming_tests/native.rs +++ b/mm2src/coins/z_coin/tx_streaming_tests/native.rs @@ -60,13 +60,13 @@ fn test_zcoin_tx_streaming() { .expect("tx history sender shutdown"); log!("{:?}", event.get()); - let (event_type, event_data) = event.get(); + let (streamer_id, event_data) = event.get(); // Make sure this is not an error event, - assert!(!event_type.starts_with("ERROR_")); + assert!(!streamer_id.starts_with("ERROR:")); // from the expected streamer, assert_eq!( - event_type, - ZCoinTxHistoryEventStreamer::derive_streamer_id(coin.ticker()) + streamer_id, + ZCoinTxHistoryEventStreamer::derive_streamer_id(coin.ticker()).to_string() ); // and has the expected data. assert_eq!(event_data["tx_hash"].as_str().unwrap(), tx.txid().to_string()); diff --git a/mm2src/coins/z_coin/z_balance_streaming.rs b/mm2src/coins/z_coin/z_balance_streaming.rs index 0760bfc929..c5b012fb3b 100644 --- a/mm2src/coins/z_coin/z_balance_streaming.rs +++ b/mm2src/coins/z_coin/z_balance_streaming.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use common::log::error; use futures::channel::oneshot; use futures_util::StreamExt; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; pub struct ZCoinBalanceEventStreamer { coin: ZCoin, @@ -17,14 +17,18 @@ impl ZCoinBalanceEventStreamer { pub fn new(coin: ZCoin) -> Self { Self { coin } } #[inline(always)] - pub fn derive_streamer_id(coin: &str) -> String { format!("BALANCE:{coin}") } + pub fn derive_streamer_id(coin: &str) -> StreamerId { StreamerId::Balance { coin: coin.to_string() } } } #[async_trait] impl EventStreamer for ZCoinBalanceEventStreamer { type DataInType = (); - fn streamer_id(&self) -> String { Self::derive_streamer_id(self.coin.ticker()) } + fn streamer_id(&self) -> StreamerId { + StreamerId::Balance { + coin: self.coin.ticker().to_string(), + } + } async fn handle( self, diff --git a/mm2src/coins/z_coin/z_coin_errors.rs b/mm2src/coins/z_coin/z_coin_errors.rs index 3c6e44a32a..23b1664a5b 100644 --- a/mm2src/coins/z_coin/z_coin_errors.rs +++ b/mm2src/coins/z_coin/z_coin_errors.rs @@ -1,3 +1,4 @@ +use super::storage::LockedNotesStorageError; use crate::my_tx_history_v2::MyTxHistoryErrorV2; use crate::utxo::rpc_clients::UtxoRpcError; use crate::utxo::utxo_builder::UtxoCoinBuildError; @@ -9,6 +10,7 @@ use common::jsonrpc_client::JsonRpcError; #[cfg(not(target_arch = "wasm32"))] use db_common::sqlite::rusqlite::Error as SqliteError; use derive_more::Display; +use enum_derives::EnumFromStringify; use http::uri::InvalidUri; #[cfg(target_arch = "wasm32")] use mm2_db::indexed_db::cursor_prelude::*; @@ -100,7 +102,7 @@ pub enum UrlIterError { ConnectionFailure(tonic::transport::Error), } -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum GenTxError { DecryptedOutputNotFound, GetWitnessErr(GetUnspentWitnessErr), @@ -130,6 +132,8 @@ pub enum GenTxError { FailedToCreateNote, SpendableNotesError(String), Internal(String), + #[from_stringify("LockedNotesStorageError")] + SaveLockedNotesError(String), } impl From for GenTxError { @@ -177,7 +181,8 @@ impl From for WithdrawError { | GenTxError::LightClientErr(_) | GenTxError::SpendableNotesError(_) | GenTxError::FailedToCreateNote - | GenTxError::Internal(_) => WithdrawError::InternalError(gen_tx.to_string()), + | GenTxError::Internal(_) + | GenTxError::SaveLockedNotesError(_) => WithdrawError::InternalError(gen_tx.to_string()), } } } @@ -231,10 +236,11 @@ impl From for GetUnspentWitnessErr { fn from(err: SqliteError) -> GetUnspentWitnessErr { GetUnspentWitnessErr::ZcashDBError(err.to_string()) } } -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum ZCoinBuildError { UtxoBuilderError(UtxoCoinBuildError), GetAddressError, + #[from_stringify("LockedNotesStorageError")] ZcashDBError(String), Rpc(UtxoRpcError), #[display(fmt = "Sapling cache DB does not exist at {}. Please download it.", path)] diff --git a/mm2src/coins/z_coin/z_coin_native_tests.rs b/mm2src/coins/z_coin/z_coin_native_tests.rs deleted file mode 100644 index 4e5ffc4325..0000000000 --- a/mm2src/coins/z_coin/z_coin_native_tests.rs +++ /dev/null @@ -1,379 +0,0 @@ -//! Native tests for zcoin -//! -//! To run zcoin tests in this source you need `--features zhtlc-native-tests` -//! ZOMBIE chain must be running for zcoin tests: -//! komodod -ac_name=ZOMBIE -ac_supply=0 -ac_reward=25600000000 -ac_halving=388885 -ac_private=1 -ac_sapling=1 -testnode=1 -addnode=65.21.51.116 -addnode=116.203.120.163 -addnode=168.119.236.239 -addnode=65.109.1.121 -addnode=159.69.125.84 -addnode=159.69.10.44 -//! Also check the test z_key (spending key) has balance: -//! `komodo-cli -ac_name=ZOMBIE z_getbalance zs10hvyxf3ajm82e4gvxem3zjlf9xf3yxhjww9fvz3mfqza9zwumvluzy735e29c3x5aj2nu0ua6n0` -//! If no balance, you may mine some transparent coins and send to the test z_key. -//! When tests are run for the first time (or have not been run for a long) synching to fill ZOMBIE_wallet.db is started which may take hours. -//! So it is recommended to run prepare_zombie_sapling_cache to sync ZOMBIE_wallet.db before running zcoin tests: -//! cargo test -p coins --features zhtlc-native-tests -- --nocapture prepare_zombie_sapling_cache -//! If you did not run prepare_zombie_sapling_cache waiting for ZOMBIE_wallet.db sync will be done in the first call to ZCoin::gen_tx. -//! In tests, for ZOMBIE_wallet.db to be filled, another database ZOMBIE_cache.db is created in memory, -//! so if db sync in tests is cancelled and restarted this would cause restarting of building ZOMBIE_cache.db in memory -//! -//! Note that during the ZOMBIE_wallet.db sync an error may be reported: -//! 'error trying to connect: tcp connect error: Can't assign requested address (os error 49)'. -//! Also during the sync other apps like ssh or komodo-cli may return same error or even crash. TODO: fix this problem, maybe it is due to too much load on TCP stack -//! Errors like `No one seems interested in SyncStatus: send failed because channel is full` in the debug log may be ignored (means that update status is temporarily not watched) -//! -//! To monitor sync status in logs you may add logging support into the beginning of prepare_zombie_sapling_cache test (or other tests): -//! common::log::UnifiedLoggerBuilder::default().init(); -//! and run cargo test with var RUST_LOG=debug - -use bitcrypto::dhash160; -use common::{block_on, now_sec}; -use mm2_core::mm_ctx::MmCtxBuilder; -use mm2_test_helpers::for_tests::zombie_conf; -use std::path::PathBuf; -use std::time::Duration; -use zcash_client_backend::encoding::decode_extended_spending_key; - -use super::{z_coin_from_conf_and_params_with_z_key, z_mainnet_constants, PrivKeyBuildPolicy, RefundPaymentArgs, - SendPaymentArgs, SpendPaymentArgs, SwapOps, ValidateFeeArgs, ValidatePaymentError, ZTransaction}; -use crate::z_coin::{z_htlc::z_send_dex_fee, ZcoinActivationParams, ZcoinRpcMode}; -use crate::{CoinProtocol, SwapTxTypeWithSecretHash}; -use crate::{DexFee, DexFeeBurnDestination}; -use mm2_number::MmNumber; - -fn native_zcoin_activation_params() -> ZcoinActivationParams { - ZcoinActivationParams { - mode: ZcoinRpcMode::Native, - ..Default::default() - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_send_and_refund_maker_payment() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let pk_data = [1; 32]; - let db_dir = PathBuf::from("./for_tests"); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), - db_dir, - z_key, - protocol_info, - ) - .await - .unwrap(); - - let time_lock = now_sec() - 3600; - let maker_uniq_data = [3; 32]; - - let taker_uniq_data = [5; 32]; - let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); - let taker_pub = taker_key_pair.public(); - - let secret_hash = [0; 20]; - - let args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: taker_pub, - secret_hash: &secret_hash, - amount: "0.01".parse().unwrap(), - swap_contract_address: &None, - swap_unique_data: maker_uniq_data.as_slice(), - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = coin.send_maker_payment(args).await.unwrap(); - log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); - - let refund_args = RefundPaymentArgs { - payment_tx: &tx.tx_hex(), - time_lock, - other_pubkey: taker_pub, - tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { - maker_secret_hash: &secret_hash, - }, - swap_contract_address: &None, - swap_unique_data: maker_uniq_data.as_slice(), - watcher_reward: false, - }; - let refund_tx = coin.send_maker_refunds_payment(refund_args).await.unwrap(); - log!("refund tx {}", hex::encode(refund_tx.tx_hash_as_bytes().0)); -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_send_and_spend_maker_payment() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let pk_data = [1; 32]; - let db_dir = PathBuf::from("./for_tests"); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), - db_dir, - z_key, - protocol_info, - ) - .await - .unwrap(); - - let lock_time = now_sec() - 1000; - - let maker_uniq_data = [3; 32]; - let maker_key_pair = coin.derive_htlc_key_pair(maker_uniq_data.as_slice()); - let maker_pub = maker_key_pair.public(); - - let taker_uniq_data = [5; 32]; - let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); - let taker_pub = taker_key_pair.public(); - - let secret = [0; 32]; - let secret_hash = dhash160(&secret); - - let maker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock: lock_time, - other_pubkey: taker_pub, - secret_hash: secret_hash.as_slice(), - amount: "0.01".parse().unwrap(), - swap_contract_address: &None, - swap_unique_data: maker_uniq_data.as_slice(), - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - - let tx = coin.send_maker_payment(maker_payment_args).await.unwrap(); - log!("swap tx {}", hex::encode(tx.tx_hash_as_bytes().0)); - - let spends_payment_args = SpendPaymentArgs { - other_payment_tx: &tx.tx_hex(), - time_lock: lock_time, - other_pubkey: maker_pub, - secret: &secret, - secret_hash: secret_hash.as_slice(), - swap_contract_address: &None, - swap_unique_data: taker_uniq_data.as_slice(), - watcher_reward: false, - }; - let spend_tx = coin.send_taker_spends_maker_payment(spends_payment_args).await.unwrap(); - log!("spend tx {}", hex::encode(spend_tx.tx_hash_as_bytes().0)); -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_send_dex_fee() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let db_dir = PathBuf::from("./for_tests"); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = - z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) - .await - .unwrap(); - - let dex_fee = DexFee::WithBurn { - fee_amount: "0.0075".into(), - burn_amount: "0.0025".into(), - burn_destination: DexFeeBurnDestination::PreBurnAccount, - }; - let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); - log!("dex fee tx {}", tx.txid()); -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_send_standard_dex_fee() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let db_dir = PathBuf::from("./for_tests"); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = - z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) - .await - .unwrap(); - - let dex_fee = DexFee::Standard("0.01".into()); - let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); - log!("dex fee tx {}", tx.txid()); -} - -/// Use to create ZOMBIE_wallet.db -#[test] -fn prepare_zombie_sapling_cache() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let db_dir = PathBuf::from("./for_tests"); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = block_on(z_coin_from_conf_and_params_with_z_key( - &ctx, - "ZOMBIE", - &conf, - ¶ms, - priv_key, - db_dir, - z_key, - protocol_info, - )) - .unwrap(); - - while !block_on(coin.is_sapling_state_synced()) { - std::thread::sleep(Duration::from_secs(1)); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn zombie_coin_validate_dex_fee() { - let ctx = MmCtxBuilder::default().into_mm_arc(); - let mut conf = zombie_conf(); - let params = native_zcoin_activation_params(); - let priv_key = PrivKeyBuildPolicy::IguanaPrivKey([1; 32].into()); - let db_dir = PathBuf::from("./for_tests"); - let z_key = decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, "secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").unwrap().unwrap(); - let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { - CoinProtocol::ZHTLC(protocol_info) => protocol_info, - other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), - }; - - let coin = - z_coin_from_conf_and_params_with_z_key(&ctx, "ZOMBIE", &conf, ¶ms, priv_key, db_dir, z_key, protocol_info) - .await - .unwrap(); - - // https://zombie.explorer.lordofthechains.com/tx/9390a26810342151f48f455b09e5d087a5429cbba08f2381b02c43b76f813e29 - let tx_hex = "0400008085202f8900000000000001030c00e8030000000000000169e7017fbd969be53da2c1b8812002baaf59ce98b230a9c1001397ba7f4db8676bd77e8ea644b67067d1f996d8d81c279961343f00a10095bccbddc341c98539287c900cf969688ddc574786e0e34bd6d3ec2ffaab5e2d472848781b116906669786c14c5c608b20dc23c9566fd46861f6a258b5ffc6de73495b56f4823e098c8664eab895d5cd31c013428ae2cbe940dc236ca40465ea2b912ce6c36555b2affb1f38b99b28dc593d865b0b948d567f9315df666d2e65e666d829b9823154bae0410bd885582b4a8a6eb4b9ae214b59ffd9b1167b7cd48f48a11cbd67c08f4e01ed4fd78fc91d0c9e70baa4f25761ef6c78cd7268b307aaa6ece2b443937eb4beac2c8843279a8879adbe0b381e65d0b674f2feeb54b78f80b377f66baab72c4cf9f10dde48f343c001df91a1a6d252ad8eca26eea0fdee49ad7024b505e55b4e082e94616794ddd7c2b852594b4b7af2292f0aa9e34f38322f548f1a21c015e92dbfd239ce18144f3b8045e9efa3de6b4c6b338f01d0adeb26a088a3c8c00503b67b2980b7663e97541e2944e4ad3588554966b6a930d2dc01d9fc7f8a846583fcf3b721f979705eff5bb9bb1fb0cad9ad941ceb3f581710efd8c50713a53751a0a196322ef8618bf1e097383666e91b5133ba81645d2b542181476eba2326cd02fb29a9f09edc46ea04b32ed9243597318d23b955a2570d78cbfb46cc26c1807eddd1de4785b6e752f859f7e25fc67f9e8a00feafac6fd7781eb72a663d9b80c10e9c387abc4d41294b3573785fd53bc56ccac2edf5c7bbb99cb3bcf87161fa893d2e1aabfee75754767cef07a12e44bb707720e727e585a258356cc797ecee8263c0f61cfc8ffa0360c758f1348ac44c186e12ce0f4faad43b4638abd4a0bc9fd4a6fa4352c20cc771241f95c26f1671ca95c8f4a63a8318dc43299f54e8a899df78ccfd3112a0d5ea637847dd2e3b05be8c0658dd0d7d814473fa5369957c00e84df600df23faaee5faa17b9ededad4731e5e9c1099dfddf5264756800dcfcad4b006b736d1d47c59a019acde4dc22249fc40846b77b43294e32a21db745e1bec790324c3d505edc79388a6e44b02841b26306ed48cfce1e941642c30792315016dba03797c8e4e279eec5b78aad602620471f24c25aea3aaa57509aa9eef2057f11bc95bad708918f2f0df74ac179d7dffc772b2c603dd89e7aea0e8f94f1a8bab4a4fba10bf05c88fbe4b021b3faff3d558e32e4bc20be4bed62d653674ce697390e098e590a3e354cb4a1e703474de8aab30cd76cf7e237f2e66bf486c4fc6c22028764e95adf7d8fa018f44b51ae6acfa3bf80f14c45c06623b916d79649abe0a2b229f96e60e421f6e734160da37f01e915cf73d1cacd1eb7f06c26c33b4d8e4dde264f3cfe84bada0601d1c03aa31c5938750ca0b852f3177883cae9f285d582a4eb38c05f8ef6e5cff5be0745e1ec66e20752bfd5bd5a1590fa280ace3e9786e0022e7ae3c48bcca14e9c5513bc8b57e15820a685f8348159862be0579a35d8ac9d1abaf36d9274c7e750fd9ad265c0d8f08c95ed9ce69eef3a55aef05f2d5d601f80f472689f3428e4f0095829a459813d5dace7e6137a752ae5567982e67b2092afeba99561fbe4e716f67bd1b4e8de1f376dec30eed27371bcc42d7de2ea0f4288054618e9afa002a2d1996b7a70a9683229f28bab811b67629dad527f325c0f12e19d92bac51e5924f27048fa118673b52b296b3642ec946d9915ded0ae84e1a2236da65f672bdad75a22cc0ea751c07e56d2ec22caa41afc98ec6b37a8c1b6a5378a81f2cdb2228f4efb8d7f35c0086a955e1b04bd09bd7e056c949fab1805f733a8b2061adad0c2b7fae33d21363de911e517b21a1539dfa1b3cbb1ea0dbfa3ffff23bbac01183f852de41e798fca5a278b711893175aeaded90873574d8de30b360f39ea239492c630eda4a811d3bb7a125054d5ca74bb6698aeea1a417ad19415ca0e5ca36abc2f96725986f73bcbe3113e391010d08f58f05979c7cef26ff92506c5d1eb2a2f6f5689e9a39957f0723bef3262f5190de996234d4f00b73ed74d78fdf1e6bf31161e16bd083bc6fbddc4eba85c17067e15f08019e5ed943de8e23a974d516abc641e85e641b03779816c30b3449a16b142417c1ff93ab7fa8f96a175e9ef73b3f06ac76788c27889d426efa78d5b8ce35be4591902f7766fe579a0aa28229235a920d26264c09625dea807f619a040f08931d6e1fe57ff0c48ea476be93a16d1fc8de3617984eeebcf14b63c839b41f8f9305402d1288c8e481a4fa5c3302bb1f83e3f0dc8ff9550f9bacb44bccb58f3de152abef5d578afed1c29dc89495b9e54a0c6d00f1dba45a2cf68c9512d9a9ff0b2531e58e47428a99cb246ca23f867b660dc71785b57407cc292f735634c602409792c4640831809f1f1e51903273b623aa0ae0cdd335c7b9db360b0bceb0d15f2313e1944800f30f82ed5bb07cfa1c4740c2bf2806539a4afac1f79d779b923ad8dc2493ebb2d2fce9aea58a009d64e7d1b71ca6893b076e41f7e88a4b51b5402e3fa6c60fa65a686adea229f0164318c9fa1b6d2d2218e5ada710daffecb6b7dd8bf7447658795c4c7a0ad710c4f02fd19017a0575f9467600cdca019793f2f49d197dbfc937828e5790b90929e5ca16037ec79734b64feec36b36c220a2979c45dd51e24c9fb21d8634471aac20c6f179f90c0d61c7b3d89826d146b157bedd8f6b66f6edfabfe04b49f2f2d999fc2e578a440bafd524c82ae614dc8017e379cf926e042f4fbd6f0628fde52de18d764ba8385b77569eda30d5a3617fb0a0c7fd26c821308c3ae98498d33b974cb318a04af3ea3fbcb13fc62fc952aaef095423da9ec7bdc7b77adbd403931189ddc98fe19a06711415b40a9a68812bb7c5453b7b2377910c7b89c99b379e038a7940487c0fd2405456ee55ab6ead3ef25a8a5b1abcae479c24f5e6869057e0bdabcdf352b4a64a3e385171a6e14c8102b2a187034e21705e3a457167fe0dc0d63d6e8d489c9a18c9d84b541504d36b086c2c63cc1a34c0080122c5d60ca33ab60289d16f21e1ded753607267c2093b1c587b89da9df65584fbe3ff9eb7f91d64e33912b8e91adc27191d22f8e835be6bb24546f21488f7abcb29339c34058d4f4093096144b17b8ab76a346275b7e7c80bca59d20e0bb482bb2a9cc3c9515cc1b5be17348c65c73e9fb1ed77d423c509f7cff0e355a34d080d310f3b848dbc209bbba6b6b109fb8d9556dca0fab086e197327ab423d5d762b68961244d8d22c30a8a3a116770bb15b5a0a347091a843b68d6a8e0f1c79f12523a7561c1233cd44db90f6cd3c1ce5fc13f8382177b5522aae028379269b71ae2a42f41dff7374ed7e83c89566f57297b82478b04359a2c199ce8f842112b7450cc1e2e2e394cda4c67e0b2302e21f6af997607ceefd067f77be8900bb3ecb3e30782477aa76861b286b9ddc9e36fcebb50f04f9516e02da31e6219bb5bcb81ee673d95be14c1bd2be4909556d6dbca0365292c582dedcafcc60b255ab7bcd9d977a4139f394ca1da81040e784fd8e7534f230bc5201e7f1db47eadc30f37609d5bbaba624157d98d65029bbab766b6c23c3049a32b894c0cfcb40913ba1cd2d5acda7d2acc920fd01c36f28fc6b7ffd01a37b17fc3235d0dbe9b8098530bed6894b288604b8689f4aafc22cdf211fb95ef5c90cae62a250234e6f790e9a15012acac88305dc4f91fd564a9ab8bb27c057ec5dd46fe952a7be557caea9b7b1d6118aa42df79b8c207e2bae6c34d67dc32b4360ad20b3e609e9caeb7f432ad51cfce139f2d4eb9ed219f4323acd5685e0e0409939eb662175a83fa083f500516dbcb091a3448cb24c3198c8fc547fbda3cb0894edeceef7ccb4ad746aa06f4038b63ab4095a9c390656520561ba3763b1057b3af7cb548342a2bfc2ab725b01b12a7adfc30d7d9632acafd2595cde406b8637a911b7c86f7b09b11f58acec3f1a1bd7cf6853331b48d7907ed699d91fbdbcab8001e3d8d3a26b491b6e2d98c5e149847a07a2b7faa1f567cd4bc9c83ad553339632f3dcacb890c5222656b3349ddd5c8eacaa490ac0b2b38f8a26da9ce7789f5601769a7f10b93125cb93b589bda4ddb4e8795817b60cc149af7c0699b2bbbf655f2f5ec170d6af51213e8c725e699d181923ecf10c6f1069f46e6bc89c7a29d2ebe133b5c0c4b67826a93add7d4824e60b4c5f0cee358abedb50c54a59e95185d7a80081f2dddba5c7c7c637b2dfe8575ddaa71306a2725c9ec17b8e4e1f271a442f6798cc21bbd55c2d69819ddde37a8e8d6a812c41a3e58719b7c96e9375155c4a873ed698ad37144ef32e3fe41cce9c48bbe31441dbbeec7b97734769063d6d04cd8d4963f09f7101bf57cb97a83452cc5de873c5ac0ce001c471c9fcd3275d90a118dd4c25a525d9fb358ff85104b98136850786b387fa17cc1a1d128bc5f7c365ec7920ea677e4c8023071a958647d9fbd27e29d7d099b4dfbbac086ac2af00407fd12092ef1f4847bf8988d839e49a6b5b42482c3dde77022ace66e1ca15b46f2df88d053c1bc3623110b3be74b08749eba6d22f87a44cf7cc1997e7e45d0e"; - let tx_bytes = hex::decode(tx_hex).unwrap(); - let tx = ZTransaction::read(tx_bytes.as_slice()).unwrap(); - let tx = tx.into(); - - let expected_fee = DexFee::WithBurn { - fee_amount: "0.0075".into(), - burn_amount: "0.0025".into(), - burn_destination: DexFeeBurnDestination::PreBurnAccount, - }; - - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, - expected_sender: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.001")), - min_block_number: 12000, - uuid: &[1; 16], - }; - // Invalid amount should return an error - let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); - match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), - _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } - - // Invalid memo should return an error - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, - expected_sender: &[], - dex_fee: &expected_fee, - min_block_number: 12000, - uuid: &[2; 16], - }; - let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); - match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid memo")), - _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } - - /* Fix realtime min_block_number to run this test: - // Confirmed before min block - let min_block_number = 451208; - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, - expected_sender: &[], - dex_fee: &expected_fee, - min_block_number: , - uuid: &[1; 16], - }; - let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); - match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min block")), - _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } */ - - // Success validation - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, - expected_sender: &[], - dex_fee: &expected_fee, - min_block_number: 12000, - uuid: &[1; 16], - }; - coin.validate_fee(validate_fee_args).await.unwrap(); - - // Test old standard dex fee with no burn output - // TODO: disable when the upgrade transition period ends - - // https://zombie.explorer.lordofthechains.com/tx/9eb7fc697b280499df33e5838af6e67540d436fd8f565f47a7f03e6013e8342c - let tx_2_hex = "0400008085202f8900000000000006030c00e80300000000000001c167d6e78e09dfbac2973bfd8acac75fc603f6ffb481377e3ec790f1cc812a8a3979ecfb8a0c7c3a966d90675261568550f9363f9384a21390d7f58bde6f7b03270d88e1fa61d739c27d7f585c9bbc81a3d522fbb88fe8dc8567e27a048d475ce14fdfd11455fd54c577538438decbf6954f1ffba86c78896178ce514c5f1762a7de9e83552533eb4c558c4f9950b1806f266b25d6437f5aac08048d6f48100d49ecb2253e85c3b555a7cd84c9628ae58e5d68ddad61e69edfcdc0fa12170dd80340c417bff9e1711bf6e9728a6a52c42598d7ffd00c35679b1555cab075e54b134901d02ca9b07bb20c5719b2728faa020fb844c183c2ae649034a5476c4d129c3f97cd00a87be1ca7e73d027188cdab57fbb34b5addb7432f51454299b8cf47b389f98bad8abd42d82a2f8c2d11312e39272d44409540bcfa4c6b445e8e6dc63cc2fd5db1448875adb055ea8665c863bd07bf3aa8eb210f638287789957c96c54819061ee215eb7ba7b6048591a57f097a3e5da06b6359325d830d5b74c20c025996a113e4bb9fd2c853b7360d4961396cd99c23a13de972097eede3a955a5d5d8c8695a7290581a248fc03ea87606e71564d8e8fb00ebb8d5c10fc8fefe1660171524264060d15363fc2dc0ac0ab21fcbae1dc53786873cb9e8716f3ada651e79c3306ad49adeeb354213cc37499e217fa1c0f219e85bd22cf493f5e76f053543dd3b36bd180b1dcf17f781e35d6955c33c06426a885138f1e21b78ee87a27624f33b6567bfa6a0fe43e2d623578f6917d300a408c4dd48683213ffad453de1003e120fbfa74a6db4628af9d446e26492fde67bf52d034fcaf2b9b959472404fd631ef599815c6f190807b75f638e134148a5813424ba6cf59cf86ce515a14b95f7b8f80b1aa1b3cbbc091fa2a686277a9cc613e48b2c227aed7b4b093ac8b12a238bc99f9983c8bac21bb0f897eada35bf0e01b1436cf6d44b959595bdcdfd4676e28b500b9ad6b8a5825c3d3c0c38a4a5a2c3ded205584439621eaa7ee639b09aca1f533bb4892b29d761d94887fa78f605b9b8f5b3ab44ea578d9329bd78d7a6ae903f1960e16007a924be79ab31ea6ed7466485488b5c71eb02d6b99f345f2f61cb3cd994045c502d19f615233b3ebb263981de26674de082d384cc04c09a309567780f7f24298847fc2dff5f22082074684aa9efa260b8aaf4357bff2e9d32f8918b16876051b5459136dcc8788aba7b2ead435c3bf662f9f1acddd4a8a71b593e99ed50e158028946195ee991666bf88f4cf4d30a04c877ce8a9e6d224aed662e85a32f5cb9029a3dd4ba663b6f6314ef58fbce623171946d01d1ff456f90131159e5209cb41329061a0dd8a5fc35576108681e783fb173f67dda33134a9b1f07494a1d6273810fd77a25c92f7444d6226738d5c7161b7b198be069ac65d50a22d728292e95d1859e0c646db62aa3f401e55026a551b1edfe8fd5eca8e4c6836bd09429b5e22f64a09db4c6935b6febcbac6430f66dc0280c9be046133795f1f59ec32cbf4511749984f7b2ba131588f86f82322901ee7d709550ecadb5b915d5cfb2e950d2a8c5eda57da49d2ac9562b851f81e70a32178989e83807f04a6324cf7320a26a91b41e31a06c706431794ffb8b9ce5f3d853fb9106c8a98ea3b2948356948bfbbd63eb30e3cb68d7e373df80221d1b1211c717afe8b7b0b46a3208859254d9ae3517b8e031f413178c0fd408e76ccdc580a9a19edf4b3c70c273f4c8c626fad225e5aeee890c65328437b8bf316066e54a4741d8ac8ab9b5555f09b89b79165f9aa08a59be8f10c121b1b425bd5e3a64b6e4db3e1cacb00a5867fd05b454b75ff1eb8560770f21af7680107560a2209373d2999eb21bed2a10bafe1eaf5a31c18e69c63cce9b8c6cddcfc1088f956bcf3c9adeb77ef0589ab6405f0a9ba5650819a48fb42597fcd2f4ad67bdc89870d82eaa0d8dbd298a59ff552576dedb539834de725638e0f68307d4ac203d8e2e4649e31abc4e8748251c8fb6df3459300d1badfc19ad4d2f680f466b02680bb3e5a13c0c8a5db3665bc9fc2093c4d38acb176754db556ebd1663c23f284bec95279957b112131f8aa09af15ff26eebea3215c96b9df43c9fc9134d9db4e588aff293f3084db13e1d92bc33ca07a1b534b4a4e5fcbf098be7d26f9312db7f9d6b160318a4562c3c3b0c87688c59f402e0032242324339ef33713bf39c2110e7eb155bf926888385fe4b18bf3ef13dc2601b76def3d763f5b2ddea363f7e3697112194fb6332be96540a53a86e1e34fd70429dcfc39c5e2f68fa72e0045fe4ef12b965f0827c5bee9cd4f0c9b4cf6468316384fe33df5703c7742f9b409b9a508e94faa8be3c27ad75d21f85ee31753c96deb909221befd62bae084885c890d89f775dc0eee940ffbcad0aa65c08a71d09e234ad150e82610ba03deb608d44e9019d8579f9e9351daa6f3bcbbc8ec170c8b700bcb495c333b32136721f6417a3f3b12500641eb7af9e5813fafd27794a7b2476320fde18f3019302d49d77c3536af214e6c8357a36029a37a07011d1cdbe0db3fe7443a6908f5d3b6e08d61f33bad2a0bfbc9db86022d4f91b0ba6ef1b5ec30f0187f4c540eeb117c4d3d78659e46540df4b9301c6fce031d7e438abeb13a747be6ce9c0a33a2bd6f6092d0a26d5ba138bb6f2c3113ea6cff868853dacfc5df0433049a59d2b365e9a87ee6a6203e52121d60bc709feb1c1a30e95fbc600f648dfa5fadc8cf324a4c5d91e1f80501661aa51a518b381933932a1367e4369e07943f291012f5a9394692d9984fc2dc55c0ec4fe3d18a4a0b9f9d7c9d3f57b2e2a0c31f08f17ffe7355fec963b8ae364ed8cff046aa8220dc813f2dc78405069c707afadb77cfc8d64803a25eab7ebc74c738b41f9b3f2d881f1e2b77d37f38c1b5991daf5c911c04947891909f9c3e50e1314884207f0ea99d9310c9cfe93fea53fb57c93efbd412702e283e61196b9158de774333893b51c768ae48ec086e47b105d0b21357bd14f85b9f145fbfd63c0e998d6e54900915c8ffaf1234fa910ede3035e5e47ee9b22559459d0ea2b0f3242c5ec2782d09a7b477b560b1ecfd14d82f24600334d2c85dc2def0f457ea199e266c52fb9a596de02da05a9df8e4731cf941e1ada11c66d0954742745d5ef1b36dc7628614ed28ba9358ab38c2d007aa90147906270ab35ae26fa3473ec5881f8e6ed04c592a403386c4061becc70b5735531f8d249abb079317f43f111de58c6678e62a6d2dc83193acef928c906"; - let tx_2_bytes = hex::decode(tx_2_hex).unwrap(); - let tx_2 = ZTransaction::read(tx_2_bytes.as_slice()).unwrap(); - let tx_2 = tx_2.into(); - - // Success validation - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx_2, - expected_sender: &[], - dex_fee: &DexFee::Standard("0.00999999".into()), - min_block_number: 12000, - uuid: &[1; 16], - }; - let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); - match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), - _ => panic!("Expected `WrongPaymentTx`: {:?}", err), - } - - // Success validation - let expected_std_fee = DexFee::Standard("0.01".into()); - let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx_2, - expected_sender: &[], - dex_fee: &expected_std_fee, - min_block_number: 12000, - uuid: &[1; 16], - }; - coin.validate_fee(validate_fee_args).await.unwrap(); -} diff --git a/mm2src/coins/z_coin/z_rpc.rs b/mm2src/coins/z_coin/z_rpc.rs index 40eef387c0..9b35081f7c 100644 --- a/mm2src/coins/z_coin/z_rpc.rs +++ b/mm2src/coins/z_coin/z_rpc.rs @@ -1,9 +1,10 @@ use super::{z_coin_errors::*, BlockDbImpl, CheckPointBlockInfo, WalletDbShared, ZCoinBuilder, ZcoinConsensusParams}; -use crate::utxo::rpc_clients::NO_TX_ERROR_CODE; use crate::utxo::utxo_builder::{UtxoCoinBuilderCommonOps, DAY_IN_SECONDS}; +use crate::z_coin::storage::z_locked_notes::LockedNotesStorage; use crate::z_coin::storage::{BlockProcessingMode, DataConnStmtCacheWrapper}; use crate::z_coin::SyncStartPoint; use crate::RpcCommonOps; + use async_trait::async_trait; use common::executor::Timer; use common::executor::{spawn_abortable, AbortOnDropHandle}; @@ -29,7 +30,6 @@ use z_coin_grpc::{BlockId, BlockRange, TreeState, TxFilter}; use zcash_extras::{WalletRead, WalletWrite}; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::TxId; -use zcash_primitives::zip32::ExtendedSpendingKey; pub(crate) mod z_coin_grpc { tonic::include_proto!("pirate.wallet.sdk.rpc"); @@ -339,13 +339,11 @@ impl ZRpcOps for LightRpcClient { match client.get_transaction(request).await { Ok(_) => break, Err(e) => { - error!("Error on getting tx {}", tx_id); - if e.message().contains(NO_TX_ERROR_CODE) { - if attempts >= 3 { - return false; - } - attempts += 1; + error!("Error on getting tx {}: err: {}", tx_id, e.to_string()); + if attempts >= 5 { + return false; } + attempts += 1; Timer::sleep(30.).await; }, } @@ -476,16 +474,16 @@ impl ZRpcOps for NativeClient { async fn check_tx_existence(&self, tx_id: TxId) -> bool { let mut attempts = 0; loop { - match self.get_raw_transaction_bytes(&H256Json::from(tx_id.0)).compat().await { + let tx_hash = H256Json::from(tx_id.0).reversed(); + let tx = self.get_raw_transaction_bytes(&tx_hash).compat().await; + match tx { Ok(_) => break, Err(e) => { - error!("Error on getting tx {}", tx_id); - if e.to_string().contains(NO_TX_ERROR_CODE) { - if attempts >= 3 { - return false; - } - attempts += 1; + error!("Error on getting tx {}: err: {}", tx_id, e.to_string()); + if attempts >= 5 { + return false; } + attempts += 1; Timer::sleep(30.).await; }, } @@ -508,7 +506,7 @@ pub(super) async fn init_light_client<'a>( blocks_db: BlockDbImpl, sync_params: &Option, skip_sync_params: bool, - z_spending_key: &ExtendedSpendingKey, + locked_notes_db: LockedNotesStorage, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -541,8 +539,7 @@ pub(super) async fn init_light_client<'a>( // check if no sync_params was provided and continue syncing from last height in db if it's > 0 or skip_sync_params is true. let continue_from_prev_sync = (min_height > 0 && sync_params.is_none()) || (skip_sync_params && min_height < sapling_activation_height); - let wallet_db = - WalletDbShared::new(builder, maybe_checkpoint_block, z_spending_key, continue_from_prev_sync).await?; + let wallet_db = WalletDbShared::new(builder, maybe_checkpoint_block, continue_from_prev_sync).await?; // Check min_height in blocks_db and rewind blocks_db to 0 if sync_height != min_height if !continue_from_prev_sync && (sync_height != min_height) { // let user know we're clearing cache and re-syncing from new provided height. @@ -571,6 +568,7 @@ pub(super) async fn init_light_client<'a>( scan_interval_ms: builder.z_coin_params.scan_interval_ms, first_sync_block: first_sync_block.clone(), streaming_manager: builder.ctx.event_stream_manager.clone(), + locked_notes_db, }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(light_rpc_clients))); @@ -586,7 +584,7 @@ pub(super) async fn init_native_client<'a>( builder: &ZCoinBuilder<'a>, native_client: NativeClient, blocks_db: BlockDbImpl, - z_spending_key: &ExtendedSpendingKey, + locked_notes_db: LockedNotesStorage, ) -> Result<(AsyncMutex, WalletDbShared), MmError> { let coin = builder.ticker.to_string(); let (sync_status_notifier, sync_watcher) = channel(1); @@ -600,7 +598,7 @@ pub(super) async fn init_native_client<'a>( is_pre_sapling: false, actual: checkpoint_height, }; - let wallet_db = WalletDbShared::new(builder, checkpoint_block, z_spending_key, true) + let wallet_db = WalletDbShared::new(builder, checkpoint_block, true) .await .mm_err(|err| ZcoinClientInitError::ZcoinStorageError(err.to_string()))?; @@ -618,6 +616,7 @@ pub(super) async fn init_native_client<'a>( scan_interval_ms: builder.z_coin_params.scan_interval_ms, first_sync_block: first_sync_block.clone(), streaming_manager: builder.ctx.event_stream_manager.clone(), + locked_notes_db, }; let abort_handle = spawn_abortable(light_wallet_db_sync_loop(sync_handle, Box::new(native_client))); @@ -704,6 +703,7 @@ pub struct SaplingSyncLoopHandle { current_block: BlockHeight, blocks_db: BlockDbImpl, wallet_db: WalletDbShared, + locked_notes_db: LockedNotesStorage, consensus_params: ZcoinConsensusParams, /// Notifies about sync status without stopping the loop, e.g. on coin activation sync_status_notifier: AsyncSender, @@ -804,6 +804,7 @@ impl SaplingSyncLoopHandle { BlockProcessingMode::Validate, wallet_ops.get_max_height_hash().await?, None, + &self.locked_notes_db, ) .await { @@ -848,6 +849,7 @@ impl SaplingSyncLoopHandle { BlockProcessingMode::Scan(scan, self.streaming_manager.clone()), None, Some(self.scan_blocks_per_iteration), + &self.locked_notes_db, ) .await?; @@ -918,7 +920,7 @@ async fn light_wallet_db_sync_loop(mut sync_handle: SaplingSyncLoopHandle, mut c let walletdb = &sync_handle.wallet_db; if let Ok(is_tx_imported) = walletdb.is_tx_imported(tx_id).await { if !is_tx_imported { - info!("Tx {} is not imported yet", tx_id); + error!("Tx {} is not imported yet", tx_id); Timer::sleep(10.).await; continue; } diff --git a/mm2src/coins/z_coin/z_unit_tests.rs b/mm2src/coins/z_coin/z_unit_tests.rs new file mode 100644 index 0000000000..25ce04ff3b --- /dev/null +++ b/mm2src/coins/z_coin/z_unit_tests.rs @@ -0,0 +1,344 @@ +use super::*; +use crate::utxo::rpc_clients::ElectrumClient; +use crate::utxo::rpc_clients::UtxoRpcClientOps; +use crate::z_coin::storage::WalletDbShared; +use crate::CoinProtocol; +use crate::DexFeeBurnDestination; +use common::executor::spawn_abortable; +use core::convert::AsRef; +use ff::{Field, PrimeField}; +use futures::channel::mpsc::channel; +use futures::lock::Mutex as AsyncMutex; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +use mm2_net::transport::slurp_url_with_headers; +use mm2_test_helpers::for_tests::zombie_conf; +use mocktopus::mocking::*; +use rand::rngs::OsRng; +use rand::RngCore; +use std::fs::{self, create_dir}; +use std::path::Path; +use url::Url; +use zcash_primitives::merkle_tree::CommitmentTree; +use zcash_primitives::merkle_tree::IncrementalWitness; +use zcash_primitives::sapling::Node; +use zcash_primitives::sapling::Rseed; +use zcash_primitives::transaction::components::amount::DEFAULT_FEE; + +const GITHUB_CLIENT_USER_AGENT: &str = "mm2"; + +/// Download zcash params from komodo repo +async fn fetch_and_save_params(param: &str, fname: &Path) -> Result<(), String> { + let url = Url::parse(&format!("{}/", DOWNLOAD_URL)).unwrap().join(param).unwrap(); + println!("downloading zcash params {}...", url); + let data = slurp_url_with_headers(url.as_str(), vec![( + http::header::USER_AGENT.as_str(), + GITHUB_CLIENT_USER_AGENT, + )]) + .await + .map_err(|err| format!("could not download zcash params: {}", err))? + .2; + println!("saving zcash params to file {}...", fname.display()); + fs::write(fname, data).map_err(|err| format!("could not save zcash params: {}", err)) +} + +/// download zcash params, if not exist +pub(super) async fn download_parameters_for_tests(z_params_path: &Path) { + let sapling_spend_fname = z_params_path.join(SAPLING_SPEND_NAME); + let sapling_output_fname = z_params_path.join(SAPLING_OUTPUT_NAME); + if !sapling_spend_fname.exists() + || !sapling_output_fname.exists() + || !verify_checksum_zcash_params(&sapling_spend_fname, &sapling_output_fname).is_ok_and(|r| r) + { + let _ = create_dir(z_params_path); + fetch_and_save_params(SAPLING_SPEND_NAME, sapling_spend_fname.as_path()) + .await + .unwrap(); + fetch_and_save_params(SAPLING_OUTPUT_NAME, sapling_output_fname.as_path()) + .await + .unwrap(); + } +} + +pub(super) async fn create_test_sync_connector<'a>( + builder: &ZCoinBuilder<'a>, +) -> (AsyncMutex, WalletDbShared) { + let wallet_db = WalletDbShared::new(builder, None, true).await.unwrap(); // Note: assuming we have a spending key in the builder + let (_, sync_watcher) = channel(1); + let (on_tx_gen_notifier, _) = channel(1); + let abort_handle = spawn_abortable(futures::future::ready(())); + let first_sync_block = FirstSyncBlock { + requested: 0, + is_pre_sapling: false, + actual: 0, + }; + let sync_state_connector = + SaplingSyncConnector::new_mutex_wrapped(sync_watcher, on_tx_gen_notifier, abort_handle, first_sync_block); + (sync_state_connector, wallet_db) +} + +#[allow(clippy::too_many_arguments)] +async fn z_coin_from_conf_and_params_for_tests( + ctx: &MmArc, + ticker: &str, + conf: &Json, + params: &ZcoinActivationParams, + priv_key_policy: PrivKeyBuildPolicy, + protocol_info: ZcoinProtocolInfo, + spending_key: &str, +) -> Result> { + use zcash_client_backend::encoding::decode_extended_spending_key; + let z_spending_key = + decode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, spending_key) + .unwrap() + .unwrap(); + + let builder = ZCoinBuilder::new( + ctx, + ticker, + conf, + params, + priv_key_policy, + Some(z_spending_key), + protocol_info, + )?; + + builder.build().await +} + +/// Build asset `ZCoin` for unit tests. +async fn z_coin_from_spending_key_for_unit_test(spending_key: &str) -> (MmArc, ZCoin) { + let ctx = MmCtxBuilder::new().into_mm_arc(); + let mut conf = zombie_conf(); + let params = ZcoinActivationParams { + mode: ZcoinRpcMode::UnitTests, + ..Default::default() + }; + let pk_data = [1; 32]; + let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { + CoinProtocol::ZHTLC(protocol_info) => protocol_info, + other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), + }; + + let coin = z_coin_from_conf_and_params_for_tests( + &ctx, + "ZOMBIE", + &conf, + ¶ms, + PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), + protocol_info, + spending_key, + ) + .await + .unwrap(); + (ctx, coin) +} + +fn add_test_spend(coin: &ZCoin, tx_builder: &mut ZTxBuilder, amount: u64) { + let extsk = coin.z_fields.z_spending_key.clone(); + let extfvk = coin.z_fields.evk.clone(); + let to = extfvk.default_address().unwrap().1; + let mut rng = OsRng; + let note1 = to + .create_note(amount, Rseed::BeforeZip212(jubjub::Fr::random(&mut rng))) + .unwrap(); + let cmu1 = Node::new(note1.cmu().to_repr()); + let mut tree = CommitmentTree::empty(); + tree.append(cmu1).unwrap(); + let witness1 = IncrementalWitness::from_tree(&tree); + + tx_builder + .add_sapling_spend(extsk, *to.diversifier(), note1, witness1.path().unwrap()) + .unwrap(); +} + +async fn validate_fee_caller( + coin: &ZCoin, + dex_params: (PaymentAddress, u64), + burn_params: Option<(PaymentAddress, u64)>, + dex_fee: &DexFee, +) -> ValidatePaymentResult<()> { + let uuid = &[1; 16]; + let mut z_outputs = vec![]; + let mut tx_builder = ZTxBuilder::new(coin.consensus_params(), BlockHeight::from_u32(1)); + + add_test_spend( + coin, + &mut tx_builder, + dex_params.1 + + if let Some(ref burn_params) = burn_params { + burn_params.1 + } else { + 0 + } + + u64::from(DEFAULT_FEE), + ); + + let dex_fee_out = ZOutput { + to_addr: dex_params.0, + amount: Amount::from_u64(dex_params.1).unwrap(), + viewing_key: Some(DEX_FEE_OVK), + memo: Some(MemoBytes::from_bytes(uuid).expect("uuid length < 512")), + }; + z_outputs.push(dex_fee_out); + + // add output to the dex burn address: + if let Some(burn_params) = burn_params { + let dex_burn_out = ZOutput { + to_addr: burn_params.0, + amount: Amount::from_u64(burn_params.1).unwrap(), + viewing_key: Some(DEX_FEE_OVK), + memo: Some(MemoBytes::from_bytes(uuid).expect("uuid length < 512")), + }; + z_outputs.push(dex_burn_out); + } + for z_out in z_outputs { + tx_builder + .add_sapling_output(z_out.viewing_key, z_out.to_addr, z_out.amount, z_out.memo) + .unwrap(); + } + let (tx, _) = async_blocking({ + let prover = coin.z_fields.z_tx_prover.clone(); + move || tx_builder.build(BranchId::Sapling, prover.as_ref()) + }) + .await + .unwrap(); + + let tx: TransactionEnum = tx.into(); + let tx_ret = tx.clone(); + ElectrumClient::get_verbose_transaction.mock_safe(move |_, txid| { + let bytes: BytesJson = tx_ret.tx_hex().into(); + MockResult::Return(Box::new(futures01::future::ok(RpcTransaction { + txid: *txid, + hash: None, + blockhash: H256Json::default(), + confirmations: 0, + time: 0, + blocktime: 0, + hex: bytes, + vout: vec![], + vin: vec![], + size: None, + version: 4, + locktime: 0, + vsize: None, + rawconfirmations: None, + height: None, + }))) + }); + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx, + expected_sender: &[], + dex_fee, + min_block_number: 1, + uuid: &[1; 16], + }; + coin.validate_fee(validate_fee_args).await +} + +#[test] +fn derive_z_key_from_mm_seed() { + use crypto::privkey::key_pair_from_seed; + use zcash_client_backend::encoding::encode_extended_spending_key; + + let seed = "spice describe gravity federal blast come thank unfair canal monkey style afraid"; + let secp_keypair = key_pair_from_seed(seed).unwrap(); + let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); + let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); + assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqqytwz2zjt587n63kyz6jawmflttqu5rxavvqx3lzfs0tdr0w7g5tgntxzf5erd3jtvva5s52qx0ms598r89vrmv30r69zehxy2r3vesghtqd6dfwdtnauzuj8u8eeqfx7qpglzu6z54uzque6nzzgnejkgq569ax4lmk0v95rfhxzxlq3zrrj2z2kqylx2jp8g68lqu6alczdxd59lzp4hlfuj3jp54fp06xsaaay0uyass992g507tdd7psua5w6q76dyq3"); + + let (_, address) = z_spending_key.default_address().unwrap(); + let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); + assert_eq!( + encoded_addr, + "zs182ht30wnnnr8jjhj2j9v5dkx3qsknnr5r00jfwk2nczdtqy7w0v836kyy840kv2r8xle5gcl549" + ); + + let seed = "also shoot benefit prefer juice shell elder veteran woman mimic image kidney"; + let secp_keypair = key_pair_from_seed(seed).unwrap(); + let z_spending_key = ExtendedSpendingKey::master(&*secp_keypair.private().secret); + let encoded = encode_extended_spending_key(z_mainnet_constants::HRP_SAPLING_EXTENDED_SPENDING_KEY, &z_spending_key); + assert_eq!(encoded, "secret-extended-key-main1qqqqqqqqqqqqqq8jnhc9stsqwts6pu5ayzgy4szplvy03u227e50n3u8e6dwn5l0q5s3s8xfc03r5wmyh5s5dq536ufwn2k89ngdhnxy64sd989elwas6kr7ygztsdkw6k6xqyvhtu6e0dhm4mav8rus0fy8g0hgy9vt97cfjmus0m2m87p4qz5a00um7gwjwk494gul0uvt3gqyjujcclsqry72z57kr265jsajactgfn9m3vclqvx8fsdnwp4jwj57ffw560vvwks9g9hpu"); + + let (_, address) = z_spending_key.default_address().unwrap(); + let encoded_addr = encode_payment_address(z_mainnet_constants::HRP_SAPLING_PAYMENT_ADDRESS, &address); + assert_eq!( + encoded_addr, + "zs1funuwrjr2stlr6fnhkdh7fyz3p7n0p8rxase9jnezdhc286v5mhs6q3myw0phzvad5mvqgfxpam" + ); +} + +#[test] +fn test_interpret_memo_string() { + use std::str::FromStr; + use zcash_primitives::memo::Memo; + + let actual = interpret_memo_string("68656c6c6f207a63617368").unwrap(); + let expected = Memo::from_str("68656c6c6f207a63617368").unwrap().encode(); + assert_eq!(actual, expected); + + let actual = interpret_memo_string("A custom memo").unwrap(); + let expected = Memo::from_str("A custom memo").unwrap().encode(); + assert_eq!(actual, expected); + + let actual = interpret_memo_string("0x68656c6c6f207a63617368").unwrap(); + let expected = MemoBytes::from_bytes(&hex::decode("68656c6c6f207a63617368").unwrap()).unwrap(); + assert_eq!(actual, expected); +} + +#[tokio::test] +async fn test_validate_zcoin_dex_fee() { + let (_ctx, coin) = z_coin_from_spending_key_for_unit_test("secret-extended-key-main1qvqstxphqyqqpqqnh3hstqpdjzkpadeed6u7fz230jmm2mxl0aacrtu9vt7a7rmr2w5az5u79d24t0rudak3newknrz5l0m3dsd8m4dffqh5xwyldc5qwz8pnalrnhlxdzf900x83jazc52y25e9hvyd4kepaze6nlcvk8sd8a4qjh3e9j5d6730t7ctzhhrhp0zljjtwuptadnksxf8a8y5axwdhass5pjaxg0hzhg7z25rx0rll7a6txywl32s6cda0s5kexr03uqdtelwe").await; + + let std_fee = DexFee::Standard("0.001".into()); + let with_burn = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + assert!( + validate_fee_caller(&coin, (coin.z_fields.dex_fee_addr.clone(), 100000), None, &std_fee) + .await + .is_ok() + ); + assert!(validate_fee_caller( + &coin, + (coin.z_fields.dex_fee_addr.clone(), 750000), + Some((coin.z_fields.dex_burn_addr.clone(), 250000)), + &with_burn + ) + .await + .is_ok()); + // try reverted addresses + assert!(validate_fee_caller( + &coin, + (coin.z_fields.dex_burn_addr.clone(), 750000), + Some((coin.z_fields.dex_fee_addr.clone(), 250000)), + &with_burn + ) + .await + .is_err()); + let other_addr = decode_payment_address( + coin.z_fields.consensus_params.hrp_sapling_payment_address(), + "zs182ht30wnnnr8jjhj2j9v5dkx3qsknnr5r00jfwk2nczdtqy7w0v836kyy840kv2r8xle5gcl549", + ) + .expect("valid z address format") + .expect("valid z address"); + // try invalid dex address + assert!(validate_fee_caller( + &coin, + (other_addr.clone(), 750000), + Some((coin.z_fields.dex_burn_addr.clone(), 250000)), + &with_burn + ) + .await + .is_err()); + // try invalid burn address + assert!(validate_fee_caller( + &coin, + (coin.z_fields.dex_fee_addr.clone(), 750000), + Some((other_addr.clone(), 250000)), + &with_burn + ) + .await + .is_err()); +} diff --git a/mm2src/coins_activation/Cargo.toml b/mm2src/coins_activation/Cargo.toml index be951c67d4..69f020c809 100644 --- a/mm2src/coins_activation/Cargo.toml +++ b/mm2src/coins_activation/Cargo.toml @@ -12,33 +12,35 @@ default = [] for-tests = [] [dependencies] -async-trait = "0.1" +async-trait.workspace = true coins = { path = "../coins" } common = { path = "../common" } crypto = { path = "../crypto" } -derive_more = "0.99" -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } -hex = "0.4.2" +derive_more.workspace = true +ethereum-types.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } +hex.workspace = true +kdf_walletconnect = { path = "../kdf_walletconnect" } mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_metrics = { path = "../mm2_metrics" } mm2_number = { path = "../mm2_number" } -parking_lot = { version = "0.12.0", features = ["nightly"] } +parking_lot = { workspace = true, features = ["nightly"] } rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } +secp256k1 = { version = "0.24" } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -url = { version = "2.2.2", features = ["serde"] } +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +url.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] mm2_metamask = { path = "../mm2_metamask" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -lightning = "0.0.113" -lightning-background-processor = "0.0.113" -lightning-invoice = { version = "0.21.0", features = ["serde"] } +lightning.workspace = true +lightning-background-processor.workspace = true +lightning-invoice.workspace = true diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index d8c4a0f49e..95b74f71a4 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -10,14 +10,16 @@ use crate::prelude::*; use async_trait::async_trait; use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, - EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy}; -use coins::eth::v2_activation::{EthTokenActivationError, NftActivationRequest, NftProviderEnum}; -use coins::eth::{Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; + EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy, + EthTokenActivationError, NftActivationRequest, NftProviderEnum}; +use coins::eth::wallet_connect::eth_request_wc_personal_sign; +use coins::eth::{ChainSpec, Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; use coins::hd_wallet::{DisplayAddress, RpcTaskXPubExtractor}; use coins::my_tx_history_v2::TxHistoryStorage; use coins::nft::nft_structs::NftInfo; use coins::{CoinBalance, CoinBalanceMap, CoinProtocol, CoinWithDerivationMethod, DerivationMethod, MarketCoinOps, MmCoin, MmCoinEnum}; +use kdf_walletconnect::WalletConnectCtx; use crate::platform_coin_with_tokens::InitPlatformCoinWithTokensTask; use common::Future01CompatExt; @@ -46,6 +48,9 @@ impl From for EnablePlatformCoinWithTokensError { EthActivationV2Error::ChainIdNotSet => { EnablePlatformCoinWithTokensError::Internal("`chain_id` is not set in coin config".to_string()) }, + EthActivationV2Error::UnsupportedChain { .. } => { + EnablePlatformCoinWithTokensError::Internal("Unsupported chain".to_string()) + }, EthActivationV2Error::ActivationFailed { ticker, error } => { EnablePlatformCoinWithTokensError::PlatformCoinCreationError { ticker, error } }, @@ -62,6 +67,7 @@ impl From for EnablePlatformCoinWithTokensError { EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(e) }, EthActivationV2Error::HDWalletStorageError(e) => EnablePlatformCoinWithTokensError::Internal(e), + EthActivationV2Error::WalletConnectError(e) => EnablePlatformCoinWithTokensError::WalletConnectError(e), #[cfg(target_arch = "wasm32")] EthActivationV2Error::MetamaskError(metamask) => { EnablePlatformCoinWithTokensError::Transport(metamask.to_string()) @@ -89,13 +95,14 @@ impl From for EnablePlatformCoinWithTokensError { } } -impl TryFromCoinProtocol for EthCoinType { +impl TryFromCoinProtocol for ChainSpec { fn try_from_coin_protocol(proto: CoinProtocol) -> Result> where Self: Sized, { match proto { - CoinProtocol::ETH => Ok(EthCoinType::Eth), + CoinProtocol::ETH { chain_id } => Ok(ChainSpec::Evm { chain_id }), + CoinProtocol::TRX { network } => Ok(ChainSpec::Tron { network }), protocol => MmError::err(protocol), } } @@ -263,7 +270,7 @@ impl CurrentBlock for EthWithTokensActivationResult { #[async_trait] impl PlatformCoinWithTokensActivationOps for EthCoin { type ActivationRequest = EthWithTokensActivationRequest; - type PlatformProtocolInfo = EthCoinType; + type PlatformProtocolInfo = ChainSpec; type ActivationResult = EthWithTokensActivationResult; type ActivationError = EthActivationV2Error; @@ -276,9 +283,10 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { ticker: String, platform_conf: &Json, activation_request: Self::ActivationRequest, - _protocol: Self::PlatformProtocolInfo, + protocol: Self::PlatformProtocolInfo, ) -> Result> { - let priv_key_policy = eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy)?; + let priv_key_policy = + eth_priv_key_build_policy(&ctx, &activation_request.platform_request.priv_key_policy, &protocol).await?; let platform_coin = eth_coin_from_conf_and_request_v2( &ctx, @@ -286,6 +294,7 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { platform_conf, activation_request.platform_request, priv_key_policy, + protocol, ) .await?; @@ -411,7 +420,14 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { &ctx, task_handle, platform_coin_xpub_extractor_rpc_statuses(), - CoinProtocol::ETH, + // Todo: add support for Tron by checking self.chain_spec + CoinProtocol::ETH { + chain_id: self.chain_id().ok_or_else(|| { + EthActivationV2Error::InternalError( + "chain_id should be available for an EVM coin".to_string(), + ) + })?, + }, ) .map_err(|_| MmError::new(EthActivationV2Error::HwError(HwRpcError::NotInitialized)))?, ) @@ -452,9 +468,10 @@ impl PlatformCoinWithTokensActivationOps for EthCoin { } } -fn eth_priv_key_build_policy( +async fn eth_priv_key_build_policy( ctx: &MmArc, activation_policy: &EthPrivKeyActivationPolicy, + protocol: &ChainSpec, ) -> MmResult { match activation_policy { EthPrivKeyActivationPolicy::ContextPrivKey => Ok(EthPrivKeyBuildPolicy::detect_priv_key_policy(ctx)?), @@ -466,5 +483,20 @@ fn eth_priv_key_build_policy( Ok(EthPrivKeyBuildPolicy::Metamask(metamask_ctx)) }, EthPrivKeyActivationPolicy::Trezor => Ok(EthPrivKeyBuildPolicy::Trezor), + EthPrivKeyActivationPolicy::WalletConnect { session_topic } => { + let wc = WalletConnectCtx::from_ctx(ctx) + .expect("TODO: handle error when enable kdf initialization without key."); + let chain_id = protocol.chain_id().ok_or(EthActivationV2Error::ChainIdNotSet)?; + let (public_key_uncompressed, address) = + eth_request_wc_personal_sign(&wc, session_topic, chain_id) + .await + .mm_err(|err| EthActivationV2Error::WalletConnectError(err.to_string()))?; + + Ok(EthPrivKeyBuildPolicy::WalletConnect { + address, + public_key_uncompressed, + session_topic: session_topic.clone(), + }) + }, } } diff --git a/mm2src/coins_activation/src/lightning_activation.rs b/mm2src/coins_activation/src/lightning_activation.rs index 1d2f9ec232..105fb2c422 100644 --- a/mm2src/coins_activation/src/lightning_activation.rs +++ b/mm2src/coins_activation/src/lightning_activation.rs @@ -20,7 +20,7 @@ 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; +use lightning::chain::keysinterface::{KeysInterface, Recipient}; use lightning::chain::Access; use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; @@ -29,6 +29,7 @@ use lightning_invoice::payment; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use parking_lot::Mutex as PaMutex; +use secp256k1::Secp256k1; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; use serde_json::{self as json, Value as Json}; @@ -350,11 +351,16 @@ async fn start_lightning( // Initialize the Logger let logger = ctx.log.0.clone(); - // Initialize Persister - let persister = init_persister(ctx, conf.ticker.clone(), params.backup_path).await?; - // Initialize the KeysManager let keys_manager = init_keys_manager(&platform)?; + let node_id = keys_manager + .get_node_secret(Recipient::Node) + .map_err(|e| EnableLightningError::Internal(format!("Error while getting node id: {:?}", e)))? + .public_key(&Secp256k1::new()); + let node_id = node_id.to_string(); + + // Initialize Persister + let persister = init_persister(ctx, &node_id, conf.ticker.clone(), params.backup_path).await?; // Initialize the P2PGossipSync. This is used for providing routes to send payments over task_handle.update_in_progress_status(LightningInProgressStatus::ReadingNetworkGraphFromFile)?; @@ -371,7 +377,7 @@ async fn start_lightning( )); // Initialize DB - let db = init_db(ctx, conf.ticker.clone()).await?; + let db = init_db(ctx, &node_id, conf.ticker.clone()).await?; // Initialize the ChannelManager task_handle.update_in_progress_status(LightningInProgressStatus::InitializingChannelManager)?; diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index 5c1677865e..24a1aca710 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -83,7 +83,7 @@ pub trait TokenAsMmCoinInitializer: Send + Sync { async fn enable_tokens_as_mm_coins( &self, - ctx: MmArc, + ctx: &MmArc, request: &Self::ActivationRequest, ) -> Result, MmError>; } @@ -134,15 +134,14 @@ where async fn enable_tokens_as_mm_coins( &self, - ctx: MmArc, + ctx: &MmArc, request: &Self::ActivationRequest, ) -> Result, MmError> { let tokens_requests = T::tokens_requests_from_platform_request(request); let token_params = tokens_requests .into_iter() .map(|req| -> Result<_, MmError> { - let (token_conf, protocol): (_, T::TokenProtocol) = - coin_conf_with_protocol(&ctx, &req.ticker, req.protocol.clone())?; + let (token_conf, protocol) = coin_conf_with_protocol(ctx, &req.ticker, req.protocol.clone())?; Ok(TokenActivationParams { ticker: req.ticker, conf: token_conf, @@ -285,6 +284,8 @@ pub enum EnablePlatformCoinWithTokensError { UnexpectedDeviceActivationPolicy, #[display(fmt = "Custom token error: {}", _0)] CustomTokenError(CustomTokenError), + #[display(fmt = "WalletConnect Error: {}", _0)] + WalletConnectError(String), } impl From for EnablePlatformCoinWithTokensError { @@ -374,7 +375,8 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::NoSuchTask(_) | EnablePlatformCoinWithTokensError::UnexpectedDeviceActivationPolicy | EnablePlatformCoinWithTokensError::FailedSpawningBalanceEvents(_) - | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } => StatusCode::BAD_REQUEST, + | EnablePlatformCoinWithTokensError::UnexpectedTokenProtocol { .. } + | EnablePlatformCoinWithTokensError::WalletConnectError(_) => StatusCode::BAD_REQUEST, EnablePlatformCoinWithTokensError::Transport(_) => StatusCode::BAD_GATEWAY, } } @@ -393,7 +395,7 @@ where { let mut mm_tokens = Vec::new(); for initializer in platform_coin.token_initializers() { - let tokens = initializer.enable_tokens_as_mm_coins(ctx.clone(), &req.request).await?; + let tokens = initializer.enable_tokens_as_mm_coins(&ctx, &req.request).await?; mm_tokens.extend(tokens); } @@ -463,7 +465,7 @@ where let mut mm_tokens = Vec::new(); for initializer in platform_coin.token_initializers() { - let tokens = initializer.enable_tokens_as_mm_coins(ctx.clone(), &req.request).await?; + let tokens = initializer.enable_tokens_as_mm_coins(&ctx, &req.request).await?; mm_tokens.extend(tokens); } diff --git a/mm2src/coins_activation/src/tendermint_token_activation.rs b/mm2src/coins_activation/src/tendermint_token_activation.rs index 12808505a9..8f31ef75b2 100644 --- a/mm2src/coins_activation/src/tendermint_token_activation.rs +++ b/mm2src/coins_activation/src/tendermint_token_activation.rs @@ -13,7 +13,6 @@ use std::collections::HashMap; impl From for EnableTokenError { fn from(err: TendermintTokenInitError) -> Self { match err { - TendermintTokenInitError::InvalidDenom(e) => EnableTokenError::InvalidConfig(e), TendermintTokenInitError::MyAddressError(e) | TendermintTokenInitError::Internal(e) => { EnableTokenError::Internal(e) }, diff --git a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs index 18cf645110..349fce564a 100644 --- a/mm2src/coins_activation/src/tendermint_with_assets_activation.rs +++ b/mm2src/coins_activation/src/tendermint_with_assets_activation.rs @@ -11,13 +11,15 @@ use async_trait::async_trait; use coins::hd_wallet::HDPathAccountToAddressId; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tendermint::tendermint_tx_history_v2::tendermint_history_loop; -use coins::tendermint::{tendermint_priv_key_policy, RpcNode, TendermintActivationPolicy, TendermintCoin, - TendermintCommons, TendermintConf, TendermintInitError, TendermintInitErrorKind, - TendermintProtocolInfo, TendermintPublicKey, TendermintToken, TendermintTokenActivationParams, - TendermintTokenInitError, TendermintTokenProtocolInfo}; +use coins::tendermint::{cosmos_get_accounts_impl, tendermint_priv_key_policy, CosmosAccountAlgo, RpcNode, + TendermintActivationPolicy, TendermintCoin, TendermintCommons, TendermintConf, + TendermintInitError, TendermintInitErrorKind, TendermintProtocolInfo, TendermintPublicKey, + TendermintToken, TendermintTokenActivationParams, TendermintTokenInitError, + TendermintTokenProtocolInfo, TendermintWalletConnectionType}; use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum, PrivKeyBuildPolicy}; use common::executor::{AbortSettings, SpawnAbortable}; use common::{true_f, Future01CompatExt}; +use kdf_walletconnect::WalletConnectCtx; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -38,6 +40,19 @@ impl RegisterTokenInfo for TendermintCoin { } } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "type", content = "params")] +pub enum TendermintPubkeyActivationParams { + /// Activate with public key + WithPubkey { + #[serde(deserialize_with = "deserialize_account_public_key")] + pubkey: TendermintPublicKey, + is_ledger_connection: bool, + }, + /// Activate with WalletConnect + WalletConnect { session_topic: String }, +} + #[derive(Clone, Deserialize)] pub struct TendermintActivationParams { nodes: Vec, @@ -50,13 +65,10 @@ pub struct TendermintActivationParams { #[serde(default)] pub path_to_address: HDPathAccountToAddressId, #[serde(default)] - #[serde(deserialize_with = "deserialize_account_public_key")] - with_pubkey: Option, - #[serde(default)] - is_keplr_from_ledger: bool, + pub activation_params: Option, } -fn deserialize_account_public_key<'de, D>(deserializer: D) -> Result, D::Error> +fn deserialize_account_public_key<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { @@ -74,7 +86,7 @@ where .iter() .map(|i| i.as_u64().unwrap() as u8) .collect(); - Ok(Some(TendermintPublicKey::from_raw_ed25519(&value).unwrap())) + Ok(TendermintPublicKey::from_raw_ed25519(&value).unwrap()) }, Some("secp256k1") => { let value: Vec = value @@ -83,7 +95,7 @@ where .iter() .map(|i| i.as_u64().unwrap() as u8) .collect(); - Ok(Some(TendermintPublicKey::from_raw_secp256k1(&value).unwrap())) + Ok(TendermintPublicKey::from_raw_secp256k1(&value).unwrap()) }, _ => Err(serde::de::Error::custom( "Unsupported pubkey algorithm. Use one of ['ed25519', 'secp256k1']", @@ -112,10 +124,7 @@ struct TendermintTokenInitializer { platform_coin: TendermintCoin, } -struct TendermintTokenInitializerErr { - ticker: String, - inner: TendermintTokenInitError, -} +struct TendermintTokenInitializerErr(TendermintTokenInitError); #[async_trait] impl TokenInitializer for TendermintTokenInitializer { @@ -137,14 +146,13 @@ impl TokenInitializer for TendermintTokenInitializer { params .into_iter() .map(|param| { - let ticker = param.ticker.clone(); TendermintToken::new( param.ticker, self.platform_coin.clone(), param.protocol.decimals, param.protocol.denom, ) - .mm_err(|inner| TendermintTokenInitializerErr { ticker, inner }) + .mm_err(TendermintTokenInitializerErr) }) .collect() } @@ -172,11 +180,7 @@ impl TryFromCoinProtocol for TendermintTokenProtocolInfo { impl From for InitTokensAsMmCoinsError { fn from(err: TendermintTokenInitializerErr) -> Self { - match err.inner { - TendermintTokenInitError::InvalidDenom(error) => InitTokensAsMmCoinsError::TokenProtocolParseError { - ticker: err.ticker, - error, - }, + match err.0 { TendermintTokenInitError::MyAddressError(error) | TendermintTokenInitError::Internal(error) => { InitTokensAsMmCoinsError::Internal(error) }, @@ -217,6 +221,37 @@ impl From for EnablePlatformCoinWithTokensError { } } +async fn activate_with_walletconnect( + ctx: &MmArc, + session_topic: String, + chain_id: &str, + ticker: &str, +) -> MmResult<(TendermintActivationPolicy, TendermintWalletConnectionType), TendermintInitError> { + let wc = WalletConnectCtx::from_ctx(ctx).expect("TODO: handle error when enable kdf initialization without key."); + let account = cosmos_get_accounts_impl(&wc, &session_topic, chain_id) + .await + .mm_err(|err| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::UnableToFetchChainAccount(err.to_string()), + })?; + let wallet_type = if wc.is_ledger_connection(&session_topic) { + TendermintWalletConnectionType::WcLedger(session_topic) + } else { + TendermintWalletConnectionType::Wc(session_topic) + }; + + let pubkey = match account.algo { + CosmosAccountAlgo::Secp256k1 | CosmosAccountAlgo::TendermintSecp256k1 => { + TendermintPublicKey::from_raw_secp256k1(&account.pubkey).ok_or(TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal("Invalid secp256k1 pubkey".to_owned()), + })? + }, + }; + + Ok((TendermintActivationPolicy::with_public_key(pubkey), wallet_type)) +} + #[async_trait] impl PlatformCoinWithTokensActivationOps for TendermintCoin { type ActivationRequest = TendermintActivationParams; @@ -246,10 +281,28 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { } let conf = TendermintConf::try_from_json(&ticker, coin_conf)?; - let is_keplr_from_ledger = activation_request.is_keplr_from_ledger && activation_request.with_pubkey.is_some(); - let activation_policy = if let Some(pubkey) = activation_request.with_pubkey { - TendermintActivationPolicy::with_public_key(pubkey) + let (activation_policy, wallet_connection_type) = if let Some(params) = activation_request.activation_params { + match params { + TendermintPubkeyActivationParams::WithPubkey { + pubkey, + is_ledger_connection, + } => { + let wallet_connection_type = if is_ledger_connection { + TendermintWalletConnectionType::KeplrLedger + } else { + TendermintWalletConnectionType::Keplr + }; + + ( + TendermintActivationPolicy::with_public_key(pubkey), + wallet_connection_type, + ) + }, + TendermintPubkeyActivationParams::WalletConnect { session_topic } => { + activate_with_walletconnect(&ctx, session_topic, protocol_conf.chain_id.as_ref(), &ticker).await? + }, + } } else { let private_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(&ctx).mm_err(|e| TendermintInitError { @@ -260,22 +313,23 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin { let tendermint_private_key_policy = tendermint_priv_key_policy(&conf, &ticker, private_key_policy, activation_request.path_to_address)?; - TendermintActivationPolicy::with_private_key_policy(tendermint_private_key_policy) + ( + TendermintActivationPolicy::with_private_key_policy(tendermint_private_key_policy), + TendermintWalletConnectionType::Native, + ) }; - let coin = TendermintCoin::init( + TendermintCoin::init( &ctx, - ticker.clone(), + ticker, conf, protocol_conf, activation_request.nodes, activation_request.tx_history, activation_policy, - is_keplr_from_ledger, + wallet_connection_type, ) - .await?; - - Ok(coin) + .await } async fn enable_global_nft( diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index d515ee5f18..39590862d8 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -14,69 +14,69 @@ for-tests = [] track-ctx-pointer = ["shared_ref_counter/enable", "shared_ref_counter/log"] [dependencies] -arrayref = "0.3" -async-trait = "0.1" -backtrace = "0.3" -bytes = "1.1" -cfg-if = "1.0" -compatible-time = { version = "1.1.0", package = "web-time" } -crossbeam = "0.8" -env_logger = "0.9.3" -derive_more = "0.99" -fnv = "1.0.6" -futures01 = { version = "0.1", package = "futures" } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -futures-timer = "3.0" -gstuff = "0.7" -hex = "0.4.2" -http = "0.2" -http-body = "0.1" -itertools = "0.10" -lazy_static = "1.4" -log = "0.4.17" -parking_lot = { version = "0.12.0", features = ["nightly"] } -parking_lot_core = { version = "0.6", features = ["nightly"] } -paste = "1.0" -primitive-types = "0.11.1" -rand = { version = "0.7", features = ["std", "small_rng"] } -rustc-hash = "2.0" -regex = "1" -serde = "1" -serde_derive = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +arrayref.workspace = true +async-trait.workspace = true +backtrace.workspace = true +bytes.workspace = true +cfg-if.workspace = true +compatible-time.workspace = true +crossbeam.workspace = true +env_logger.workspace = true +derive_more.workspace = true +fnv.workspace = true +futures01.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +futures-timer.workspace = true +gstuff.workspace = true +hex.workspace = true +http.workspace = true +http-body.workspace = true +itertools.workspace = true +lazy_static.workspace = true +log.workspace = true +parking_lot = { workspace = true, features = ["nightly"] } +parking_lot_core.workspace = true +paste.workspace = true +primitive-types.workspace = true +rand = { workspace = true, features = ["std", "small_rng"] } +rustc-hash.workspace = true +regex.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -sha2 = "0.10" +sha2.workspace = true shared_ref_counter = { path = "shared_ref_counter", optional = true } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +uuid.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -chrono = { version = "0.4", features = ["wasmbind"] } -js-sys = "0.3.27" -serde_repr = "0.1.6" -serde-wasm-bindgen = "0.4.3" -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = "0.4.21" -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"] } +chrono = { workspace = true, features = ["wasmbind"] } +js-sys.workspace = true +serde_repr.workspace = true +serde-wasm-bindgen.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -anyhow = "1.0" -chrono = "0.4" -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } +anyhow.workspace = true +chrono.workspace = true +hyper = { workspace = true, features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features -hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -libc = { version = "0.2" } -lightning = "0.0.113" -tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } +hyper-rustls = { workspace = true, default-features = false, features = ["http1", "http2", "webpki-tokio"] } +libc.workspace = true +lightning.workspace = true +tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "net"] } [target.'cfg(windows)'.dependencies] -winapi = "0.3" +winapi.workspace = true [target.'cfg(not(windows))'.dependencies] -findshlibs = "0.5" +findshlibs.workspace = true [build-dependencies] -cc = "1.0" -gstuff = "0.7" +cc.workspace = true +gstuff.workspace = true diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 078f601f66..b2e00d5e57 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -158,6 +158,7 @@ use http::header::CONTENT_TYPE; use http::Response; use parking_lot::{Mutex as PaMutex, MutexGuard as PaMutexGuard}; pub use paste::paste; +use primitive_types::U256; use rand::RngCore; use rand::{rngs::SmallRng, SeedableRng}; use serde::{de, ser}; @@ -1187,6 +1188,10 @@ pub fn http_uri_to_ws_address(uri: http::Uri) -> String { format!("{}{}{}{}", address_prefix, host_address, port, path) } +/// Converts a U256 value to a lowercase hexadecimal string with "0x" prefix +#[inline] +pub fn u256_to_hex(value: U256) -> String { format!("0x{:x}", value) } + /// If 0x prefix exists in an str strip it or return the str as-is #[macro_export] macro_rules! str_strip_0x { diff --git a/mm2src/common/shared_ref_counter/Cargo.toml b/mm2src/common/shared_ref_counter/Cargo.toml index 36fe1e2807..ee510ed9fc 100644 --- a/mm2src/common/shared_ref_counter/Cargo.toml +++ b/mm2src/common/shared_ref_counter/Cargo.toml @@ -10,4 +10,4 @@ doctest = false enable = [] [dependencies] -log = { version = "0.4.17", optional = true } +log = { workspace = true, optional = true } diff --git a/mm2src/crypto/Cargo.toml b/mm2src/crypto/Cargo.toml index dd3bbec752..52c36a6cdb 100644 --- a/mm2src/crypto/Cargo.toml +++ b/mm2src/crypto/Cargo.toml @@ -7,56 +7,57 @@ edition = "2018" doctest = false [dependencies] -aes = "0.8.3" -argon2 = { version = "0.5.2", features = ["zeroize"] } -arrayref = "0.3" -async-trait = "0.1" -base64 = "0.21.2" -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -bip39 = { version = "2.0.0", features = ["rand_core", "zeroize"], default-features = false } +aes.workspace = true +argon2.workspace = true +arrayref.workspace = true +async-trait.workspace = true +base64.workspace = true +bip32.workspace = true +bip39.workspace = true bitcrypto = { path = "../mm2_bitcoin/crypto" } -bs58 = "0.4.0" -cbc = "0.1.2" -cipher = "0.4.4" +bs58.workspace = true +cbc.workspace = true +cipher.workspace = true common = { path = "../common" } -derive_more = "0.99" +derive_more.workspace = true enum_derives = { path = "../derives/enum_derives" } -enum-primitive-derive = "0.2" -futures = "0.3" -hex = "0.4.2" -hmac = "0.12.1" -http = "0.2" +enum-primitive-derive.workspace = true +futures.workspace = true +hex.workspace = true +hmac.workspace = true +http.workspace = true hw_common = { path = "../hw_common" } keys = { path = "../mm2_bitcoin/keys" } -lazy_static = "1.4" +lazy_static.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } -num-traits = "0.2" -parking_lot = { version = "0.12.0", features = ["nightly"] } +num-traits.workspace = true +parking_lot = { workspace = true, features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } -rustc-hex = "2" -secp256k1 = "0.20" +rustc-hex.workspace = true +secp256k1.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -sha2 = "0.10" +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +sha2.workspace = true trezor = { path = "../trezor" } -zeroize = { version = "1.5", features = ["zeroize_derive"] } +zeroize.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -cfg-if = "1.0" +cfg-if.workspace = true mm2_eth = { path = "../mm2_eth" } mm2_metamask = { path = "../mm2_metamask" } -wasm-bindgen-test = { version = "0.3.2" } -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false } +wasm-bindgen-test.workspace = true +web3 = { workspace = true, default-features = false } + [dev-dependencies] -cfg-if = "1.0" -tokio = { version = "1.20", default-features = false } +cfg-if.workspace = true +tokio.workspace = true [features] trezor-udp = ["trezor/trezor-udp"] diff --git a/mm2src/crypto/src/bip32_child.rs b/mm2src/crypto/src/bip32_child.rs index 17b5064f97..a187398563 100644 --- a/mm2src/crypto/src/bip32_child.rs +++ b/mm2src/crypto/src/bip32_child.rs @@ -104,8 +104,8 @@ impl Bip32ChildValue for AnyValue { #[derive(Clone, PartialEq)] pub struct Bip32Child { - value: Value, - child: Child, + pub(crate) value: Value, + pub(crate) child: Child, } impl fmt::Debug for Bip32Child { diff --git a/mm2src/crypto/src/standard_hd_path.rs b/mm2src/crypto/src/standard_hd_path.rs index 9e51dc5f9f..a0fbbd6f66 100644 --- a/mm2src/crypto/src/standard_hd_path.rs +++ b/mm2src/crypto/src/standard_hd_path.rs @@ -41,6 +41,23 @@ impl StandardHDPath { pub fn chain(&self) -> Bip44Chain { self.child().child().child().value() } pub fn address_id(&self) -> u32 { self.child().child().child().child().value() } + + /// Derive `HDPathToCoin` from `StandardHDPath` + pub fn path_to_coin(&self) -> HDPathToCoin { + let Bip32Child { + value: purpose, + child: rest, + } = self; + let Bip32Child { value: coin_type, .. } = rest; + + Bip32Child { + value: purpose.clone(), + child: Bip32Child { + value: coin_type.clone(), + child: Bip44Tail, + }, + } + } } impl HDPathToCoin { @@ -78,6 +95,10 @@ pub enum StandardHDPathError { }, #[display(fmt = "Unknown BIP32 error: {}", _0)] Bip32Error(Bip32Error), + #[display(fmt = "Invalid coin type '{}', expected '{}'", found, expected)] + InvalidCoinType { expected: u32, found: u32 }, + #[display(fmt = "Invalid path to coin '{}', expected '{}'", found, expected)] + InvalidPathToCoin { expected: String, found: String }, } impl From for StandardHDPathError { diff --git a/mm2src/db_common/Cargo.toml b/mm2src/db_common/Cargo.toml index e161be857e..b21615141b 100644 --- a/mm2src/db_common/Cargo.toml +++ b/mm2src/db_common/Cargo.toml @@ -8,13 +8,13 @@ doctest = false [dependencies] common = { path = "../common" } -hex = "0.4.2" -log = "0.4.17" -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +hex.workspace = true +log.workspace = true +uuid.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -crossbeam-channel = "0.5.1" -futures = "0.3.1" -rusqlite = { version = "0.28", features = ["bundled"] } -sql-builder = "3.1.1" -tokio = { version = "1.20", default-features = false, features = ["macros"] } +crossbeam-channel.workspace = true +futures.workspace = true +rusqlite.workspace = true +sql-builder.workspace = true +tokio = { workspace = true, default-features = false, features = ["macros"] } diff --git a/mm2src/db_common/src/async_sql_conn.rs b/mm2src/db_common/src/async_sql_conn.rs index 78357405a1..f9e8747fd7 100644 --- a/mm2src/db_common/src/async_sql_conn.rs +++ b/mm2src/db_common/src/async_sql_conn.rs @@ -43,6 +43,10 @@ impl std::error::Error for AsyncConnError { } } +impl From for AsyncConnError { + fn from(err: String) -> Self { Self::Internal(InternalError(err)) } +} + #[derive(Debug)] pub struct InternalError(pub String); diff --git a/mm2src/derives/enum_derives/Cargo.toml b/mm2src/derives/enum_derives/Cargo.toml index 9518bc6d1a..d4f378c6f2 100644 --- a/mm2src/derives/enum_derives/Cargo.toml +++ b/mm2src/derives/enum_derives/Cargo.toml @@ -8,7 +8,7 @@ proc-macro = true doctest = false [dependencies] -syn = { version = "1.0", features = ["full"] } -quote = "1.0" -proc-macro2 = "1.0" -itertools = "0.10" +syn = { workspace = true, features = ["full"] } +quote.workspace = true +proc-macro2.workspace = true +itertools.workspace = true diff --git a/mm2src/derives/ser_error/Cargo.toml b/mm2src/derives/ser_error/Cargo.toml index ace2ba57fd..b9c5197109 100644 --- a/mm2src/derives/ser_error/Cargo.toml +++ b/mm2src/derives/ser_error/Cargo.toml @@ -8,4 +8,4 @@ edition = "2018" doctest = false [dependencies] -serde = "1.0" +serde.workspace = true diff --git a/mm2src/derives/ser_error_derive/Cargo.toml b/mm2src/derives/ser_error_derive/Cargo.toml index 640becbeed..5461242ce0 100644 --- a/mm2src/derives/ser_error_derive/Cargo.toml +++ b/mm2src/derives/ser_error_derive/Cargo.toml @@ -9,7 +9,7 @@ proc-macro = true doctest = false [dependencies] -proc-macro2 = "1.0" -quote = "1.0" +proc-macro2.workspace = true +quote.workspace = true ser_error = { path = "../ser_error" } -syn = { version = "1.0", features = ["full"] } +syn = { workspace = true, features = ["full"] } diff --git a/mm2src/hw_common/Cargo.toml b/mm2src/hw_common/Cargo.toml index 7d2b43ac12..f3390fb087 100644 --- a/mm2src/hw_common/Cargo.toml +++ b/mm2src/hw_common/Cargo.toml @@ -7,22 +7,22 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } +async-trait.workspace = true +bip32.workspace = true common = { path = "../common" } mm2_err_handle = { path = "../mm2_err_handle" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } -secp256k1 = { version = "0.20", features = ["rand"] } -serde = "1.0" -serde_derive = "1.0" +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } +secp256k1 = { workspace = true, features = ["rand"] } +serde.workspace = true +serde_derive.workspace = true [target.'cfg(all(not(target_arch = "wasm32"), not(target_os = "ios")))'.dependencies] -rusb = { version = "0.7.0", features = ["vendored"] } +rusb.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.55", features = ["console", "Navigator", "Usb", "UsbDevice", "UsbDeviceRequestOptions", "UsbInTransferResult"] } +js-sys.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console", "Navigator", "Usb", "UsbDevice", "UsbDeviceRequestOptions", "UsbInTransferResult"] } diff --git a/mm2src/kdf_walletconnect/Cargo.toml b/mm2src/kdf_walletconnect/Cargo.toml new file mode 100644 index 0000000000..88978fd0d5 --- /dev/null +++ b/mm2src/kdf_walletconnect/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "kdf_walletconnect" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +base64.workspace = true +chrono = { workspace = true, "features" = ["serde"] } +common = { path = "../common" } +hex.workspace = true +cfg-if.workspace = true +db_common = { path = "../db_common" } +derive_more.workspace = true +enum_derives = { path = "../derives/enum_derives" } +futures = { workspace = true, features = ["compat", "async-await"] } +hkdf.workspace = true +mm2_db = { path = "../mm2_db" } +mm2_core = { path = "../mm2_core" } +mm2_err_handle = { path = "../mm2_err_handle" } +parking_lot = { workspace = true, features = ["nightly"] } +pairing_api.workspace = true +rand = "0.8" +relay_client.workspace = true +relay_rpc.workspace = true +secp256k1.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +sha2.workspace = true +timed-map = { workspace = true, features = ["rustc-hash"] } +thiserror.workspace = true +tokio.workspace = true +x25519-dalek.workspace = true +wc_common.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys.workspace = true +mm2_db = { path = "../mm2_db" } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } +wasm-bindgen.workspace = true +wasm-bindgen-test.workspace = true +wasm-bindgen-futures.workspace = true +web-sys = { workspace = true, features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", + "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", + "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", + "IdbVersionChangeEvent", "MessageEvent", "MessagePort", "ReadableStreamDefaultReader", "ReadableStream"]} + +[dev-dependencies] +mm2_test_helpers = { path = "../mm2_test_helpers" } diff --git a/mm2src/kdf_walletconnect/src/chain.rs b/mm2src/kdf_walletconnect/src/chain.rs new file mode 100644 index 0000000000..20e1acd6a8 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/chain.rs @@ -0,0 +1,107 @@ +use mm2_err_handle::prelude::{MmError, MmResult}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +use crate::error::WalletConnectError; + +pub(crate) const SUPPORTED_PROTOCOL: &str = "irn"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum WcChain { + Eip155, + Cosmos, +} + +impl FromStr for WcChain { + type Err = MmError; + fn from_str(s: &str) -> Result { + match s { + "eip155" => Ok(WcChain::Eip155), + "cosmos" => Ok(WcChain::Cosmos), + _ => MmError::err(WalletConnectError::InvalidChainId(format!( + "chain_id not supported: {s}" + ))), + } + } +} + +impl AsRef for WcChain { + fn as_ref(&self) -> &str { + match self { + Self::Eip155 => "eip155", + Self::Cosmos => "cosmos", + } + } +} + +impl WcChain { + pub(crate) fn derive_chain_id(&self, id: String) -> WcChainId { + WcChainId { + chain: self.clone(), + id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WcChainId { + pub chain: WcChain, + pub id: String, +} + +impl std::fmt::Display for WcChainId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.chain.as_ref(), self.id) + } +} + +impl WcChainId { + pub fn new_eip155(id: String) -> Self { + Self { + chain: WcChain::Eip155, + id, + } + } + + pub fn new_cosmos(id: String) -> Self { + Self { + chain: WcChain::Cosmos, + id, + } + } + + pub fn try_from_str(chain_id: &str) -> MmResult { + let sp = chain_id.split(':').collect::>(); + if sp.len() != 2 { + return MmError::err(WalletConnectError::InvalidChainId(chain_id.to_string())); + }; + + Ok(Self { + chain: WcChain::from_str(sp[0])?, + id: sp[1].to_owned(), + }) + } +} + +#[derive(Debug, Clone)] +pub enum WcRequestMethods { + CosmosSignDirect, + CosmosSignAmino, + CosmosGetAccounts, + EthSignTransaction, + EthSendTransaction, + PersonalSign, +} + +impl AsRef for WcRequestMethods { + fn as_ref(&self) -> &str { + match self { + Self::CosmosSignDirect => "cosmos_signDirect", + Self::CosmosSignAmino => "cosmos_signAmino", + Self::CosmosGetAccounts => "cosmos_getAccounts", + Self::EthSignTransaction => "eth_signTransaction", + Self::EthSendTransaction => "eth_sendTransaction", + Self::PersonalSign => "personal_sign", + } + } +} diff --git a/mm2src/kdf_walletconnect/src/connection_handler.rs b/mm2src/kdf_walletconnect/src/connection_handler.rs new file mode 100644 index 0000000000..3bd57b6fdc --- /dev/null +++ b/mm2src/kdf_walletconnect/src/connection_handler.rs @@ -0,0 +1,65 @@ +use common::log::{debug, error}; +use futures::channel::mpsc::UnboundedSender; +use relay_client::error::ClientError; +use relay_client::websocket::{CloseFrame, ConnectionHandler, PublishedMessage}; + +pub(crate) const MAX_BACKOFF: u64 = 60; + +pub struct Handler { + name: &'static str, + msg_sender: UnboundedSender, + conn_live_sender: UnboundedSender>, +} + +impl Handler { + pub fn new( + name: &'static str, + msg_sender: UnboundedSender, + conn_live_sender: UnboundedSender>, + ) -> Self { + Self { + name, + msg_sender, + conn_live_sender, + } + } +} + +impl ConnectionHandler for Handler { + fn connected(&mut self) { + debug!("[{}] connection to WalletConnect relay server successful", self.name); + } + + fn disconnected(&mut self, frame: Option>) { + debug!("[{}] connection closed: frame={frame:?}", self.name); + + if let Err(e) = self.conn_live_sender.unbounded_send(frame.map(|f| f.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn message_received(&mut self, message: PublishedMessage) { + debug!( + "[{}] inbound message: message_id={} topic={} tag={} message={}", + self.name, message.message_id, message.topic, message.tag, message.message, + ); + + if let Err(e) = self.msg_sender.unbounded_send(message) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn inbound_error(&mut self, error: ClientError) { + debug!("[{}] inbound error: {error}", self.name); + if let Err(e) = self.conn_live_sender.unbounded_send(Some(error.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } + + fn outbound_error(&mut self, error: ClientError) { + debug!("[{}] outbound error: {error}", self.name); + if let Err(e) = self.conn_live_sender.unbounded_send(Some(error.to_string())) { + error!("[{}] failed to send to the receiver: {e}", self.name); + } + } +} diff --git a/mm2src/kdf_walletconnect/src/error.rs b/mm2src/kdf_walletconnect/src/error.rs new file mode 100644 index 0000000000..1d8b6bcf93 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/error.rs @@ -0,0 +1,186 @@ +use enum_derives::EnumFromStringify; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::cursor_prelude::*; +#[cfg(target_arch = "wasm32")] +use mm2_db::indexed_db::{DbTransactionError, InitDbError}; +use pairing_api::PairingClientError; +use relay_client::error::{ClientError, Error}; +use relay_rpc::rpc::{PublishError, SubscriptionError}; +use serde::{Deserialize, Serialize}; + +// Error codes for various cases +pub(crate) const INVALID_METHOD: i32 = 1001; +pub(crate) const INVALID_EVENT: i32 = 1002; +pub(crate) const INVALID_UPDATE_REQUEST: i32 = 1003; +pub(crate) const INVALID_EXTEND_REQUEST: i32 = 1004; +pub(crate) const INVALID_SESSION_SETTLE_REQUEST: i32 = 1005; + +// Unauthorized error codes +pub(crate) const UNAUTHORIZED_METHOD: i32 = 3001; +pub(crate) const UNAUTHORIZED_EVENT: i32 = 3002; +pub(crate) const UNAUTHORIZED_UPDATE_REQUEST: i32 = 3003; +pub(crate) const UNAUTHORIZED_EXTEND_REQUEST: i32 = 3004; +pub(crate) const UNAUTHORIZED_CHAIN: i32 = 3005; + +// EIP-1193 error code +pub(crate) const USER_REJECTED_REQUEST: i32 = 4001; + +// Rejected (CAIP-25) error codes +pub(crate) const USER_REJECTED: i32 = 5000; +pub(crate) const USER_REJECTED_CHAINS: i32 = 5001; +pub(crate) const USER_REJECTED_METHODS: i32 = 5002; +pub(crate) const USER_REJECTED_EVENTS: i32 = 5003; + +// Unsupported error codes +pub(crate) const UNSUPPORTED_CHAINS: i32 = 5100; +pub(crate) const UNSUPPORTED_METHODS: i32 = 5101; +pub(crate) const UNSUPPORTED_EVENTS: i32 = 5102; +pub(crate) const UNSUPPORTED_ACCOUNTS: i32 = 5103; +pub(crate) const UNSUPPORTED_NAMESPACE_KEY: i32 = 5104; + +pub(crate) const USER_REQUESTED: i64 = 6000; + +#[derive(Debug, Serialize, Deserialize, EnumFromStringify, thiserror::Error)] +pub enum WalletConnectError { + #[error("Pairing Error: {0}")] + #[from_stringify("PairingClientError")] + PairingError(String), + #[error("Publish Error: {0}")] + PublishError(String), + #[error("Client Error: {0}")] + #[from_stringify("ClientError")] + ClientError(String), + #[error("Subscription Error: {0}")] + SubscriptionError(String), + #[error("Internal Error: {0}")] + InternalError(String), + #[error("Serde Error: {0}")] + #[from_stringify("serde_json::Error")] + SerdeError(String), + #[error("UnSuccessfulResponse Error: {0}")] + UnSuccessfulResponse(String), + #[error("Session Error: {0}")] + #[from_stringify("SessionError")] + SessionError(String), + #[error("Unknown params")] + InvalidRequest, + #[error("Request is not yet implemented")] + NotImplemented, + #[error("Hex Error: {0}")] + #[from_stringify("hex::FromHexError")] + HexError(String), + #[error("Payload Error: {0}")] + #[from_stringify("wc_common::PayloadError")] + PayloadError(String), + #[error("Account not found for chain_id: {0}")] + NoAccountFound(String), + #[error("Account not found for index: {0}")] + NoAccountFoundForIndex(usize), + #[error("Empty account approved for chain_id: {0}")] + EmptyAccount(String), + #[error("WalletConnect is not initaliazed yet!")] + NotInitialized, + #[error("Storage Error: {0}")] + StorageError(String), + #[error("ChainId mismatch")] + ChainIdMismatch, + #[error("No feedback from wallet")] + NoWalletFeedback, + #[error("Invalid ChainId Error: {0}")] + InvalidChainId(String), + #[error("ChainId not supported: {0}")] + ChainIdNotSupported(String), + #[error("Request timeout error")] + TimeoutError, +} + +impl From> for WalletConnectError { + fn from(error: Error) -> Self { WalletConnectError::PublishError(format!("{error:?}")) } +} + +impl From> for WalletConnectError { + fn from(error: Error) -> Self { WalletConnectError::SubscriptionError(format!("{error:?}")) } +} + +/// Session key and topic derivation errors. +#[derive(Debug, Clone, thiserror::Error)] +pub enum SessionError { + #[error("Failed to generate symmetric session key: {0}")] + SymKeyGeneration(String), +} + +#[cfg(target_arch = "wasm32")] +#[derive(Debug, Clone, thiserror::Error)] +pub enum WcIndexedDbError { + #[error("Internal Error: {0}")] + InternalError(String), + #[error("Not supported: {0}")] + NotSupported(String), + #[error("Delete Error: {0}")] + DeletionError(String), + #[error("Insert Error: {0}")] + AddToStorageErr(String), + #[error("GetFromStorage Error: {0}")] + GetFromStorageError(String), + #[error("Decoding Error: {0}")] + DecodingError(String), +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(e: InitDbError) -> Self { + match &e { + InitDbError::NotSupported(_) => WcIndexedDbError::NotSupported(e.to_string()), + InitDbError::EmptyTableList + | InitDbError::DbIsOpenAlready { .. } + | InitDbError::InvalidVersion(_) + | InitDbError::OpeningError(_) + | InitDbError::TypeMismatch { .. } + | InitDbError::UnexpectedState(_) + | InitDbError::UpgradingError { .. } => WcIndexedDbError::InternalError(e.to_string()), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(e: DbTransactionError) -> Self { + match e { + DbTransactionError::ErrorSerializingItem(_) | DbTransactionError::ErrorDeserializingItem(_) => { + WcIndexedDbError::DecodingError(e.to_string()) + }, + DbTransactionError::ErrorUploadingItem(_) => WcIndexedDbError::AddToStorageErr(e.to_string()), + DbTransactionError::ErrorGettingItems(_) | DbTransactionError::ErrorCountingItems(_) => { + WcIndexedDbError::GetFromStorageError(e.to_string()) + }, + DbTransactionError::ErrorDeletingItems(_) => WcIndexedDbError::DeletionError(e.to_string()), + DbTransactionError::NoSuchTable { .. } + | DbTransactionError::ErrorCreatingTransaction(_) + | DbTransactionError::ErrorOpeningTable { .. } + | DbTransactionError::ErrorSerializingIndex { .. } + | DbTransactionError::UnexpectedState(_) + | DbTransactionError::TransactionAborted + | DbTransactionError::MultipleItemsByUniqueIndex { .. } + | DbTransactionError::NoSuchIndex { .. } + | DbTransactionError::InvalidIndex { .. } => WcIndexedDbError::InternalError(e.to_string()), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for WcIndexedDbError { + fn from(value: CursorError) -> Self { + match value { + CursorError::ErrorSerializingIndexFieldValue { .. } + | CursorError::ErrorDeserializingIndexValue { .. } + | CursorError::ErrorDeserializingItem(_) => Self::DecodingError(value.to_string()), + CursorError::ErrorOpeningCursor { .. } + | CursorError::AdvanceError { .. } + | CursorError::InvalidKeyRange { .. } + | CursorError::IncorrectNumberOfKeysPerIndex { .. } + | CursorError::UnexpectedState(_) + | CursorError::IncorrectUsage { .. } + | CursorError::TypeMismatch { .. } => Self::InternalError(value.to_string()), + } + } +} diff --git a/mm2src/kdf_walletconnect/src/inbound_message.rs b/mm2src/kdf_walletconnect/src/inbound_message.rs new file mode 100644 index 0000000000..f2ac60036a --- /dev/null +++ b/mm2src/kdf_walletconnect/src/inbound_message.rs @@ -0,0 +1,98 @@ +use crate::{error::WalletConnectError, + pairing::{reply_pairing_delete_response, reply_pairing_extend_response, reply_pairing_ping_response}, + session::rpc::{delete::reply_session_delete_request, + event::handle_session_event, + extend::reply_session_extend_request, + ping::reply_session_ping_request, + propose::{process_session_propose_response, reply_session_proposal_request}, + settle::reply_session_settle_request, + update::reply_session_update_request}, + WalletConnectCtxImpl}; + +use common::log::{info, LogOnError}; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::{params::ResponseParamsSuccess, Params, Request, Response}; + +pub(crate) type SessionMessageType = MmResult; + +#[derive(Debug)] +pub struct SessionMessage { + pub message_id: MessageId, + pub topic: Topic, + pub data: ResponseParamsSuccess, +} + +/// Processes an inbound WalletConnect request and performs the appropriate action based on the request type. +/// +/// Handles various session and pairing requests, routing them to their corresponding handlers. +pub(crate) async fn process_inbound_request( + ctx: &WalletConnectCtxImpl, + request: Request, + topic: &Topic, +) -> MmResult<(), WalletConnectError> { + let message_id = request.id; + match request.params { + Params::SessionPropose(proposal) => reply_session_proposal_request(ctx, proposal, topic, &message_id).await?, + Params::SessionExtend(param) => reply_session_extend_request(ctx, topic, &message_id, param).await?, + Params::SessionDelete(param) => reply_session_delete_request(ctx, topic, &message_id, param).await?, + Params::SessionPing(()) => reply_session_ping_request(ctx, topic, &message_id).await?, + Params::SessionSettle(param) => reply_session_settle_request(ctx, topic, param).await?, + Params::SessionUpdate(param) => reply_session_update_request(ctx, topic, &message_id, param).await?, + Params::SessionEvent(param) => handle_session_event(ctx, topic, &message_id, param).await?, + Params::SessionRequest(_param) => { + // TODO: Implement when integrating KDF as a Dapp. + return MmError::err(WalletConnectError::NotImplemented); + }, + + Params::PairingPing(_param) => reply_pairing_ping_response(ctx, topic, &message_id).await?, + Params::PairingDelete(param) => reply_pairing_delete_response(ctx, topic, &message_id, param).await?, + Params::PairingExtend(param) => reply_pairing_extend_response(ctx, topic, &message_id, param).await?, + _ => { + info!("Unknown request params received."); + return MmError::err(WalletConnectError::InvalidRequest); + }, + }; + + Ok(()) +} + +/// Processes an inbound WalletConnect response and sends the result to the provided message channel. +/// +/// Handles successful responses, errors, and specific session proposal processing. +pub(crate) async fn process_inbound_response(ctx: &WalletConnectCtxImpl, response: Response, topic: &Topic) { + let message_id = response.id(); + let result = match &response { + Response::Success(value) => match serde_json::from_value::(value.result.clone()) { + Ok(ResponseParamsSuccess::SessionPropose(propose)) => { + // If this is a session propose response, process it right away and return. + // Session proposal responses are not waited for since it might take a long time + // for the proposal to be accepted (user interaction). So they are handled in async fashion. + ctx.pending_requests + .lock() + .expect("pending request lock shouldn't fail!") + .remove(&message_id); + return process_session_propose_response(ctx, topic, &propose) + .await + .error_log_with_msg("Failed to process session propose response"); + }, + Ok(data) => Ok(SessionMessage { + message_id, + topic: topic.clone(), + data, + }), + Err(err) => MmError::err(WalletConnectError::SerdeError(err.to_string())), + }, + Response::Error(err) => MmError::err(WalletConnectError::UnSuccessfulResponse(format!("{err:?}"))), + }; + + let mut pending_requests = ctx + .pending_requests + .lock() + .expect("pending request lock shouldn't fail!"); + if let Some(tx) = pending_requests.remove(&message_id) { + tx.send(result).ok(); + } else { + common::log::error!("[{topic}] unrecognized inbound response/message: {response:?}"); + }; +} diff --git a/mm2src/kdf_walletconnect/src/lib.rs b/mm2src/kdf_walletconnect/src/lib.rs new file mode 100644 index 0000000000..8acf4895a2 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/lib.rs @@ -0,0 +1,678 @@ +pub mod chain; +mod connection_handler; +#[allow(unused)] pub mod error; +pub mod inbound_message; +mod metadata; +#[allow(unused)] mod pairing; +pub mod session; +mod storage; + +use crate::connection_handler::{Handler, MAX_BACKOFF}; +use crate::session::rpc::propose::send_proposal_request; +use chain::{WcChainId, WcRequestMethods, SUPPORTED_PROTOCOL}; +use common::custom_futures::timeout::FutureTimerExt; +use common::executor::abortable_queue::AbortableQueue; +use common::executor::{AbortableSystem, SpawnFuture, Timer}; +use common::log::{debug, error, info, LogOnError}; +use error::WalletConnectError; +use futures::channel::mpsc::{unbounded, UnboundedReceiver}; +use futures::StreamExt; +use inbound_message::{process_inbound_request, process_inbound_response, SessionMessageType}; +use metadata::{generate_metadata, AUTH_TOKEN_DURATION, AUTH_TOKEN_SUB, PROJECT_ID, RELAY_ADDRESS}; +use mm2_core::mm_ctx::{from_ctx, MmArc}; +use mm2_err_handle::prelude::*; +use pairing_api::PairingClient; +use relay_client::websocket::{connection_event_loop as client_event_loop, Client, PublishedMessage}; +use relay_client::{ConnectionOptions, MessageIdGenerator}; +use relay_rpc::auth::{ed25519_dalek::SigningKey, AuthToken}; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::session::{Namespace, ProposeNamespaces}; +use relay_rpc::rpc::params::session_request::SessionRequestRequest; +use relay_rpc::rpc::params::{session_request::Request as SessionRequest, IrnMetadata, Metadata, Relay, + RelayProtocolMetadata, RequestParams, ResponseParamsError, ResponseParamsSuccess}; +use relay_rpc::rpc::{ErrorResponse, Payload, Request, Response, SuccessfulResponse}; +use serde::de::DeserializeOwned; +use session::rpc::delete::send_session_delete_request; +use session::{key::SymKeyPair, SessionManager}; +use session::{EncodingAlgo, Session, SessionProperties, FIVE_MINUTES}; +use std::collections::BTreeSet; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use storage::SessionStorageDb; +use storage::WalletConnectStorageOps; +use timed_map::TimedMap; +use tokio::sync::{oneshot, watch}; +use wc_common::{decode_and_decrypt_type0, encrypt_and_encode, EnvelopeType, SymKey}; + +const PUBLISH_TIMEOUT_SECS: f64 = 6.; +const CONNECTION_TIMEOUT_S: f64 = 30.; + +/// Broadcast by the lifecycle task so every RPC can cheaply await connectivity. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConnectionState { + Connecting, + Connected, + Disconnected, +} + +#[async_trait::async_trait] +pub trait WalletConnectOps { + type Error; + type Params<'a>; + type SignTxData; + type SendTxData; + + /// Unique chain_id associated with an activated/supported coin. + async fn wc_chain_id(&self, ctx: &WalletConnectCtx) -> Result; + + /// Send sign transaction request to WalletConnect Wallet. + async fn wc_sign_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result; + + /// Send sign and send/broadcast transaction request to WalletConnect Wallet. + async fn wc_send_tx<'a>( + &self, + wc: &WalletConnectCtx, + params: Self::Params<'a>, + ) -> Result; + + /// Session topic used to activate this. + fn session_topic(&self) -> Result<&str, Self::Error>; +} + +/// Implements the WalletConnect context, providing functionality for +/// establishing and managing wallet connections. +/// This struct contains the necessary state and methods to handle +/// wallet connection sessions, signing requests, and connection events. +pub struct WalletConnectCtxImpl { + pub(crate) client: Client, + pub(crate) pairing: PairingClient, + pub(crate) key_pair: SymKeyPair, + pub session_manager: SessionManager, + relay: Relay, + metadata: Metadata, + message_id_generator: MessageIdGenerator, + pending_requests: Mutex>>, + abortable_system: AbortableQueue, + connection_state_rx: watch::Receiver, +} + +/// A newtype wrapper around a thread-safe reference to `WalletConnectCtxImpl`. +/// Provides shared access to wallet connection functionality through an Arc pointer. +pub struct WalletConnectCtx(pub Arc); +impl Deref for WalletConnectCtx { + type Target = WalletConnectCtxImpl; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl WalletConnectCtx { + /// Attempt to initialize a new WalletConnect context. + pub fn try_init(ctx: &MmArc) -> MmResult { + let abortable_system = ctx + .abortable_system + .create_subsystem::() + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + let storage = SessionStorageDb::new(ctx)?; + let pairing = PairingClient::new(); + let relay = Relay { + protocol: SUPPORTED_PROTOCOL.to_string(), + data: None, + }; + let (inbound_message_tx, inbound_message_rx) = unbounded(); + let (conn_live_sender, conn_live_receiver) = unbounded(); + let (connection_state_tx, connection_state_rx) = watch::channel(ConnectionState::Disconnected); + let (client, _) = Client::new_with_callback( + Handler::new("KDF", inbound_message_tx, conn_live_sender), + |receiver, handler| { + abortable_system + .weak_spawner() + .spawn(client_event_loop(receiver, handler)) + }, + ); + + let message_id_generator = MessageIdGenerator::new(); + let context = Arc::new(WalletConnectCtxImpl { + client, + pairing, + relay, + metadata: generate_metadata(), + key_pair: SymKeyPair::new(), + session_manager: SessionManager::new(storage), + pending_requests: Default::default(), + message_id_generator, + abortable_system, + connection_state_rx, + }); + + // Spawn the relayer connection lifecycle task. + context.abortable_system.weak_spawner().spawn( + context + .clone() + .connection_lifecycle_task(conn_live_receiver, connection_state_tx), + ); + + // spawn message handler event loop + context + .abortable_system + .weak_spawner() + .spawn(context.clone().spawn_published_message_fut(inbound_message_rx)); + + Ok(Self(context)) + } + + pub fn from_ctx(ctx: &MmArc) -> MmResult, WalletConnectError> { + from_ctx(&ctx.wallet_connect, move || { + Self::try_init(ctx).map_err(|err| err.to_string()) + }) + .map_to_mm(WalletConnectError::InternalError) + } +} + +impl WalletConnectCtxImpl { + /// Centralised task owning **all** connection logic (connect → monitor → reconnect). + async fn connection_lifecycle_task( + self: Arc, + mut conn_status_rx: UnboundedReceiver>, + connection_state_tx: watch::Sender, + ) { + if let Err(err) = self.session_manager.storage().init().await { + error!("Failed to initialize WalletConnect storage, shutting down: {err:?}"); + connection_state_tx.send(ConnectionState::Disconnected).error_log(); + self.abortable_system.abort_all().error_log(); + return; + } + + if let Err(err) = self.load_sessions_from_storage().await { + error!("Failed to load sessions from storage, shutting down: {err:?}"); + connection_state_tx.send(ConnectionState::Disconnected).error_log(); + self.abortable_system.abort_all().error_log(); + return; + } + + loop { + connection_state_tx.send(ConnectionState::Connecting).error_log(); + info!("WalletConnect: connecting…"); + + let mut backoff = 1; + while let Err(e) = self.connect_and_subscribe().await { + error!("Connection attempt failed: {e:?}; retrying in {backoff}s"); + Timer::sleep(backoff as f64).await; + backoff = std::cmp::min(backoff * 2, MAX_BACKOFF); + } + + connection_state_tx.send(ConnectionState::Connected).error_log(); + info!("WalletConnect: online."); + + if let Some(msg) = conn_status_rx.next().await { + info!("WalletConnect: disconnected with message: {msg:?}, will reconnect."); + connection_state_tx.send(ConnectionState::Disconnected).error_log(); + } else { + connection_state_tx.send(ConnectionState::Disconnected).error_log(); + self.abortable_system.abort_all().error_log(); + break; + } + } + } + + /// Waits until the current state is `Connected`. + async fn await_connection(&self) -> MmResult<(), WalletConnectError> { + let mut rx = self.connection_state_rx.clone(); + + let wait_for_connected = async move { + loop { + if *rx.borrow() == ConnectionState::Connected { + return Ok(()); + } + + if rx.changed().await.is_err() { + let last_state = *rx.borrow(); + return MmError::err(WalletConnectError::InternalError(format!( + "Connection task dropped, last state was: {:?}", + last_state + ))); + } + } + }; + + Box::pin(wait_for_connected) + .timeout_secs(CONNECTION_TIMEOUT_S) + .await + .map_to_mm(|_timeout_err| WalletConnectError::TimeoutError)? + } + + /// Attempt to connect to a wallet connection relay server. + pub async fn connect_client(&self) -> MmResult<(), WalletConnectError> { + let auth = { + let key = SigningKey::generate(&mut rand::thread_rng()); + AuthToken::new(AUTH_TOKEN_SUB) + .aud(RELAY_ADDRESS) + .ttl(AUTH_TOKEN_DURATION) + .as_jwt(&key) + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))? + }; + let opts = ConnectionOptions::new(PROJECT_ID, auth).with_address(RELAY_ADDRESS); + self.client.connect(&opts).await?; + + Ok(()) + } + + /// Connects to WalletConnect relayer and re-subscribes to previously active session topics if it's a reconnection. + pub(crate) async fn connect_and_subscribe(&self) -> MmResult<(), WalletConnectError> { + self.connect_client().await?; + let sessions = self + .session_manager + .get_sessions() + .flat_map(|s| vec![s.topic, s.pairing_topic]) + .collect::>(); + + if !sessions.is_empty() { + self.client.batch_subscribe(sessions).await?; + } + + Ok(()) + } + + /// Create a WalletConnect pairing connection url. + pub async fn new_connection( + &self, + required_namespaces: serde_json::Value, + optional_namespaces: Option, + ) -> MmResult { + self.await_connection().await?; + + let required_namespaces = serde_json::from_value(required_namespaces)?; + let optional_namespaces = match optional_namespaces { + Some(value) => serde_json::from_value(value)?, + None => ProposeNamespaces::default(), + }; + let (topic, url) = self.pairing.create(self.metadata.clone(), None)?; + + info!("[{topic}] Subscribing to topic"); + + self.client + .subscribe(topic.clone()) + .timeout_secs(PUBLISH_TIMEOUT_SECS) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|e| e)?; + + info!("[{topic}] Subscribed to topic"); + + send_proposal_request(self, &topic, required_namespaces, optional_namespaces).await?; + + Ok(url) + } + + /// Get symmetric key associated with a for `topic`. + fn sym_key(&self, topic: &Topic) -> MmResult { + self.session_manager + .sym_key(topic) + .or_else(|| self.pairing.sym_key(topic).ok()) + .ok_or_else(|| { + error!("Failed to find sym_key for topic: {topic}"); + MmError::new(WalletConnectError::InternalError(format!( + "topic sym_key not found: {topic}" + ))) + }) + } + + /// Handles an inbound published message by decrypting, decoding, and processing it. + async fn handle_published_message(&self, msg: PublishedMessage) -> MmResult<(), WalletConnectError> { + let message = { + let key = self.sym_key(&msg.topic)?; + decode_and_decrypt_type0(msg.message.as_bytes(), &key)? + }; + + info!("[{}] Inbound message payload={message}", msg.topic); + + match serde_json::from_str(&message)? { + Payload::Request(request) => process_inbound_request(self, request, &msg.topic).await?, + Payload::Response(response) => process_inbound_response(self, response, &msg.topic).await, + } + + debug!("[{}] Inbound message was handled successfully", msg.topic); + + Ok(()) + } + + /// Spawns a task that continuously processes published messages from inbound message channel. + async fn spawn_published_message_fut(self: Arc, mut recv: UnboundedReceiver) { + while let Some(msg) = recv.next().await { + self.handle_published_message(msg) + .await + .error_log_with_msg("Error processing message"); + } + } + + /// Loads sessions from storage, activates valid ones, and deletes expired. + async fn load_sessions_from_storage(&self) -> MmResult<(), WalletConnectError> { + info!("Loading WalletConnect session from storage"); + let now = chrono::Utc::now().timestamp() as u64; + let sessions = self + .session_manager + .storage() + .get_all_sessions() + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // bring most recent active session to the back. + for session in sessions.into_iter().rev() { + // delete expired session + if now > session.expiry { + debug!("Session {} expired, trying to delete from storage", session.topic); + self.session_manager + .storage() + .delete_session(&session.topic) + .await + .error_log_with_msg(&format!("[{}] Unable to delete session from storage", session.topic)); + continue; + }; + + debug!("[{}] Session found! activating", session.topic); + self.session_manager.add_session(session); + } + + info!("Loaded WalletConnect session from storage"); + + Ok(()) + } + + pub fn encode>(&self, session_topic: &str, data: T) -> String { + let session_topic = session_topic.into(); + let algo = self + .session_manager + .get_session(&session_topic) + .map(|session| session.encoding_algo.unwrap_or(EncodingAlgo::Hex)) + .unwrap_or(EncodingAlgo::Hex); + + algo.encode(data) + } + + /// Private function to publish a WC request. + pub(crate) async fn publish_request( + &self, + topic: &Topic, + param: RequestParams, + ) -> MmResult<(oneshot::Receiver, Duration), WalletConnectError> { + let irn_metadata = param.irn_metadata(); + let ttl = irn_metadata.ttl; + let message_id = self.message_id_generator.next(); + let request = Request::new(message_id, param.into()); + + self.publish_payload(topic, irn_metadata, Payload::Request(request)) + .await?; + + let (tx, rx) = oneshot::channel(); + // insert request to map with a reasonable expiration time of 5 minutes + self.pending_requests + .lock() + .unwrap() + .insert_expirable(message_id, tx, Duration::from_secs(FIVE_MINUTES)); + + Ok((rx, Duration::from_secs(ttl))) + } + + /// Private function to publish a success WC request response. + pub(crate) async fn publish_response_ok( + &self, + topic: &Topic, + result: ResponseParamsSuccess, + message_id: &MessageId, + ) -> MmResult<(), WalletConnectError> { + let irn_metadata = result.irn_metadata(); + let value = serde_json::to_value(result)?; + let response = Response::Success(SuccessfulResponse::new(*message_id, value)); + + self.publish_payload(topic, irn_metadata, Payload::Response(response)) + .await + } + + /// Private function to publish an error WC request response. + pub(crate) async fn publish_response_err( + &self, + topic: &Topic, + error_data: ResponseParamsError, + message_id: &MessageId, + ) -> MmResult<(), WalletConnectError> { + let error = error_data.error(); + let irn_metadata = error_data.irn_metadata(); + let response = Response::Error(ErrorResponse::new(*message_id, error)); + + self.publish_payload(topic, irn_metadata, Payload::Response(response)) + .await + } + + /// Private function to publish a WC payload. + pub(crate) async fn publish_payload( + &self, + topic: &Topic, + irn_metadata: IrnMetadata, + payload: Payload, + ) -> MmResult<(), WalletConnectError> { + self.await_connection().await?; + + info!("[{topic}] Publishing message={payload:?}"); + let message = { + let sym_key = self.sym_key(topic)?; + let payload = serde_json::to_string(&payload)?; + encrypt_and_encode(EnvelopeType::Type0, payload, &sym_key)? + }; + + self.client + .publish( + topic.clone(), + &*message, + None, + irn_metadata.tag, + Duration::from_secs(irn_metadata.ttl), + irn_metadata.prompt, + ) + .timeout_secs(PUBLISH_TIMEOUT_SECS) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|e| e)?; + + info!("[{topic}] Message published successfully"); + Ok(()) + } + + /// Checks if the current session is connected to a Ledger device. + /// NOTE: for COSMOS chains only. + pub fn is_ledger_connection(&self, session_topic: &str) -> bool { + let session_topic = session_topic.into(); + self.session_manager + .get_session(&session_topic) + .and_then(|session| session.session_properties) + .and_then(|props| props.keys.as_ref().cloned()) + .and_then(|keys| keys.first().cloned()) + .map(|key| key.is_nano_ledger) + .unwrap_or(false) + } + + /// Checks if the current session is connected via Keplr wallet. + /// NOTE: for COSMOS chains only. + pub fn is_keplr_connection(&self, session_topic: &str) -> bool { + let session_topic = session_topic.into(); + self.session_manager + .get_session(&session_topic) + .map(|session| session.controller.metadata.name == "Keplr") + .unwrap_or_default() + } + + /// Checks if a given chain ID is supported. + pub(crate) fn validate_chain_id( + &self, + session: &Session, + chain_id: &WcChainId, + ) -> MmResult<(), WalletConnectError> { + if let Some(Namespace { chains, .. }) = session.namespaces.get(chain_id.chain.as_ref()) { + match chains { + Some(chains) => { + if chains.contains(&chain_id.to_string()) { + return Ok(()); + } + }, + None => { + // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index + if let Some(SessionProperties { keys: Some(keys) }) = &session.session_properties { + if keys.iter().any(|k| k.chain_id == chain_id.id) { + return Ok(()); + } + } + }, + }; + } + + // https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#13-chains-might-be-omitted-if-the-caip-2-is-defined-in-the-index + if session.namespaces.contains_key(&chain_id.to_string()) { + return Ok(()); + } + + MmError::err(WalletConnectError::ChainIdNotSupported(chain_id.to_string())) + } + + /// Validate and send update active chain to WC if needed. + pub async fn validate_update_active_chain_id( + &self, + session_topic: &str, + chain_id: &WcChainId, + ) -> MmResult<(), WalletConnectError> { + let session_topic = session_topic.into(); + let session = + self.session_manager + .get_session(&session_topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + self.validate_chain_id(&session, chain_id)?; + + // TODO: uncomment when WalletConnect wallets start listening to chainChanged event + // if WcChain::Eip155 != chain_id.chain { + // return Ok(()); + // }; + // + // if let Some(active_chain_id) = session.get_active_chain_id().await { + // if chain_id == active_chain_id { + // return Ok(()); + // } + // }; + // + // let event = SessionEventRequest { + // event: Event { + // name: "chainChanged".to_string(), + // data: serde_json::to_value(&chain_id.id)?, + // }, + // chain_id: chain_id.to_string(), + // }; + // self.publish_request(&session.topic, RequestParams::SessionEvent(event)) + // .await?; + // + // let wait_duration = Duration::from_secs(60); + // if let Ok(Some(resp)) = self.message_rx.lock().await.next().timeout(wait_duration).await { + // let result = resp.mm_err(WalletConnectError::InternalError)?; + // if let ResponseParamsSuccess::SessionEvent(data) = result.data { + // if !data { + // return MmError::err(WalletConnectError::PayloadError( + // "Please approve chain id change".to_owned(), + // )); + // } + // + // self.session + // .get_session_mut(&session.topic) + // .ok_or(MmError::new(WalletConnectError::SessionError( + // "No active WalletConnect session found".to_string(), + // )))? + // .set_active_chain_id(chain_id.clone()) + // .await; + // } + // } + + Ok(()) + } + + /// Get available account for a given chain ID. + pub fn get_account_and_properties_for_chain_id( + &self, + session_topic: &str, + chain_id: &WcChainId, + ) -> MmResult<(String, Option), WalletConnectError> { + let session_topic = session_topic.into(); + let session = + self.session_manager + .get_session(&session_topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + if let Some(Namespace { + accounts: Some(accounts), + .. + }) = &session.namespaces.get(chain_id.chain.as_ref()) + { + if let Some(account) = find_account_in_namespace(accounts, &chain_id.id) { + return Ok((account, session.session_properties)); + } + }; + + MmError::err(WalletConnectError::NoAccountFound(chain_id.to_string())) + } + + /// Waits for and handles a WalletConnect session response with arbitrary data. + /// https://specs.walletconnect.com/2.0/specs/clients/sign/session-events#session_request + pub async fn send_session_request_and_wait( + &self, + session_topic: &str, + chain_id: &WcChainId, + method: WcRequestMethods, + params: serde_json::Value, + ) -> MmResult + where + R: DeserializeOwned, + { + let session_topic = session_topic.into(); + self.session_manager.validate_session_exists(&session_topic)?; + + let request = SessionRequestRequest { + chain_id: chain_id.to_string(), + request: SessionRequest { + method: method.as_ref().to_string(), + expiry: None, + params, + }, + }; + let (rx, ttl) = self + .publish_request(&session_topic, RequestParams::SessionRequest(request)) + .await?; + + let response = rx + .timeout(ttl) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))??; + match response.data { + ResponseParamsSuccess::Arbitrary(data) => Ok(serde_json::from_value::(data)?), + _ => MmError::err(WalletConnectError::PayloadError("Unexpected response type".to_string())), + } + } + + // Destroy WC session. + pub async fn drop_session(&self, topic: &Topic) -> MmResult<(), WalletConnectError> { + send_session_delete_request(self, topic).await + } +} + +fn find_account_in_namespace<'a>(accounts: &'a BTreeSet, chain_id: &'a str) -> Option { + accounts.iter().find_map(move |account_name| { + let parts: Vec<&str> = account_name.split(':').collect(); + if parts.len() >= 3 && parts[1] == chain_id { + Some(parts[2].to_string()) + } else { + None + } + }) +} diff --git a/mm2src/kdf_walletconnect/src/metadata.rs b/mm2src/kdf_walletconnect/src/metadata.rs new file mode 100644 index 0000000000..600695d9a9 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/metadata.rs @@ -0,0 +1,20 @@ +use std::time::Duration; + +use relay_rpc::rpc::params::Metadata; + +pub(crate) const RELAY_ADDRESS: &str = "wss://relay.walletconnect.com"; +pub(crate) const PROJECT_ID: &str = "86e916bcbacee7f98225dde86b697f5b"; +pub(crate) const AUTH_TOKEN_SUB: &str = "http://127.0.0.1:3000"; +pub(crate) const AUTH_TOKEN_DURATION: Duration = Duration::from_secs(5 * 60 * 60); +pub(crate) const APP_NAME: &str = "Komodefi Framework"; +pub(crate) const APP_DESCRIPTION: &str = "WallectConnect Komodefi Framework Playground"; + +#[inline] +pub(crate) fn generate_metadata() -> Metadata { + Metadata { + description: APP_DESCRIPTION.to_owned(), + url: AUTH_TOKEN_SUB.to_owned(), + icons: vec!["https://avatars.githubusercontent.com/u/21276113?s=200&v=4".to_owned()], + name: APP_NAME.to_owned(), + } +} diff --git a/mm2src/kdf_walletconnect/src/pairing.rs b/mm2src/kdf_walletconnect/src/pairing.rs new file mode 100644 index 0000000000..4990dd197a --- /dev/null +++ b/mm2src/kdf_walletconnect/src/pairing.rs @@ -0,0 +1,48 @@ +use crate::session::{WcRequestResponseResult, THIRTY_DAYS}; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use chrono::Utc; +use mm2_err_handle::prelude::MmResult; +use relay_rpc::domain::MessageId; +use relay_rpc::rpc::params::pairing_ping::PairingPingRequest; +use relay_rpc::rpc::params::{RelayProtocolMetadata, RequestParams}; +use relay_rpc::{domain::Topic, + rpc::params::{pairing_delete::PairingDeleteRequest, pairing_extend::PairingExtendRequest, + ResponseParamsSuccess}}; + +pub(crate) async fn reply_pairing_ping_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::PairingPing(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub(crate) async fn reply_pairing_extend_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + extend: PairingExtendRequest, +) -> MmResult<(), WalletConnectError> { + ctx.pairing.activate(topic)?; + let param = ResponseParamsSuccess::PairingExtend(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub(crate) async fn reply_pairing_delete_response( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + _delete: PairingDeleteRequest, +) -> MmResult<(), WalletConnectError> { + ctx.pairing.disconnect_rpc(topic, &ctx.client).await?; + let param = ResponseParamsSuccess::PairingDelete(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/key.rs b/mm2src/kdf_walletconnect/src/session/key.rs new file mode 100644 index 0000000000..7ac299cae6 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/key.rs @@ -0,0 +1,197 @@ +use crate::error::SessionError; + +use serde::{Deserialize, Serialize}; +use wc_common::SymKey; +use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; +use {hkdf::Hkdf, + rand::{rngs::OsRng, CryptoRng, RngCore}, + sha2::{Digest, Sha256}}; + +pub(crate) struct SymKeyPair { + pub(crate) secret: StaticSecret, + pub(crate) public_key: PublicKey, +} + +impl SymKeyPair { + pub(crate) fn new() -> Self { + let static_secret = StaticSecret::random_from_rng(OsRng); + let public_key = PublicKey::from(&static_secret); + Self { + secret: static_secret, + public_key, + } + } +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionKey { + pub(crate) sym_key: SymKey, + pub(crate) public_key: SymKey, +} + +impl std::fmt::Debug for SessionKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SessionKey") + .field("sym_key", &"*******") + .field("public_key", &self.public_key) + .finish() + } +} + +impl SessionKey { + /// Creates a new `SessionKey` with the given public key and an empty symmetric key. + pub fn new(public_key: PublicKey) -> Self { + Self { + sym_key: [0u8; 32], + public_key: public_key.to_bytes(), + } + } + + /// Creates a new `SessionKey` using a random number generator and a peer's public key. + pub fn from_osrng(other_public_key: &SymKey) -> Result { + SessionKey::diffie_hellman(OsRng, other_public_key) + } + + /// Performs Diffie-Hellman key exchange to derive a symmetric key. + pub fn diffie_hellman(csprng: T, other_public_key: &SymKey) -> Result + where + T: RngCore + CryptoRng, + { + let static_private_key = StaticSecret::random_from_rng(csprng); + let public_key = PublicKey::from(&static_private_key); + let shared_secret = static_private_key.diffie_hellman(&PublicKey::from(*other_public_key)); + + let mut session_key = Self { + sym_key: [0u8; 32], + public_key: public_key.to_bytes(), + }; + session_key.derive_symmetric_key(&shared_secret)?; + + Ok(session_key) + } + + /// Generates the symmetric key using the static secret and the peer's public key. + pub fn generate_symmetric_key( + &mut self, + static_secret: &StaticSecret, + peer_public_key: &SymKey, + ) -> Result<(), SessionError> { + let shared_secret = static_secret.diffie_hellman(&PublicKey::from(*peer_public_key)); + self.derive_symmetric_key(&shared_secret) + } + + /// Derives the symmetric key from a shared secret. + fn derive_symmetric_key(&mut self, shared_secret: &SharedSecret) -> Result<(), SessionError> { + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + hk.expand(&[], &mut self.sym_key) + .map_err(|e| SessionError::SymKeyGeneration(e.to_string())) + } + + /// Gets symmetic key reference. + pub fn symmetric_key(&self) -> SymKey { self.sym_key } + + /// Gets "our" public key used in symmetric key derivation. + pub fn diffie_public_key(&self) -> SymKey { self.public_key } + + /// Generates new session topic. + pub fn generate_topic(&self) -> String { + let mut hasher = Sha256::new(); + hasher.update(self.sym_key); + hex::encode(hasher.finalize()) + } +} + +#[cfg(test)] +mod session_key_tests { + use super::*; + use anyhow::Result; + use rand::rngs::OsRng; + use x25519_dalek::{PublicKey, StaticSecret}; + + #[test] + fn test_diffie_hellman_key_exchange() -> Result<()> { + // Alice's key pair + let alice_static_secret = StaticSecret::random_from_rng(OsRng); + let alice_public_key = PublicKey::from(&alice_static_secret); + + // Bob's key pair + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice computes shared secret and session key + let alice_shared_secret = alice_static_secret.diffie_hellman(&bob_public_key); + let mut alice_session_key = SessionKey::new(alice_public_key); + alice_session_key.derive_symmetric_key(&alice_shared_secret)?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&alice_public_key); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + // Ensure public keys are different + assert_ne!(alice_session_key.public_key, bob_session_key.public_key); + + Ok(()) + } + + #[test] + fn test_generate_symmetric_key() -> Result<()> { + // Alice's key pair + let alice_static_secret = StaticSecret::random_from_rng(OsRng); + let alice_public_key = PublicKey::from(&alice_static_secret); + + // Bob's public key + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice initializes session key + let mut alice_session_key = SessionKey::new(alice_public_key); + + // Alice generates symmetric key using Bob's public key + alice_session_key.generate_symmetric_key(&alice_static_secret, &bob_public_key.to_bytes())?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&alice_public_key); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + Ok(()) + } + + #[test] + fn test_from_osrng() -> Result<()> { + // Bob's public key + let bob_static_secret = StaticSecret::random_from_rng(OsRng); + let bob_public_key = PublicKey::from(&bob_static_secret); + + // Alice creates session key using from_osrng + let alice_session_key = SessionKey::from_osrng(&bob_public_key.to_bytes())?; + + // Bob computes shared secret and session key + let bob_shared_secret = bob_static_secret.diffie_hellman(&PublicKey::from(alice_session_key.public_key)); + let mut bob_session_key = SessionKey::new(bob_public_key); + bob_session_key.derive_symmetric_key(&bob_shared_secret)?; + + // Both symmetric keys should be the same + assert_eq!(alice_session_key.symmetric_key(), bob_session_key.symmetric_key()); + + Ok(()) + } + + #[test] + fn test_debug_trait() { + let static_secret = StaticSecret::random_from_rng(OsRng); + let public_key = PublicKey::from(&static_secret); + let session_key = SessionKey::new(public_key); + + let debug_str = format!("{:?}", session_key); + assert!(debug_str.contains("SessionKey")); + assert!(debug_str.contains("sym_key: \"*******\"")); + } +} diff --git a/mm2src/kdf_walletconnect/src/session/mod.rs b/mm2src/kdf_walletconnect/src/session/mod.rs new file mode 100644 index 0000000000..de43ea967b --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/mod.rs @@ -0,0 +1,376 @@ +pub(crate) mod key; +pub mod rpc; + +use crate::chain::WcChainId; +use crate::storage::SessionStorageDb; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use chrono::Utc; +use common::log::info; +use derive_more::Display; +use key::SessionKey; +use mm2_err_handle::prelude::{MmError, MmResult}; +use relay_rpc::domain::Topic; +use relay_rpc::rpc::params::session::Namespace; +use relay_rpc::rpc::params::session_propose::Proposer; +use relay_rpc::rpc::params::IrnMetadata; +use relay_rpc::{domain::SubscriptionId, + rpc::params::{session::ProposeNamespaces, session_settle::Controller, Metadata, Relay}}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Debug; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use wc_common::SymKey; + +pub(crate) const FIVE_MINUTES: u64 = 5 * 60; +pub(crate) const THIRTY_DAYS: u64 = 30 * 24 * 60 * 60; + +pub(crate) type WcRequestResponseResult = MmResult<(Value, IrnMetadata), WalletConnectError>; + +/// In the WalletConnect protocol, a session involves two parties: a controller +/// (typically a wallet) and a proposer (typically a dApp). This enum is used +/// to distinguish between these two roles. +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum SessionType { + /// Represents the controlling party in a session, typically a wallet. + Controller, + /// Represents the proposing party in a session, typically a dApp. + Proposer, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct SessionRpcInfo { + pub topic: Topic, + pub metadata: Metadata, + pub pairing_topic: Topic, + pub namespaces: BTreeMap, + pub expiry: u64, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct KeyInfo { + pub chain_id: String, + pub name: String, + pub algo: String, + pub pub_key: String, + pub address: String, + pub bech32_address: String, + pub ethereum_hex_address: String, + pub is_nano_ledger: bool, + pub is_keystone: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionProperties { + #[serde(default, deserialize_with = "deserialize_keys_from_string")] + pub keys: Option>, +} + +fn deserialize_keys_from_string<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum KeysField { + String(String), + Vec(Vec), + None, + } + + match KeysField::deserialize(deserializer)? { + KeysField::String(key_string) => serde_json::from_str(&key_string) + .map(Some) + .map_err(serde::de::Error::custom), + KeysField::Vec(keys) => Ok(Some(keys)), + KeysField::None => Ok(None), + } +} + +/// Encoding Algorithm for encoding data sent over to external wallets. +/// Most wallets relies on hex. However, Keplr uses base64. +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +pub enum EncodingAlgo { + /// HEX encoding format + #[default] + Hex, + /// BASE64 encoding format + Base64, +} + +impl EncodingAlgo { + fn new(name: &str) -> Self { + match name { + "Keplr" => Self::Base64, + _ => Self::Hex, + } + } + + pub fn encode>(&self, data: T) -> String { + match self { + Self::Hex => hex::encode(data), + Self::Base64 => STANDARD.encode(data), + } + } +} + +/// This struct is typically used in the core session management logic of a WalletConnect +/// implementation. It's used to store, retrieve, and update session information throughout +/// the lifecycle of a WalletConnect connection. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct Session { + /// Session topic + pub topic: Topic, + /// Pairing subscription id. + pub subscription_id: SubscriptionId, + /// Session symmetric key + pub session_key: SessionKey, + /// Information about the controlling party (typically a wallet). + pub controller: Controller, + /// Information about the proposing party (typically a dApp). + pub proposer: Proposer, + /// Details about the relay used for communication. + pub relay: Relay, + /// Agreed-upon namespaces for the session, mapping namespace strings to their definitions. + pub namespaces: BTreeMap, + /// Namespaces proposed for the session, may differ from agreed namespaces. + pub propose_namespaces: ProposeNamespaces, + /// Unix timestamp (in seconds) when the session expires. + pub expiry: u64, + /// Topic used for the initial pairing process. + pub pairing_topic: Topic, + /// Indicates whether this session info represents a Controller or Proposer perspective. + pub session_type: SessionType, + pub session_properties: Option, + /// Session active chain_id + pub active_chain_id: Option, + /// Encoding algorithm. + pub encoding_algo: Option, +} + +impl Session { + pub fn new( + ctx: &WalletConnectCtxImpl, + session_topic: Topic, + subscription_id: SubscriptionId, + session_key: SessionKey, + pairing_topic: Topic, + metadata: Metadata, + session_type: SessionType, + ) -> Self { + let (proposer, controller) = match session_type { + SessionType::Proposer => ( + Proposer { + public_key: hex::encode(session_key.diffie_public_key()), + metadata, + }, + Controller::default(), + ), + SessionType::Controller => (Proposer::default(), Controller { + public_key: hex::encode(session_key.diffie_public_key()), + metadata, + }), + }; + + Self { + subscription_id, + session_key, + encoding_algo: Some(EncodingAlgo::new(&controller.metadata.name)), + controller, + namespaces: BTreeMap::new(), + proposer, + propose_namespaces: ProposeNamespaces::default(), + relay: ctx.relay.clone(), + expiry: Utc::now().timestamp() as u64 + FIVE_MINUTES, + pairing_topic, + session_type, + topic: session_topic, + session_properties: None, + active_chain_id: Default::default(), + } + } + + pub(crate) fn extend(&mut self, till: u64) { self.expiry = till; } + + /// Get the active chain ID for the current session. + pub fn get_active_chain_id(&self) -> &Option { &self.active_chain_id } + + /// Sets the active chain ID for the current session. + pub fn set_active_chain_id(&mut self, chain_id: WcChainId) { self.active_chain_id = Some(chain_id); } +} + +/// Internal implementation of session management. +struct SessionManagerImpl { + /// A thread-safe map of sessions indexed by topic. + sessions: Arc>>, + pub(crate) storage: SessionStorageDb, +} + +pub struct SessionManager(Arc); + +impl From for SessionRpcInfo { + fn from(value: Session) -> Self { + Self { + topic: value.topic, + metadata: value.controller.metadata, + pairing_topic: value.pairing_topic, + namespaces: value.namespaces, + expiry: value.expiry, + } + } +} + +#[allow(unused)] +impl SessionManager { + pub(crate) fn new(storage: SessionStorageDb) -> Self { + Self( + SessionManagerImpl { + sessions: Default::default(), + storage, + } + .into(), + ) + } + + pub(crate) fn read(&self) -> RwLockReadGuard> { + self.0.sessions.read().expect("read shouldn't fail") + } + + pub(crate) fn write(&self) -> RwLockWriteGuard> { + self.0.sessions.write().expect("read shouldn't fail") + } + + pub(crate) fn storage(&self) -> &SessionStorageDb { &self.0.storage } + + /// Inserts `Session` into the session store, associated with the specified topic. + /// If a session with the same topic already exists, it will be overwritten. + pub(crate) fn add_session(&self, session: Session) { + // insert session + self.write().insert(session.topic.clone(), session); + } + + /// Removes session corresponding to the specified topic from the session store. + /// If the session does not exist, this method does nothing. + pub(crate) fn delete_session(&self, topic: &Topic) -> Option { + info!("[{topic}] Deleting session with topic"); + // Remove the session and return the removed session (if any) + self.write().remove(topic) + } + + /// Retrieves a cloned session associated with a given topic. + pub fn get_session(&self, topic: &Topic) -> Option { self.read().get(topic).cloned() } + + /// Retrieves a cloned session associated with a given sessionn or pairing topic. + pub fn get_session_with_any_topic(&self, topic: &Topic, with_pairing_topic: bool) -> Option { + if with_pairing_topic { + return self.read().values().find(|s| &s.pairing_topic == topic).cloned(); + } + + self.read().get(topic).cloned() + } + + /// Retrieves all sessions(active and inactive) + pub fn get_sessions(&self) -> impl Iterator { + self.read().clone().into_values().map(|session| session.into()) + } + + /// Retrieves all active session topic with their controller. + pub(crate) fn get_sessions_topic_and_controller(&self) -> Vec<(Topic, Controller)> { + self.read() + .iter() + .map(|(topic, session)| (topic.clone(), session.controller.clone())) + .collect::>() + } + + /// Updates the expiry time of the session associated with the given topic to the specified timestamp. + /// If the session does not exist, this method does nothing. + pub(crate) fn extend_session(&self, topic: &Topic, till: u64) { + info!("[{topic}] Extending session with topic"); + if let Some(mut session) = self.write().get_mut(topic) { + session.extend(till); + } + } + + /// Retrieves the symmetric key associated with a given topic. + pub(crate) fn sym_key(&self, topic: &Topic) -> Option { + self.get_session(topic).map(|sess| sess.session_key.symmetric_key()) + } + + /// Check if a session exists. + pub(crate) fn validate_session_exists(&self, topic: &Topic) -> Result<(), MmError> { + if self.read().contains_key(topic) { + return Ok(()); + }; + + MmError::err(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_sample_key_info() -> KeyInfo { + KeyInfo { + chain_id: "test-chain".to_string(), + name: "Test Key".to_string(), + algo: "secp256k1".to_string(), + pub_key: "0123456789ABCDEF".to_string(), + address: "test_address".to_string(), + bech32_address: "bech32_test_address".to_string(), + ethereum_hex_address: "0xtest_eth_address".to_string(), + is_nano_ledger: false, + is_keystone: false, + } + } + + #[test] + fn test_deserialize_keys_from_string() { + let key_info = create_sample_key_info(); + let key_json = serde_json::to_string(&vec![key_info.clone()]).unwrap(); + let json = format!(r#"{{"keys": "{}"}}"#, key_json.replace('\"', "\\\"")); + let session: SessionProperties = serde_json::from_str(&json).unwrap(); + assert!(session.keys.is_some()); + assert_eq!(session.keys.unwrap(), vec![key_info]); + } + + #[test] + fn test_deserialize_keys_from_vec() { + let key_info = create_sample_key_info(); + let json = format!(r#"{{"keys": [{}]}}"#, serde_json::to_string(&key_info).unwrap()); + let session: SessionProperties = serde_json::from_str(&json).unwrap(); + assert!(session.keys.is_some()); + assert_eq!(session.keys.unwrap(), vec![key_info]); + } + + #[test] + fn test_deserialize_empty_keys() { + let json = r#"{"keys": []}"#; + let session: SessionProperties = serde_json::from_str(json).unwrap(); + assert_eq!(session.keys, Some(vec![])); + } + + #[test] + fn test_deserialize_no_keys() { + let json = r#"{}"#; + let session: SessionProperties = serde_json::from_str(json).unwrap(); + assert_eq!(session.keys, None); + } + + #[test] + fn test_serialize_deserialize_roundtrip() { + let key_info = create_sample_key_info(); + let original = SessionProperties { + keys: Some(vec![key_info]), + }; + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: SessionProperties = serde_json::from_str(&serialized).unwrap(); + assert_eq!(original, deserialized); + } +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/delete.rs b/mm2src/kdf_walletconnect/src/session/rpc/delete.rs new file mode 100644 index 0000000000..c88922575a --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/delete.rs @@ -0,0 +1,58 @@ +use crate::{error::{WalletConnectError, USER_REQUESTED}, + storage::WalletConnectStorageOps, + WalletConnectCtxImpl}; + +use common::log::debug; +use mm2_err_handle::prelude::{MapMmError, MmResult}; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::{session_delete::SessionDeleteRequest, RequestParams, ResponseParamsSuccess}; + +pub(crate) async fn reply_session_delete_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + _delete_params: SessionDeleteRequest, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::SessionDelete(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + session_delete_cleanup(ctx, topic).await +} + +pub(crate) async fn send_session_delete_request( + ctx: &WalletConnectCtxImpl, + session_topic: &Topic, +) -> MmResult<(), WalletConnectError> { + let delete_request = SessionDeleteRequest { + code: USER_REQUESTED, + message: "User Disconnected".to_owned(), + }; + let param = RequestParams::SessionDelete(delete_request); + + ctx.publish_request(session_topic, param).await?; + + session_delete_cleanup(ctx, session_topic).await +} + +async fn session_delete_cleanup(ctx: &WalletConnectCtxImpl, topic: &Topic) -> MmResult<(), WalletConnectError> { + ctx.client.unsubscribe(topic.clone()).await?; + + if let Some(session) = ctx.session_manager.delete_session(topic) { + debug!( + "[{}] No active sessions for pairing disconnecting", + session.pairing_topic + ); + //Attempt to unsubscribe from topic + ctx.client.unsubscribe(session.pairing_topic.clone()).await?; + // Attempt to delete/disconnect the pairing + ctx.pairing.delete(&session.pairing_topic); + // delete session from storage as well. + ctx.session_manager + .storage() + .delete_session(topic) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + }; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/event.rs b/mm2src/kdf_walletconnect/src/session/rpc/event.rs new file mode 100644 index 0000000000..62159e6b91 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/event.rs @@ -0,0 +1,73 @@ +use crate::{chain::{WcChain, WcChainId}, + error::{WalletConnectError, UNSUPPORTED_CHAINS}, + WalletConnectCtxImpl}; + +use common::log::{error, info}; +use mm2_err_handle::prelude::*; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::{params::{session_event::SessionEventRequest, ResponseParamsError}, + ErrorData}}; + +pub async fn handle_session_event( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + event: SessionEventRequest, +) -> MmResult<(), WalletConnectError> { + let chain_id = WcChainId::try_from_str(&event.chain_id)?; + let event_name = event.event.name.as_str(); + + match event_name { + "chainChanged" => { + let session = + ctx.session_manager + .get_session(topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))?; + + if WcChain::Eip155 != chain_id.chain { + return Ok(()); + }; + + ctx.validate_chain_id(&session, &chain_id)?; + + if session.get_active_chain_id().as_ref().map_or(false, |c| c == &chain_id) { + return Ok(()); + }; + + // check if if new chain_id is supported. + let new_id = serde_json::from_value::(event.event.data)?; + let new_chain = chain_id.chain.derive_chain_id(new_id.to_string()); + if let Err(err) = ctx.validate_chain_id(&session, &new_chain) { + error!("[{topic}] {err:?}"); + let error_data = ErrorData { + code: UNSUPPORTED_CHAINS, + message: "Unsupported chain id".to_string(), + data: None, + }; + let params = ResponseParamsError::SessionEvent(error_data); + ctx.publish_response_err(topic, params, message_id).await?; + } else { + { + ctx.session_manager + .write() + .get_mut(topic) + .ok_or(MmError::new(WalletConnectError::SessionError( + "No active WalletConnect session found".to_string(), + )))? + .set_active_chain_id(chain_id); + } + }; + }, + "accountsChanged" => { + // TODO: Handle accountsChanged event logic. + }, + _ => { + // TODO: Handle other event logic., + }, + }; + + info!("[{topic}] {event_name} event handled successfully"); + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/extend.rs b/mm2src/kdf_walletconnect/src/session/rpc/extend.rs new file mode 100644 index 0000000000..0574277af1 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/extend.rs @@ -0,0 +1,20 @@ +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use mm2_err_handle::prelude::MmResult; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{session_extend::SessionExtendRequest, ResponseParamsSuccess}}; + +/// Process session extend request. +pub(crate) async fn reply_session_extend_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + extend: SessionExtendRequest, +) -> MmResult<(), WalletConnectError> { + ctx.session_manager.extend_session(topic, extend.expiry); + + let param = ResponseParamsSuccess::SessionExtend(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/mod.rs b/mm2src/kdf_walletconnect/src/session/rpc/mod.rs new file mode 100644 index 0000000000..b94443c191 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/mod.rs @@ -0,0 +1,9 @@ +pub mod delete; +pub(crate) mod event; +pub(crate) mod extend; +pub mod ping; +pub(crate) mod propose; +pub(crate) mod settle; +pub(crate) mod update; + +pub use ping::*; diff --git a/mm2src/kdf_walletconnect/src/session/rpc/ping.rs b/mm2src/kdf_walletconnect/src/session/rpc/ping.rs new file mode 100644 index 0000000000..ad4423c7d5 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/ping.rs @@ -0,0 +1,30 @@ +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::custom_futures::timeout::FutureTimerExt; +use mm2_err_handle::prelude::*; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{RequestParams, ResponseParamsSuccess}}; + +pub(crate) async fn reply_session_ping_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let param = ResponseParamsSuccess::SessionPing(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +pub async fn send_session_ping_request(ctx: &WalletConnectCtxImpl, topic: &Topic) -> MmResult<(), WalletConnectError> { + let param = RequestParams::SessionPing(()); + let (rx, ttl) = ctx.publish_request(topic, param).await?; + println!("ping sent successfuly"); + rx.timeout(ttl) + .await + .map_to_mm(|_| WalletConnectError::TimeoutError)? + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))??; + println!("ping sent successfuly"); + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/propose.rs b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs new file mode 100644 index 0000000000..d3626430ce --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/propose.rs @@ -0,0 +1,152 @@ +use super::settle::send_session_settle_request; +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, + metadata::generate_metadata, + session::{Session, SessionKey, SessionType, THIRTY_DAYS}, + WalletConnectCtxImpl}; + +use chrono::Utc; +use mm2_err_handle::map_to_mm::MapToMmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::rpc::params::session::ProposeNamespaces; +use relay_rpc::{domain::{MessageId, Topic}, + rpc::params::{session_propose::{Proposer, SessionProposeRequest, SessionProposeResponse}, + RequestParams, ResponseParamsSuccess}}; + +/// Creates a new session proposal from topic and metadata. +pub(crate) async fn send_proposal_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + required_namespaces: ProposeNamespaces, + optional_namespaces: ProposeNamespaces, +) -> MmResult<(), WalletConnectError> { + let proposer = Proposer { + metadata: ctx.metadata.clone(), + public_key: hex::encode(ctx.key_pair.public_key.as_bytes()), + }; + let session_proposal = RequestParams::SessionPropose(SessionProposeRequest { + relays: vec![ctx.relay.clone()], + proposer, + required_namespaces, + optional_namespaces: Some(optional_namespaces), + }); + let _ = ctx.publish_request(topic, session_proposal).await?; + + Ok(()) +} + +/// Process session proposal request +/// https://specs.walletconnect.com/2.0/specs/clients/sign/session-proposal +pub async fn reply_session_proposal_request( + ctx: &WalletConnectCtxImpl, + proposal: SessionProposeRequest, + topic: &Topic, + message_id: &MessageId, +) -> MmResult<(), WalletConnectError> { + let session = { + let sender_public_key = hex::decode(&proposal.proposer.public_key)? + .as_slice() + .try_into() + .map_to_mm(|_| WalletConnectError::InternalError("Invalid sender_public_key".to_owned()))?; + let session_key = SessionKey::from_osrng(&sender_public_key)?; + let session_topic: Topic = session_key.generate_topic().into(); + let subscription_id = ctx + .client + .subscribe(session_topic.clone()) + .await + .map_to_mm(|err| WalletConnectError::SubscriptionError(err.to_string()))?; + + Session::new( + ctx, + session_topic.clone(), + subscription_id, + session_key, + topic.clone(), + proposal.proposer.metadata, + SessionType::Controller, + ) + }; + session + .propose_namespaces + .supported(&proposal.required_namespaces) + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + + { + // save session to storage + ctx.session_manager + .storage() + .save_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // Add session to session lists + ctx.session_manager.add_session(session.clone()); + } + + send_session_settle_request(ctx, &session).await?; + + // Respond to incoming session propose. + let param = ResponseParamsSuccess::SessionPropose(SessionProposeResponse { + relay: ctx.relay.clone(), + responder_public_key: proposal.proposer.public_key, + }); + + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} + +/// Process session propose reponse. +pub(crate) async fn process_session_propose_response( + ctx: &WalletConnectCtxImpl, + pairing_topic: &Topic, + response: &SessionProposeResponse, +) -> MmResult<(), WalletConnectError> { + let session_key = { + let other_public_key = hex::decode(&response.responder_public_key)? + .as_slice() + .try_into() + .unwrap(); + let mut session_key = SessionKey::new(ctx.key_pair.public_key); + session_key.generate_symmetric_key(&ctx.key_pair.secret, &other_public_key)?; + session_key + }; + + let session = { + let session_topic: Topic = session_key.generate_topic().into(); + let subscription_id = ctx + .client + .subscribe(session_topic.clone()) + .await + .map_to_mm(|err| WalletConnectError::SubscriptionError(err.to_string()))?; + + let mut session = Session::new( + ctx, + session_topic.clone(), + subscription_id, + session_key, + pairing_topic.clone(), + generate_metadata(), + SessionType::Proposer, + ); + session.relay = response.relay.clone(); + session.expiry = Utc::now().timestamp() as u64 + THIRTY_DAYS; + session.controller.public_key = response.responder_public_key.clone(); + session + }; + + // save session to storage + ctx.session_manager + .storage() + .save_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // Add session to session lists + ctx.session_manager.add_session(session.clone()); + + // Activate pairing_topic + ctx.pairing.activate(pairing_topic)?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/settle.rs b/mm2src/kdf_walletconnect/src/session/rpc/settle.rs new file mode 100644 index 0000000000..2c03131e74 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/settle.rs @@ -0,0 +1,83 @@ +use crate::session::{EncodingAlgo, Session, SessionProperties}; +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::log::{debug, info}; +use mm2_err_handle::prelude::{MapMmError, MmError, MmResult}; +use relay_rpc::domain::Topic; +use relay_rpc::rpc::params::session_settle::SessionSettleRequest; + +/// TODO: Finish when implementing KDF as a Wallet. +pub(crate) async fn send_session_settle_request( + _ctx: &WalletConnectCtxImpl, + _session_info: &Session, +) -> MmResult<(), WalletConnectError> { + // let mut settled_namespaces = BTreeMap::::new(); + // let nam + // settled_namespaces.insert("eip155".to_string(), Namespace { + // chains: Some(SUPPORTED_CHAINS.iter().map(|c| c.to_string()).collect()), + // methods: SUPPORTED_METHODS.iter().map(|m| m.to_string()).collect(), + // events: SUPPORTED_EVENTS.iter().map(|e| e.to_string()).collect(), + // accounts: None, + // }); + // + // let request = RequestParams::SessionSettle(SessionSettleRequest { + // relay: session_info.relay.clone(), + // controller: session_info.controller.clone(), + // namespaces: SettleNamespaces(settled_namespaces), + // expiry: Utc::now().timestamp() as u64 + THIRTY_DAYS, + // session_properties: None, + // }); + // + // ctx.publish_request(&session_info.topic, request).await?; + + Ok(()) +} + +/// Process session settle request. +pub(crate) async fn reply_session_settle_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + settle: SessionSettleRequest, +) -> MmResult<(), WalletConnectError> { + let current_session = { + let mut sessions = ctx.session_manager.write(); + let Some(session) = sessions.get_mut(topic) else { + return MmError::err(WalletConnectError::SessionError(format!( + "No session found for topic: {topic}" + ))); + }; + if let Some(value) = settle.session_properties { + let session_properties = serde_json::from_value::(value)?; + session.session_properties = Some(session_properties); + }; + session.encoding_algo = Some(EncodingAlgo::new(&settle.controller.metadata.name)); + session.namespaces = settle.namespaces.0; + session.controller = settle.controller; + session.relay = settle.relay; + session.expiry = settle.expiry; + + session.clone() + }; + + // Update storage session. + ctx.session_manager + .storage() + .update_session(¤t_session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + // Delete other sessions with same controller + let sessions = ctx.session_manager.get_sessions_topic_and_controller(); + for (topic, _) in sessions + .into_iter() + .filter(|(topic, controller)| controller == ¤t_session.controller && topic != ¤t_session.topic) + { + ctx.drop_session(&topic).await?; + debug!("[{topic}] session deleted"); + } + + info!("[{topic}] Session successfully settled for topic"); + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/session/rpc/update.rs b/mm2src/kdf_walletconnect/src/session/rpc/update.rs new file mode 100644 index 0000000000..e15793fe76 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/session/rpc/update.rs @@ -0,0 +1,48 @@ +use crate::storage::WalletConnectStorageOps; +use crate::{error::WalletConnectError, WalletConnectCtxImpl}; + +use common::log::info; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::{MessageId, Topic}; +use relay_rpc::rpc::params::{session_update::SessionUpdateRequest, ResponseParamsSuccess}; + +pub(crate) async fn reply_session_update_request( + ctx: &WalletConnectCtxImpl, + topic: &Topic, + message_id: &MessageId, + update: SessionUpdateRequest, +) -> MmResult<(), WalletConnectError> { + { + let mut session = ctx.session_manager.write(); + let Some(session) = session.get_mut(topic) else { + return MmError::err(WalletConnectError::SessionError(format!( + "No session found for topic: {topic}" + ))); + }; + update + .namespaces + .caip2_validate() + .map_to_mm(|err| WalletConnectError::InternalError(err.to_string()))?; + session.namespaces = update.namespaces.0; + let session = session; + info!("Updated extended, info: {:?}", session.topic); + } + + // Update storage session. + let session = ctx + .session_manager + .get_session(topic) + .ok_or(MmError::new(WalletConnectError::SessionError(format!( + "session not foun topic: {topic}" + ))))?; + ctx.session_manager + .storage() + .update_session(&session) + .await + .mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + let param = ResponseParamsSuccess::SessionUpdate(true); + ctx.publish_response_ok(topic, param, message_id).await?; + + Ok(()) +} diff --git a/mm2src/kdf_walletconnect/src/storage/indexed_db.rs b/mm2src/kdf_walletconnect/src/storage/indexed_db.rs new file mode 100644 index 0000000000..ac9205600f --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/indexed_db.rs @@ -0,0 +1,129 @@ +use super::WalletConnectStorageOps; +use crate::error::WcIndexedDbError; +use crate::session::Session; +use async_trait::async_trait; +use common::log::debug; +use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::{ConstructibleDb, DbIdentifier, DbInstance, DbLocked, DbUpgrader, IndexedDb, IndexedDbBuilder, + InitDbResult, OnUpgradeResult, SharedDb, TableSignature}; +use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; + +const DB_VERSION: u32 = 1; + +pub type IDBSessionStorageLocked<'a> = DbLocked<'a, IDBSessionStorageInner>; + +impl TableSignature for Session { + const TABLE_NAME: &'static str = "sessions"; + + fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + if let (0, 1) = (old_version, new_version) { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_index("topic", false)?; + } + Ok(()) + } +} + +pub struct IDBSessionStorageInner(IndexedDb); + +#[async_trait] +impl DbInstance for IDBSessionStorageInner { + const DB_NAME: &'static str = "wc_session_storage"; + + async fn init(db_id: DbIdentifier) -> InitDbResult { + let inner = IndexedDbBuilder::new(db_id) + .with_version(DB_VERSION) + .with_table::() + .build() + .await?; + + Ok(Self(inner)) + } +} + +impl IDBSessionStorageInner { + pub(crate) fn get_inner(&self) -> &IndexedDb { &self.0 } +} + +#[derive(Clone)] +pub struct IDBSessionStorage(SharedDb); + +impl IDBSessionStorage { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + Ok(Self(ConstructibleDb::new(ctx).into_shared())) + } + + async fn lock_db(&self) -> MmResult, WcIndexedDbError> { + self.0 + .get_or_initialize() + .await + .mm_err(|err| WcIndexedDbError::InternalError(err.to_string())) + } +} + +#[async_trait::async_trait] +impl WalletConnectStorageOps for IDBSessionStorage { + type Error = WcIndexedDbError; + + async fn init(&self) -> MmResult<(), Self::Error> { + debug!("Initializing WalletConnect session storage"); + Ok(()) + } + + async fn is_initialized(&self) -> MmResult { Ok(true) } + + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Saving WalletConnect session to storage", session.topic); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + session_table + .replace_item_by_unique_index("topic", session.topic.clone(), session) + .await?; + + Ok(()) + } + + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error> { + debug!("[{topic}] Retrieving WalletConnect session from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + Ok(session_table + .get_item_by_unique_index("topic", topic) + .await? + .map(|s| s.1)) + } + + async fn get_all_sessions(&self) -> MmResult, Self::Error> { + debug!("Loading WalletConnect sessions from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + Ok(session_table + .get_all_items() + .await? + .into_iter() + .map(|s| s.1) + .collect::>()) + } + + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error> { + debug!("[{topic}] Deleting WalletConnect session from storage"); + let lock_db = self.lock_db().await?; + let transaction = lock_db.get_inner().transaction().await?; + let session_table = transaction.table::().await?; + + session_table.delete_item_by_unique_index("topic", topic).await?; + Ok(()) + } + + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Updating WalletConnect session in storage", session.topic); + self.save_session(session).await + } +} diff --git a/mm2src/kdf_walletconnect/src/storage/mod.rs b/mm2src/kdf_walletconnect/src/storage/mod.rs new file mode 100644 index 0000000000..088e052e2f --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/mod.rs @@ -0,0 +1,202 @@ +use std::ops::Deref; + +use async_trait::async_trait; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; + +use crate::{error::WalletConnectError, session::Session}; + +#[cfg(target_arch = "wasm32")] pub(crate) mod indexed_db; +#[cfg(not(target_arch = "wasm32"))] pub(crate) mod sqlite; + +#[async_trait] +pub(crate) trait WalletConnectStorageOps { + type Error: std::fmt::Debug + NotMmError + NotEqual + Send; + + async fn init(&self) -> MmResult<(), Self::Error>; + async fn is_initialized(&self) -> MmResult; + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error>; + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error>; + async fn get_all_sessions(&self) -> MmResult, Self::Error>; + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error>; + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error>; +} + +#[cfg(target_arch = "wasm32")] +type DB = indexed_db::IDBSessionStorage; +#[cfg(not(target_arch = "wasm32"))] +type DB = sqlite::SqliteSessionStorage; + +pub(crate) struct SessionStorageDb(DB); + +impl Deref for SessionStorageDb { + type Target = DB; + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl SessionStorageDb { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + let db = DB::new(ctx).mm_err(|err| WalletConnectError::StorageError(err.to_string()))?; + + Ok(SessionStorageDb(db)) + } +} + +#[cfg(test)] +pub(crate) mod session_storage_tests { + common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + use common::cross_test; + use mm2_test_helpers::for_tests::mm_ctx_with_custom_async_db; + use relay_rpc::{domain::SubscriptionId, rpc::params::Metadata}; + + use crate::{session::key::SessionKey, + session::{Session, SessionType}, + WalletConnectCtx}; + + use super::WalletConnectStorageOps; + + fn sample_test_session(wc_ctx: &WalletConnectCtx) -> Session { + let session_key = SessionKey { + sym_key: [ + 115, 159, 247, 31, 199, 84, 88, 59, 158, 252, 98, 225, 51, 125, 201, 239, 142, 34, 9, 201, 128, 114, + 144, 166, 102, 131, 87, 191, 33, 24, 153, 7, + ], + public_key: [ + 115, 159, 247, 31, 199, 84, 88, 59, 158, 252, 98, 225, 51, 125, 201, 239, 142, 34, 9, 201, 128, 114, + 144, 166, 102, 131, 87, 191, 33, 24, 153, 7, + ], + }; + + Session::new( + wc_ctx, + "bb89e3bae8cb89e5549f4d9bcc5a1ac2aae6dd90ef37eb2f59d80c5773f36343".into(), + SubscriptionId::generate(), + session_key, + "5af44bdf8d6b11f4635c964a15e9e2d50942534824791757b2c26528e8feef39".into(), + Metadata::default(), + SessionType::Controller, + ) + } + + cross_test!(save_and_get_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap(); + assert_eq!(sample_session, db_session.unwrap()); + }); + + cross_test!(delete_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_eq!(sample_session, db_session); + + // try delete session + wc_ctx + .session_manager + .storage() + .delete_session(&db_session.topic) + .await + .unwrap(); + + // try get_session deleted again + let db_session = wc_ctx.session_manager.storage().get_session(&db_session.topic).await; + assert!(db_session.is_err()); + }); + + cross_test!(update_session_test, { + let mm_ctx = mm_ctx_with_custom_async_db().await; + let wc_ctx = WalletConnectCtx::try_init(&mm_ctx).unwrap(); + wc_ctx.session_manager.storage().init().await.unwrap(); + + let sample_session = sample_test_session(&wc_ctx); + + // try save session + wc_ctx + .session_manager + .storage() + .save_session(&sample_session) + .await + .unwrap(); + + // try get session + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_eq!(sample_session, db_session); + + // modify sample_session + let mut modified_sample_session = sample_session.clone(); + modified_sample_session.expiry = 100; + + // assert that original session expiry isn't the same as our new expiry. + assert_ne!(sample_session.expiry, modified_sample_session.expiry); + + // try update session + wc_ctx + .session_manager + .storage() + .update_session(&modified_sample_session) + .await + .unwrap(); + + // try get_session again with new updated expiry + let db_session = wc_ctx + .session_manager + .storage() + .get_session(&sample_session.topic) + .await + .unwrap() + .unwrap(); + assert_ne!(sample_session.expiry, db_session.expiry); + + assert_eq!(modified_sample_session, db_session); + assert_eq!(100, db_session.expiry); + }); +} diff --git a/mm2src/kdf_walletconnect/src/storage/sqlite.rs b/mm2src/kdf_walletconnect/src/storage/sqlite.rs new file mode 100644 index 0000000000..40bfcf9431 --- /dev/null +++ b/mm2src/kdf_walletconnect/src/storage/sqlite.rs @@ -0,0 +1,176 @@ +use async_trait::async_trait; +use common::log::debug; +use db_common::async_sql_conn::InternalError; +use db_common::sqlite::rusqlite::Result as SqlResult; +use db_common::sqlite::{query_single_row, string_from_row, CHECK_TABLE_EXISTS_SQL}; +use db_common::{async_sql_conn::{AsyncConnError, AsyncConnection}, + sqlite::validate_table_name}; +use futures::lock::{Mutex, MutexGuard}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use relay_rpc::domain::Topic; +use std::sync::Arc; + +use super::WalletConnectStorageOps; +use crate::session::Session; + +const SESSION_TABLE_NAME: &str = "wc_session"; + +/// Sessions table +fn create_sessions_table() -> SqlResult { + validate_table_name(SESSION_TABLE_NAME)?; + Ok(format!( + "CREATE TABLE IF NOT EXISTS {SESSION_TABLE_NAME} ( + topic char(32) PRIMARY KEY, + data TEXT NOT NULL, + expiry BIGINT NOT NULL + );" + )) +} + +#[derive(Clone, Debug)] +pub(crate) struct SqliteSessionStorage { + pub conn: Arc>, +} + +impl SqliteSessionStorage { + pub(crate) fn new(ctx: &MmArc) -> MmResult { + let conn = ctx + .async_sqlite_connection + .get() + .ok_or(AsyncConnError::Internal(InternalError( + "async_sqlite_connection is not initialized".to_owned(), + )))?; + + Ok(Self { conn: conn.clone() }) + } + + pub(crate) async fn lock_db(&self) -> MutexGuard<'_, AsyncConnection> { self.conn.lock().await } +} + +#[async_trait] +impl WalletConnectStorageOps for SqliteSessionStorage { + type Error = AsyncConnError; + + async fn init(&self) -> MmResult<(), Self::Error> { + debug!("Initializing WalletConnect session storage"); + let lock = self.lock_db().await; + lock.call(move |conn| { + conn.execute(&create_sessions_table()?, []).map(|_| ())?; + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn is_initialized(&self) -> MmResult { + let lock = self.lock_db().await; + validate_table_name(SESSION_TABLE_NAME).map_err(AsyncConnError::from)?; + lock.call(move |conn| { + let initialized = query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [SESSION_TABLE_NAME], string_from_row)?; + Ok(initialized.is_some()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn save_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Saving WalletConnect session to storage", session.topic); + let lock = self.lock_db().await; + + let session = session.clone(); + lock.call(move |conn| { + let sql = format!( + "INSERT INTO {} (topic, data, expiry) VALUES (?1, ?2, ?3);", + SESSION_TABLE_NAME + ); + let transaction = conn.transaction()?; + + let session_data = serde_json::to_string(&session).map_err(|err| AsyncConnError::from(err.to_string()))?; + + let params = [session.topic.to_string(), session_data, session.expiry.to_string()]; + + transaction.execute(&sql, params)?; + transaction.commit()?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn get_session(&self, topic: &Topic) -> MmResult, Self::Error> { + debug!("[{topic}] Retrieving WalletConnect session from storage"); + let lock = self.lock_db().await; + let topic = topic.clone(); + let session_str = lock + .call(move |conn| { + let sql = format!("SELECT topic, data, expiry FROM {} WHERE topic=?1;", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let session: String = stmt.query_row([topic.to_string()], |row| row.get::<_, String>(1))?; + Ok(session) + }) + .await + .map_to_mm(AsyncConnError::from)?; + + let session = serde_json::from_str(&session_str).map_to_mm(|err| AsyncConnError::from(err.to_string()))?; + Ok(session) + } + + async fn get_all_sessions(&self) -> MmResult, Self::Error> { + debug!("Loading WalletConnect sessions from storage"); + let lock = self.lock_db().await; + let sessions_str = lock + .call(move |conn| { + let sql = format!("SELECT topic, data, expiry FROM {};", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let sessions = stmt.query_map([], |row| row.get::<_, String>(1))?.collect::>(); + Ok(sessions) + }) + .await + .map_to_mm(AsyncConnError::from)?; + + let mut sessions = Vec::with_capacity(sessions_str.len()); + for session in sessions_str { + let session = serde_json::from_str(&session.map_to_mm(AsyncConnError::from)?) + .map_to_mm(|err| AsyncConnError::from(err.to_string()))?; + sessions.push(session); + } + + Ok(sessions) + } + + async fn delete_session(&self, topic: &Topic) -> MmResult<(), Self::Error> { + debug!("[{topic}] Deleting WalletConnect session from storage"); + let topic = topic.clone(); + let lock = self.lock_db().await; + lock.call(move |conn| { + let sql = format!("DELETE FROM {} WHERE topic = ?1", SESSION_TABLE_NAME); + let mut stmt = conn.prepare(&sql)?; + let _ = stmt.execute([topic.to_string()])?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn update_session(&self, session: &Session) -> MmResult<(), Self::Error> { + debug!("[{}] Updating WalletConnect session in storage", session.topic); + let session = session.clone(); + let lock = self.lock_db().await; + lock.call(move |conn| { + let sql = format!( + "UPDATE {} SET data = ?1, expiry = ?2 WHERE topic = ?3", + SESSION_TABLE_NAME + ); + let session_data = serde_json::to_string(&session).map_err(|err| AsyncConnError::from(err.to_string()))?; + let params = [session_data, session.expiry.to_string(), session.topic.to_string()]; + let _row = conn.prepare(&sql)?.execute(params)?; + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } +} diff --git a/mm2src/ledger/Cargo.toml b/mm2src/ledger/Cargo.toml index c6883fc75c..10c5178b2a 100644 --- a/mm2src/ledger/Cargo.toml +++ b/mm2src/ledger/Cargo.toml @@ -4,18 +4,18 @@ version = "0.1.0" edition = "2018" [dependencies] -async-trait = "0.1" -byteorder = "1.3.2" +async-trait.workspace = true +byteorder.workspace = true common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } hw_common = { path = "../hw_common" } -serde = "1.0" -serde_derive = "1.0" +serde.workspace = true +serde_derive.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.55" } +js-sys.workspace = true +wasm-bindgen.workspace.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys.workspace = true diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index 2097703ed7..b2300f509a 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "mm2_bin_lib" -version = "2.4.0-beta" +version = "2.5.0-beta" authors = ["James Lee", "Artem Pikulin", "Artem Grinblat", "Omar S.", "Onur Ozkan", "Alina Sharon", "Caglar Kaya", "Cipi", "Sergey Boiko", "Samuel Onoja", "Roman Sztergbaum", "Kadan Stadelmann ", "Dimxy", "Omer Yacine", "DeckerSU"] edition = "2018" default-run = "kdf" @@ -17,13 +17,6 @@ track-ctx-pointer = ["mm2_main/track-ctx-pointer"] zhtlc-native-tests = ["mm2_main/zhtlc-native-tests"] test-ext-api = ["mm2_main/test-ext-api"] -[[bin]] -name = "mm2" -path = "src/mm2_bin.rs" -test = false -doctest = false -bench = false - [[bin]] name = "kdf" path = "src/mm2_bin.rs" @@ -40,27 +33,27 @@ bench = false [dependencies] common = { path = "../common" } -enum-primitive-derive = "0.2" -libc = "0.2" +enum-primitive-derive.workspace = true +libc.workspace = true mm2_core = { path = "../mm2_core" } mm2_main = { path = "../mm2_main" } -num-traits = "0.2" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +num-traits.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } +gstuff.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } +js-sys.workspace = true mm2_rpc = { path = "../mm2_rpc", features=["rpc_facilities"] } -serde = "1.0" -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } +serde.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true [target.x86_64-unknown-linux-gnu.dependencies] -jemallocator = "0.5.0" +jemallocator.workspace = true [build-dependencies] -chrono = "0.4" -gstuff = { version = "0.7", features = ["nightly"] } -regex = "1" +chrono.workspace = true +gstuff.workspace = true +regex.workspace = true diff --git a/mm2src/mm2_bitcoin/chain/Cargo.toml b/mm2src/mm2_bitcoin/chain/Cargo.toml index 58a1929c96..3f85ae7db7 100644 --- a/mm2src/mm2_bitcoin/chain/Cargo.toml +++ b/mm2src/mm2_bitcoin/chain/Cargo.toml @@ -7,11 +7,11 @@ authors = ["debris "] doctest = false [dependencies] -rustc-hex = "2" +rustc-hex.workspace = true bitcrypto = { path = "../crypto" } primitives = { path = "../primitives" } serialization = { path = "../serialization" } serialization_derive = { path = "../serialization_derive" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -bitcoin = "0.29" +bitcoin.workspace = true diff --git a/mm2src/mm2_bitcoin/crypto/Cargo.toml b/mm2src/mm2_bitcoin/crypto/Cargo.toml index 20a62aa693..1ca5ee8257 100644 --- a/mm2src/mm2_bitcoin/crypto/Cargo.toml +++ b/mm2src/mm2_bitcoin/crypto/Cargo.toml @@ -9,9 +9,9 @@ doctest = false [dependencies] groestl = "0.9" primitives = { path = "../primitives" } -ripemd160 = "0.9.0" -sha-1 = "0.9" -sha2 = "0.10" -sha3 = "0.9" -siphasher = "0.1.1" +ripemd160.workspace = true +sha-1.workspace = true +sha2.workspace = true +sha3.workspace = true +siphasher.workspace = true serialization = { path = "../serialization" } diff --git a/mm2src/mm2_bitcoin/keys/Cargo.toml b/mm2src/mm2_bitcoin/keys/Cargo.toml index 990bd3dff1..e840322fd1 100644 --- a/mm2src/mm2_bitcoin/keys/Cargo.toml +++ b/mm2src/mm2_bitcoin/keys/Cargo.toml @@ -7,14 +7,14 @@ authors = ["debris "] doctest = false [dependencies] -rustc-hex = "2" -base58 = "0.2" -bech32 = "0.9.1" +bech32.workspace = true +bs58.workspace = true bitcrypto = { path = "../crypto" } -derive_more = "0.99" -lazy_static = "1.4" -rand = {version = "0.6", features = ["wasm-bindgen"] } +derive_more.workspace = true +lazy_static.workspace = true primitives = { path = "../primitives" } -secp256k1 = { version = "0.20", features = ["rand", "recovery"] } -serde = { version = "1.0", features = ["derive"] } -serde_derive = "1.0" +rand = { version = "0.6", features = ["wasm-bindgen"] } +rustc-hex.workspace = true +secp256k1 = { workspace = true, features = ["rand", "recovery"] } +serde = { workspace = true, features = ["derive"] } +serde_derive.workspace = true diff --git a/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs b/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs index a9e93127af..e6a4bf4cd9 100644 --- a/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs +++ b/mm2src/mm2_bitcoin/keys/src/legacyaddress.rs @@ -1,7 +1,6 @@ use std::str::FromStr; use std::{convert::TryInto, fmt}; -use base58::{FromBase58, ToBase58}; use crypto::{checksum, ChecksumType}; use std::ops::Deref; use {AddressHashEnum, AddressPrefix, DisplayLayout}; @@ -82,7 +81,7 @@ impl FromStr for LegacyAddress { where Self: Sized, { - let hex = s.from_base58().map_err(|_| Error::InvalidAddress)?; + let hex = bs58::decode(s).into_vec().map_err(|_| Error::InvalidAddress)?; LegacyAddress::from_layout(&hex) } } @@ -92,7 +91,9 @@ impl From<&'static str> for LegacyAddress { } impl fmt::Display for LegacyAddress { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { self.layout().to_base58().fmt(fmt) } + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + bs58::encode(self.layout().as_ref()).into_string().fmt(fmt) + } } impl LegacyAddress { diff --git a/mm2src/mm2_bitcoin/keys/src/lib.rs b/mm2src/mm2_bitcoin/keys/src/lib.rs index d3a01d854d..88eea9408f 100644 --- a/mm2src/mm2_bitcoin/keys/src/lib.rs +++ b/mm2src/mm2_bitcoin/keys/src/lib.rs @@ -1,8 +1,8 @@ //! Bitcoin keys. -extern crate base58; extern crate bech32; extern crate bitcrypto as crypto; +extern crate bs58; extern crate derive_more; extern crate lazy_static; extern crate primitives; diff --git a/mm2src/mm2_bitcoin/keys/src/private.rs b/mm2src/mm2_bitcoin/keys/src/private.rs index ce4e64a908..435a3b7a46 100644 --- a/mm2src/mm2_bitcoin/keys/src/private.rs +++ b/mm2src/mm2_bitcoin/keys/src/private.rs @@ -2,7 +2,6 @@ use crate::SECP_SIGN; use address::detect_checksum; -use base58::{FromBase58, ToBase58}; use crypto::{checksum, ChecksumType}; use hex::ToHex; use secp256k1::{Message as SecpMessage, SecretKey}; @@ -110,7 +109,7 @@ impl fmt::Debug for Private { } impl fmt::Display for Private { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.layout().to_base58().fmt(f) } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { bs58::encode(self.layout()).into_string().fmt(f) } } impl FromStr for Private { @@ -120,7 +119,7 @@ impl FromStr for Private { where Self: Sized, { - let hex = s.from_base58().map_err(|_| Error::InvalidPrivate)?; + let hex = bs58::decode(s).into_vec().map_err(|_| Error::InvalidPrivate)?; Private::from_layout(&hex) } } diff --git a/mm2src/mm2_bitcoin/keys/src/public.rs b/mm2src/mm2_bitcoin/keys/src/public.rs index cb801585d8..c21eda0259 100644 --- a/mm2src/mm2_bitcoin/keys/src/public.rs +++ b/mm2src/mm2_bitcoin/keys/src/public.rs @@ -3,8 +3,8 @@ use crypto::dhash160; use hash::{H160, H264, H520}; use hex::ToHex; use secp256k1::{recovery::{RecoverableSignature, RecoveryId}, - Message as SecpMessage, PublicKey, Signature as SecpSignature}; -use std::{fmt, ops}; + Error as SecpError, Message as SecpMessage, PublicKey, Signature as SecpSignature}; +use std::{fmt, ops::Deref}; use {CompactSignature, Error, Message, Signature}; /// Secret public key @@ -80,9 +80,12 @@ impl Public { Public::Normal(_) => None, } } + + #[inline(always)] + pub fn to_secp256k1_pubkey(&self) -> Result { PublicKey::from_slice(self.deref()) } } -impl ops::Deref for Public { +impl Deref for Public { type Target = [u8]; fn deref(&self) -> &Self::Target { diff --git a/mm2src/mm2_bitcoin/primitives/Cargo.toml b/mm2src/mm2_bitcoin/primitives/Cargo.toml index 3da53cdf33..ac1000a9fb 100644 --- a/mm2src/mm2_bitcoin/primitives/Cargo.toml +++ b/mm2src/mm2_bitcoin/primitives/Cargo.toml @@ -7,7 +7,7 @@ authors = ["debris "] doctest = false [dependencies] -rustc-hex = "2" -bitcoin_hashes = "0.11" -byteorder = "1.0" -uint = "0.9.3" +rustc-hex.workspace = true +bitcoin_hashes.workspace = true +byteorder.workspace = true +uint.workspace = true diff --git a/mm2src/mm2_bitcoin/rpc/Cargo.toml b/mm2src/mm2_bitcoin/rpc/Cargo.toml index 4fbba4129f..a9e0841695 100644 --- a/mm2src/mm2_bitcoin/rpc/Cargo.toml +++ b/mm2src/mm2_bitcoin/rpc/Cargo.toml @@ -7,11 +7,11 @@ authors = ["Ethcore "] doctest = false [dependencies] -log = "0.4.17" -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_derive = "1.0" -rustc-hex = "2" +log.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_derive.workspace = true +rustc-hex.workspace = true serialization = { path = "../serialization" } chain = { path = "../chain" } @@ -20,4 +20,4 @@ keys = { path = "../keys" } script = { path = "../script" } [dev-dependencies] -lazy_static = "1.4" \ No newline at end of file +lazy_static.workspace = true diff --git a/mm2src/mm2_bitcoin/script/Cargo.toml b/mm2src/mm2_bitcoin/script/Cargo.toml index e340cd8944..6a97b950b2 100644 --- a/mm2src/mm2_bitcoin/script/Cargo.toml +++ b/mm2src/mm2_bitcoin/script/Cargo.toml @@ -11,7 +11,7 @@ bitcrypto = { path = "../crypto" } chain = { path = "../chain" } keys = { path = "../keys" } primitives = { path = "../primitives" } -serde = "1.0" +serde.workspace = true serialization = { path = "../serialization" } -log = "0.4.17" -blake2b_simd = "0.5" \ No newline at end of file +log.workspace = true +blake2b_simd.workspace = true diff --git a/mm2src/mm2_bitcoin/script/src/script.rs b/mm2src/mm2_bitcoin/script/src/script.rs index c5e4b57d0f..8614961879 100644 --- a/mm2src/mm2_bitcoin/script/src/script.rs +++ b/mm2src/mm2_bitcoin/script/src/script.rs @@ -513,6 +513,30 @@ impl Script { script.sigops_count(true) } + + /// Extracts the signature from a scriptSig at instruction 0. + /// + /// Usable for P2PK and P2PKH scripts. + pub fn extract_signature(&self) -> Result, String> { + match self.get_instruction(0) { + Some(Ok(instruction)) => match instruction.opcode { + Opcode::OP_PUSHBYTES_70 | Opcode::OP_PUSHBYTES_71 | Opcode::OP_PUSHBYTES_72 => match instruction.data { + Some(bytes) => Ok(bytes.to_vec()), + None => Err(format!("No data at instruction 0 of script {:?}", self)), + }, + opcode => Err(format!("Unexpected opcode {:?}", opcode)), + }, + Some(Err(e)) => Err(format!("Error {} on getting instruction 0 of script {:?}", e, self)), + None => Err(format!("None instruction 0 of script {:?}", self)), + } + } + + /// Checks if a scriptSig is a script that spends a P2PK output. + pub fn does_script_spend_p2pk(&self) -> bool { + // P2PK scriptSig is just a single signature. The script should consist of a single push bytes + // instruction with the data as the signature. + self.extract_signature().is_ok() && self.get_instruction(1).is_none() + } } pub struct Instructions<'a> { @@ -971,4 +995,13 @@ OP_ADD } assert_eq!(max_idx, 3); } + + #[test] + fn test_does_script_spend_p2pk() { + let script_sig = Script::from("473044022071edae37cf518e98db3f7637b9073a7a980b957b0c7b871415dbb4898ec3ebdc022031b402a6b98e64ffdf752266449ca979a9f70144dba77ed7a6a25bfab11648f6012103ad6f89abc2e5beaa8a3ac28e22170659b3209fe2ddf439681b4b8f31508c36fa"); + assert!(!script_sig.does_script_spend_p2pk()); + // The scriptSig of the input spent from: https://mempool.space/tx/1db6251a9afce7025a2061a19e63c700dffc3bec368bd1883decfac353357a9d + let script_sig = Script::from("483045022078e86c021003cca23842d4b2862dfdb68d2478a98c08c10dcdffa060e55c72be022100f6a41da12cdc2e350045f4c97feeab76a7c0ab937bd8a9e507293ce6d37c9cc201"); + assert!(script_sig.does_script_spend_p2pk()); + } } diff --git a/mm2src/mm2_bitcoin/serialization/Cargo.toml b/mm2src/mm2_bitcoin/serialization/Cargo.toml index 590b18296b..6a843442a2 100644 --- a/mm2src/mm2_bitcoin/serialization/Cargo.toml +++ b/mm2src/mm2_bitcoin/serialization/Cargo.toml @@ -7,7 +7,7 @@ authors = ["debris "] doctest = false [dependencies] -byteorder = "1.0" +byteorder.workspace = true primitives = { path = "../primitives" } -derive_more = "0.99" +derive_more.workspace = true test_helpers = { path = "../test_helpers" } diff --git a/mm2src/mm2_bitcoin/spv_validation/Cargo.toml b/mm2src/mm2_bitcoin/spv_validation/Cargo.toml index 53e840955b..1c3bd46ef7 100644 --- a/mm2src/mm2_bitcoin/spv_validation/Cargo.toml +++ b/mm2src/mm2_bitcoin/spv_validation/Cargo.toml @@ -8,20 +8,20 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true chain = {path = "../chain"} -derive_more = "0.99" +derive_more.workspace = true keys = {path = "../keys"} primitives = { path = "../primitives" } -ripemd160 = "0.9.0" -rustc-hex = "2" -serde = "1.0" +ripemd160.workspace = true +rustc-hex.workspace = true +serde.workspace = true serialization = { path = "../serialization" } -sha2 = "0.10" +sha2.workspace = true test_helpers = { path = "../test_helpers" } [dev-dependencies] common = { path = "../../common" } -lazy_static = "1.4" -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +lazy_static.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_bitcoin/test_helpers/Cargo.toml b/mm2src/mm2_bitcoin/test_helpers/Cargo.toml index 1528506ab5..f91e832519 100644 --- a/mm2src/mm2_bitcoin/test_helpers/Cargo.toml +++ b/mm2src/mm2_bitcoin/test_helpers/Cargo.toml @@ -7,4 +7,4 @@ edition = "2018" doctest = false [dependencies] -hex = "0.4.2" +hex.workspace = true diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index b3756f9b94..d0350c766a 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -10,37 +10,38 @@ doctest = false new-db-arch = [] [dependencies] -arrayref = "0.3" -async-std = { version = "1.5", features = ["unstable"] } -async-trait = "0.1" -cfg-if = "1.0" +arrayref.workspace = true +async-std = { workspace = true, features = ["unstable"] } +async-trait.workspace = true +cfg-if.workspace = true common = { path = "../common" } -compatible-time = { version = "1.1.0", package = "web-time" } +compatible-time.workspace = true db_common = { path = "../db_common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -gstuff = { version = "0.7", features = ["nightly"] } -hex = "0.4.2" -lazy_static = "1.4" +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +gstuff.workspace = true +hex.workspace = true +lazy_static.workspace = true libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify"] } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_metrics = { path = "../mm2_metrics" } primitives = { path = "../mm2_bitcoin/primitives" } -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -serde = "1" +rand.workspace = true +serde.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } shared_ref_counter = { path = "../common/shared_ref_counter" } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +uuid.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } -timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } -wasm-bindgen-test = { version = "0.3.2" } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } +wasm-bindgen-test.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -rustls = { version = "0.21", default-features = false } -tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } -timed-map = { version = "1.3", features = ["rustc-hash"] } +mm2_io = { path = "../mm2_io" } +rustls.workspace = true +tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "net"] } +timed-map = { workspace = true, features = ["rustc-hash"] } diff --git a/mm2src/mm2_core/src/data_asker.rs b/mm2src/mm2_core/src/data_asker.rs index 2e9a125d56..eba0158b24 100644 --- a/mm2src/mm2_core/src/data_asker.rs +++ b/mm2src/mm2_core/src/data_asker.rs @@ -5,7 +5,7 @@ use derive_more::Display; use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; use mm2_err_handle::prelude::*; -use mm2_event_stream::Event; +use mm2_event_stream::{Event, StreamerId}; use ser_error_derive::SerializeErrorType; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -16,8 +16,6 @@ use timed_map::{MapKind, TimedMap}; use crate::mm_ctx::{MmArc, MmCtx}; -const EVENT_NAME: &str = "DATA_NEEDED"; - #[derive(Clone, Debug)] pub struct DataAsker { data_id: Arc, @@ -81,8 +79,12 @@ impl MmCtx { "data": data }); - self.event_stream_manager - .broadcast_all(Event::new(format!("{EVENT_NAME}:{data_type}"), input)); + self.event_stream_manager.broadcast_all(Event::new( + StreamerId::DataNeeded { + data_type: data_type.to_string(), + }, + input, + )); match receiver.timeout(timeout).await { Ok(Ok(response)) => match serde_json::from_value::(response) { diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 292dc69ca6..79da897fc4 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -38,6 +38,7 @@ cfg_native! { use mm2_metrics::MmMetricsError; use std::net::{IpAddr, SocketAddr, AddrParseError}; use std::path::{Path, PathBuf}; + use derive_more::Display; use std::sync::MutexGuard; } @@ -161,6 +162,7 @@ pub struct MmCtx { pub async_sqlite_connection: OnceLock>>, /// Links the RPC context to the P2P context to handle health check responses. pub healthcheck_response_handler: AsyncMutex>>, + pub wallet_connect: Mutex>>, } impl MmCtx { @@ -219,9 +221,12 @@ impl MmCtx { healthcheck_response_handler: AsyncMutex::new( TimedMap::new_with_map_kind(MapKind::FxHashMap).expiration_tick_cap(3), ), + wallet_connect: Mutex::new(None), } } + pub fn enable_hd(&self) -> bool { self.conf["enable_hd"].as_bool().unwrap_or(false) } + pub fn rmd160(&self) -> &H160 { lazy_static! { static ref DEFAULT: H160 = [0; 20].into(); @@ -322,7 +327,7 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] pub fn db_root(&self) -> PathBuf { path_to_db_root(self.conf["dbdir"].as_str()) } - /// MM database path. + /// MM database path. /// Defaults to a relative "DB". /// /// Can be changed via the "dbdir" configuration field, for example: @@ -348,8 +353,13 @@ impl MmCtx { /// /// Such directory isn't bound to a specific seed/wallet or address. /// Data that should be stored there is public and shared between all seeds and addresses (e.g. stats, block headers, etc...). - #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] - pub fn global_dir(&self) -> PathBuf { self.db_root().join("global") } + #[cfg(not(target_arch = "wasm32"))] + pub fn global_dir(&self) -> PathBuf { + if cfg!(not(feature = "new-db-arch")) { + return self.dbdir(); + } + self.db_root().join("global") + } /// Returns the path to wallet's data directory. /// @@ -357,8 +367,11 @@ impl MmCtx { /// For HD wallets, this `rmd160` is derived from `mm2_internal_derivation_path`. /// For Iguana, this `rmd160` is simply a hash of the seed. /// Use this directory to store seed/wallet related data rather than address related data (e.g. HD wallet accounts, HD wallet tx history, etc...) - #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + #[cfg(not(target_arch = "wasm32"))] pub fn wallet_dir(&self) -> PathBuf { + if cfg!(not(feature = "new-db-arch")) { + return self.dbdir(); + } self.db_root() .join("wallets") .join(hex::encode(self.rmd160().as_slice())) @@ -368,13 +381,12 @@ impl MmCtx { /// /// Use this directory for data related to a specific address and only that specific address (e.g. swap data, order data, etc...). /// This makes sure that when this address is activated using a different technique, this data is still accessible. - #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] - pub fn address_dir(&self, address: &str) -> Result { - let path = self.db_root().join("addresses").join(address); - if !path.exists() { - std::fs::create_dir_all(&path).map_err(AddressDataError::CreateAddressDirFailure)?; + #[cfg(not(target_arch = "wasm32"))] + pub fn address_dir(&self, address: &str) -> PathBuf { + if cfg!(not(feature = "new-db-arch")) { + return self.dbdir(); } - Ok(path) + self.db_root().join("addresses").join(address) } /// Returns a SQL connection to the global database. @@ -396,9 +408,10 @@ impl MmCtx { } /// Returns a SQL connection to the address database. - #[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] + #[cfg(not(target_arch = "wasm32"))] pub fn address_db(&self, address: &str) -> Result { - let path = self.address_dir(address)?.join("MM2.db"); + let path = self.address_dir(address).join("MM2.db"); + mm2_io::fs::create_parents(&path).map_err(|err| AddressDataError::CreateAddressDirFailure(err.into_inner()))?; log_sqlite_file_open_attempt(&path); let connection = Connection::open(path).map_err(AddressDataError::SqliteConnectionFailure)?; Ok(connection) @@ -416,6 +429,29 @@ impl MmCtx { netid as u16 } + pub fn disable_p2p(&self) -> bool { + if let Some(disable_p2p) = self.conf["disable_p2p"].as_bool() { + return disable_p2p; + } + + let default = !self.conf["is_bootstrap_node"].as_bool().unwrap_or(false) + && self.conf["seednodes"].as_array().is_none() + && !self.p2p_in_memory(); + + default + } + + pub fn is_bootstrap_node(&self) -> bool { + if let Some(is_bootstrap_node) = self.conf["is_bootstrap_node"].as_bool() { + return is_bootstrap_node; + } + + let default = !self.conf["disable_p2p"].as_bool().unwrap_or(false) + && self.conf["seednodes"].as_array().map_or(true, |t| t.is_empty()); + + default + } + pub fn p2p_in_memory(&self) -> bool { self.conf["p2p_in_memory"].as_bool().unwrap_or(false) } pub fn p2p_in_memory_port(&self) -> Option { self.conf["p2p_in_memory_port"].as_u64() } @@ -533,7 +569,8 @@ impl Drop for MmCtx { } } -#[cfg(all(feature = "new-db-arch", not(target_arch = "wasm32")))] +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, Display)] pub enum AddressDataError { CreateAddressDirFailure(std::io::Error), SqliteConnectionFailure(db_common::sqlite::rusqlite::Error), @@ -709,7 +746,7 @@ impl MmArc { } } - /// Tries getting access to the MM context. + /// Tries getting access to the MM context. /// Fails if an invalid MM context handler is passed (no such context or dropped context). #[track_caller] pub fn from_ffi_handle(ffi_handle: u32) -> Result { diff --git a/mm2src/mm2_db/Cargo.toml b/mm2src/mm2_db/Cargo.toml index 5f5374acad..0bfa1795ad 100644 --- a/mm2src/mm2_db/Cargo.toml +++ b/mm2src/mm2_db/Cargo.toml @@ -7,24 +7,24 @@ edition = "2021" doctest = false [target.'cfg(target_arch = "wasm32")'.dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } -derive_more = "0.99" +derive_more.workspace = true enum_derives = { path = "../derives/enum_derives" } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -itertools = "0.10" -hex = "0.4.2" -js-sys = "0.3.27" -lazy_static = "1.4" +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +itertools.workspace = true +hex.workspace = true +js-sys.workspace = true +lazy_static.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_number = { path = "../mm2_number" } -num-traits = "0.2" +num-traits.workspace = true primitives = { path = "../mm2_bitcoin/primitives" } -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.2" } -web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbCursorDirection", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "WebSocket"] } +rand.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, 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_err_handle/Cargo.toml b/mm2src/mm2_err_handle/Cargo.toml index 0e2faaa8d2..247ad2d161 100644 --- a/mm2src/mm2_err_handle/Cargo.toml +++ b/mm2src/mm2_err_handle/Cargo.toml @@ -7,12 +7,12 @@ edition = "2018" doctest = false [dependencies] -futures01 = { version = "0.1", package = "futures" } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -derive_more = "0.99" -itertools = "0.10" -serde = { version = "1.0", features = ["derive"] } +futures01.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +derive_more.workspace = true +itertools.workspace = true +serde = { workspace = true, features = ["derive"] } ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } common = { path = "../common" } -http = "0.2" +http.workspace = true diff --git a/mm2src/mm2_eth/Cargo.toml b/mm2src/mm2_eth/Cargo.toml index 5992896424..7c4e2e68d2 100644 --- a/mm2src/mm2_eth/Cargo.toml +++ b/mm2src/mm2_eth/Cargo.toml @@ -7,13 +7,13 @@ edition = "2021" doctest = false [dependencies] -ethabi = { version = "17.0.0" } -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -hex = "0.4.2" -indexmap = "1.7.0" -itertools = "0.10" +ethabi.workspace = true +ethkey.workspace = true +hex.workspace = true +indexmap.workspace = true +itertools.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } -secp256k1 = { version = "0.20", features = ["recovery"] } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false } +secp256k1 = { workspace = true, features = ["recovery"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +web3 = { workspace = true, default-features = false } diff --git a/mm2src/mm2_event_stream/Cargo.toml b/mm2src/mm2_event_stream/Cargo.toml index 5b1677fa0e..d6787e4346 100644 --- a/mm2src/mm2_event_stream/Cargo.toml +++ b/mm2src/mm2_event_stream/Cargo.toml @@ -4,17 +4,17 @@ version = "0.1.0" edition = "2021" [dependencies] -async-trait = "0.1" -cfg-if = "1.0" +async-trait.workspace = true +cfg-if.workspace = true common = { path = "../common" } -futures = { version = "0.3", default-features = false } -parking_lot = "0.12" -serde = { version = "1", features = ["derive", "rc"] } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -tokio = "1.20" +futures.workspace = true +parking_lot = { workspace = true } +serde = { workspace = true, features = ["derive", "rc"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +tokio.workspace = true [dev-dependencies] -tokio = { version = "1.20", features = ["macros"] } +tokio = { workspace = true, features = ["macros"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen-test = { version = "0.3.2" } +wasm-bindgen-test.workspace = true diff --git a/mm2src/mm2_event_stream/src/event.rs b/mm2src/mm2_event_stream/src/event.rs index 306bbc9e49..d652b75f09 100644 --- a/mm2src/mm2_event_stream/src/event.rs +++ b/mm2src/mm2_event_stream/src/event.rs @@ -1,13 +1,13 @@ +use crate::StreamerId; use serde_json::Value as Json; // Note `Event` shouldn't be `Clone`able, but rather Arc/Rc wrapped and then shared. // This is only for testing. /// Multi-purpose/generic event type that can easily be used over the event streaming #[cfg_attr(any(test, target_arch = "wasm32"), derive(Clone, Debug, PartialEq))] -#[derive(Default)] pub struct Event { /// The type of the event (balance, network, swap, etc...). - event_type: String, + streamer_id: StreamerId, /// The message to be sent to the client. message: Json, /// Indicating whether this event is an error event or a normal one. @@ -17,9 +17,9 @@ pub struct Event { impl Event { /// Creates a new `Event` instance with the specified event type and message. #[inline(always)] - pub fn new(streamer_id: String, message: Json) -> Self { + pub fn new(streamer_id: StreamerId, message: Json) -> Self { Self { - event_type: streamer_id, + streamer_id, message, error: false, } @@ -27,21 +27,25 @@ impl Event { /// Create a new error `Event` instance with the specified error event type and message. #[inline(always)] - pub fn err(streamer_id: String, message: Json) -> Self { + pub fn err(streamer_id: StreamerId, message: Json) -> Self { Self { - event_type: streamer_id, + streamer_id, message, error: true, } } - /// Returns the `event_type` (the ID of the streamer firing this event). + /// Returns whether this event is an error or not #[inline(always)] - pub fn origin(&self) -> &str { &self.event_type } + pub fn is_error(&self) -> bool { self.error } + + /// Returns the `streamer_id` (the ID of the streamer firing this event). + #[inline(always)] + pub fn origin(&self) -> &StreamerId { &self.streamer_id } /// Returns the event type and message as a pair. pub fn get(&self) -> (String, &Json) { let prefix = if self.error { "ERROR:" } else { "" }; - (format!("{prefix}{}", self.event_type), &self.message) + (format!("{prefix}{}", self.streamer_id), &self.message) } } diff --git a/mm2src/mm2_event_stream/src/lib.rs b/mm2src/mm2_event_stream/src/lib.rs index db4587a77a..25a5758fff 100644 --- a/mm2src/mm2_event_stream/src/lib.rs +++ b/mm2src/mm2_event_stream/src/lib.rs @@ -2,9 +2,11 @@ pub mod configuration; pub mod event; pub mod manager; pub mod streamer; +pub mod streamer_ids; // Re-export important types. pub use configuration::EventStreamingConfiguration; pub use event::Event; pub use manager::{StreamingManager, StreamingManagerError}; pub use streamer::{Broadcaster, EventStreamer, NoDataIn, StreamHandlerInput}; +pub use streamer_ids::StreamerId; diff --git a/mm2src/mm2_event_stream/src/manager.rs b/mm2src/mm2_event_stream/src/manager.rs index b480ddd070..1bef846ef5 100644 --- a/mm2src/mm2_event_stream/src/manager.rs +++ b/mm2src/mm2_event_stream/src/manager.rs @@ -4,7 +4,7 @@ use std::ops::{Deref, DerefMut}; use std::sync::Arc; use crate::streamer::spawn; -use crate::{Event, EventStreamer}; +use crate::{Event, EventStreamer, StreamerId}; use common::executor::abortable_queue::WeakSpawner; use common::log::{error, LogOnError}; @@ -62,7 +62,7 @@ impl StreamerInfo { #[derive(Debug)] struct ClientInfo { /// The streamers the client is listening to. - listening_to: HashSet, + listening_to: HashSet, /// The communication/stream-out channel to the client. // NOTE: Here we are using `tokio`'s `mpsc` because the one in `futures` have some extra feature // (ref: https://users.rust-lang.org/t/why-does-try-send-from-crate-futures-require-mut-self/100389). @@ -80,11 +80,11 @@ impl ClientInfo { } } - fn add_streamer(&mut self, streamer_id: String) { self.listening_to.insert(streamer_id); } + fn add_streamer(&mut self, streamer_id: StreamerId) { self.listening_to.insert(streamer_id); } - fn remove_streamer(&mut self, streamer_id: &str) { self.listening_to.remove(streamer_id); } + fn remove_streamer(&mut self, streamer_id: &StreamerId) { self.listening_to.remove(streamer_id); } - fn listens_to(&self, streamer_id: &str) -> bool { self.listening_to.contains(streamer_id) } + fn listens_to(&self, streamer_id: &StreamerId) -> bool { self.listening_to.contains(streamer_id) } fn send_event(&self, event: Arc) { // Only `try_send` here. If the channel is full (client is slow), the message @@ -97,7 +97,7 @@ impl ClientInfo { #[derive(Default, Debug)] struct StreamingManagerInner { /// A map from streamer IDs to their communication channels (if present) and shutdown handles. - streamers: HashMap, + streamers: HashMap, /// An inverse map from client IDs to the streamers they are listening to and the communication channel with the client. clients: HashMap, } @@ -118,7 +118,7 @@ impl StreamingManager { client_id: u64, streamer: impl EventStreamer, spawner: WeakSpawner, - ) -> Result { + ) -> Result { let streamer_id = streamer.streamer_id(); // Remove the streamer if it died for some reason. self.remove_streamer_if_down(&streamer_id); @@ -173,7 +173,7 @@ impl StreamingManager { } /// Sends data to a streamer with `streamer_id`. - pub fn send(&self, streamer_id: &str, data: T) -> Result<(), StreamingManagerError> { + pub fn send(&self, streamer_id: &StreamerId, data: T) -> Result<(), StreamingManagerError> { let this = self.read(); let streamer_info = this .streamers @@ -192,7 +192,7 @@ impl StreamingManager { /// `data_fn` will only be evaluated if the streamer is found and accepts an input. pub fn send_fn( &self, - streamer_id: &str, + streamer_id: &StreamerId, data_fn: impl FnOnce() -> T, ) -> Result<(), StreamingManagerError> { let this = self.read(); @@ -207,7 +207,7 @@ impl StreamingManager { } /// Stops streaming from the streamer with `streamer_id` to the client with `client_id`. - pub fn stop(&self, client_id: u64, streamer_id: &str) -> Result<(), StreamingManagerError> { + pub fn stop(&self, client_id: u64, streamer_id: &StreamerId) -> Result<(), StreamingManagerError> { let mut this = self.write(); let client_info = this .clients @@ -312,7 +312,7 @@ impl StreamingManager { /// Aside from us shutting down a streamer when all its clients are disconnected, /// the streamer might die by itself (e.g. the spawner it was spawned with aborted). /// In this case, we need to remove the streamer and de-list it from all clients. - fn remove_streamer_if_down(&self, streamer_id: &str) { + fn remove_streamer_if_down(&self, streamer_id: &StreamerId) { let mut this = self.write(); let Some(streamer_info) = this.streamers.get(streamer_id) else { return; @@ -400,7 +400,12 @@ mod tests { let manager = StreamingManager::default(); let mut client1 = manager.new_client(1).unwrap(); let mut client2 = manager.new_client(2).unwrap(); - let event = Event::new("test".to_string(), json!("test")); + let event = Event::new( + StreamerId::ForTesting { + test_streamer: "test".to_string(), + }, + json!("test"), + ); // Broadcast the event to all clients. manager.broadcast_all(event.clone()); @@ -440,7 +445,7 @@ mod tests { // The streamer should send an event every 0.1s. Wait for 0.15s for safety. Timer::sleep(0.15).await; let event = client1.try_recv().unwrap(); - assert_eq!(event.origin(), streamer_id); + assert_eq!(event.origin(), &streamer_id); } // The other client shouldn't have received any events. @@ -472,7 +477,7 @@ mod tests { Timer::sleep(0.1).await; // The streamer should broadcast some event to the subscribed clients. let event = client1.try_recv().unwrap(); - assert_eq!(event.origin(), streamer_id); + assert_eq!(event.origin(), &streamer_id); // It's an echo streamer, so the message should be the same. assert_eq!(event.get().1, &json!(msg)); } diff --git a/mm2src/mm2_event_stream/src/streamer.rs b/mm2src/mm2_event_stream/src/streamer.rs index 6c319cb89c..dee79af8d1 100644 --- a/mm2src/mm2_event_stream/src/streamer.rs +++ b/mm2src/mm2_event_stream/src/streamer.rs @@ -1,6 +1,6 @@ use std::any::{self, Any}; -use crate::{Event, StreamingManager}; +use crate::{Event, StreamerId, StreamingManager}; use common::executor::{abortable_queue::WeakSpawner, AbortSettings, SpawnAbortable}; use common::log::{error, info}; @@ -25,7 +25,7 @@ where /// Returns a human readable unique identifier for the event streamer. /// No other event streamer should have the same identifier. - fn streamer_id(&self) -> String; + fn streamer_id(&self) -> StreamerId; /// Event handler that is responsible for broadcasting event data to the streaming channels. /// @@ -129,7 +129,11 @@ pub mod test_utils { impl EventStreamer for PeriodicStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { "periodic_streamer".to_string() } + fn streamer_id(&self) -> StreamerId { + StreamerId::ForTesting { + test_streamer: "periodic_streamer".to_string(), + } + } async fn handle( self, @@ -152,7 +156,11 @@ pub mod test_utils { impl EventStreamer for ReactiveStreamer { type DataInType = String; - fn streamer_id(&self) -> String { "reactive_streamer".to_string() } + fn streamer_id(&self) -> StreamerId { + StreamerId::ForTesting { + test_streamer: "reactive_streamer".to_string(), + } + } async fn handle( self, @@ -175,7 +183,11 @@ pub mod test_utils { impl EventStreamer for InitErrorStreamer { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { "init_error_streamer".to_string() } + fn streamer_id(&self) -> StreamerId { + StreamerId::ForTesting { + test_streamer: "init_error_streamer".to_string(), + } + } async fn handle( self, diff --git a/mm2src/mm2_event_stream/src/streamer_ids.rs b/mm2src/mm2_event_stream/src/streamer_ids.rs new file mode 100644 index 0000000000..d019b9a9e0 --- /dev/null +++ b/mm2src/mm2_event_stream/src/streamer_ids.rs @@ -0,0 +1,129 @@ +use serde::de::{self, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; + +const NETWORK: &str = "NETWORK"; +const HEARTBEAT: &str = "HEARTBEAT"; +const SWAP_STATUS: &str = "SWAP_STATUS"; +const ORDER_STATUS: &str = "ORDER_STATUS"; + +const TASK_PREFIX: &str = "TASK:"; +const BALANCE_PREFIX: &str = "BALANCE:"; +const TX_HISTORY_PREFIX: &str = "TX_HISTORY:"; +const FEE_ESTIMATION_PREFIX: &str = "FEE_ESTIMATION:"; +const DATA_NEEDED_PREFIX: &str = "DATA_NEEDED:"; +const ORDERBOOK_UPDATE_PREFIX: &str = "ORDERBOOK_UPDATE:"; +#[cfg(any(test, target_arch = "wasm32"))] +const FOR_TESTING_PREFIX: &str = "TEST_STREAMER:"; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum StreamerId { + Network, + Heartbeat, + SwapStatus, + OrderStatus, + Task { + task_id: u64, // TODO: should be TaskId (from rpc_task) + }, + Balance { + coin: String, + }, + DataNeeded { + data_type: String, + }, + TxHistory { + coin: String, + }, + FeeEstimation { + coin: String, + }, + OrderbookUpdate { + topic: String, + }, + #[cfg(any(test, target_arch = "wasm32"))] + ForTesting { + test_streamer: String, + }, +} + +impl fmt::Display for StreamerId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StreamerId::Network => write!(f, "{}", NETWORK), + StreamerId::Heartbeat => write!(f, "{}", HEARTBEAT), + StreamerId::SwapStatus => write!(f, "{}", SWAP_STATUS), + StreamerId::OrderStatus => write!(f, "{}", ORDER_STATUS), + StreamerId::Task { task_id } => write!(f, "{}{}", TASK_PREFIX, task_id), + StreamerId::Balance { coin } => write!(f, "{}{}", BALANCE_PREFIX, coin), + StreamerId::TxHistory { coin } => write!(f, "{}{}", TX_HISTORY_PREFIX, coin), + StreamerId::FeeEstimation { coin } => write!(f, "{}{}", FEE_ESTIMATION_PREFIX, coin), + StreamerId::DataNeeded { data_type } => write!(f, "{}{}", DATA_NEEDED_PREFIX, data_type), + StreamerId::OrderbookUpdate { topic } => write!(f, "{}{}", ORDERBOOK_UPDATE_PREFIX, topic), + #[cfg(any(test, target_arch = "wasm32"))] + StreamerId::ForTesting { test_streamer } => write!(f, "{}{}", FOR_TESTING_PREFIX, test_streamer), + } + } +} + +impl Serialize for StreamerId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for StreamerId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StreamerIdVisitor; + + impl<'de> Visitor<'de> for StreamerIdVisitor { + type Value = StreamerId; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representing a StreamerId") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + NETWORK => Ok(StreamerId::Network), + HEARTBEAT => Ok(StreamerId::Heartbeat), + SWAP_STATUS => Ok(StreamerId::SwapStatus), + ORDER_STATUS => Ok(StreamerId::OrderStatus), + v if v.starts_with(TASK_PREFIX) => Ok(StreamerId::Task { + task_id: v[TASK_PREFIX.len()..].parse().map_err(de::Error::custom)?, + }), + v if v.starts_with(BALANCE_PREFIX) => Ok(StreamerId::Balance { + coin: v[BALANCE_PREFIX.len()..].to_string(), + }), + v if v.starts_with(TX_HISTORY_PREFIX) => Ok(StreamerId::TxHistory { + coin: v[TX_HISTORY_PREFIX.len()..].to_string(), + }), + v if v.starts_with(FEE_ESTIMATION_PREFIX) => Ok(StreamerId::FeeEstimation { + coin: v[FEE_ESTIMATION_PREFIX.len()..].to_string(), + }), + v if v.starts_with(DATA_NEEDED_PREFIX) => Ok(StreamerId::DataNeeded { + data_type: v[DATA_NEEDED_PREFIX.len()..].to_string(), + }), + v if v.starts_with(ORDERBOOK_UPDATE_PREFIX) => Ok(StreamerId::OrderbookUpdate { + topic: v[ORDERBOOK_UPDATE_PREFIX.len()..].to_string(), + }), + #[cfg(any(test, target_arch = "wasm32"))] + v if v.starts_with(FOR_TESTING_PREFIX) => Ok(StreamerId::ForTesting { + test_streamer: v[FOR_TESTING_PREFIX.len()..].to_string(), + }), + _ => Err(de::Error::custom(format!("Invalid StreamerId: {}", value))), + } + } + } + + deserializer.deserialize_str(StreamerIdVisitor) + } +} diff --git a/mm2src/mm2_git/Cargo.toml b/mm2src/mm2_git/Cargo.toml index ee06101400..cf90802e90 100644 --- a/mm2src/mm2_git/Cargo.toml +++ b/mm2src/mm2_git/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } -http = "0.2" +http.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } mm2_net = { path = "../mm2_net" } -serde = "1" +serde.workspace = true serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_gui_storage/Cargo.toml b/mm2src/mm2_gui_storage/Cargo.toml index 8beb78b863..96ded12dd2 100644 --- a/mm2src/mm2_gui_storage/Cargo.toml +++ b/mm2src/mm2_gui_storage/Cargo.toml @@ -7,24 +7,24 @@ edition = "2021" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } db_common = { path = "../db_common" } -derive_more = "0.99" +derive_more.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_number = { path = "../mm2_number" } rpc = { path = "../mm2_bitcoin/rpc" } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_repr = "0.1" +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_repr.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } [target.'cfg(target_arch = "wasm32")'.dependencies] mm2_db = { path = "../mm2_db" } mm2_test_helpers = { path = "../mm2_test_helpers" } -wasm-bindgen-test = { version = "0.3.2" } +wasm-bindgen-test.workspace = true [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } diff --git a/mm2src/mm2_io/Cargo.toml b/mm2src/mm2_io/Cargo.toml index 925e0944b0..b8b5bc101d 100644 --- a/mm2src/mm2_io/Cargo.toml +++ b/mm2src/mm2_io/Cargo.toml @@ -7,17 +7,12 @@ edition = "2018" doctest = false [dependencies] +async-std = { workspace = true, features = ["unstable"] } common = { path = "../common" } +gstuff.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -derive_more = "0.99" -async-std = { version = "1.5", features = ["unstable"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +rand.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +derive_more.workspace = true diff --git a/mm2src/mm2_io/src/file_lock.rs b/mm2src/mm2_io/src/file_lock.rs index 9a2e04bdef..334919a44f 100644 --- a/mm2src/mm2_io/src/file_lock.rs +++ b/mm2src/mm2_io/src/file_lock.rs @@ -4,6 +4,8 @@ use gstuff::now_float; use mm2_err_handle::prelude::*; use std::path::{Path, PathBuf}; +use crate::fs::create_parents; + pub type FileLockResult = std::result::Result>; #[derive(Debug, Display)] @@ -45,6 +47,10 @@ fn read_timestamp(path: &dyn AsRef) -> FileLockResult> { impl> FileLock { pub fn lock(lock_path: T, ttl_sec: f64) -> FileLockResult>> { + create_parents(&lock_path.as_ref()).map_err(|e| FileLockError::ErrorCreatingLockFile { + path: lock_path.as_ref().to_path_buf(), + error: e.to_string(), + })?; match std::fs::OpenOptions::new() .write(true) .create_new(true) diff --git a/mm2src/mm2_io/src/fs.rs b/mm2src/mm2_io/src/fs.rs index 960886a2b2..e24ee04284 100644 --- a/mm2src/mm2_io/src/fs.rs +++ b/mm2src/mm2_io/src/fs.rs @@ -111,11 +111,6 @@ pub async fn remove_file_async>(path: P) -> IoResult<()> { Ok(async_fs::remove_file(path.as_ref()).await?) } -pub fn write(path: &dyn AsRef, contents: &dyn AsRef<[u8]>) -> Result<(), String> { - try_s!(fs::write(path, contents)); - Ok(()) -} - /// Read a folder asynchronously and return a list of files. pub async fn read_dir_async>(dir: P) -> IoResult> { use futures::StreamExt; @@ -276,10 +271,86 @@ where read_files_with_extension(dir_path, "json").await } +/// Creates all the directories along the path to a file if they do not exist. +pub fn create_parents(path: &impl AsRef) -> IoResult<()> { + let parent_dir = path.as_ref().parent(); + let Some(parent_dir) = parent_dir else { + return MmError::err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} has no parent directory", path.as_ref().display()), + )); + }; + match fs::metadata(parent_dir) { + // Path exists, make sure it's a directory (and not a file for example). + Ok(metadata) => { + if !metadata.is_dir() { + return MmError::err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} is not a directory", parent_dir.display()), + )); + } + }, + // This path doesn't exist, create it. + Err(_) => fs::create_dir_all(parent_dir)?, + } + Ok(()) +} + +/// Similar to [`create_parents`], but using non-blocking async IO operations. +/// +/// Creates all the directories along the path to a file if they do not exist. +pub async fn create_parents_async(path: &Path) -> IoResult<()> { + let parent_dir = path.parent(); + let Some(parent_dir) = parent_dir else { + return MmError::err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} has no parent directory", path.display()), + )); + }; + match async_fs::metadata(parent_dir).await { + // Path exists, make sure it's a directory (and not a file, for instance). + Ok(metadata) => { + if !metadata.is_dir() { + return MmError::err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{} is not a directory", parent_dir.display()), + )); + } + }, + // This path doesn't exist, try to create it. + Err(_) => async_fs::create_dir_all(parent_dir).await?, + } + Ok(()) +} + +/// Writes the `content` to the file at `path`. +/// +/// This also creates any intermediary directories up to the file itself if they do not exist. +/// If `use_tmp_file` is true, it writes to a temporary file first and then renames it to the final file name +/// to ensure atomicity. +pub fn write(path: &impl AsRef, content: &[u8], use_tmp_file: bool) -> IoResult<()> { + // Create all the directories in the path. + create_parents(path)?; + let path_tmp = if use_tmp_file { + PathBuf::from(format!("{}.tmp", path.as_ref().display())) + } else { + path.as_ref().to_path_buf() + }; + // Write the file content into the temp file and then rename the temp file into the desired name. + fs::write(&path_tmp, content)?; + if use_tmp_file { + fs::rename(&path_tmp, path.as_ref()).error_log_passthrough()? + } + Ok(()) +} + pub async fn write_json(t: &T, path: &Path, use_tmp_file: bool) -> FsJsonResult<()> where T: Serialize, { + create_parents_async(path) + .await + .map_err(|err| FsJsonError::IoWriting(err.into_inner()))?; let content = json::to_vec(t).map_to_mm(FsJsonError::Serializing)?; let path_tmp = if use_tmp_file { diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 28291cb210..b2fe520cfb 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -25,43 +25,44 @@ enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] sepolia-maker-swap-v2-tests = [] sepolia-taker-swap-v2-tests = [] test-ext-api = ["trading_api/test-ext-api"] -new-db-arch = [] # A temporary feature to integrate the new db architecture incrementally +new-db-arch = ["mm2_core/new-db-arch"] # A temporary feature to integrate the new db architecture incrementally [dependencies] -async-std = { version = "1.5", features = ["unstable"] } -async-trait = "0.1" +async-std = { workspace = true, features = ["unstable"] } +async-trait.workspace = true bitcrypto = { path = "../mm2_bitcoin/crypto" } -blake2 = "0.10.6" -bytes = "0.4" +blake2.workspace = true +bytes.workspace = true chain = { path = "../mm2_bitcoin/chain" } -chrono = "0.4" -cfg-if = "1.0" +chrono.workspace = true +cfg-if.workspace = true coins = { path = "../coins" } coins_activation = { path = "../coins_activation" } common = { path = "../common" } -compatible-time = { version = "1.1.0", package = "web-time" } -crc32fast = { version = "1.3.2", features = ["std", "nightly"] } -crossbeam = "0.8" +compatible-time.workspace = true +crc32fast.workspace = true +crossbeam.workspace = true crypto = { path = "../crypto" } db_common = { path = "../db_common" } -derive_more = "0.99" -either = "1.6" -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +derive_more.workspace = true +either.workspace = true +ethereum-types.workspace = true enum_derives = { path = "../derives/enum_derives" } -enum-primitive-derive = "0.2" -futures01 = { version = "0.1", package = "futures" } -futures = { version = "0.3.1", package = "futures", features = ["compat", "async-await"] } -gstuff = { version = "0.7", features = ["nightly"] } -hash256-std-hasher = "0.15.2" -hash-db = "0.15.2" -hex = "0.4.2" -http = "0.2" +enum-primitive-derive.workspace = true +futures01.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } +gstuff.workspace = true +hash256-std-hasher.workspace = true +hash-db.workspace = true +hex.workspace = true +http.workspace = true hw_common = { path = "../hw_common" } -itertools = "0.10" +itertools.workspace = true +kdf_walletconnect = { path = "../kdf_walletconnect" } keys = { path = "../mm2_bitcoin/keys" } -lazy_static = "1.4" +lazy_static.workspace = true # ledger = { path = "../ledger" } -libc = "0.2" +libc.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } @@ -71,61 +72,62 @@ mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net"} mm2_number = { path = "../mm2_number" } -mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} +mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"] } mm2_state_machine = { path = "../mm2_state_machine" } trading_api = { path = "../trading_api" } -num-traits = "0.2" -parity-util-mem = "0.11" -parking_lot = { version = "0.12.0", features = ["nightly"] } +num-traits.workspace = true +parity-util-mem.workspace = true +parking_lot = { workspace = true, features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } -primitive-types = "0.11.1" -prost = "0.12" -rand = { version = "0.7", features = ["std", "small_rng"] } +primitive-types.workspace = true +prost.workspace = true +rand = { workspace = true, features = ["std", "small_rng"] } rand6 = { version = "0.6", package = "rand" } -rmp-serde = "0.14.3" +rmp-serde.workspace = true rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } script = { path = "../mm2_bitcoin/script" } -secp256k1 = { version = "0.20", features = ["rand"] } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_derive = "1.0" +secp256k1 = { workspace = true, features = ["rand"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_derive.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } spv_validation = { path = "../mm2_bitcoin/spv_validation" } -sp-runtime-interface = { version = "6.0.0", default-features = false, features = ["disable_target_static_assertions"] } -sp-trie = { version = "6.0", default-features = false } -trie-db = { version = "0.23.1", default-features = false } -trie-root = "0.16.0" -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +sp-runtime-interface.workspace = true +sp-trie.workspace = true +tempfile.workspace = true +trie-db.workspace = true +trie-root.workspace = true +uuid.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Removing this causes `wasm-pack` to fail when starting a web session (even though we don't use this crate). # Investigate why. -instant = { version = "0.1.12", features = ["wasm-bindgen"] } -js-sys = { version = "0.3.27" } +instant = { workspace = true, features = ["wasm-bindgen"] } +js-sys.workspace = true mm2_db = { path = "../mm2_db" } mm2_test_helpers = { path = "../mm2_test_helpers" } -timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.55", features = ["console"] } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys = { workspace = true, features = ["console"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -dirs = { version = "1" } -futures-rustls = { version = "0.24" } -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } -rcgen = "0.10" -rustls = { version = "0.21", default-features = false } -rustls-pemfile = "1.0.2" -timed-map = { version = "1.3", features = ["rustc-hash"] } -tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net", "signal"] } +dirs.workspace = true +futures-rustls.workspace = true +hyper = { workspace = true, features = ["client", "http2", "server", "tcp"] } +rcgen.workspace = true +rustls = { workspace = true, default-features = false } +rustls-pemfile.workspace = true +timed-map = { workspace = true, features = ["rustc-hash"] } +tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "net", "signal"] } [target.'cfg(windows)'.dependencies] -winapi = "0.3" +winapi.workspace = true [dev-dependencies] coins = { path = "../coins", features = ["for-tests"] } @@ -133,19 +135,19 @@ coins_activation = { path = "../coins_activation", features = ["for-tests"] } common = { path = "../common", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } trading_api = { path = "../trading_api", features = ["for-tests"] } -mocktopus = "0.8.0" -testcontainers = "0.15.0" -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["http-rustls-tls"] } -ethabi = { version = "17.0.0" } -rlp = { version = "0.5" } -ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -rustc-hex = "2" -sia-rust = { git = "https://github.com/KomodoPlatform/sia-rust", rev = "9f188b80b3213bcb604e7619275251ce08fae808" } -url = { version = "2.2.2", features = ["serde"] } +mocktopus.workspace = true +testcontainers.workspace = true +web3 = { workspace = true, default-features = false, features = ["http-rustls-tls"] } +ethabi.workspace = true +rlp.workspace = true +ethcore-transaction.workspace = true +rustc-hex.workspace = true +sia-rust.workspace = true +url.workspace = true [build-dependencies] -chrono = "0.4" -gstuff = { version = "0.7", features = ["nightly"] } +chrono.workspace = true +gstuff.workspace = true prost-build = { version = "0.12", default-features = false } -regex = "1" +regex.workspace = true diff --git a/mm2src/mm2_main/src/heartbeat_event.rs b/mm2src/mm2_main/src/heartbeat_event.rs index a2c46f2fb6..6645f407fe 100644 --- a/mm2src/mm2_main/src/heartbeat_event.rs +++ b/mm2src/mm2_main/src/heartbeat_event.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use common::executor::Timer; use futures::channel::oneshot; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use serde::Deserialize; #[derive(Deserialize)] @@ -31,7 +31,7 @@ impl HeartbeatEvent { impl EventStreamer for HeartbeatEvent { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { "HEARTBEAT".to_string() } + fn streamer_id(&self) -> StreamerId { StreamerId::Heartbeat } async fn handle( self, diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index fc37bc8624..4877ef8de8 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -40,8 +40,8 @@ use mm2_err_handle::common_errors::InternalError; use mm2_err_handle::prelude::*; use mm2_libp2p::behaviours::atomicdex::{generate_ed25519_keypair, GossipsubConfig, DEPRECATED_NETID_LIST}; use mm2_libp2p::p2p_ctx::P2PContext; -use mm2_libp2p::{spawn_gossipsub, AdexBehaviourError, NodeType, RelayAddress, RelayAddressError, SeedNodeInfo, - SwarmRuntime, WssCerts}; +use mm2_libp2p::{spawn_gossipsub, AdexBehaviourError, NodeType, RelayAddress, RelayAddressError, SwarmRuntime, + WssCerts}; use mm2_metrics::mm_gauge; use rpc_task::RpcTaskError; use serde_json as json; @@ -69,45 +69,6 @@ cfg_wasm32! { pub mod init_metamask; } -const DEFAULT_NETID_SEEDNODES: &[SeedNodeInfo] = &[ - SeedNodeInfo::new( - "12D3KooWHKkHiNhZtKceQehHhPqwU5W1jXpoVBgS1qst899GjvTm", - "viserion.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWAToxtunEBWCoAHjefSv74Nsmxranw8juy3eKEdrQyGRF", - "rhaegal.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWSmEi8ypaVzFA1AGde2RjxNW5Pvxw3qa2fVe48PjNs63R", - "drogon.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWMrjLmrv8hNgAoVf1RfumfjyPStzd4nv5XL47zN4ZKisb", - "falkor.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWEWzbYcosK2JK9XpFXzumfgsWJW1F7BZS15yLTrhfjX2Z", - "smaug.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWJWBnkVsVNjiqUEPjLyHpiSmQVAJ5t6qt1Txv5ctJi9Xd", - "balerion.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWPR2RoPi19vQtLugjCdvVmCcGLP2iXAzbDfP3tp81ZL4d", - "kalessin.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWEaZpH61H4yuQkaNG5AsyGdpBhKRppaLdAY52a774ab5u", - "seed01.kmdefi.net", - ), - SeedNodeInfo::new( - "12D3KooWAd5gPXwX7eDvKWwkr2FZGfoJceKDCA53SHmTFFVkrN7Q", - "seed02.kmdefi.net", - ), -]; - pub type P2PResult = Result>; pub type MmInitResult = Result>; @@ -132,8 +93,10 @@ pub enum P2PInitError { #[display(fmt = "Invalid relay address: '{}'", _0)] InvalidRelayAddress(RelayAddressError), #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] - #[display(fmt = "WASM node can be a seed if only 'p2p_in_memory' is true")] + #[display(fmt = "WASM node can be a seed only if 'p2p_in_memory' is true")] WasmNodeCannotBeSeed, + #[display(fmt = "Precheck failed: '{}'", reason)] + Precheck { reason: String }, #[display(fmt = "Internal error: '{}'", _0)] Internal(String), } @@ -150,7 +113,6 @@ impl From for P2PInitError { } } } - #[derive(Clone, Debug, Display, EnumFromTrait, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum MmInitError { @@ -295,31 +257,6 @@ impl MmInitError { } } -#[cfg(target_arch = "wasm32")] -fn default_seednodes(netid: u16) -> Vec { - if netid == 8762 { - DEFAULT_NETID_SEEDNODES - .iter() - .map(|SeedNodeInfo { domain, .. }| RelayAddress::Dns(domain.to_string())) - .collect() - } else { - Vec::new() - } -} - -#[cfg(not(target_arch = "wasm32"))] -fn default_seednodes(netid: u16) -> Vec { - if netid == 8762 { - DEFAULT_NETID_SEEDNODES - .iter() - .filter_map(|SeedNodeInfo { domain, .. }| mm2_net::ip_addr::addr_to_ipv4_string(domain).ok()) - .map(RelayAddress::IPv4) - .collect() - } else { - Vec::new() - } -} - #[cfg(not(target_arch = "wasm32"))] pub fn fix_directories(ctx: &MmCtx) -> MmInitResult<()> { fix_shared_dbdir(ctx)?; @@ -535,9 +472,9 @@ async fn kick_start(ctx: MmArc) -> MmInitResult<()> { Ok(()) } -fn get_p2p_key(ctx: &MmArc, i_am_seed: bool) -> P2PResult<[u8; 32]> { +fn get_p2p_key(ctx: &MmArc, is_seed_node: bool) -> P2PResult<[u8; 32]> { // TODO: Use persistent peer ID regardless the node type. - if i_am_seed { + if is_seed_node { if let Ok(crypto_ctx) = CryptoCtx::from_ctx(ctx) { let key = sha256(crypto_ctx.mm2_internal_privkey_slice()); return Ok(key.take()); @@ -549,21 +486,78 @@ fn get_p2p_key(ctx: &MmArc, i_am_seed: bool) -> P2PResult<[u8; 32]> { Ok(p2p_key) } -pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { - let i_am_seed = ctx.is_seed_node(); +fn p2p_precheck(ctx: &MmArc) -> P2PResult<()> { + let is_seed_node = ctx.is_seed_node(); + let is_bootstrap_node = ctx.is_bootstrap_node(); + let disable_p2p = ctx.disable_p2p(); + let p2p_in_memory = ctx.p2p_in_memory(); let netid = ctx.netid(); if DEPRECATED_NETID_LIST.contains(&netid) { return MmError::err(P2PInitError::InvalidNetId(NetIdError::Deprecated { netid })); } + let seednodes = seednodes(ctx)?; + + let precheck_err = |reason: &str| { + MmError::err(P2PInitError::Precheck { + reason: reason.to_owned(), + }) + }; + + if is_bootstrap_node { + if !is_seed_node { + return precheck_err("Bootstrap node must also be a seed node."); + } + + if !seednodes.is_empty() { + return precheck_err("Bootstrap node cannot have seed nodes to connect."); + } + } + + if !is_bootstrap_node && seednodes.is_empty() && !disable_p2p { + return precheck_err("Non-bootstrap node must have seed nodes configured to connect."); + } + + if disable_p2p { + if !seednodes.is_empty() { + return precheck_err("Cannot disable P2P while seed nodes are configured."); + } + + if p2p_in_memory { + return precheck_err("Cannot disable P2P while using in-memory P2P mode."); + } + + if is_seed_node { + return precheck_err("Seed nodes cannot disable P2P."); + } + } + + if is_seed_node && !CryptoCtx::is_init(ctx).unwrap_or(false) { + return precheck_err("Seed node requires a persistent identity to generate its P2P key."); + } + + Ok(()) +} + +pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { + p2p_precheck(&ctx)?; + + if ctx.disable_p2p() { + warn!("P2P is disabled. Features that require a P2P network (like swaps, peer health checks, etc.) will not work."); + return Ok(()); + } + + let is_seed_node = ctx.is_seed_node(); + let netid = ctx.netid(); + let seednodes = seednodes(&ctx)?; let ctx_on_poll = ctx.clone(); - let p2p_key = get_p2p_key(&ctx, i_am_seed)?; + let p2p_key = get_p2p_key(&ctx, is_seed_node)?; - let node_type = if i_am_seed { + let node_type = if is_seed_node { relay_node_type(&ctx).await? } else { light_node_type(&ctx)? @@ -616,7 +610,7 @@ pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { let p2p_context = P2PContext::new(cmd_tx, generate_ed25519_keypair(p2p_key)); p2p_context.store_to_mm_arc(&ctx); - let fut = p2p_event_process_loop(ctx.weak(), event_rx, i_am_seed); + let fut = p2p_event_process_loop(ctx.weak(), event_rx, is_seed_node); ctx.spawner().spawn(fut); // Listen for health check messages. @@ -626,15 +620,9 @@ pub async fn init_p2p(ctx: MmArc) -> P2PResult<()> { } fn seednodes(ctx: &MmArc) -> P2PResult> { - if ctx.conf["seednodes"].is_null() { - if ctx.p2p_in_memory() { - // If the network is in memory, there is no need to use default seednodes. - return Ok(Vec::new()); - } - return Ok(default_seednodes(ctx.netid())); - } + let seednodes_value = ctx.conf.get("seednodes").unwrap_or(&json!([])).clone(); - json::from_value(ctx.conf["seednodes"].clone()).map_to_mm(|e| P2PInitError::ErrorDeserializingConfig { + json::from_value(seednodes_value).map_to_mm(|e| P2PInitError::ErrorDeserializingConfig { field: "seednodes".to_owned(), error: e.to_string(), }) diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 18475ce191..9f57101d42 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -89,6 +89,9 @@ use crate::swap_versioning::{legacy_swap_version, SwapVersion}; #[cfg(any(test, feature = "run-docker-tests"))] use crate::lp_swap::taker_swap::FailAt; +#[cfg(feature = "ibc-routing-for-swaps")] +use coins::rpc_command::tendermint::ibc::ChannelId; + pub use best_orders::{best_orders_rpc, best_orders_rpc_v2}; use crypto::secret_hash_algo::SecretHashAlgo; pub use orderbook_depth::orderbook_depth_rpc; @@ -1195,6 +1198,8 @@ pub struct TakerRequest { pub rel_protocol_info: Option>, #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] pub swap_version: SwapVersion, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, } impl TakerRequest { @@ -1216,6 +1221,9 @@ impl TakerRequest { base_protocol_info: message.base_protocol_info, rel_protocol_info: message.rel_protocol_info, swap_version: message.swap_version, + /// TODO: Support the new protocol types. + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), } } @@ -1288,6 +1296,8 @@ pub struct TakerOrderBuilder<'a> { timeout: u64, save_in_history: bool, swap_version: u8, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, } pub enum TakerOrderBuildError { @@ -1368,6 +1378,8 @@ impl<'a> TakerOrderBuilder<'a> { timeout: TAKER_ORDER_TIMEOUT, save_in_history: true, swap_version: SWAP_VERSION_DEFAULT, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), } } @@ -1526,6 +1538,8 @@ impl<'a> TakerOrderBuilder<'a> { base_protocol_info: Some(base_protocol_info), rel_protocol_info: Some(rel_protocol_info), swap_version: SwapVersion::from(self.swap_version), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: self.order_metadata, }, matches: Default::default(), min_volume, @@ -1567,6 +1581,8 @@ impl<'a> TakerOrderBuilder<'a> { base_protocol_info: Some(base_protocol_info), rel_protocol_info: Some(rel_protocol_info), swap_version: SwapVersion::from(self.swap_version), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: self.order_metadata, }, matches: HashMap::new(), min_volume: Default::default(), @@ -1728,8 +1744,12 @@ pub struct MakerOrder { /// A custom priv key for more privacy to prevent linking orders of the same node between each other /// Commonly used with privacy coins (ARRR, ZCash, etc.) p2p_privkey: Option, + /// TODO: Move this into the `OrderMetadata` type when we are doing BC + /// on orders already. #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] pub swap_version: SwapVersion, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, } pub struct MakerOrderBuilder<'a> { @@ -1743,6 +1763,18 @@ pub struct MakerOrderBuilder<'a> { conf_settings: Option, save_in_history: bool, swap_version: u8, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, +} + +/// Contains extra and/or optional metadata (e.g., protocol-specific information) that can +/// be used for both taker and maker orders. +/// +/// TODO: `swap_version` should likely be moved into this type. +#[cfg(feature = "ibc-routing-for-swaps")] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +struct OrderMetadata { + channel_id_if_ibc_routing: Option, } pub enum MakerOrderBuildError { @@ -1893,6 +1925,8 @@ impl<'a> MakerOrderBuilder<'a> { conf_settings: None, save_in_history: true, swap_version: SWAP_VERSION_DEFAULT, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), } } @@ -1994,6 +2028,8 @@ impl<'a> MakerOrderBuilder<'a> { rel_orderbook_ticker: self.rel_orderbook_ticker, p2p_privkey, swap_version: SwapVersion::from(self.swap_version), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: self.order_metadata, }) } @@ -2019,6 +2055,8 @@ impl<'a> MakerOrderBuilder<'a> { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::from(self.swap_version), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: self.order_metadata, } } } @@ -2150,6 +2188,9 @@ impl From for MakerOrder { rel_orderbook_ticker: taker_order.rel_orderbook_ticker, p2p_privkey: taker_order.p2p_privkey, swap_version: taker_order.request.swap_version, + // TODO: Add test coverage for this once we have an integration test for this feature. + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: taker_order.request.order_metadata, }, // The "buy" taker order is recreated with reversed pair as Maker order is always considered as "sell" TakerAction::Buy => { @@ -2173,6 +2214,9 @@ impl From for MakerOrder { rel_orderbook_ticker: taker_order.base_orderbook_ticker, p2p_privkey: taker_order.p2p_privkey, swap_version: taker_order.request.swap_version, + // TODO: Add test coverage for this once we have an integration test for this feature. + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: taker_order.request.order_metadata, } }, } @@ -2225,6 +2269,8 @@ pub struct MakerReserved { pub rel_protocol_info: Option>, #[serde(default, skip_serializing_if = "SwapVersion::is_legacy")] pub swap_version: SwapVersion, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata, } impl MakerReserved { @@ -2253,6 +2299,9 @@ impl MakerReserved { base_protocol_info: message.base_protocol_info, rel_protocol_info: message.rel_protocol_info, swap_version: message.swap_version, + /// TODO: Support the new protocol types. + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), } } } @@ -3042,6 +3091,18 @@ fn lp_connect_start_bob(ctx: MmArc, maker_match: MakerMatch, maker_order: MakerO let maker_amount = maker_match.reserved.get_base_amount().clone(); let taker_amount = maker_match.reserved.get_rel_amount().clone(); + #[cfg(feature = "ibc-routing-for-swaps")] + { + let _taker_order_metadata = &maker_match.request.order_metadata; + let _maker_order_metadata = &maker_order.order_metadata; + + // TODO + // - If this is non-HTLC tendermint swap, cross-check IBC channels for routing before start. + // - Could malformed orders trick us by intentionally modfying channel IDs? + // - Unify this logic with `lp_connected_alice`. + unreachable!(); + } + // lp_connect_start_bob is called only from process_taker_connect, which returns if CryptoCtx is not initialized let crypto_ctx = CryptoCtx::from_ctx(&ctx).expect("'CryptoCtx' must be initialized already"); let raw_priv = crypto_ctx.mm2_internal_privkey_secret(); @@ -3276,6 +3337,18 @@ fn lp_connected_alice(ctx: MmArc, taker_order: TakerOrder, taker_match: TakerMat let raw_priv = crypto_ctx.mm2_internal_privkey_secret(); let my_persistent_pub = compressed_pub_key_from_priv_raw(&raw_priv.take(), ChecksumType::DSHA256).unwrap(); + #[cfg(feature = "ibc-routing-for-swaps")] + { + let _taker_order_metadata = &taker_order.request.order_metadata; + let _maker_order_metadata = &taker_match.reserved.order_metadata; + + // TODO + // - If this is non-HTLC tendermint swap, cross-check IBC channels for routing before start. + // - Could malformed orders trick us by intentionally modfying channel IDs? + // - Unify this logic with `lp_connect_start_bob`. + unreachable!(); + } + let maker_amount = taker_match.reserved.get_base_amount().clone(); let taker_amount = taker_match.reserved.get_rel_amount().clone(); @@ -3835,7 +3908,7 @@ async fn process_maker_reserved(ctx: MmArc, from_pubkey: H256Json, reserved_msg: }; ctx.event_stream_manager - .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + .send_fn(&OrderStatusStreamer::derive_streamer_id(), || { OrderStatusEvent::TakerMatch(taker_match.clone()) }) .ok(); @@ -3890,7 +3963,7 @@ async fn process_maker_connected(ctx: MmArc, from_pubkey: PublicKey, connected: } ctx.event_stream_manager - .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + .send_fn(&OrderStatusStreamer::derive_streamer_id(), || { OrderStatusEvent::TakerConnected(order_match.clone()) }) .ok(); @@ -3991,6 +4064,8 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: base_protocol_info: Some(base_coin.coin_protocol_info(None)), rel_protocol_info: Some(rel_coin.coin_protocol_info(Some(rel_amount.clone()))), swap_version: order.swap_version, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: order.order_metadata.clone(), }; let topic = order.orderbook_topic(); log::debug!("Request matched sending reserved {:?}", reserved); @@ -4004,7 +4079,7 @@ async fn process_taker_request(ctx: MmArc, from_pubkey: H256Json, taker_request: }; ctx.event_stream_manager - .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + .send_fn(&OrderStatusStreamer::derive_streamer_id(), || { OrderStatusEvent::MakerMatch(maker_match.clone()) }) .ok(); @@ -4075,7 +4150,7 @@ async fn process_taker_connect(ctx: MmArc, sender_pubkey: PublicKey, connect_msg let order_match = order_match.clone(); ctx.event_stream_manager - .send_fn(OrderStatusStreamer::derive_streamer_id(), || { + .send_fn(&OrderStatusStreamer::derive_streamer_id(), || { OrderStatusEvent::MakerConnected(order_match.clone()) }) .ok(); @@ -4107,12 +4182,9 @@ pub async fn buy(ctx: MmArc, req: Json) -> Result>, String> { let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); let base_coin = try_s!(lp_coinfind(&ctx, &input.base).await); let base_coin: MmCoinEnum = try_s!(base_coin.ok_or("Base coin is not found or inactive")); - if base_coin.wallet_only(&ctx) { - return ERR!("Base coin {} is wallet only", input.base); - } - if rel_coin.wallet_only(&ctx) { - return ERR!("Rel coin {} is wallet only", input.rel); - } + + try_s!(base_coin.pre_check_for_order_creation(&ctx, &rel_coin).await); + let my_amount = &input.volume * &input.price; try_s!( check_balance_for_taker_swap( @@ -4139,12 +4211,9 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { let base_coin = try_s!(base_coin.ok_or("Base coin is not found or inactive")); let rel_coin = try_s!(lp_coinfind(&ctx, &input.rel).await); let rel_coin = try_s!(rel_coin.ok_or("Rel coin is not found or inactive")); - if base_coin.wallet_only(&ctx) { - return ERR!("Base coin {} is wallet only", input.base); - } - if rel_coin.wallet_only(&ctx) { - return ERR!("Rel coin {} is wallet only", input.rel); - } + + try_s!(base_coin.pre_check_for_order_creation(&ctx, &rel_coin).await); + try_s!( check_balance_for_taker_swap( &ctx, @@ -4157,6 +4226,7 @@ pub async fn sell(ctx: MmArc, req: Json) -> Result>, String> { ) .await ); + let res = try_s!(lp_auto_buy(&ctx, &base_coin, &rel_coin, input).await); Ok(try_s!(Response::builder().body(res))) } @@ -4226,6 +4296,7 @@ pub async fn lp_auto_buy( rel_confs: input.rel_confs.unwrap_or_else(|| rel_coin.required_confirmations()), rel_nota: input.rel_nota.unwrap_or_else(|| rel_coin.requires_notarization()), }; + let mut order_builder = TakerOrderBuilder::new(base_coin, rel_coin) .with_base_amount(input.volume) .with_rel_amount(rel_volume) @@ -4238,10 +4309,21 @@ pub async fn lp_auto_buy( .with_save_in_history(input.save_in_history) .with_base_orderbook_ticker(ordermatch_ctx.orderbook_ticker(base_coin.ticker())) .with_rel_orderbook_ticker(ordermatch_ctx.orderbook_ticker(rel_coin.ticker())); + if !ctx.use_trading_proto_v2() { order_builder.set_legacy_swap_v(); } + // For non-HTLC Tendermint orders, include the channel information which will be used + // later from the other pair. + #[cfg(feature = "ibc-routing-for-swaps")] + if let MmCoinEnum::Tendermint(tendermint_coin) = &base_coin { + if !tendermint_coin.supports_htlc() { + let channel_id = try_s!(tendermint_coin.get_healthy_ibc_channel_to_htlc_chain().await); + order_builder.order_metadata.channel_id_if_ibc_routing = Some(channel_id); + } + } + if let Some(timeout) = input.timeout { order_builder = order_builder.with_timeout(timeout); } @@ -4936,12 +5018,7 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result return ERR!("Rel coin {} is not found", req.rel), }; - if base_coin.wallet_only(ctx) { - return ERR!("Base coin {} is wallet only", req.base); - } - if rel_coin.wallet_only(ctx) { - return ERR!("Rel coin {} is wallet only", req.rel); - } + try_s!(base_coin.pre_check_for_order_creation(ctx, &rel_coin).await); let (volume, balance) = if req.max { let CoinVolumeInfo { volume, balance, .. } = try_s!( @@ -4979,6 +5056,7 @@ pub async fn create_maker_order(ctx: &MmArc, req: SetPriceReq) -> Result Result Result> { let protocol: CoinProtocol = json::from_value(conf["protocol"].clone())?; match protocol { - CoinProtocol::ERC20 { .. } | CoinProtocol::ETH | CoinProtocol::NFT { .. } => { + CoinProtocol::ERC20 { .. } | CoinProtocol::ETH { .. } | CoinProtocol::NFT { .. } => { coins::eth::addr_from_pubkey_str(pubkey) .map(OrderbookAddress::Transparent) .map_to_mm(OrderbookAddrErr::AddrFromPubkeyError) }, + // Todo: implement TRX address generation + CoinProtocol::TRX { .. } => MmError::err(OrderbookAddrErr::CoinIsNotSupported(coin.to_owned())), CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { coins::utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) .map(OrderbookAddress::Transparent) 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 1847720416..9faa4f4722 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/my_orders_storage.rs @@ -726,6 +726,8 @@ mod tests { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: crate::lp_ordermatch::OrderMetadata::default(), } } @@ -755,6 +757,8 @@ mod tests { base_orderbook_ticker: None, rel_orderbook_ticker: None, p2p_privkey: None, + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: crate::lp_ordermatch::OrderMetadata::default(), } } diff --git a/mm2src/mm2_main/src/lp_ordermatch/order_events.rs b/mm2src/mm2_main/src/lp_ordermatch/order_events.rs index 547ee7df4e..9e0fa97593 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/order_events.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/order_events.rs @@ -1,5 +1,5 @@ use super::{MakerMatch, TakerMatch}; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -12,7 +12,7 @@ impl OrderStatusStreamer { pub fn new() -> Self { Self } #[inline(always)] - pub const fn derive_streamer_id() -> &'static str { "ORDER_STATUS" } + pub const fn derive_streamer_id() -> StreamerId { StreamerId::OrderStatus } } #[derive(Serialize)] @@ -28,7 +28,7 @@ pub enum OrderStatusEvent { impl EventStreamer for OrderStatusStreamer { type DataInType = OrderStatusEvent; - fn streamer_id(&self) -> String { Self::derive_streamer_id().to_string() } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id() } async fn handle( self, diff --git a/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs b/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs index f7149bd05e..852da7fb1f 100644 --- a/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs +++ b/mm2src/mm2_main/src/lp_ordermatch/orderbook_events.rs @@ -1,7 +1,7 @@ use super::{orderbook_topic_from_base_rel, subscribe_to_orderbook_topic, OrderbookP2PItem}; use coins::{is_wallet_only_ticker, lp_coinfind}; use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -17,8 +17,10 @@ pub struct OrderbookStreamer { impl OrderbookStreamer { pub fn new(ctx: MmArc, base: String, rel: String) -> Self { Self { ctx, base, rel } } - pub fn derive_streamer_id(base: &str, rel: &str) -> String { - format!("ORDERBOOK_UPDATE/{}", orderbook_topic_from_base_rel(base, rel)) + pub fn derive_streamer_id(base: &str, rel: &str) -> StreamerId { + StreamerId::OrderbookUpdate { + topic: orderbook_topic_from_base_rel(base, rel), + } } } @@ -36,7 +38,7 @@ pub enum OrderbookItemChangeEvent { impl EventStreamer for OrderbookStreamer { type DataInType = OrderbookItemChangeEvent; - fn streamer_id(&self) -> String { Self::derive_streamer_id(&self.base, &self.rel) } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id(&self.base, &self.rel) } async fn handle( self, diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index b15806c059..7744b555d8 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -532,7 +532,7 @@ struct LockedAmountInfo { struct SwapsContext { running_swaps: Mutex>>, active_swaps_v2_infos: Mutex>, - banned_pubkeys: Mutex>, + banned_pubkeys: Mutex>, swap_msgs: Mutex>, swap_v2_msgs: Mutex>, taker_swap_watchers: PaMutex, ()>>, @@ -548,7 +548,7 @@ impl SwapsContext { Ok(SwapsContext { running_swaps: Mutex::new(HashMap::new()), active_swaps_v2_infos: Mutex::new(HashMap::new()), - banned_pubkeys: Mutex::new(HashMap::new()), + banned_pubkeys: Mutex::new(TimedMap::new_with_map_kind(MapKind::FxHashMap)), swap_msgs: Mutex::new(HashMap::new()), swap_v2_msgs: Mutex::new(HashMap::new()), taker_swap_watchers: PaMutex::new(TimedMap::new_with_map_kind(MapKind::FxHashMap)), @@ -962,10 +962,12 @@ pub struct TransactionIdentifier { } #[cfg(not(target_arch = "wasm32"))] -pub fn my_swaps_dir(ctx: &MmArc) -> PathBuf { ctx.dbdir().join("SWAPS").join("MY") } +pub fn my_swaps_dir(ctx: &MmArc, address: &str) -> PathBuf { ctx.address_dir(address).join("SWAPS").join("MY") } #[cfg(not(target_arch = "wasm32"))] -pub fn my_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { my_swaps_dir(ctx).join(format!("{}.json", uuid)) } +pub fn my_swap_file_path(ctx: &MmArc, address: &str, uuid: &Uuid) -> PathBuf { + my_swaps_dir(ctx, address).join(format!("{}.json", uuid)) +} pub async fn insert_new_swap_to_db( ctx: MmArc, @@ -1080,7 +1082,7 @@ pub async fn my_swap_status(ctx: MmArc, req: Json) -> Result>, match swap_type { Some(LEGACY_SWAP_TYPE) => { - let status = match SavedSwap::load_my_swap_from_db(&ctx, uuid).await { + let status = match SavedSwap::load_my_swap_from_db(&ctx, None, uuid).await { Ok(Some(status)) => status, Ok(None) => return Err("swap data is not found".to_owned()), Err(e) => return ERR!("{}", e), @@ -1142,7 +1144,7 @@ struct SwapStatus { /// Broadcasts `my` swap status to P2P network async fn broadcast_my_swap_status(ctx: &MmArc, uuid: Uuid) -> Result<(), String> { - let mut status = match try_s!(SavedSwap::load_my_swap_from_db(ctx, uuid).await) { + let mut status = match try_s!(SavedSwap::load_my_swap_from_db(ctx, None, uuid).await) { Some(status) => status, None => return ERR!("swap data is not found"), }; @@ -1251,7 +1253,7 @@ pub async fn latest_swaps_for_pair( let mut swaps = Vec::with_capacity(db_result.uuids_and_types.len()); // TODO this is needed for trading bot, which seems not used as of now. Remove the code? for (uuid, _) in db_result.uuids_and_types.iter() { - let swap = match SavedSwap::load_my_swap_from_db(&ctx, *uuid).await { + let swap = match SavedSwap::load_my_swap_from_db(&ctx, None, *uuid).await { Ok(Some(swap)) => swap, Ok(None) => { error!("No such swap with the uuid '{}'", uuid); @@ -1278,7 +1280,7 @@ pub async fn my_recent_swaps_rpc(ctx: MmArc, req: Json) -> Result match SavedSwap::load_my_swap_from_db(&ctx, *uuid).await { + LEGACY_SWAP_TYPE => match SavedSwap::load_my_swap_from_db(&ctx, None, *uuid).await { Ok(Some(swap)) => { let swap_json = try_s!(json::to_value(MySwapStatusResponse::from(swap))); swaps.push(swap_json) @@ -1329,7 +1331,7 @@ pub async fn swap_kick_starts(ctx: MmArc) -> Result, String> { let mut coins = HashSet::new(); let legacy_unfinished_uuids = try_s!(get_unfinished_swaps_uuids(ctx.clone(), LEGACY_SWAP_TYPE).await); for uuid in legacy_unfinished_uuids { - let swap = match SavedSwap::load_my_swap_from_db(&ctx, uuid).await { + let swap = match SavedSwap::load_my_swap_from_db(&ctx, None, uuid).await { Ok(Some(s)) => s, Ok(None) => { warn!("Swap {} is indexed, but doesn't exist in DB", uuid); @@ -1477,7 +1479,7 @@ pub async fn coins_needed_for_kick_start(ctx: MmArc) -> Result> pub async fn recover_funds_of_swap(ctx: MmArc, req: Json) -> Result>, String> { let uuid: Uuid = try_s!(json::from_value(req["params"]["uuid"].clone())); - let swap = match SavedSwap::load_my_swap_from_db(&ctx, uuid).await { + let swap = match SavedSwap::load_my_swap_from_db(&ctx, None, uuid).await { Ok(Some(swap)) => swap, Ok(None) => return ERR!("swap data is not found"), Err(e) => return ERR!("{}", e), @@ -1554,7 +1556,7 @@ pub async fn active_swaps_rpc(ctx: MmArc, req: Json) -> Result> for (uuid, swap_type) in uuids_with_types.iter() { match *swap_type { LEGACY_SWAP_TYPE => { - let status = match SavedSwap::load_my_swap_from_db(&ctx, *uuid).await { + let status = match SavedSwap::load_my_swap_from_db(&ctx, None, *uuid).await { Ok(Some(status)) => status, Ok(None) => continue, Err(e) => { @@ -2152,6 +2154,7 @@ mod lp_swap_tests { "p2p_in_memory": true, "p2p_in_memory_port": 777, "i_am_seed": true, + "is_bootstrap_node": true }); let maker_ctx = MmCtxBuilder::default().with_conf(maker_ctx_conf).into_mm_arc(); @@ -2240,7 +2243,7 @@ mod lp_swap_tests { uuid, None, conf_settings, - rick_maker.into(), + rick_maker.clone().into(), morty_maker.into(), lock_duration, None, @@ -2261,7 +2264,7 @@ mod lp_swap_tests { uuid, None, conf_settings, - rick_taker.into(), + rick_taker.clone().into(), morty_taker.into(), lock_duration, None, @@ -2274,15 +2277,18 @@ mod lp_swap_tests { run_taker_swap(RunTakerSwapInput::StartNew(taker_swap), taker_ctx.clone()), )); + let makers_maker_coin_address = rick_maker.my_address().unwrap(); + let takers_maker_coin_address = rick_taker.my_address().unwrap(); + println!( "Maker swap path {}", - std::fs::canonicalize(my_swap_file_path(&maker_ctx, &uuid)) + std::fs::canonicalize(my_swap_file_path(&maker_ctx, &makers_maker_coin_address, &uuid)) .unwrap() .display() ); println!( "Taker swap path {}", - std::fs::canonicalize(my_swap_file_path(&taker_ctx, &uuid)) + std::fs::canonicalize(my_swap_file_path(&taker_ctx, &takers_maker_coin_address, &uuid)) .unwrap() .display() ); diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index c7e5a43329..b74953f42b 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -80,7 +80,7 @@ pub const MAKER_ERROR_EVENTS: [&str; 15] = [ pub const MAKER_PAYMENT_SENT_LOG: &str = "Maker payment sent"; #[cfg(not(target_arch = "wasm32"))] -pub fn stats_maker_swap_dir(ctx: &MmArc) -> PathBuf { ctx.dbdir().join("SWAPS").join("STATS").join("MAKER") } +pub fn stats_maker_swap_dir(ctx: &MmArc) -> PathBuf { ctx.global_dir().join("SWAPS").join("STATS").join("MAKER") } #[cfg(not(target_arch = "wasm32"))] pub fn stats_maker_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { @@ -88,10 +88,14 @@ pub fn stats_maker_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { } async fn save_my_maker_swap_event(ctx: &MmArc, swap: &MakerSwap, event: MakerSavedEvent) -> Result<(), String> { - let swap = match SavedSwap::load_my_swap_from_db(ctx, swap.uuid).await { + let maker_coin_pub = swap.my_maker_coin_htlc_pub(); + let maker_coin_address = try_s!(swap.maker_coin.address_from_pubkey(&maker_coin_pub)); + let swap = match SavedSwap::load_my_swap_from_db(ctx, Some(&maker_coin_address), swap.uuid).await { Ok(Some(swap)) => swap, Ok(None) => SavedSwap::Maker(MakerSavedSwap { uuid: swap.uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: maker_coin_address, my_order_uuid: swap.my_order_uuid, maker_amount: Some(swap.maker_amount.clone()), maker_coin: Some(swap.maker_coin.ticker().to_owned()), @@ -1360,7 +1364,7 @@ impl MakerSwap { taker_coin: MmCoinEnum, swap_uuid: &Uuid, ) -> Result<(Self, Option), String> { - let saved = match SavedSwap::load_my_swap_from_db(&ctx, *swap_uuid).await { + let saved = match SavedSwap::load_my_swap_from_db(&ctx, None, *swap_uuid).await { Ok(Some(saved)) => saved, Ok(None) => return ERR!("Couldn't find a swap with the uuid '{}'", swap_uuid), Err(e) => return ERR!("{}", e), @@ -1855,6 +1859,8 @@ impl MakerSwapStatusChanged { #[derive(Debug, Default, PartialEq, Serialize, Deserialize)] pub struct MakerSavedSwap { pub uuid: Uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + pub maker_address: String, pub my_order_uuid: Option, pub events: Vec, pub maker_amount: Option, @@ -1911,6 +1917,8 @@ impl MakerSavedSwap { MakerSavedSwap { uuid: Default::default(), + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: "".to_string(), my_order_uuid: None, events, maker_amount: Some(maker_amount.to_decimal()), @@ -2182,7 +2190,7 @@ pub async fn run_maker_swap(swap: RunMakerSwapInput, ctx: MmArc) { drop(dispatcher); // Send a notification to the swap status streamer about a new event. ctx.event_stream_manager - .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::MakerV1 { + .send_fn(&SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::MakerV1 { uuid: running_swap.uuid, event: to_save.clone(), }) @@ -2347,6 +2355,7 @@ pub async fn maker_swap_trade_preimage( rel_confs: rel_coin.required_confirmations(), rel_nota: rel_coin.requires_notarization(), }; + let builder = MakerOrderBuilder::new(&base_coin, &rel_coin) .with_max_base_vol(volume.clone()) .with_price(req.price) diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index c4592fd494..3783e43950 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -792,7 +792,7 @@ impl bool { @@ -47,6 +56,7 @@ pub async fn list_banned_pubkeys_rpc(ctx: MmArc) -> Result>, St struct BanPubkeysReq { pubkey: H256Json, reason: String, + duration_min: Option, } pub async fn ban_pubkey_rpc(ctx: MmArc, req: Json) -> Result>, String> { @@ -54,16 +64,25 @@ pub async fn ban_pubkey_rpc(ctx: MmArc, req: Json) -> Result>, let ctx = try_s!(SwapsContext::from_ctx(&ctx)); let mut banned_pubs = try_s!(ctx.banned_pubkeys.lock()); - match banned_pubs.entry(req.pubkey) { - Entry::Occupied(_) => ERR!("Pubkey is banned already"), - Entry::Vacant(entry) => { - entry.insert(BanReason::Manual { reason: req.reason }); - let res = try_s!(json::to_vec(&json!({ - "result": "success", - }))); - Ok(try_s!(Response::builder().body(res))) - }, + if banned_pubs.contains_key(&req.pubkey) { + return ERR!("Pubkey is banned already"); } + + if let Some(duration_min) = req.duration_min { + banned_pubs.insert_expirable( + req.pubkey, + BanReason::Manual { reason: req.reason }, + Duration::from_secs(duration_min as u64 * 60), + ); + } else { + banned_pubs.insert_constant(req.pubkey, BanReason::Manual { reason: req.reason }); + } + + let res = try_s!(json::to_vec(&json!({ + "result": "success", + }))); + + Response::builder().body(res).map_err(|e| e.to_string()) } #[derive(Deserialize)] @@ -77,13 +96,16 @@ pub async fn unban_pubkeys_rpc(ctx: MmArc, req: Json) -> Result let req: UnbanPubkeysReq = try_s!(json::from_value(req["unban_by"].clone())); let ctx = try_s!(SwapsContext::from_ctx(&ctx)); let mut banned_pubs = try_s!(ctx.banned_pubkeys.lock()); - let mut unbanned = HashMap::new(); let mut were_not_banned = vec![]; - match req { + + let unbanned = match req { UnbanPubkeysReq::All => { - unbanned = banned_pubs.drain().collect(); + let unbanned = json!(*banned_pubs); + banned_pubs.clear(); + unbanned }, UnbanPubkeysReq::Few(pubkeys) => { + let mut unbanned = HashMap::new(); for pubkey in pubkeys { match banned_pubs.remove(&pubkey) { Some(removed) => { @@ -92,8 +114,11 @@ pub async fn unban_pubkeys_rpc(ctx: MmArc, req: Json) -> Result None => were_not_banned.push(pubkey), } } + + json!(unbanned) }, - } + }; + let res = try_s!(json::to_vec(&json!({ "result": { "still_banned": *banned_pubs, 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 e389597f92..1dd92dd0f8 100644 --- a/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs +++ b/mm2src/mm2_main/src/lp_swap/recreate_swap_data.rs @@ -73,15 +73,18 @@ pub async fn recreate_swap_data(ctx: MmArc, args: RecreateSwapRequest) -> Recrea }, InputSwap::SavedSwap(SavedSwap::Taker(taker_swap)) | InputSwap::TakerSavedSwap(taker_swap) => { recreate_maker_swap(ctx, taker_swap) + .await .map(SavedSwap::from) .map(|swap| RecreateSwapResponse { swap }) }, } } -fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapResult { +async fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapResult { let mut maker_swap = MakerSavedSwap { uuid: taker_swap.uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: String::new(), my_order_uuid: taker_swap.my_order_uuid, events: Vec::new(), maker_amount: taker_swap.maker_amount, @@ -119,9 +122,10 @@ fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapRe let mut taker_p2p_pubkey = [0; 32]; taker_p2p_pubkey.copy_from_slice(&started_event.my_persistent_pub.0[1..33]); + let maker_started_event = MakerSwapEvent::Started(MakerSwapData { taker_coin: started_event.taker_coin, - maker_coin: started_event.maker_coin, + maker_coin: started_event.maker_coin.clone(), taker_pubkey: H256Json::from(taker_p2p_pubkey), // We could parse the `TakerSwapEvent::TakerPaymentSpent` event. // As for now, don't try to find the secret in the events since we can refund without it. @@ -176,6 +180,23 @@ fn recreate_maker_swap(ctx: MmArc, taker_swap: TakerSavedSwap) -> RecreateSwapRe .events .extend(convert_taker_to_maker_events(event_it, wait_refund_until)); + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + { + // TODO(new-db-arch): Execute this plan: https://github.com/KomodoPlatform/komodo-defi-framework/pull/2398#discussion_r2036035916 + // instead of making the maker_address/address_dir available for the importer (i.e. let them find it themselves). + let maker_coin_ticker = started_event.maker_coin; + let maker_coin = lp_coinfind(&ctx, &maker_coin_ticker) + .await + .map_to_mm(RecreateSwapError::Internal)? + .or_mm_err(move || RecreateSwapError::NoSuchCoin { + coin: maker_coin_ticker, + })?; + maker_swap.maker_address = negotiated_event + .maker_coin_htlc_pubkey + .and_then(|pubkey| maker_coin.address_from_pubkey(&pubkey).ok()) + .unwrap_or("Couldn't get the maker coin address. Please set it manually.".to_string()); + } + Ok(maker_swap) } @@ -285,6 +306,8 @@ fn convert_taker_to_maker_events( async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> RecreateSwapResult { let mut taker_swap = TakerSavedSwap { uuid: maker_swap.uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: String::new(), my_order_uuid: Some(maker_swap.uuid), events: Vec::new(), maker_amount: maker_swap.maker_amount, @@ -382,6 +405,14 @@ async fn recreate_taker_swap(ctx: MmArc, maker_swap: MakerSavedSwap) -> Recreate coin: maker_coin_ticker, })?; + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + { + taker_swap.maker_address = negotiated_event + .maker_coin_htlc_pubkey + .and_then(|pubkey| maker_coin.address_from_pubkey(&pubkey).ok()) + .unwrap_or("Couldn't get the maker coin address. Please set it manually.".to_string()); + } + // Then we can continue to process success Maker events. let wait_refund_until = negotiated_event.taker_payment_locktime + 3700; taker_swap @@ -509,7 +540,7 @@ mod tests { let ctx = MmCtxBuilder::default().into_mm_arc(); - let maker_actual_swap = recreate_maker_swap(ctx, taker_saved_swap).expect("!recreate_maker_swap"); + let maker_actual_swap = block_on(recreate_maker_swap(ctx, taker_saved_swap)).expect("!recreate_maker_swap"); println!("{}", json::to_string(&maker_actual_swap).unwrap()); assert_eq!(maker_actual_swap, maker_expected_swap); } @@ -527,7 +558,7 @@ mod tests { let ctx = MmCtxBuilder::default().into_mm_arc(); - let maker_actual_swap = recreate_maker_swap(ctx, taker_saved_swap).expect("!recreate_maker_swap"); + let maker_actual_swap = block_on(recreate_maker_swap(ctx, taker_saved_swap)).expect("!recreate_maker_swap"); println!("{}", json::to_string(&maker_actual_swap).unwrap()); assert_eq!(maker_actual_swap, maker_expected_swap); } diff --git a/mm2src/mm2_main/src/lp_swap/saved_swap.rs b/mm2src/mm2_main/src/lp_swap/saved_swap.rs index 185edd1584..158843dadb 100644 --- a/mm2src/mm2_main/src/lp_swap/saved_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/saved_swap.rs @@ -75,6 +75,14 @@ impl SavedSwap { } } + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + pub fn maker_address(&self) -> &str { + match self { + SavedSwap::Maker(swap) => &swap.maker_address, + SavedSwap::Taker(swap) => &swap.maker_address, + } + } + pub fn maker_coin_ticker(&self) -> Result { match self { SavedSwap::Maker(swap) => swap.maker_coin(), @@ -159,7 +167,11 @@ impl SavedSwap { #[async_trait] pub trait SavedSwapIo { - async fn load_my_swap_from_db(ctx: &MmArc, uuid: Uuid) -> SavedSwapResult>; + async fn load_my_swap_from_db( + ctx: &MmArc, + address_dir: Option<&str>, + uuid: Uuid, + ) -> SavedSwapResult>; async fn load_all_my_swaps_from_db(ctx: &MmArc) -> SavedSwapResult>; @@ -208,13 +220,30 @@ mod native_impl { #[async_trait] impl SavedSwapIo for SavedSwap { - async fn load_my_swap_from_db(ctx: &MmArc, uuid: Uuid) -> SavedSwapResult> { - let path = my_swap_file_path(ctx, &uuid); + async fn load_my_swap_from_db( + ctx: &MmArc, + address_dir: Option<&str>, + uuid: Uuid, + ) -> SavedSwapResult> { + // TODO(new-db-arch): Set the correct address directory for the new db arch branch (via a query to the global DB). + #[cfg(feature = "new-db-arch")] + let address_dir = address_dir.unwrap_or("Fetch the address directory from the global DB given the UUID."); + #[cfg(not(feature = "new-db-arch"))] + let address_dir = address_dir.unwrap_or("no address directory for old DB architecture (has no effect)"); + let path = my_swap_file_path(ctx, address_dir, &uuid); Ok(read_json(&path).await?) } + #[cfg_attr(feature = "new-db-arch", allow(unreachable_code, unused_variables))] async fn load_all_my_swaps_from_db(ctx: &MmArc) -> SavedSwapResult> { - let path = my_swaps_dir(ctx); + #[cfg(feature = "new-db-arch")] + { + // This method is solely used for migrations. Which we should ditch or refactor with the new DB architecture. + // If we ditch the old migrations, this method should never be called (and should be deleted when we are + // done with the incremental architecture change). + todo!("Fix the dummy address directory in `my_swaps_dir` below or remove this method all together"); + } + let path = my_swaps_dir(ctx, "has no effect in not(feature = 'new-db-arch')"); Ok(read_dir_json(&path).await?) } @@ -239,7 +268,11 @@ mod native_impl { } async fn save_to_db(&self, ctx: &MmArc) -> SavedSwapResult<()> { - let path = my_swap_file_path(ctx, self.uuid()); + #[cfg(feature = "new-db-arch")] + let address_dir = self.maker_address(); + #[cfg(not(feature = "new-db-arch"))] + let address_dir = "no address directory for old DB architecture (has no effect)"; + let path = my_swap_file_path(ctx, address_dir, self.uuid()); write_json(self, &path, USE_TMP_FILE).await?; Ok(()) } @@ -376,7 +409,11 @@ mod wasm_impl { #[async_trait] impl SavedSwapIo for SavedSwap { - async fn load_my_swap_from_db(ctx: &MmArc, uuid: Uuid) -> SavedSwapResult> { + async fn load_my_swap_from_db( + ctx: &MmArc, + _address_dir: Option<&str>, + uuid: Uuid, + ) -> SavedSwapResult> { let swaps_ctx = SwapsContext::from_ctx(ctx).map_to_mm(SavedSwapError::InternalError)?; let db = swaps_ctx.swap_db().await?; let transaction = db.transaction().await?; @@ -486,7 +523,7 @@ mod tests { assert_eq!(item, second_saved_item); } - let actual_saved_swap = SavedSwap::load_my_swap_from_db(&ctx, *saved_swap.uuid()) + let actual_saved_swap = SavedSwap::load_my_swap_from_db(&ctx, None, *saved_swap.uuid()) .await .expect("!load_from_db") .expect("Swap not found"); diff --git a/mm2src/mm2_main/src/lp_swap/swap_events.rs b/mm2src/mm2_main/src/lp_swap/swap_events.rs index 7f4aaa90eb..6e60f31611 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_events.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_events.rs @@ -2,7 +2,7 @@ use super::maker_swap::MakerSavedEvent; use super::maker_swap_v2::MakerSwapEvent; use super::taker_swap::TakerSavedEvent; use super::taker_swap_v2::TakerSwapEvent; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -16,7 +16,7 @@ impl SwapStatusStreamer { pub fn new() -> Self { Self } #[inline(always)] - pub const fn derive_streamer_id() -> &'static str { "SWAP_STATUS" } + pub const fn derive_streamer_id() -> StreamerId { StreamerId::SwapStatus } } #[derive(Serialize)] @@ -32,7 +32,7 @@ pub enum SwapStatusEvent { impl EventStreamer for SwapStatusStreamer { type DataInType = SwapStatusEvent; - fn streamer_id(&self) -> String { Self::derive_streamer_id().to_string() } + fn streamer_id(&self) -> StreamerId { Self::derive_streamer_id() } async fn handle( self, diff --git a/mm2src/mm2_main/src/lp_swap/swap_lock.rs b/mm2src/mm2_main/src/lp_swap/swap_lock.rs index f4347dde3b..38591deeff 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_lock.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_lock.rs @@ -32,7 +32,6 @@ pub trait SwapLockOps: Sized { #[cfg(not(target_arch = "wasm32"))] mod native_lock { use super::*; - use crate::lp_swap::my_swaps_dir; use mm2_io::file_lock::{FileLock, FileLockError}; use std::path::PathBuf; @@ -57,7 +56,14 @@ mod native_lock { #[async_trait] impl SwapLockOps for SwapLock { async fn lock(ctx: &MmArc, swap_uuid: Uuid, ttl_sec: f64) -> SwapLockResult> { - let lock_path = my_swaps_dir(ctx).join(format!("{}.lock", swap_uuid)); + let lock_path = if cfg!(feature = "new-db-arch") { + ctx.global_dir().join("swap_locks").join(format!("{}.lock", swap_uuid)) + } else { + ctx.global_dir() + .join("SWAPS") + .join("MY") + .join(format!("{}.lock", swap_uuid)) + }; let file_lock = some_or_return_ok_none!(FileLock::lock(lock_path, ttl_sec)?); Ok(Some(SwapLock { file_lock })) diff --git a/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs b/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs index 669d17a492..92a305f252 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_v2_rpcs.rs @@ -301,7 +301,7 @@ async fn get_swap_data_by_uuid_and_type( ) -> MmResult, GetSwapDataErr> { match swap_type { LEGACY_SWAP_TYPE => { - let saved_swap = SavedSwap::load_my_swap_from_db(ctx, uuid).await?; + let saved_swap = SavedSwap::load_my_swap_from_db(ctx, None, uuid).await?; Ok(saved_swap.map(|swap| match swap { SavedSwap::Maker(m) => SwapRpcData::MakerV1(m), SavedSwap::Taker(t) => SwapRpcData::TakerV1(t), diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 54f5ab3bfe..1fdb9e728f 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -103,7 +103,7 @@ pub const WATCHER_MESSAGE_SENT_LOG: &str = "Watcher message sent..."; pub const MAKER_PAYMENT_SPENT_BY_WATCHER_LOG: &str = "Maker payment is spent by the watcher..."; #[cfg(not(target_arch = "wasm32"))] -pub fn stats_taker_swap_dir(ctx: &MmArc) -> PathBuf { ctx.dbdir().join("SWAPS").join("STATS").join("TAKER") } +pub fn stats_taker_swap_dir(ctx: &MmArc) -> PathBuf { ctx.global_dir().join("SWAPS").join("STATS").join("TAKER") } #[cfg(not(target_arch = "wasm32"))] pub fn stats_taker_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { @@ -111,10 +111,14 @@ pub fn stats_taker_swap_file_path(ctx: &MmArc, uuid: &Uuid) -> PathBuf { } async fn save_my_taker_swap_event(ctx: &MmArc, swap: &TakerSwap, event: TakerSavedEvent) -> Result<(), String> { - let swap = match SavedSwap::load_my_swap_from_db(ctx, swap.uuid).await { + let maker_coin_pub = swap.my_maker_coin_htlc_pub(); + let maker_coin_address = try_s!(swap.maker_coin.address_from_pubkey(&maker_coin_pub)); + let swap = match SavedSwap::load_my_swap_from_db(ctx, Some(&maker_coin_address), swap.uuid).await { Ok(Some(swap)) => swap, Ok(None) => SavedSwap::Taker(TakerSavedSwap { uuid: swap.uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + maker_address: maker_coin_address, my_order_uuid: swap.my_order_uuid, maker_amount: Some(swap.maker_amount.to_decimal()), maker_coin: Some(swap.maker_coin.ticker().to_owned()), @@ -204,6 +208,8 @@ impl TakerSavedEvent { #[derive(Debug, Deserialize, PartialEq, Serialize)] pub struct TakerSavedSwap { pub uuid: Uuid, + #[cfg(all(not(target_arch = "wasm32"), feature = "new-db-arch"))] + pub maker_address: String, pub my_order_uuid: Option, pub events: Vec, pub maker_amount: Option, @@ -485,7 +491,7 @@ pub async fn run_taker_swap(swap: RunTakerSwapInput, ctx: MmArc) { // Send a notification to the swap status streamer about a new event. ctx.event_stream_manager - .send_fn(SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::TakerV1 { + .send_fn(&SwapStatusStreamer::derive_streamer_id(), || SwapStatusEvent::TakerV1 { uuid: running_swap.uuid, event: to_save.clone(), }) @@ -2085,7 +2091,7 @@ impl TakerSwap { taker_coin: MmCoinEnum, swap_uuid: &Uuid, ) -> Result<(Self, Option), String> { - let saved = match SavedSwap::load_my_swap_from_db(&ctx, *swap_uuid).await { + let saved = match SavedSwap::load_my_swap_from_db(&ctx, None, *swap_uuid).await { Ok(Some(saved)) => saved, Ok(None) => return ERR!("Couldn't find a swap with the uuid '{}'", swap_uuid), Err(e) => return ERR!("{}", e), @@ -2652,6 +2658,7 @@ pub async fn taker_swap_trade_preimage( rel_nota: rel_coin.requires_notarization(), }; let our_public_id = CryptoCtx::from_ctx(ctx)?.mm2_internal_public_id(); + let order_builder = TakerOrderBuilder::new(&base_coin, &rel_coin) .with_base_amount(base_amount) .with_rel_amount(rel_amount) @@ -2659,6 +2666,8 @@ pub async fn taker_swap_trade_preimage( .with_match_by(MatchBy::Any) .with_conf_settings(conf_settings) .with_sender_pubkey(H256Json::from(our_public_id.bytes)); + + // perform an additional validation let _ = order_builder .build() .map_to_mm(|e| TradePreimageRpcError::from_taker_order_build_error(e, &req.base, &req.rel))?; diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs index eea040316d..7f8270f139 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -909,7 +909,7 @@ impl = DbLocked<'a, WalletsDb>; } cfg_native! { - use mnemonics_storage::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError}; + use mnemonics_storage::{delete_wallet, read_all_wallet_names, read_encrypted_passphrase, save_encrypted_passphrase, WalletsStorageError}; } #[cfg(not(target_arch = "wasm32"))] mod mnemonics_storage; #[cfg(target_arch = "wasm32")] mod mnemonics_wasm_db; @@ -69,6 +69,8 @@ pub enum ReadPassphraseError { WalletsStorageError(String), #[display(fmt = "Error decrypting passphrase: {}", _0)] DecryptionError(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), } impl From for WalletInitError { @@ -76,6 +78,7 @@ impl From for WalletInitError { match e { ReadPassphraseError::WalletsStorageError(e) => WalletInitError::WalletsStorageError(e), ReadPassphraseError::DecryptionError(e) => WalletInitError::MnemonicError(e), + ReadPassphraseError::Internal(e) => WalletInitError::InternalError(e), } } } @@ -121,25 +124,39 @@ async fn encrypt_and_save_passphrase( .mm_err(|e| WalletInitError::WalletsStorageError(e.to_string())) } -/// Reads and decrypts the passphrase from a file associated with the given wallet name, if available. -/// -/// This function first checks if a passphrase is available. If a passphrase is found, -/// since it is stored in an encrypted format, it decrypts it before returning. If no passphrase is found, -/// it returns `None`. -/// -/// # Returns -/// `MmInitResult` - The decrypted passphrase or an error if any operation fails. +/// A convenience wrapper that calls [`try_load_wallet_passphrase`] for the currently active wallet. +async fn try_load_active_wallet_passphrase( + ctx: &MmArc, + wallet_password: &str, +) -> MmResult, ReadPassphraseError> { + let wallet_name = ctx + .wallet_name + .get() + .ok_or(ReadPassphraseError::Internal( + "`wallet_name` not initialized yet!".to_string(), + ))? + .clone() + .ok_or_else(|| { + ReadPassphraseError::Internal("Cannot read stored passphrase: no active wallet is set.".to_string()) + })?; + + try_load_wallet_passphrase(ctx, &wallet_name, wallet_password).await +} + +/// Loads (reads from storage and decrypts) a passphrase for a specific wallet by name. /// -/// # Errors -/// Returns specific `MmInitError` variants for different failure scenarios. -async fn read_and_decrypt_passphrase_if_available( +/// Returns `Ok(None)` if the passphrase is not found in storage. This is an expected +/// outcome for a new wallet or when using a legacy config where the passphrase is not saved. +async fn try_load_wallet_passphrase( ctx: &MmArc, + wallet_name: &str, wallet_password: &str, ) -> MmResult, ReadPassphraseError> { - match read_encrypted_passphrase_if_available(ctx) + let encrypted = read_encrypted_passphrase(ctx, wallet_name) .await - .mm_err(|e| ReadPassphraseError::WalletsStorageError(e.to_string()))? - { + .mm_err(|e| ReadPassphraseError::WalletsStorageError(e.to_string()))?; + + match encrypted { Some(encrypted_passphrase) => { let mnemonic = decrypt_mnemonic(&encrypted_passphrase, wallet_password) .mm_err(|e| ReadPassphraseError::DecryptionError(e.to_string()))?; @@ -171,7 +188,7 @@ async fn retrieve_or_create_passphrase( wallet_name: &str, wallet_password: &str, ) -> WalletInitResult> { - match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { + match try_load_active_wallet_passphrase(ctx, wallet_password).await? { Some(passphrase_from_file) => { // If an existing passphrase is found, return it Ok(Some(passphrase_from_file)) @@ -202,7 +219,7 @@ async fn confirm_or_encrypt_and_store_passphrase( passphrase: &str, wallet_password: &str, ) -> WalletInitResult> { - match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { + match try_load_active_wallet_passphrase(ctx, wallet_password).await? { Some(passphrase_from_file) if passphrase == passphrase_from_file => { // If an existing passphrase is found and it matches the provided passphrase, return it Ok(Some(passphrase_from_file)) @@ -238,7 +255,7 @@ async fn decrypt_validate_or_save_passphrase( // Decrypt the provided encrypted passphrase let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?; - match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { + match try_load_active_wallet_passphrase(ctx, wallet_password).await? { Some(passphrase_from_file) if decrypted_passphrase == passphrase_from_file => { // If an existing passphrase is found and it matches the decrypted passphrase, return it Ok(Some(decrypted_passphrase)) @@ -297,7 +314,7 @@ async fn process_passphrase_logic( fn initialize_crypto_context(ctx: &MmArc, passphrase: &str) -> WalletInitResult<()> { // This defaults to false to maintain backward compatibility. - match ctx.conf["enable_hd"].as_bool().unwrap_or(false) { + match ctx.enable_hd() { true => CryptoCtx::init_with_global_hd_account(ctx.clone(), passphrase)?, false => CryptoCtx::init_with_iguana_passphrase(ctx.clone(), passphrase)?, }; @@ -476,7 +493,13 @@ impl From for MnemonicRpcError { } impl From for MnemonicRpcError { - fn from(e: ReadPassphraseError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) } + fn from(e: ReadPassphraseError) -> Self { + match e { + ReadPassphraseError::DecryptionError(e) => MnemonicRpcError::InvalidPassword(e), + ReadPassphraseError::WalletsStorageError(e) => MnemonicRpcError::WalletsStorageError(e), + ReadPassphraseError::Internal(e) => MnemonicRpcError::Internal(e), + } + } } /// Retrieves the wallet mnemonic in the requested format. @@ -513,7 +536,19 @@ impl From for MnemonicRpcError { pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { match req.mnemonic_format { MnemonicFormat::Encrypted => { - let encrypted_mnemonic = read_encrypted_passphrase_if_available(&ctx) + let wallet_name = ctx + .wallet_name + .get() + .ok_or(MnemonicRpcError::Internal( + "`wallet_name` not initialized yet!".to_string(), + ))? + .as_ref() + .ok_or_else(|| { + MnemonicRpcError::Internal( + "Cannot get encrypted mnemonic: This operation requires an active named wallet.".to_string(), + ) + })?; + let encrypted_mnemonic = read_encrypted_passphrase(&ctx, wallet_name) .await? .ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; Ok(GetMnemonicResponse { @@ -521,7 +556,7 @@ pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { - let plaintext_mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &wallet_password) + let plaintext_mnemonic = try_load_active_wallet_passphrase(&ctx, &wallet_password) .await? .ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; Ok(GetMnemonicResponse { @@ -584,7 +619,7 @@ pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq .as_ref() .ok_or_else(|| MnemonicRpcError::Internal("`wallet_name` cannot be None!".to_string()))?; // read mnemonic for a wallet_name using current user's password. - let mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &req.current_password) + let mnemonic = try_load_active_wallet_passphrase(&ctx, &req.current_password) .await? .ok_or(MmError::new(MnemonicRpcError::Internal(format!( "{wallet_name}: wallet mnemonic file not found" @@ -596,3 +631,48 @@ pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq Ok(()) } + +#[derive(Debug, Deserialize)] +pub struct DeleteWalletRequest { + /// The name of the wallet to be deleted. + pub wallet_name: String, + /// The password to confirm wallet deletion. + pub password: String, +} + +/// Deletes a wallet. Requires password confirmation. +/// The active wallet cannot be deleted. +pub async fn delete_wallet_rpc(ctx: MmArc, req: DeleteWalletRequest) -> MmResult<(), MnemonicRpcError> { + let active_wallet = ctx + .wallet_name + .get() + .ok_or(MnemonicRpcError::Internal( + "`wallet_name` not initialized yet!".to_string(), + ))? + .as_ref(); + + if active_wallet == Some(&req.wallet_name) { + return MmError::err(MnemonicRpcError::InvalidRequest(format!( + "Cannot delete wallet '{}' as it is currently active.", + req.wallet_name + ))); + } + + // Verify the password by attempting to decrypt the mnemonic. + let maybe_mnemonic = try_load_wallet_passphrase(&ctx, &req.wallet_name, &req.password).await?; + + match maybe_mnemonic { + Some(_) => { + // Password is correct, proceed with deletion. + delete_wallet(&ctx, &req.wallet_name).await?; + Ok(()) + }, + None => { + // This case implies no mnemonic file was found for the given wallet. + MmError::err(MnemonicRpcError::InvalidRequest(format!( + "Wallet '{}' not found.", + req.wallet_name + ))) + }, + } +} diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs index 25e77c27d1..f2636be4a0 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs @@ -57,27 +57,22 @@ pub(super) async fn save_encrypted_passphrase( /// Reads the encrypted passphrase data from the file associated with the given wallet name, if available. /// -/// This function is responsible for retrieving the encrypted passphrase data from a file, if it exists. +/// This function is responsible for retrieving the encrypted passphrase data from a file for a specific wallet. /// The data is expected to be in the format of `EncryptedData`, which includes /// all necessary components for decryption, such as the encryption algorithm, key derivation /// /// # Returns -/// `io::Result` - The encrypted passphrase data or an error if the -/// reading process fails. +/// `WalletsStorageResult>` - The encrypted passphrase data or an error if the +/// reading process fails. An `Ok(None)` is returned if the wallet file does not exist. /// /// # Errors -/// Returns an `io::Error` if the file cannot be read or the data cannot be deserialized into +/// Returns a `WalletsStorageError` if the file cannot be read or the data cannot be deserialized into /// `EncryptedData`. -pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsStorageResult> { - let wallet_name = ctx - .wallet_name - .get() - .ok_or(WalletsStorageError::Internal( - "`wallet_name` not initialized yet!".to_string(), - ))? - .clone() - .ok_or_else(|| WalletsStorageError::Internal("`wallet_name` cannot be None!".to_string()))?; - let wallet_path = wallet_file_path(ctx, &wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; +pub(super) async fn read_encrypted_passphrase( + ctx: &MmArc, + wallet_name: &str, +) -> WalletsStorageResult> { + let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; mm2_io::fs::read_json(&wallet_path).await.mm_err(|e| { WalletsStorageError::FsReadError(format!( "Error reading passphrase from file {}: {}", @@ -93,3 +88,11 @@ pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsStorageResult WalletsStorageResult<()> { + let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; + mm2_io::fs::remove_file_async(&wallet_path) + .await + .mm_err(|e| WalletsStorageError::FsWriteError(e.to_string())) +} diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs index e4733a132d..6eeaebc8d4 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs @@ -119,21 +119,16 @@ pub(super) async fn save_encrypted_passphrase( Ok(()) } -pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsDBResult> { +pub(super) async fn read_encrypted_passphrase( + ctx: &MmArc, + wallet_name: &str, +) -> WalletsDBResult> { let wallets_ctx = WalletsContext::from_ctx(ctx).map_to_mm(WalletsDBError::Internal)?; let db = wallets_ctx.wallets_db().await?; let transaction = db.transaction().await?; let table = transaction.table::().await?; - let wallet_name = ctx - .wallet_name - .get() - .ok_or(WalletsDBError::Internal( - "`wallet_name` not initialized yet!".to_string(), - ))? - .clone() - .ok_or_else(|| WalletsDBError::Internal("`wallet_name` can't be None!".to_string()))?; table .get_item_by_unique_index("wallet_name", wallet_name) .await? @@ -160,3 +155,14 @@ pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsDBResult WalletsDBResult<()> { + let wallets_ctx = WalletsContext::from_ctx(ctx).map_to_mm(WalletsDBError::Internal)?; + + let db = wallets_ctx.wallets_db().await?; + let transaction = db.transaction().await?; + let table = transaction.table::().await?; + + table.delete_item_by_unique_index("wallet_name", wallet_name).await?; + Ok(()) +} diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 9e7e465a78..37ad202ed8 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -56,7 +56,6 @@ use lp_swap::PAYMENT_LOCKTIME; use std::sync::atomic::Ordering; use gstuff::slurp; -use serde::ser::Serialize; use serde_json::{self as json, Value as Json}; use std::env; @@ -67,7 +66,6 @@ use std::str; pub use self::lp_native_dex::init_hw; pub use self::lp_native_dex::lp_init; -use coins::update_coins_config; use mm2_err_handle::prelude::*; #[cfg(not(target_arch = "wasm32"))] pub mod database; @@ -249,12 +247,12 @@ Environment variables: Defaults to `MM2.json` MM_COINS_PATH .. File path. MM2 will try to load coins data from this file. File must contain valid json. - Recommended: https://github.com/jl777/coins/blob/master/coins. + Recommended: https://github.com/komodoplatform/coins/blob/master/coins. Defaults to `coins`. MM_LOG .. File path. Must end with '.log'. MM will log to this file. See also the online documentation at -https://developers.atomicdex.io +https://komodoplatform.com/en/docs "#; println!("{}", HELP_MSG); @@ -284,14 +282,6 @@ pub fn mm2_main(version: String, datetime: String) { // we're not checking them for the mode switches in order not to risk [untrusted] data being mistaken for a mode switch. let first_arg = args_os.get(1).and_then(|arg| arg.to_str()); - if first_arg == Some("update_config") { - match on_update_config(&args_os) { - Ok(_) => println!("Success"), - Err(e) => eprintln!("{}", e), - } - return; - } - if first_arg == Some("--version") || first_arg == Some("-v") || first_arg == Some("version") { println!("Komodo DeFi Framework: {version}"); return; @@ -388,35 +378,6 @@ pub fn run_lp_main( Ok(()) } -#[cfg(not(target_arch = "wasm32"))] -fn on_update_config(args: &[OsString]) -> Result<(), String> { - use mm2_io::fs::safe_slurp; - - let src_path = args.get(2).ok_or(ERRL!("Expect path to the source coins config."))?; - let dst_path = args.get(3).ok_or(ERRL!("Expect destination path."))?; - - let config = try_s!(safe_slurp(src_path)); - let mut config: Json = try_s!(json::from_slice(&config)); - - let result = if config.is_array() { - try_s!(update_coins_config(config)) - } else { - // try to get config["coins"] as array - let conf_obj = config.as_object_mut().ok_or(ERRL!("Expected coin list"))?; - let coins = conf_obj.remove("coins").ok_or(ERRL!("Expected coin list"))?; - let updated_coins = try_s!(update_coins_config(coins)); - conf_obj.insert("coins".into(), updated_coins); - config - }; - - let buf = Vec::new(); - let formatter = json::ser::PrettyFormatter::with_indent(b"\t"); - let mut ser = json::Serializer::with_formatter(buf, formatter); - try_s!(result.serialize(&mut ser)); - try_s!(std::fs::write(dst_path, ser.into_inner())); - Ok(()) -} - #[cfg(not(target_arch = "wasm32"))] fn init_logger(_level: LogLevel, silent_console: bool) -> Result<(), String> { common::log::UnifiedLoggerBuilder::default() diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 7f88f129b3..c1788e2c2c 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -40,6 +40,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -56,6 +58,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -80,6 +84,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -96,6 +102,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -120,6 +128,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -136,6 +146,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -160,6 +172,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -176,6 +190,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -200,6 +216,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -216,6 +234,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -240,6 +260,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { @@ -256,6 +278,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); @@ -282,6 +306,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { base: "KMD".to_owned(), @@ -297,6 +323,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); assert_eq!(actual, OrderMatchResult::NotMatched); @@ -324,6 +352,8 @@ fn test_match_maker_order_and_taker_request() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let request = TakerRequest { base: "REL".to_owned(), @@ -339,6 +369,8 @@ fn test_match_maker_order_and_taker_request() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let actual = maker.match_with_request(&request); let expected_base_amount = MmNumber::from(3); @@ -397,6 +429,8 @@ fn test_maker_order_available_amount() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; maker.matches.insert(new_uuid(), MakerMatch { request: TakerRequest { @@ -413,6 +447,8 @@ fn test_maker_order_available_amount() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, reserved: MakerReserved { base: "BASE".into(), @@ -427,6 +463,8 @@ fn test_maker_order_available_amount() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, connect: None, connected: None, @@ -447,6 +485,8 @@ fn test_maker_order_available_amount() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, reserved: MakerReserved { base: "BASE".into(), @@ -461,6 +501,8 @@ fn test_maker_order_available_amount() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, connect: None, connected: None, @@ -490,6 +532,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -518,6 +562,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -536,6 +582,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -564,6 +612,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -582,6 +632,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -610,6 +662,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -628,6 +682,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -656,6 +712,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -674,6 +732,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -702,6 +762,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -720,6 +782,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -748,6 +812,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -766,6 +832,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -794,6 +862,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -812,6 +882,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -840,6 +912,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -862,6 +936,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, matches: HashMap::new(), order_type: OrderType::GoodTillCancelled, @@ -886,6 +962,8 @@ fn test_taker_match_reserved() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::Matched, order.match_reserved(&reserved)); @@ -907,6 +985,8 @@ fn test_taker_order_cancellable() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let order = TakerOrder { @@ -938,6 +1018,8 @@ fn test_taker_order_cancellable() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let mut order = TakerOrder { @@ -968,6 +1050,8 @@ fn test_taker_order_cancellable() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, connect: TakerConnect { sender_pubkey: H256Json::default(), @@ -1017,6 +1101,8 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, None, ); @@ -1040,6 +1126,8 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, None, ); @@ -1063,6 +1151,8 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, None, ); @@ -1083,6 +1173,8 @@ fn prepare_for_cancel_by(ctx: &MmArc) -> mpsc::Receiver { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }, order_type: OrderType::GoodTillCancelled, min_volume: 0.into(), @@ -1186,6 +1278,8 @@ fn test_taker_order_match_by() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let mut order = TakerOrder { @@ -1214,6 +1308,8 @@ fn test_taker_order_match_by() { base_protocol_info: None, rel_protocol_info: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert_eq!(MatchReservedResult::NotMatched, order.match_reserved(&reserved)); @@ -1255,6 +1351,8 @@ fn test_maker_order_was_updated() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let mut update_msg = MakerOrderUpdated::new(maker_order.uuid); update_msg.with_new_price(BigRational::from_integer(2.into())); @@ -3265,6 +3363,8 @@ fn test_maker_order_balance_loops() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; let morty_order = MakerOrder { @@ -3285,6 +3385,8 @@ fn test_maker_order_balance_loops() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; assert!(!maker_orders_ctx.balance_loop_exists(rick_ticker)); @@ -3318,6 +3420,8 @@ fn test_maker_order_balance_loops() { rel_orderbook_ticker: None, p2p_privkey: None, swap_version: SwapVersion::default(), + #[cfg(feature = "ibc-routing-for-swaps")] + order_metadata: OrderMetadata::default(), }; maker_orders_ctx.add_order(ctx.weak(), rick_order_2.clone(), None); diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index d3df2042db..7a338bd552 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -47,6 +47,7 @@ mod dispatcher_legacy; pub mod lp_commands; mod rate_limiter; mod streaming_activations; +pub mod wc_commands; /// Lists the RPC method not requiring the "userpass" authentication. /// None is also public to skip auth and display proper error in case of method is missing @@ -175,12 +176,25 @@ fn response_from_dispatcher_error( response.serialize_http_response() } -async fn process_single_request(ctx: MmArc, req: Json, client: SocketAddr) -> Result>, String> { +async fn process_single_request(ctx: MmArc, mut req: Json, client: SocketAddr) -> Result>, String> { let local_only = ctx.conf["rpc_local_only"].as_bool().unwrap_or(true); if req["mmrpc"].is_null() { - return dispatcher_legacy::process_single_request(ctx, req, client, local_only) - .await - .map_err(|e| ERRL!("{}", e)); + match dispatcher_legacy::process_single_request(ctx.clone(), req.clone(), client, local_only).await { + Ok(t) => return Ok(t), + + Err(dispatcher_legacy::LegacyRequestProcessError::NoMatch) => { + // Try the v2 implementation + req["mmrpc"] = json!("2.0"); + info!( + "Couldn't resolve '{}' RPC using the legacy API, trying v2 (mmrpc: 2.0) instead.", + req["method"] + ); + }, + + Err(e) => { + return ERR!("{}", e); + }, + }; } let id = req["id"].as_u64().map(|id| id as usize); diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index f3286e5ab5..4b18118a7d 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,4 +1,5 @@ use super::streaming_activations; +use super::wc_commands::{disconnect_session, get_all_sessions, get_session}; use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; use crate::lp_healthcheck::peer_connection_healthcheck_rpc; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; @@ -10,7 +11,7 @@ use crate::lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, s stop_version_stat_collection, update_version_stat_collection}; use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc}; use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}; -use crate::lp_wallet::{change_mnemonic_password, get_mnemonic_rpc, get_wallet_names_rpc}; +use crate::lp_wallet::{change_mnemonic_password, delete_wallet_rpc, get_mnemonic_rpc, get_wallet_names_rpc}; use crate::rpc::lp_commands::db_id::get_shared_db_id; use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, one_inch_v6_0_classic_swap_create_rpc, @@ -22,13 +23,14 @@ use crate::rpc::lp_commands::tokens::get_token_info; use crate::rpc::lp_commands::tokens::{approve_token_rpc, get_token_allowance_rpc}; use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; +use crate::rpc::wc_commands::{new_connection, ping_session}; + use coins::eth::fee_estimation::rpc::get_eth_estimated_fee_per_gas; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; -use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, - get_enabled_coins::get_enabled_coins, + get_enabled_coins::get_enabled_coins_rpc, get_new_address::{cancel_get_new_address, get_new_address, init_get_new_address, init_get_new_address_status, init_get_new_address_user_action}, init_account_balance::{cancel_account_balance, init_account_balance, @@ -199,6 +201,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, get_token_allowance_rpc).await, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, + "delete_wallet" => handle_mmrpc(ctx, request, delete_wallet_rpc).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, "enable_slp" => handle_mmrpc(ctx, request, enable_token::).await, "enable_eth_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, @@ -209,7 +212,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, enable_token::).await, "get_current_mtp" => handle_mmrpc(ctx, request, get_current_mtp_rpc).await, - "get_enabled_coins" => handle_mmrpc(ctx, request, get_enabled_coins).await, + "get_enabled_coins" => handle_mmrpc(ctx, request, get_enabled_coins_rpc).await, "get_locked_amount" => handle_mmrpc(ctx, request, get_locked_amount_rpc).await, "get_mnemonic" => handle_mmrpc(ctx, request, get_mnemonic_rpc).await, "get_my_address" => handle_mmrpc(ctx, request, get_my_address).await, @@ -244,8 +247,6 @@ 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_chains" => handle_mmrpc(ctx, request, ibc_chains).await, - "ibc_transfer_channels" => handle_mmrpc(ctx, request, ibc_transfer_channels).await, "peer_connection_healthcheck" => handle_mmrpc(ctx, request, peer_connection_healthcheck_rpc).await, "withdraw_nft" => handle_mmrpc(ctx, request, withdraw_nft).await, "get_eth_estimated_fee_per_gas" => handle_mmrpc(ctx, request, get_eth_estimated_fee_per_gas).await, @@ -260,6 +261,11 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_tokens_rpc).await, + "wc_new_connection" => handle_mmrpc(ctx, request, new_connection).await, + "wc_get_session" => handle_mmrpc(ctx, request, get_session).await, + "wc_get_sessions" => handle_mmrpc(ctx, request, get_all_sessions).await, + "wc_delete_session" => handle_mmrpc(ctx, request, disconnect_session).await, + "wc_ping_session" => handle_mmrpc(ctx, request, ping_session).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs index 5f4b14f8b4..8d1a71db85 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs @@ -117,27 +117,49 @@ pub fn dispatcher(req: Json, ctx: MmArc) -> DispatcherRes { }) } +#[derive(Debug, Display)] +pub enum LegacyRequestProcessError { + #[display(fmt = "Selected method is not allowed: {reason}")] + NotAllowed { reason: String }, + #[display(fmt = "No such method")] + NoMatch, + #[display(fmt = "RPC call failed: {reason}")] + Failed { reason: String }, +} + pub async fn process_single_request( ctx: MmArc, req: Json, client: SocketAddr, local_only: bool, -) -> Result>, String> { +) -> Result>, LegacyRequestProcessError> { // https://github.com/artemii235/SuperNET/issues/368 if local_only && !client.ip().is_loopback() && !PUBLIC_METHODS.contains(&req["method"].as_str()) { - return ERR!("Selected method can be called from localhost only!"); + return Err(LegacyRequestProcessError::NotAllowed { + reason: "Selected method can only be called from localhost.".to_owned(), + }); } let rate_limit_ctx = RateLimitContext::from_ctx(&ctx).unwrap(); if rate_limit_ctx.is_banned(client.ip()).await { - return ERR!("Your ip is banned."); + return Err(LegacyRequestProcessError::NotAllowed { + reason: "Your IP is banned.".to_owned(), + }); } - try_s!(auth(&req, &ctx, &client).await); + auth(&req, &ctx, &client) + .await + .map_err(|reason| LegacyRequestProcessError::Failed { reason })?; let handler = match dispatcher(req, ctx.clone()) { DispatcherRes::Match(handler) => handler, - DispatcherRes::NoMatch(_) => return ERR!("No such method."), + DispatcherRes::NoMatch(_) => { + return Err(LegacyRequestProcessError::NoMatch); + }, }; - Ok(try_s!(handler.compat().await)) + + handler + .compat() + .await + .map_err(|reason| LegacyRequestProcessError::Failed { reason }) } /// The set of functions that convert the result of the updated handlers into the legacy format. diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs index aab020e64e..978a1d80b5 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -47,7 +47,7 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( let quote = ApiClient::new(ctx) .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? .call_swap_api( - base.chain_id(), + base.chain_id().ok_or(ApiIntegrationRpcError::ChainNotSupported)?, ApiClient::get_quote_method().to_owned(), Some(query_params), ) @@ -102,7 +102,7 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( let swap_with_tx = ApiClient::new(ctx) .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? .call_swap_api( - base.chain_id(), + base.chain_id().ok_or(ApiIntegrationRpcError::ChainNotSupported)?, ApiClient::get_swap_method().to_owned(), Some(query_params), ) @@ -159,7 +159,7 @@ async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, #[allow(clippy::result_large_err)] fn api_supports_pair(base: &EthCoin, rel: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { - if !ApiClient::is_chain_supported(base.chain_id()) { + if !ApiClient::is_chain_supported(base.chain_id().ok_or(ApiIntegrationRpcError::ChainNotSupported)?) { return MmError::err(ApiIntegrationRpcError::ChainNotSupported); } if base.chain_id() != rel.chain_id() { @@ -191,9 +191,11 @@ mod tests { "coin": ticker_coin, "name": "ethereum", "derivation_path": "m/44'/1'", - "chain_id": 1, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": 1, + } }, "trezor_coin": "Ethereum" }); @@ -230,7 +232,7 @@ mod tests { ], "swap_contract_address": "0xeA6D65434A15377081495a9E7C5893543E7c32cB", "erc20_tokens_requests": [{"ticker": ticker_token}], - "priv_key_policy": "ContextPrivKey" + "priv_key_policy": { "type": "ContextPrivKey" } })) .unwrap(), )) diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs b/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs index 9643e9e652..59490e173d 100644 --- a/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs +++ b/mm2src/mm2_main/src/rpc/streaming_activations/disable.rs @@ -5,6 +5,7 @@ use common::HttpStatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmResult}; +use mm2_event_stream::StreamerId; use http::StatusCode; @@ -12,7 +13,7 @@ use http::StatusCode; #[derive(Deserialize)] pub struct DisableStreamingRequest { pub client_id: u64, - pub streamer_id: String, + pub streamer_id: StreamerId, } /// The success/ok response for any event streaming deactivation request. diff --git a/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs b/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs index 05d2848f97..05adc144a2 100644 --- a/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs +++ b/mm2src/mm2_main/src/rpc/streaming_activations/mod.rs @@ -19,6 +19,8 @@ pub use orders::*; pub use swaps::*; pub use tx_history::*; +use mm2_event_stream::StreamerId; + /// The general request for enabling any streamer. /// `client_id` is common in each request, other data is request-specific. #[derive(Deserialize)] @@ -33,7 +35,7 @@ pub struct EnableStreamingRequest { /// The success/ok response for any event streaming activation request. #[derive(Serialize)] pub struct EnableStreamingResponse { - pub streamer_id: String, + pub streamer_id: StreamerId, // TODO: If the the streamer was already running, it is probably running with different configuration. // We might want to inform the client that the configuration they asked for wasn't applied and return // the active configuration instead? @@ -41,5 +43,5 @@ pub struct EnableStreamingResponse { } impl EnableStreamingResponse { - fn new(streamer_id: String) -> Self { Self { streamer_id } } + fn new(streamer_id: StreamerId) -> Self { Self { streamer_id } } } diff --git a/mm2src/mm2_main/src/rpc/wc_commands/mod.rs b/mm2src/mm2_main/src/rpc/wc_commands/mod.rs new file mode 100644 index 0000000000..8e39dac169 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/mod.rs @@ -0,0 +1,34 @@ +mod new_connection; +mod sessions; + +use common::HttpStatusCode; +use derive_more::Display; +use http::StatusCode; +pub use new_connection::new_connection; +use serde::Deserialize; +pub use sessions::*; + +#[derive(Deserialize)] +pub struct EmptyRpcRequest {} + +#[derive(Debug, Serialize)] +pub struct EmptyRpcResponse {} + +#[derive(Serialize, Display, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum WalletConnectRpcError { + InternalError(String), + InitializationError(String), + SessionRequestError(String), +} + +impl HttpStatusCode for WalletConnectRpcError { + fn status_code(&self) -> StatusCode { + match self { + WalletConnectRpcError::InitializationError(_) => StatusCode::BAD_REQUEST, + WalletConnectRpcError::SessionRequestError(_) | WalletConnectRpcError::InternalError(_) => { + StatusCode::INTERNAL_SERVER_ERROR + }, + } + } +} diff --git a/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs b/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs new file mode 100644 index 0000000000..7b95d705fc --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/new_connection.rs @@ -0,0 +1,32 @@ +use kdf_walletconnect::WalletConnectCtx; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use serde::{Deserialize, Serialize}; + +use super::WalletConnectRpcError; + +#[derive(Debug, PartialEq, Serialize)] +pub struct CreateConnectionResponse { + pub url: String, +} + +#[derive(Deserialize)] +pub struct NewConnectionRequest { + required_namespaces: serde_json::Value, + optional_namespaces: Option, +} + +/// `new_connection` RPC command implementation. +pub async fn new_connection( + ctx: MmArc, + req: NewConnectionRequest, +) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let url = wc_ctx + .new_connection(req.required_namespaces, req.optional_namespaces) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(CreateConnectionResponse { url }) +} diff --git a/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs b/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs new file mode 100644 index 0000000000..3f8c457d63 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/wc_commands/sessions.rs @@ -0,0 +1,86 @@ +use kdf_walletconnect::session::rpc::send_session_ping_request; +use kdf_walletconnect::session::SessionRpcInfo; +use kdf_walletconnect::WalletConnectCtx; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use serde::Serialize; + +use super::{EmptyRpcRequest, EmptyRpcResponse, WalletConnectRpcError}; + +#[derive(Debug, PartialEq, Serialize)] +pub struct SessionResponse { + pub result: String, +} + +#[derive(Debug, PartialEq, Serialize)] +pub struct GetSessionsResponse { + pub sessions: Vec, +} + +/// `Get all sessions connection` RPC command implementation. +pub async fn get_all_sessions( + ctx: MmArc, + _req: EmptyRpcRequest, +) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let sessions = wc_ctx + .session_manager + .get_sessions() + .map(SessionRpcInfo::from) + .collect::>(); + + Ok(GetSessionsResponse { sessions }) +} + +#[derive(Debug, Serialize)] +pub struct GetSessionResponse { + pub session: Option, +} + +#[derive(Deserialize)] +pub struct GetSessionRequest { + topic: String, + #[serde(default)] + with_pairing_topic: bool, +} + +/// `Get session connection` RPC command implementation. +pub async fn get_session(ctx: MmArc, req: GetSessionRequest) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + let session = wc_ctx + .session_manager + .get_session_with_any_topic(&req.topic.into(), req.with_pairing_topic) + .map(SessionRpcInfo::from); + + Ok(GetSessionResponse { session }) +} + +/// `Delete session connection` RPC command implementation. +pub async fn disconnect_session( + ctx: MmArc, + req: GetSessionRequest, +) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + wc_ctx + .drop_session(&req.topic.into()) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(EmptyRpcResponse {}) +} + +/// `ping session` RPC command implementation. +pub async fn ping_session(ctx: MmArc, req: GetSessionRequest) -> MmResult { + let wc_ctx = + WalletConnectCtx::from_ctx(&ctx).mm_err(|err| WalletConnectRpcError::InitializationError(err.to_string()))?; + send_session_ping_request(&wc_ctx, &req.topic.into()) + .await + .mm_err(|err| WalletConnectRpcError::SessionRequestError(err.to_string()))?; + + Ok(SessionResponse { + result: "Ping successful".to_owned(), + }) +} diff --git a/mm2src/mm2_main/src/wasm_tests.rs b/mm2src/mm2_main/src/wasm_tests.rs index 11d31fb91c..a8768b675b 100644 --- a/mm2src/mm2_main/src/wasm_tests.rs +++ b/mm2src/mm2_main/src/wasm_tests.rs @@ -2,18 +2,19 @@ use crate::{lp_init, lp_run}; use common::executor::{spawn, spawn_abortable, spawn_local_abortable, AbortOnDropHandle, Timer}; use common::log::warn; use common::log::wasm_log::register_wasm_log; +use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_number::BigDecimal; use mm2_rpc::data::legacy::OrderbookResponse; use mm2_test_helpers::electrums::{doc_electrums, marty_electrums}; -use mm2_test_helpers::for_tests::{check_recent_swaps, enable_electrum_json, enable_utxo_v2_electrum, +use mm2_test_helpers::for_tests::{check_recent_swaps, delete_wallet, enable_electrum_json, enable_utxo_v2_electrum, enable_z_coin_light, get_wallet_names, morty_conf, pirate_conf, rick_conf, start_swaps, test_qrc20_history_impl, wait_for_swaps_finish_and_check_status, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, ARRR, MORTY, PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, RICK}; use mm2_test_helpers::get_passphrase; use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalance, HDAccountAddressId}; -use serde_json::json; +use serde_json::{json, Value as Json}; use wasm_bindgen_test::wasm_bindgen_test; const PIRATE_TEST_BALANCE_SEED: &str = "pirate test seed"; @@ -80,9 +81,6 @@ async fn test_mm2_stops_immediately() { test_mm2_stops_impl(pairs, 1., 1., 0.0001).await; } -#[wasm_bindgen_test] -async fn test_qrc20_tx_history() { test_qrc20_history_impl(Some(wasm_start)).await } - async fn trade_base_rel_electrum( mut mm_bob: MarketMakerIt, mut mm_alice: MarketMakerIt, @@ -303,3 +301,95 @@ async fn test_get_wallet_names() { .await .unwrap(); } + +#[wasm_bindgen_test] +async fn test_delete_wallet_rpc() { + register_wasm_log(); + + const DB_NAMESPACE_NUM: u64 = 2; + + let coins = json!([]); + let wallet_1_name = "wallet_to_be_deleted"; + let wallet_1_pass = "pass1"; + let wallet_1_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_1_name, wallet_1_pass); + let mm_wallet_1 = MarketMakerIt::start_with_db( + wallet_1_conf.conf, + wallet_1_conf.rpc_password, + Some(wasm_start), + DB_NAMESPACE_NUM, + ) + .await + .unwrap(); + + let get_wallet_names_1 = get_wallet_names(&mm_wallet_1).await; + assert_eq!(get_wallet_names_1.wallet_names, vec![wallet_1_name]); + assert_eq!(get_wallet_names_1.activated_wallet.as_deref(), Some(wallet_1_name)); + + mm_wallet_1 + .stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS) + .await + .unwrap(); + + let wallet_2_name = "active_wallet"; + let wallet_2_pass = "pass2"; + let wallet_2_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_2_name, wallet_2_pass); + let mm_wallet_2 = MarketMakerIt::start_with_db( + wallet_2_conf.conf, + wallet_2_conf.rpc_password, + Some(wasm_start), + DB_NAMESPACE_NUM, + ) + .await + .unwrap(); + + let wallet_names = get_wallet_names(&mm_wallet_2).await.wallet_names; + assert_eq!(wallet_names, vec![wallet_2_name, wallet_1_name]); + let activated_wallet = get_wallet_names(&mm_wallet_2).await.activated_wallet; + assert_eq!(activated_wallet.as_deref(), Some(wallet_2_name)); + + // Try to delete the active wallet - should fail + let (_, body, _) = delete_wallet(&mm_wallet_2, wallet_2_name, wallet_2_pass).await; + let error: Json = serde_json::from_str(&body).unwrap(); + assert_eq!(error["error_type"], "InvalidRequest"); + assert!(error["error_data"] + .as_str() + .unwrap() + .contains("Cannot delete wallet 'active_wallet' as it is currently active.")); + + // Try to delete with the wrong password - should fail + let (_, body, _) = delete_wallet(&mm_wallet_2, wallet_1_name, "wrong_pass").await; + let error: Json = serde_json::from_str(&body).unwrap(); + assert_eq!(error["error_type"], "InvalidPassword"); + assert!(error["error_data"] + .as_str() + .unwrap() + .contains("Error decrypting mnemonic")); + + // Try to delete a non-existent wallet - should fail + let (_, body, _) = delete_wallet(&mm_wallet_2, "non_existent_wallet", "any_pass").await; + let error: Json = serde_json::from_str(&body).unwrap(); + assert_eq!(error["error_type"], "InvalidRequest"); + assert!(error["error_data"] + .as_str() + .unwrap() + .contains("Wallet 'non_existent_wallet' not found.")); + + // Delete the inactive wallet with the correct password - should succeed + let (_, body, _) = delete_wallet(&mm_wallet_2, wallet_1_name, wallet_1_pass).await; + let response: Json = serde_json::from_str(&body).expect("Response should be valid JSON"); + assert!( + response["result"].is_null(), + "Expected a successful response with null result, but got error: {}", + body + ); + + // Verify the wallet is deleted + let get_wallet_names_3 = get_wallet_names(&mm_wallet_2).await; + assert_eq!(get_wallet_names_3.wallet_names, vec![wallet_2_name]); + assert_eq!(get_wallet_names_3.activated_wallet.as_deref(), Some(wallet_2_name)); + + mm_wallet_2 + .stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS) + .await + .unwrap(); +} diff --git a/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs b/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs index 8f090c91f2..45345ef4d5 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_ordermatch_tests.rs @@ -199,6 +199,7 @@ fn test_ordermatch_custom_orderbook_ticker_both_on_maker() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -334,6 +335,7 @@ fn test_ordermatch_custom_orderbook_ticker_both_on_taker() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -467,6 +469,7 @@ fn test_ordermatch_custom_orderbook_ticker_mixed_case_one() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -608,6 +611,7 @@ fn test_ordermatch_custom_orderbook_ticker_mixed_case_two() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1183,6 +1187,7 @@ fn test_zombie_order_after_balance_reduce_and_mm_restart() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let mm_seed = MarketMakerIt::start(seed_conf, "pass".to_string(), None).unwrap(); 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 253c8adb74..6a483767af 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -65,7 +65,7 @@ lazy_static! { // Due to the SLP protocol limitations only 19 outputs (18 + change) can be sent in one transaction, which is sufficient for now though. // Supply more privkeys when 18 will be not enough. pub static ref SLP_TOKEN_OWNERS: Mutex> = Mutex::new(Vec::with_capacity(18)); - pub static ref MM_CTX: MmArc = MmCtxBuilder::new().with_conf(json!({"use_trading_proto_v2": true})).into_mm_arc(); + pub static ref MM_CTX: MmArc = MmCtxBuilder::new().with_conf(json!({"coins":[eth_dev_conf()],"use_trading_proto_v2": true})).into_mm_arc(); /// We need a second `MmCtx` instance when we use the same private keys for Maker and Taker across various tests. /// When enabling coins for both Maker and Taker, two distinct coin instances are created. /// This means that different instances of the same coin should have separate global nonce locks. @@ -239,7 +239,7 @@ impl CoinDockerOps for ZCoinAssetDockerOps { impl ZCoinAssetDockerOps { pub fn new() -> ZCoinAssetDockerOps { - let (ctx, coin) = block_on(z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe")); + let (ctx, coin) = block_on(z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe")); ZCoinAssetDockerOps { ctx, coin } } @@ -978,6 +978,7 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1124,10 +1125,10 @@ pub fn trade_base_rel((base, rel): (&str, &str)) { )); log!("Checking alice status.."); - block_on(wait_check_stats_swap_status(&mm_alice, &uuid, 30)); + block_on(wait_check_stats_swap_status(&mm_alice, &uuid, 240)); log!("Checking bob status.."); - block_on(wait_check_stats_swap_status(&mm_bob, &uuid, 30)); + block_on(wait_check_stats_swap_status(&mm_bob, &uuid, 240)); log!("Checking alice recent swaps.."); block_on(check_recent_swaps(&mm_alice, 1)); @@ -1154,6 +1155,7 @@ pub fn slp_supplied_node() -> MarketMakerIt { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, 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 ff7e6415fb..c34a2afe55 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -405,6 +405,7 @@ fn order_should_be_cancelled_when_entire_balance_is_withdrawn() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -521,6 +522,7 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_after_upda "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -654,6 +656,7 @@ fn order_should_be_updated_when_balance_is_decreased_alice_subscribes_before_upd "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -801,6 +804,7 @@ fn test_order_should_be_updated_when_matched_partially() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -906,6 +910,7 @@ fn test_match_and_trade_setprice_max() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1005,6 +1010,7 @@ fn test_max_taker_vol_swap() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1125,6 +1131,7 @@ fn test_buy_when_coins_locked_by_other_swap() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1218,6 +1225,7 @@ fn test_sell_when_coins_locked_by_other_swap() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1310,6 +1318,7 @@ fn test_buy_max() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1374,6 +1383,7 @@ fn test_maker_trade_preimage() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1511,6 +1521,7 @@ fn test_taker_trade_preimage() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1652,6 +1663,7 @@ fn test_trade_preimage_not_sufficient_balance() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1771,6 +1783,7 @@ fn test_trade_preimage_additional_validation() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1910,6 +1923,7 @@ fn test_trade_preimage_legacy() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1980,6 +1994,7 @@ fn test_get_max_taker_vol() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2032,6 +2047,7 @@ fn test_get_max_taker_vol_dex_fee_min_tx_amount() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2093,6 +2109,7 @@ fn test_get_max_taker_vol_dust_threshold() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2144,6 +2161,7 @@ fn test_get_max_taker_vol_with_kmd() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2250,6 +2268,7 @@ fn test_set_price_max() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2305,6 +2324,7 @@ fn swaps_should_stop_on_stop_rpc() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2395,6 +2415,7 @@ fn test_maker_order_should_kick_start_and_appear_in_orderbook_on_restart() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -2452,6 +2473,7 @@ fn test_maker_order_should_not_kick_start_and_appear_in_orderbook_if_balance_is_ "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -2547,6 +2569,7 @@ fn test_maker_order_kick_start_should_trigger_subscription_and_match() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let relay = MarketMakerIt::start(relay_conf, "pass".to_string(), None).unwrap(); let (_relay_dump_log, _relay_dump_dashboard) = mm_dump(&relay.log_path); @@ -2559,7 +2582,6 @@ fn test_maker_order_kick_start_should_trigger_subscription_and_match() { "coins": coins, "rpc_password": "pass", "seednodes": vec![format!("{}", relay.ip)], - "i_am_seed": false, }); let mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -2638,6 +2660,7 @@ fn test_orders_should_match_on_both_nodes_with_same_priv() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2741,6 +2764,7 @@ fn test_maker_and_taker_order_created_with_same_priv_should_not_match() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2812,6 +2836,7 @@ fn test_taker_order_converted_to_maker_should_cancel_properly_when_matched() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2935,6 +2960,7 @@ fn test_utxo_merge() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -2988,6 +3014,7 @@ fn test_utxo_merge_max_merge_at_once() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3036,6 +3063,7 @@ fn test_withdraw_not_sufficient_balance() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3122,6 +3150,7 @@ fn test_taker_should_match_with_best_price_buy() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3255,6 +3284,7 @@ fn test_taker_should_match_with_best_price_sell() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3393,6 +3423,7 @@ fn test_match_utxo_with_eth_taker_sell() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3469,6 +3500,7 @@ fn test_match_utxo_with_eth_taker_buy() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -3972,6 +4004,7 @@ fn test_withdraw_and_send_eth_erc20() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -4545,7 +4578,7 @@ fn test_set_price_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); @@ -4618,7 +4651,7 @@ fn test_buy_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); @@ -4691,7 +4724,7 @@ fn test_sell_conf_settings() { .display_priv_key() .unwrap(); - let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2,"chain_id": 1337},]); + let coins = json!([eth_dev_conf(),{"coin":"ERC20DEV","name":"erc20dev","protocol":{"type":"ERC20","protocol_data":{"platform":"ETH","contract_address":erc20_contract_checksum()}},"required_confirmations":2},]); let conf = Mm2TestConf::seednode(&private_key_str, &coins); let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 840e30279a..76b7f773c8 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -10,8 +10,8 @@ use crate::common::Future01CompatExt; use bitcrypto::{dhash160, sha256}; use coins::eth::gas_limit::ETH_MAX_TRADE_GAS; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivationV2Request, EthNode}; -use coins::eth::{checksum_address, eth_coin_from_conf_and_request, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, - SignedEthTx, SwapV2Contracts, ERC20_ABI}; +use coins::eth::{checksum_address, eth_coin_from_conf_and_request, ChainSpec, EthCoin, EthCoinType, + EthPrivKeyBuildPolicy, SignedEthTx, SwapV2Contracts, ERC20_ABI}; use coins::hd_wallet::AddrToString; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] @@ -35,7 +35,7 @@ use mm2_test_helpers::for_tests::{account_balance, active_swaps, coins_needed_fo enable_erc20_token_v2, enable_eth_coin_v2, enable_eth_with_tokens_v2, erc20_dev_conf, eth1_dev_conf, eth_dev_conf, get_locked_amount, get_new_address, get_token_info, mm_dump, my_swap_status, nft_dev_conf, start_swaps, MarketMakerIt, - Mm2TestConf, SwapV2TestContracts, TestNode}; + Mm2TestConf, SwapV2TestContracts, TestNode, ETH_SEPOLIA_CHAIN_ID}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalanceMap, EthWithTokensActivationResult, HDAccountAddressId, @@ -59,6 +59,7 @@ const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba211 const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; const ETH1: &str = "ETH1"; +const GETH_DEV_CHAIN_ID: u64 = 1337; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const ERC20: &str = "ERC20DEV"; @@ -303,7 +304,9 @@ pub fn eth_coin_with_random_privkey_using_urls(swap_contract_address: Address, u "ETH", ð_conf, &req, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: GETH_DEV_CHAIN_ID, + }, PrivKeyBuildPolicy::IguanaPrivKey(secret), )) .unwrap(); @@ -402,6 +405,9 @@ fn global_nft_with_random_privkey( &nft_dev_conf(), platform_request, build_policy, + ChainSpec::Evm { + chain_id: GETH_DEV_CHAIN_ID, + }, )) .unwrap(); @@ -432,6 +438,7 @@ fn global_nft_with_random_privkey( global_nft } +// Todo: This shouldn't be part of docker tests, move it to a separate module or stop relying on it #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// Can be used to generate coin from Sepolia Maker/Taker priv keys. fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, conf: &Json, erc20: bool) -> EthCoin { @@ -472,6 +479,9 @@ fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, co conf, platform_request, build_policy, + ChainSpec::Evm { + chain_id: ETH_SEPOLIA_CHAIN_ID, + }, )) .unwrap(); let coin = if erc20 { @@ -518,7 +528,9 @@ pub fn fill_eth_erc20_with_private_key(priv_key: Secp256k1Secret) { "ETH", ð_conf, &req, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: GETH_DEV_CHAIN_ID, + }, PrivKeyBuildPolicy::IguanaPrivKey(priv_key), )) .unwrap(); @@ -1465,6 +1477,9 @@ fn eth_coin_v2_activation_with_random_privkey( conf, platform_request, build_policy, + ChainSpec::Evm { + chain_id: GETH_DEV_CHAIN_ID, + }, )) .unwrap(); let my_address = block_on(coin.my_addr()); @@ -2657,7 +2672,9 @@ fn test_enable_custom_erc20() { .unwrap(); assert!(!buy.0.is_success(), "buy success, but should fail: {}", buy.1); assert!( - buy.1.contains(&format!("Rel coin {} is wallet only", ticker)), + buy.1.contains(&format!( + "'{ticker}' is a wallet only asset and can't be used in orders." + )), "Expected error message indicating that the token is wallet only, but got: {}", buy.1 ); diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index 7f759443a5..dfc6f3fabf 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -888,6 +888,7 @@ fn test_check_balance_on_order_post_base_coin_locked() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -987,6 +988,7 @@ fn test_get_max_taker_vol_and_trade_with_dynamic_trade_fee(coin: QtumCoin, priv_ "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1162,6 +1164,7 @@ fn test_trade_preimage_not_sufficient_base_coin_balance_for_ticker() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1221,6 +1224,7 @@ fn test_trade_preimage_dynamic_fee_not_sufficient_balance() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1282,6 +1286,7 @@ fn test_trade_preimage_deduct_fee_from_output_failed() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1341,6 +1346,7 @@ fn test_segwit_native_balance() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1387,6 +1393,7 @@ fn test_withdraw_and_send_from_segwit() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1435,6 +1442,7 @@ fn test_withdraw_and_send_legacy_to_segwit() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -1635,6 +1643,7 @@ fn segwit_address_in_the_orderbook() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, diff --git a/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs b/mm2src/mm2_main/tests/docker_tests/swaps_confs_settings_sync_tests.rs index 31c6c264e3..bda9b8fdfd 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 @@ -37,6 +37,7 @@ fn test_confirmation_settings_sync_correctly_on_buy( "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, @@ -202,6 +203,7 @@ fn test_confirmation_settings_sync_correctly_on_sell( "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".to_string(), None, diff --git a/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs b/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs index 785eb0a849..6446fa3a4f 100644 --- a/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swaps_file_lock_tests.rs @@ -45,6 +45,7 @@ fn swap_file_lock_prevents_double_swap_start_on_kick_start(swap_json: &str) { "rpc_password": "pass", "i_am_seed": true, "dbdir": db_folder.to_str().unwrap(), + "is_bootstrap_node": true }); let mut mm_bob = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -88,6 +89,7 @@ fn test_swaps_should_kick_start_if_process_was_killed() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }); let mut mm_bob = MarketMakerIt::start(bob_conf.clone(), "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); @@ -211,6 +213,7 @@ fn swap_should_not_kick_start_if_finished_during_waiting_for_file_lock( "rpc_password": "pass", "i_am_seed": true, "dbdir": db_folder.to_str().unwrap(), + "is_bootstrap_node": true }); let mut mm_bob = MarketMakerIt::start(bob_conf, "pass".to_string(), None).unwrap(); let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 45e3b4a03e..58ad67e645 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -329,7 +329,7 @@ fn test_custom_gas_limit_on_tendermint_withdraw() { fn test_tendermint_ibc_withdraw() { let _lock = SEQUENCE_LOCK.lock().unwrap(); // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels - const IBC_SOURCE_CHANNEL: &str = "channel-3"; + const IBC_SOURCE_CHANNEL: u16 = 3; const IBC_TARGET_ADDRESS: &str = "cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl"; const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; @@ -376,7 +376,7 @@ fn test_tendermint_ibc_withdraw() { fn test_tendermint_ibc_withdraw_hd() { let _lock = SEQUENCE_LOCK.lock().unwrap(); // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels - const IBC_SOURCE_CHANNEL: &str = "channel-3"; + const IBC_SOURCE_CHANNEL: u16 = 3; const IBC_TARGET_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; const MY_ADDRESS: &str = "cosmos134h9tv7866jcuw708w5w76lcfx7s3x2ysyalxy"; @@ -850,6 +850,7 @@ mod swap { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -941,6 +942,7 @@ mod swap { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -1036,6 +1038,7 @@ mod swap { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -1227,10 +1230,10 @@ mod swap { for uuid in uuids.iter() { log!("Checking alice status.."); - wait_check_stats_swap_status(&mm_alice, uuid, 30).await; + wait_check_stats_swap_status(&mm_alice, uuid, 240).await; log!("Checking bob status.."); - wait_check_stats_swap_status(&mm_bob, uuid, 30).await; + wait_check_stats_swap_status(&mm_bob, uuid, 240).await; } log!("Checking alice recent swaps.."); diff --git a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs index 9abcfbe506..1fa139ab31 100644 --- a/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/z_coin_docker_tests.rs @@ -1,38 +1,41 @@ use bitcrypto::dhash160; use coins::z_coin::{z_coin_from_conf_and_params_with_docker, z_send_dex_fee, ZCoin, ZcoinActivationParams, ZcoinRpcMode}; +use coins::DexFeeBurnDestination; use coins::{coin_errors::ValidatePaymentError, CoinProtocol, DexFee, PrivKeyBuildPolicy, RefundPaymentArgs, SendPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, ValidateFeeArgs}; use common::now_sec; use lazy_static::lazy_static; use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; use mm2_number::MmNumber; -use mm2_test_helpers::for_tests::{new_mm2_temp_folder_path, zombie_conf_for_docker}; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; +use mm2_test_helpers::for_tests::zombie_conf_for_docker; +use tempfile::TempDir; use tokio::sync::Mutex; // https://github.com/KomodoPlatform/librustzcash/blob/4e030a0f44cc17f100bf5f019563be25c5b8755f/zcash_client_backend/src/data_api/wallet.rs#L72-L73 lazy_static! { - static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); + /// For secret....fe + static ref GEN_TX_LOCK_MUTEX: Mutex<()> = Mutex::new(()); + /// For secret....we + static ref GEN_TX_LOCK_MUTEX_ADDR2: Mutex<()> = Mutex::new(()); + /// This `TempDir` is created once on first use and cleaned up when the process exits. + static ref TEMP_DIR: Mutex = Mutex::new(TempDir::new().unwrap()); } -/// Build asset `ZCoin` from ticker and spendingkey str without filling the balance. -pub async fn z_coin_from_spending_key(spending_key: &str) -> (MmArc, ZCoin) { - let ctx = MmCtxBuilder::new().into_mm_arc(); +/// Build asset `ZCoin` from ticker and spending_key. +pub async fn z_coin_from_spending_key<'a>(spending_key: &str, path: &'a str) -> (MmArc, ZCoin) { + let tmp = TEMP_DIR.lock().await; + let db_path = tmp.path().join(format!("ZOMBIE_DB_{path}")); + std::fs::create_dir_all(&db_path).unwrap(); + let ctx = MmCtxBuilder::new().with_conf(json!({ "dbdir": db_path})).into_mm_arc(); + let mut conf = zombie_conf_for_docker(); let params = ZcoinActivationParams { mode: ZcoinRpcMode::Native, ..Default::default() }; let pk_data = [1; 32]; - let salt: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(4) - .map(char::from) - .collect(); - let db_folder = new_mm2_temp_folder_path(None).join(format!("ZOMBIE_DB_{}", salt)); - std::fs::create_dir_all(&db_folder).unwrap(); + let protocol_info = match serde_json::from_value::(conf["protocol"].take()).unwrap() { CoinProtocol::ZHTLC(protocol_info) => protocol_info, other_protocol => panic!("Failed to get protocol from config: {:?}", other_protocol), @@ -44,7 +47,6 @@ pub async fn z_coin_from_spending_key(spending_key: &str) -> (MmArc, ZCoin) { &conf, ¶ms, PrivKeyBuildPolicy::IguanaPrivKey(pk_data.into()), - db_folder, protocol_info, spending_key, ) @@ -54,17 +56,24 @@ pub async fn z_coin_from_spending_key(spending_key: &str) -> (MmArc, ZCoin) { (ctx, coin) } -#[ignore] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test(flavor = "current_thread")] +async fn prepare_zombie_sapling_cache() { + let _lock = GEN_TX_LOCK_MUTEX.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe").await; + assert!(coin.is_sapling_state_synced().await); + drop(_lock) +} + +#[tokio::test(flavor = "current_thread")] async fn zombie_coin_send_and_refund_maker_payment() { - let _lock = TEST_MUTEX.lock().await; + let _lock = GEN_TX_LOCK_MUTEX.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe").await; + + assert!(coin.is_sapling_state_synced().await); - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; let time_lock = now_sec() - 3600; let secret_hash = [0; 20]; - let maker_uniq_data = [3; 32]; - let taker_uniq_data = [5; 32]; let taker_key_pair = coin.derive_htlc_key_pair(taker_uniq_data.as_slice()); let taker_pub = taker_key_pair.public(); @@ -100,12 +109,12 @@ async fn zombie_coin_send_and_refund_maker_payment() { drop(_lock); } -#[ignore] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test(flavor = "current_thread")] async fn zombie_coin_send_and_spend_maker_payment() { - let _lock = TEST_MUTEX.lock().await; + let _lock = GEN_TX_LOCK_MUTEX_ADDR2.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1qvqstxphqyqqpqqnh3hstqpdjzkpadeed6u7fz230jmm2mxl0aacrtu9vt7a7rmr2w5az5u79d24t0rudak3newknrz5l0m3dsd8m4dffqh5xwyldc5qwz8pnalrnhlxdzf900x83jazc52y25e9hvyd4kepaze6nlcvk8sd8a4qjh3e9j5d6730t7ctzhhrhp0zljjtwuptadnksxf8a8y5axwdhass5pjaxg0hzhg7z25rx0rll7a6txywl32s6cda0s5kexr03uqdtelwe", "we").await; - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + assert!(coin.is_sapling_state_synced().await); let lock_time = now_sec() - 1000; let secret = [0; 32]; @@ -149,37 +158,55 @@ async fn zombie_coin_send_and_spend_maker_payment() { drop(_lock); } -#[ignore] -#[tokio::test(flavor = "multi_thread")] -async fn prepare_zombie_sapling_cache() { - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; +#[tokio::test(flavor = "current_thread")] +async fn zombie_coin_send_standard_dex_fee() { + let _lock = GEN_TX_LOCK_MUTEX_ADDR2.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1qvqstxphqyqqpqqnh3hstqpdjzkpadeed6u7fz230jmm2mxl0aacrtu9vt7a7rmr2w5az5u79d24t0rudak3newknrz5l0m3dsd8m4dffqh5xwyldc5qwz8pnalrnhlxdzf900x83jazc52y25e9hvyd4kepaze6nlcvk8sd8a4qjh3e9j5d6730t7ctzhhrhp0zljjtwuptadnksxf8a8y5axwdhass5pjaxg0hzhg7z25rx0rll7a6txywl32s6cda0s5kexr03uqdtelwe", "we").await; assert!(coin.is_sapling_state_synced().await); + + let tx = z_send_dex_fee(&coin, DexFee::Standard("0.01".into()), &[1; 16]) + .await + .unwrap(); + log!("dex fee tx {}", tx.txid()); + drop(_lock) } -#[ignore] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test(flavor = "current_thread")] async fn zombie_coin_send_dex_fee() { - let _lock = TEST_MUTEX.lock().await; + let _lock = GEN_TX_LOCK_MUTEX_ADDR2.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1qvqstxphqyqqpqqnh3hstqpdjzkpadeed6u7fz230jmm2mxl0aacrtu9vt7a7rmr2w5az5u79d24t0rudak3newknrz5l0m3dsd8m4dffqh5xwyldc5qwz8pnalrnhlxdzf900x83jazc52y25e9hvyd4kepaze6nlcvk8sd8a4qjh3e9j5d6730t7ctzhhrhp0zljjtwuptadnksxf8a8y5axwdhass5pjaxg0hzhg7z25rx0rll7a6txywl32s6cda0s5kexr03uqdtelwe", "we").await; - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + assert!(coin.is_sapling_state_synced().await); - let dex_fee = DexFee::Standard("0.01".into()); + let dex_fee = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); log!("dex fee tx {}", tx.txid()); drop(_lock); } -#[ignore] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test(flavor = "current_thread")] async fn zombie_coin_validate_dex_fee() { - let _lock = TEST_MUTEX.lock().await; - let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe").await; + let _lock = GEN_TX_LOCK_MUTEX.lock().await; + let (_ctx, coin) = z_coin_from_spending_key("secret-extended-key-main1q0k2ga2cqqqqpq8m8j6yl0say83cagrqp53zqz54w38ezs8ly9ly5ptamqwfpq85u87w0df4k8t2lwyde3n9v0gcr69nu4ryv60t0kfcsvkr8h83skwqex2nf0vr32794fmzk89cpmjptzc22lgu5wfhhp8lgf3f5vn2l3sge0udvxnm95k6dtxj2jwlfyccnum7nz297ecyhmd5ph526pxndww0rqq0qly84l635mec0x4yedf95hzn6kcgq8yxts26k98j9g32kjc8y83fe", "fe").await; - // let balance = coin.my_balance().compat().await; + assert!(coin.is_sapling_state_synced().await); - let dex_fee = DexFee::Standard("0.01".into()); - let tx = z_send_dex_fee(&coin, dex_fee, &[1; 16]).await.unwrap(); + let tx = z_send_dex_fee( + &coin, + DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }, + &[1; 16], + ) + .await + .unwrap(); log!("dex fee tx {}", tx.txid()); let tx = tx.into(); @@ -187,52 +214,78 @@ async fn zombie_coin_validate_dex_fee() { fee_tx: &tx, expected_sender: &[], dex_fee: &DexFee::Standard(MmNumber::from("0.001")), - min_block_number: 4, + min_block_number: 12000, uuid: &[1; 16], }; // Invalid amount should return an error let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid amount")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } // Invalid memo should return an error + let expected_fee = DexFee::WithBurn { + fee_amount: "0.0075".into(), + burn_amount: "0.0025".into(), + burn_destination: DexFeeBurnDestination::PreBurnAccount, + }; + let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), - min_block_number: 10, + dex_fee: &expected_fee, + min_block_number: 12000, uuid: &[2; 16], }; + let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("Dex fee has invalid memo")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid memo")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } - // Confirmed before min block + // Success validation let validate_fee_args = ValidateFeeArgs { fee_tx: &tx, expected_sender: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), - min_block_number: 20000, + dex_fee: &expected_fee, + min_block_number: 12000, + uuid: &[1; 16], + }; + coin.validate_fee(validate_fee_args).await.unwrap(); + + // Test old standard dex fee with no burn output + // TODO: disable when the upgrade transition period ends + let tx_2 = z_send_dex_fee(&coin, DexFee::Standard("0.00879999".into()), &[1; 16]) + .await + .unwrap(); + log!("dex fee tx {}", tx_2.txid()); + let tx_2 = tx_2.into(); + + // Success validation + let validate_fee_args = ValidateFeeArgs { + fee_tx: &tx_2, + expected_sender: &[], + dex_fee: &DexFee::Standard("0.00999999".into()), + min_block_number: 12000, uuid: &[1; 16], }; let err = coin.validate_fee(validate_fee_args).await.unwrap_err().into_inner(); match err { - ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("confirmed before min block")), + ValidatePaymentError::WrongPaymentTx(err) => assert!(err.contains("invalid amount")), _ => panic!("Expected `WrongPaymentTx`: {:?}", err), } // Success validation + let expected_std_fee = DexFee::Standard("0.00879999".into()); let validate_fee_args = ValidateFeeArgs { - fee_tx: &tx, + fee_tx: &tx_2, expected_sender: &[], - dex_fee: &DexFee::Standard(MmNumber::from("0.01")), - min_block_number: 20, + dex_fee: &expected_std_fee, + min_block_number: 12000, uuid: &[1; 16], }; coin.validate_fee(validate_fee_args).await.unwrap(); - drop(_lock); + drop(_lock) } 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 cc1094e668..564e2de8cf 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 @@ -34,6 +34,7 @@ fn test_withdraw_cashaddresses() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -238,6 +239,7 @@ fn test_withdraw_to_different_cashaddress_network_should_fail() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -299,6 +301,7 @@ fn test_common_cashaddresses() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -502,6 +505,7 @@ fn test_sign_verify_message_bch() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -529,7 +533,7 @@ fn test_sign_verify_message_bch() { let electrum: Json = json::from_str(&electrum.1).unwrap(); log!("{:?}", electrum); - let response = block_on(sign_message(&mm, "BCH")); + let response = block_on(sign_message(&mm, "BCH", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -571,6 +575,7 @@ fn test_sign_verify_message_slp() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -586,7 +591,7 @@ fn test_sign_verify_message_slp() { let enable_usdf = block_on(enable_slp(&mm, "USDF")); log!("enable_usdf: {:?}", enable_usdf); - let response = block_on(sign_message(&mm, "USDF")); + let response = block_on(sign_message(&mm, "USDF", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; 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 f1149c15f3..705dec58f4 100644 --- a/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/best_orders_tests.rs @@ -30,6 +30,7 @@ fn test_best_orders_v2_exclude_mine() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -137,6 +138,7 @@ fn test_best_orders_no_duplicates_after_update() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -291,6 +293,7 @@ fn test_best_orders_address_and_confirmations() { "coins": bob_coins_config, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -603,6 +606,7 @@ fn zhtlc_best_orders() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, diff --git a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs index 4fde665bd6..7b7c15688f 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lightning_tests.rs @@ -112,6 +112,7 @@ fn start_lightning_nodes(enable_0_confs: bool) -> (MarketMakerIt, MarketMakerIt, "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -163,7 +164,6 @@ async fn open_channel( let request = mm .rpc(&json!({ "userpass": mm.userpass, - "mmrpc": "2.0", "method": "lightning::channels::open_channel", "params": { "coin": coin, @@ -197,7 +197,6 @@ async fn close_channel(mm: &MarketMakerIt, uuid: &str, force_close: bool) -> Jso let request = mm .rpc(&json!({ "userpass": mm.userpass, - "mmrpc": "2.0", "method": "lightning::channels::close_channel", "params": { "coin": "tBTC-TEST-lightning", @@ -221,7 +220,6 @@ 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", @@ -243,7 +241,6 @@ 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", @@ -267,7 +264,6 @@ 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", @@ -293,7 +289,6 @@ 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", @@ -368,6 +363,7 @@ fn test_enable_lightning() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -428,7 +424,6 @@ fn test_connect_to_node() { let connect = block_on(mm_node_2.rpc(&json!({ "userpass": mm_node_2.userpass, - "mmrpc": "2.0", "method": "lightning::nodes::connect_to_node", "params": { "coin": "tBTC-TEST-lightning", @@ -468,7 +463,6 @@ fn test_open_channel() { let list_channels_node_1 = block_on(mm_node_1.rpc(&json!({ "userpass": mm_node_1.userpass, - "mmrpc": "2.0", "method": "lightning::channels::list_open_channels_by_filter", "params": { "coin": "tBTC-TEST-lightning", @@ -497,7 +491,6 @@ fn test_open_channel() { let list_channels_node_2 = block_on(mm_node_2.rpc(&json!({ "userpass": mm_node_2.userpass, - "mmrpc": "2.0", "method": "lightning::channels::list_open_channels_by_filter", "params": { "coin": "tBTC-TEST-lightning", @@ -547,7 +540,6 @@ fn test_send_payment() { let send_payment = 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", @@ -1055,6 +1047,7 @@ fn test_sign_verify_message_lightning() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1066,7 +1059,7 @@ fn test_sign_verify_message_lightning() { block_on(enable_electrum(&mm, "tBTC-TEST-segwit", false, T_BTC_ELECTRUMS)); block_on(enable_lightning(&mm, "tBTC-TEST-lightning", 600)); - let response = block_on(sign_message(&mm, "tBTC-TEST-lightning")); + let response = block_on(sign_message(&mm, "tBTC-TEST-lightning", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; diff --git a/mm2src/mm2_main/tests/mm2_tests/lp_bot_tests.rs b/mm2src/mm2_main/tests/mm2_tests/lp_bot_tests.rs index 846508d2f2..e95fbcff38 100644 --- a/mm2src/mm2_main/tests/mm2_tests/lp_bot_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/lp_bot_tests.rs @@ -19,6 +19,7 @@ fn test_start_and_stop_simple_market_maker_bot() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, 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 a2451ac362..392a046838 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -13,10 +13,10 @@ use mm2_test_helpers::electrums::*; #[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] use mm2_test_helpers::for_tests::wait_check_stats_swap_status; use mm2_test_helpers::for_tests::{account_balance, btc_segwit_conf, btc_with_spv_conf, btc_with_sync_starting_header, - check_recent_swaps, enable_qrc20, enable_utxo_v2_electrum, eth_dev_conf, - find_metrics_in_json, from_env_file, get_new_address, get_shared_db_id, - get_wallet_names, mm_spat, morty_conf, my_balance, rick_conf, sign_message, - start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_with_spv_conf, + check_recent_swaps, delete_wallet, enable_qrc20, enable_utxo_v2_electrum, + eth_dev_conf, find_metrics_in_json, from_env_file, get_new_address, + get_shared_db_id, get_wallet_names, mm_spat, morty_conf, my_balance, rick_conf, + sign_message, start_swaps, tbtc_conf, tbtc_segwit_conf, tbtc_with_spv_conf, test_qrc20_history_impl, tqrc20_conf, verify_message, wait_for_swaps_finish_and_check_status, wait_till_history_has_records, MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, RaiiDump, @@ -53,16 +53,6 @@ cfg_wasm32! { fn test_rpc() { let (_, mm, _dump_log, _dump_dashboard) = mm_spat(); - let no_method = block_on(mm.rpc(&json! ({ - "userpass": mm.userpass, - "coin": "RICK", - "ipaddr": "electrum1.cipig.net", - "port": 10017 - }))) - .unwrap(); - assert!(no_method.0.is_server_error()); - assert_eq!((no_method.2)[ACCESS_CONTROL_ALLOW_ORIGIN], "http://localhost:4000"); - let not_json = mm.rpc_str("It's just a string").unwrap(); assert!(not_json.0.is_server_error()); assert_eq!((not_json.2)[ACCESS_CONTROL_ALLOW_ORIGIN], "http://localhost:4000"); @@ -118,6 +108,7 @@ fn orders_of_banned_pubkeys_should_not_be_displayed() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -222,6 +213,7 @@ fn test_my_balance() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -347,8 +339,8 @@ fn test_check_balance_on_order_post() { let coins = json!([ {"coin":"RICK","asset":"RICK","rpcport":8923,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, {"coin":"MORTY","asset":"MORTY","rpcport":11608,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"ETH","name":"ethereum","chain_id":1,"protocol":{"type":"ETH"},"rpcport":80}, - {"coin":"JST","name":"jst","chain_id":1,"protocol":{"type":"ERC20", "protocol_data":{"platform":"ETH","contract_address":"0x996a8aE0304680F6A69b8A9d7C6E37D65AB5AB56"}}} + {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH","protocol_data":{"chain_id":1}},"rpcport":80}, + {"coin":"JST","name":"jst","protocol":{"type":"ERC20", "protocol_data":{"platform":"ETH","contract_address":"0x996a8aE0304680F6A69b8A9d7C6E37D65AB5AB56"}}} ]); // start bob and immediately place the order @@ -363,6 +355,7 @@ fn test_check_balance_on_order_post() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -423,6 +416,7 @@ fn test_rpc_password_from_json() { "rpc_password": "", "i_am_seed": true, "skip_startup_checks": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -440,6 +434,7 @@ fn test_rpc_password_from_json() { "rpc_password": {"key":"value"}, "i_am_seed": true, "skip_startup_checks": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -455,6 +450,7 @@ fn test_rpc_password_from_json() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -549,6 +545,7 @@ fn test_mmrpc_v2() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -653,7 +650,7 @@ fn test_rpc_password_from_json_no_userpass() { "netid": 9998, "passphrase": "bob passphrase", "coins": coins, - "i_am_seed": true, + "disable_p2p": true }), "password".into(), None, @@ -794,10 +791,10 @@ async fn trade_base_rel_electrum( #[cfg(all(not(target_arch = "wasm32"), not(feature = "zhtlc-native-tests")))] for uuid in uuids.iter() { log!("Checking alice status.."); - wait_check_stats_swap_status(&mm_alice, uuid, 30).await; + wait_check_stats_swap_status(&mm_alice, uuid, 240).await; log!("Checking bob status.."); - wait_check_stats_swap_status(&mm_bob, uuid, 30).await; + wait_check_stats_swap_status(&mm_bob, uuid, 240).await; } log!("Checking alice recent swaps.."); @@ -872,7 +869,7 @@ fn withdraw_and_send( use coins::TxFeeDetails; use std::ops::Sub; - let from = from.map(WithdrawFrom::AddressId); + let from = from.map(HDAddressSelector::AddressId); let withdraw = block_on(mm.rpc(&json! ({ "mmrpc": "2.0", "userpass": mm.userpass, @@ -943,6 +940,7 @@ fn test_withdraw_and_send() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -1262,6 +1260,7 @@ fn test_swap_status() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -1315,6 +1314,7 @@ fn test_order_errors_when_base_equal_rel() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1374,6 +1374,7 @@ fn startup_passphrase(passphrase: &str, expected_address: &str) { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1441,6 +1442,7 @@ fn test_cancel_order() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1586,6 +1588,7 @@ fn test_cancel_all_orders() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1734,6 +1737,7 @@ fn test_electrum_enable_conn_errors() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1780,6 +1784,7 @@ fn test_order_should_not_be_displayed_when_node_is_down() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -1892,6 +1897,7 @@ fn test_own_orders_should_not_be_removed_from_orderbook() { "i_am_seed": true, "rpc_password": "pass", "maker_order_timeout": 5, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1968,6 +1974,7 @@ fn test_show_priv_key() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2008,6 +2015,7 @@ fn test_electrum_and_enable_response() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2111,6 +2119,7 @@ fn set_price_with_cancel_previous_should_broadcast_cancelled_message() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2236,6 +2245,7 @@ fn test_batch_requests() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2311,6 +2321,7 @@ fn test_metrics_method() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2361,7 +2372,8 @@ fn test_electrum_tx_history() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", - "metrics_interval": 30. + "metrics_interval": 30., + "is_bootstrap_node": true }), "pass".into(), None, @@ -2462,6 +2474,7 @@ fn test_convert_utxo_address() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2673,6 +2686,7 @@ fn test_convert_eth_address() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -2779,6 +2793,7 @@ fn test_add_delegation_qtum() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2865,6 +2880,7 @@ fn test_remove_delegation_qtum() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2922,6 +2938,7 @@ fn test_query_delegations_info_qtum() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -2978,6 +2995,7 @@ fn test_convert_qrc20_address() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -3125,6 +3143,7 @@ fn test_validateaddress() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -3339,6 +3358,7 @@ fn qrc20_activate_electrum() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -3387,6 +3407,7 @@ fn test_qrc20_withdraw() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -3469,6 +3490,7 @@ fn test_qrc20_withdraw_error() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -3541,7 +3563,7 @@ fn test_qrc20_withdraw_error() { fn test_get_raw_transaction() { let coins = json! ([ {"coin":"RICK","asset":"RICK","required_confirmations":0,"txversion":4,"overwintered":1,"protocol":{"type":"UTXO"}}, - {"coin":"ETH","name":"ethereum","chain_id":1,"protocol":{"type":"ETH"}}, + {"coin":"ETH","name":"ethereum","protocol":{"type":"ETH","protocol_data":{"chain_id":1}}}, ]); let mm = MarketMakerIt::start( json! ({ @@ -3552,6 +3574,7 @@ fn test_get_raw_transaction() { "i_am_seed": true, "rpc_password": "pass", "metrics_interval": 30., + "is_bootstrap_node": true }), "pass".into(), None, @@ -3728,6 +3751,7 @@ fn test_get_raw_transaction() { } #[test] +#[ignore] #[cfg(not(target_arch = "wasm32"))] fn test_qrc20_tx_history() { block_on(test_qrc20_history_impl(None)); } @@ -3947,6 +3971,7 @@ fn test_update_maker_order() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4087,6 +4112,7 @@ fn test_update_maker_order_fail() { "coins": coins, "rpc_password": "password", "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4290,6 +4316,7 @@ fn test_trade_fee_returns_numbers_in_various_formats() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -4332,6 +4359,7 @@ fn test_orderbook_is_mine_orders() { "coins": coins, "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true }), "pass".into(), None, @@ -4511,6 +4539,7 @@ fn test_mm2_db_migration() { "rpc_password": "password", "i_am_seed": true, "dbdir": mm2_folder.display().to_string(), + "is_bootstrap_node": true }), "password".into(), None, @@ -4570,6 +4599,7 @@ fn test_get_public_key() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4612,6 +4642,7 @@ fn test_get_public_key_hash() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4652,6 +4683,7 @@ fn test_get_orderbook_with_same_orderbook_ticker() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4699,6 +4731,7 @@ fn test_conf_settings_in_orderbook() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4822,6 +4855,7 @@ fn alice_can_see_confs_in_orderbook_after_sync() { "rpc_password": "password", "coins": bob_coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -4973,6 +5007,7 @@ fn test_sign_verify_message_utxo() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -4986,7 +5021,7 @@ fn test_sign_verify_message_utxo() { block_on(enable_coins_rick_morty_electrum(&mm_bob)) ); - let response = block_on(sign_message(&mm_bob, "RICK")); + let response = block_on(sign_message(&mm_bob, "RICK", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -5007,6 +5042,75 @@ fn test_sign_verify_message_utxo() { assert!(response.is_valid); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sign_verify_message_utxo_with_derivation_path() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([rick_conf()]); + + let path_to_address = HDAccountAddressId::default(); + let conf_0 = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd_0 = MarketMakerIt::start(conf_0.conf, conf_0.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm_hd_0.mm_dump(); + log!("log path: {}", mm_hd_0.log_path.display()); + + let rick = block_on(enable_utxo_v2_electrum( + &mm_hd_0, + "RICK", + doc_electrums(), + Some(path_to_address), + 60, + None, + )); + let balance = match rick.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let address0 = &balance.accounts.get(0).expect("Expected account at index 0").addresses[0].address; + let address1 = &balance.accounts.get(0).expect("Expected account at index 1").addresses[1].address; + + // Test address0 + let expected_signature = "ICnkSvQkAurwLvK6RYtCItrWMOS4ESjCf4GKp1DvBM8Xc2+dxM4si6NcSb0JJaJajYhPkwg5yWHmgR/9AmGB0KE="; + let response = block_on(sign_message( + &mm_hd_0, + "RICK", + Some(HDAddressSelector::DerivationPath { + derivation_path: "m/44'/141'/0'/0/0".to_owned(), + }), + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + assert_eq!(expected_signature, response.signature); + + let response = block_on(verify_message(&mm_hd_0, "RICK", expected_signature, address0)); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + assert!(response.is_valid); + + // Test address1. + let expected_signature = "IPGbtsPPz6u2DishjOcP0Lf6xqPfpvTcMnkP/rRUVddKPBtkN+SfUPVZcz1vagjhj95I2t4ctLzcc3vcRdQLxbY="; + let response = block_on(sign_message( + &mm_hd_0, + "RICK", + Some(HDAddressSelector::AddressId(HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + })), + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + assert_eq!(expected_signature, response.signature); + + let response = block_on(verify_message(&mm_hd_0, "RICK", expected_signature, address1)); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + assert!(response.is_valid); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_verify_message_utxo_segwit() { @@ -5040,6 +5144,7 @@ fn test_sign_verify_message_utxo_segwit() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -5053,7 +5158,7 @@ fn test_sign_verify_message_utxo_segwit() { block_on(enable_coins_rick_morty_electrum(&mm_bob)) ); - let response = block_on(sign_message(&mm_bob, "RICK")); + let response = block_on(sign_message(&mm_bob, "RICK", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -5085,6 +5190,217 @@ fn test_sign_verify_message_utxo_segwit() { assert!(response.is_valid); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sign_verify_message_segwit_with_bip84_derivation_path() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let rick_segwit_conf = json!({ + "coin": "RICK", + "asset": "RICK", + "rpcport": 8923, + "sign_message_prefix": "Komodo Signed Message:\n", + "txversion": 4, + "overwintered": 1, + "segwit": true, + "address_format": {"format": "segwit"}, + "bech32_hrp": "rck", + "protocol": {"type": "UTXO"}, + "derivation_path": "m/84'/141'", + }); + + let coins = json!([rick_segwit_conf]); + + // Start MM with HD wallet + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_dump_log, _dump_dashboard) = mm_hd.mm_dump(); + log!("log path: {}", mm_hd.log_path.display()); + + // Enable RICK with BIP84 derivation path (segwit) + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + }; + + // Enable with BIP84 path + let rick = block_on(enable_utxo_v2_electrum( + &mm_hd, + "RICK", + doc_electrums(), + Some(path_to_address), + 60, + None, + )); + + let balance = match rick.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + + let account0 = balance.accounts.get(0).expect("Expected account at index 0"); + let address0 = &account0.addresses[0].address; + let address1 = &account0.addresses[1].address; + + // Verify addresses are segwit (bech32) + assert!( + address0.starts_with("rck1"), + "Expected segwit address for address0: {}", + address0 + ); + assert!( + address1.starts_with("rck1"), + "Expected segwit address for address1: {}", + address1 + ); + + // Test 1: Sign with BIP84 path for address0 (m/84'/141'/0'/0/0) + let derivation_path_0 = "m/84'/141'/0'/0/0"; + let sign_response = block_on(sign_message( + &mm_hd, + "RICK", + Some(HDAddressSelector::DerivationPath { + derivation_path: derivation_path_0.to_owned(), + }), + )); + let sign_response: RpcV2Response = json::from_value(sign_response).unwrap(); + let signature0 = sign_response.result.signature; + + log!("Signature for {}: {}", derivation_path_0, signature0); + log!("Address0: {}", address0); + + // Verify with the segwit address + let verify_response = block_on(verify_message(&mm_hd, "RICK", &signature0, address0)); + let verify_response: RpcV2Response = json::from_value(verify_response).unwrap(); + assert!(verify_response.result.is_valid, "Verification failed for address0"); + + // Test 2: Sign with AddressId for address1 + let sign_response = block_on(sign_message( + &mm_hd, + "RICK", + Some(HDAddressSelector::AddressId(HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + })), + )); + let sign_response: RpcV2Response = json::from_value(sign_response).unwrap(); + let signature1 = sign_response.result.signature; + + log!("Signature for address1: {}", signature1); + log!("Address1: {}", address1); + + // Verify with the segwit address + let verify_response = block_on(verify_message(&mm_hd, "RICK", &signature1, address1)); + let verify_response: RpcV2Response = json::from_value(verify_response).unwrap(); + assert!(verify_response.result.is_valid, "Verification failed for address1"); + + // Test 3: Cross-verification should fail + let verify_response = block_on(verify_message(&mm_hd, "RICK", &signature0, address1)); + let verify_response: RpcV2Response = json::from_value(verify_response).unwrap(); + assert!(!verify_response.result.is_valid, "Cross-verification should fail"); +} + +///NOTE: Should not fail after [issue #2470](https://github.com/KomodoPlatform/komodo-defi-framework/issues/2470) is resolved. +#[test] +#[ignore] +#[cfg(not(target_arch = "wasm32"))] +fn test_hd_address_conflict_across_derivation_paths() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let rick_legacy_conf = json!({ + "coin": "RICK", + "asset": "RICK", + "rpcport": 8923, + "sign_message_prefix": "Komodo Signed Message:\n", + "txversion": 4, + "overwintered": 1, + "segwit": true, + "address_format": {"format": "segwit"}, + "bech32_hrp": "rck", + "protocol": {"type": "UTXO"}, + "derivation_path": "m/49'/141'", + }); + let coins = json!([rick_legacy_conf]); + + let mut conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf.clone(), conf.rpc_password.clone(), None).unwrap(); + + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 0, + }; + // Enable RICK with m/49'/141' + let rick_1 = block_on(enable_utxo_v2_electrum( + &mm_hd, + "RICK", + doc_electrums(), + Some(path_to_address.clone()), + 60, + None, + )); + let old_address = match &rick_1.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd.accounts[0].addresses[0].address.clone(), + _ => panic!("Expected HD wallet balance"), + }; + log!("Old address: {}", old_address); + + // Shutdown MM and restart RICK with derivation path m/84'/141' + log!("Conf log path: {}", mm_hd.log_path.display()); + conf.conf["dbdir"] = mm_hd.folder.join("DB").to_str().unwrap().into(); + block_on(mm_hd.stop()).unwrap(); + + let coin = json!({ + "coin": "RICK", + "asset": "RICK", + "rpcport": 8923, + "sign_message_prefix": "Komodo Signed Message:\n", + "txversion": 4, + "overwintered": 1, + "segwit": true, + "address_format": {"format": "segwit"}, + "bech32_hrp": "rck", + "protocol": {"type": "UTXO"}, + "derivation_path": "m/84'/141'", + }); + conf.conf["coins"] = json!([coin]); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + // Re-enable RICK, but it will try to reuse address0 stored under old path(m/49'/141') + let rick_2 = block_on(enable_utxo_v2_electrum( + &mm_hd, + "RICK", + doc_electrums(), + Some(path_to_address), + 60, + None, + )); + let new_address = match &rick_2.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd.accounts[0].addresses[0].address.clone(), + _ => panic!("Expected HD wallet balance"), + }; + log!("New address: {}", new_address); + + // KDF has a bug and reuses the same account (and thus the same address) for derivation paths that use different `m/purpose'/coin'` fields. + + // This stems from the fact that KDF doesn't differentiate/store the "purpose" & "coin" derivation fields in the database, but it rather stores the whole xpub + + // that repsresents `m/purpose'/coin'/account_id'` + + // Now, when KDF queries the database for already stored accounts, it specifies the specifies `COIN=ticker` in the SQL query, and since + + // we badly mutated the conf by changing the derivation path but not the coin ticker, it returns accounts belonging to the old coin ticker (old derivation path). + + // This wouldn't have happened if we gave the conf with `m/84'/141'` ticker="RICK-segwit" and `m/49'/141'` ticker="RICK-legacy", but we don't do that. + + assert_ne!( + old_address, new_address, + "Address from old derivation path(m/49'/141') should not match address from new derivation path(m/84'/141')" + ); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_verify_message_eth() { @@ -5098,10 +5414,14 @@ fn test_sign_verify_message_eth() { "sign_message_prefix": "Ethereum Signed Message:\n", "rpcport": 80, "mm2": 1, - "chain_id": 1, "required_confirmations": 3, "avg_blocktime": 0.25, - "protocol": {"type": "ETH"} + "protocol": { + "type": "ETH", + "protocol_data": { + "chain_id": 1 + } + } } ]); @@ -5117,6 +5437,7 @@ fn test_sign_verify_message_eth() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -5131,7 +5452,7 @@ fn test_sign_verify_message_eth() { block_on(enable_native(&mm_bob, "ETH", ETH_SEPOLIA_NODES, None)) ); - let response = block_on(sign_message(&mm_bob, "ETH")); + let response = block_on(sign_message(&mm_bob, "ETH", None)); let response: RpcV2Response = json::from_value(response).unwrap(); let response = response.result; @@ -5149,6 +5470,137 @@ fn test_sign_verify_message_eth() { assert!(response.is_valid); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_sign_verify_message_eth_with_derivation_path() { + use mm2_test_helpers::for_tests::ETH_SEPOLIA_CHAIN_ID; + + let seed = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + let coins = json!([ + { + "coin": "ETH", + "name": "ethereum", + "fname": "Ethereum", + "sign_message_prefix": "Ethereum Signed Message:\n", + "rpcport": 80, + "mm2": 1, + "chain_id": 1, + "required_confirmations": 3, + "avg_blocktime": 0.25, + "protocol":{ + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID, + } + }, + + "derivation_path": "m/44'/60'" + } + ]); + + // start bob and immediately place the order + let mm_bob = MarketMakerIt::start( + json! ({ + "gui": "nogui", + "netid": 9998, + "myipaddr": env::var ("BOB_TRADE_IP") .ok(), + "rpcip": env::var ("BOB_TRADE_IP") .ok(), + "canbind": env::var ("BOB_TRADE_PORT") .ok().map (|s| s.parse::().unwrap()), + "passphrase": seed.to_string(), + "enable_hd": true, + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + "is_bootstrap_node": true + }), + "pass".into(), + None, + ) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_bob.mm_dump(); + log!("Bob log path: {}", mm_bob.log_path.display()); + + // Enable coins on Bob side. Print the replies in case we need the "address". + let enable = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "enable_eth_with_tokens", + "mmrpc": "2.0", + "params": { + "ticker": "ETH", + "priv_key_policy": { "type": "ContextPrivKey" }, + "mm2": 1, + "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, + "nodes": ETH_SEPOLIA_NODES.iter().map(|node| json!({ "url": node})).collect::>(), + "erc20_tokens_requests": [] + } + }))) + .unwrap(); + + assert_eq!( + enable.0, + StatusCode::OK, + "'enable_eth_with_tokens' failed: {}", + enable.1 + ); + let result: Json = json::from_str(&enable.1).unwrap(); + let result: HDEthWithTokensActivationResult = json::from_value(result["result"].clone()).unwrap(); + log!("enable_coins (bob): {result:?}"); + + let response = block_on(sign_message( + &mm_bob, + "ETH", + Some(HDAddressSelector::DerivationPath { + derivation_path: "m/44'/60'/0'/0/0".to_owned(), + }), + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + let expected_signature = + "0x36b91a54f905f2dd88ecfd7f4a539710c699eaab2b425ba79ad959c29ec26492011674981da72d68ac0ab72bb35661a13c42bce314ecdfff0e44174f82a7ee2501"; + assert_eq!(expected_signature, response.signature); + + let address0 = match result.wallet_balance { + EnableCoinBalanceMap::HD(bal) => bal.accounts[0].addresses[0].address.clone(), + EnableCoinBalanceMap::Iguana(_) => panic!("Expected HD"), + }; + let response = block_on(verify_message(&mm_bob, "ETH", expected_signature, &address0)); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + assert!(response.is_valid); + + // Test address 1. + let get_new_address = block_on(get_new_address(&mm_bob, "ETH", 0, Some(Bip44Chain::External))); + assert!(get_new_address.new_address.balance.contains_key("ETH")); + let response = block_on(sign_message( + &mm_bob, + "ETH", + Some(HDAddressSelector::AddressId(HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + })), + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + let expected_signature = + "0xc8aa1d54c311e38edc815308dc67018aecbd6d4008a88b9af7aba9c98997b7b56f9e6eab64b3c496c6fff1762ae0eba8228370b369d505dd9087cded0a4d947a01"; + assert_eq!(expected_signature, response.signature); + + let response = block_on(verify_message( + &mm_bob, + "ETH", + expected_signature, + &get_new_address.new_address.address, + )); + let response: RpcV2Response = json::from_value(response).unwrap(); + let response = response.result; + + assert!(response.is_valid); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_no_login() { @@ -5564,6 +6016,7 @@ fn test_enable_btc_with_sync_starting_header() { "passphrase": "bob passphrase", "coins": coins, "rpc_password": "pass", + "disable_p2p": true }), "pass".into(), None, @@ -5594,6 +6047,7 @@ fn test_btc_block_header_sync() { "passphrase": "bob passphrase", "coins": coins, "rpc_password": "pass", + "disable_p2p": true }), "pass".into(), None, @@ -5632,6 +6086,7 @@ fn test_tbtc_block_header_sync() { "passphrase": "bob passphrase", "coins": coins, "rpc_password": "pass", + "disable_p2p": true }), "pass".into(), None, @@ -5903,7 +6358,7 @@ fn test_change_mnemonic_password_rpc() { .unwrap(); assert_eq!( request.0, - StatusCode::INTERNAL_SERVER_ERROR, + StatusCode::BAD_REQUEST, "'change_mnemonic_password' failed: {}", request.1 ); @@ -5927,6 +6382,119 @@ fn test_change_mnemonic_password_rpc() { ); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_delete_wallet_rpc() { + let coins = json!([]); + let wallet_1_name = "wallet_to_be_deleted"; + let wallet_1_pass = "pass1"; + let wallet_1_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_1_name, wallet_1_pass); + let mm_wallet_1 = MarketMakerIt::start(wallet_1_conf.conf, wallet_1_conf.rpc_password, None).unwrap(); + + let get_wallet_names_1 = block_on(get_wallet_names(&mm_wallet_1)); + assert_eq!(get_wallet_names_1.wallet_names, vec![wallet_1_name]); + assert_eq!(get_wallet_names_1.activated_wallet.as_deref(), Some(wallet_1_name)); + + let wallet_2_name = "active_wallet"; + let wallet_2_pass = "pass2"; + let mut wallet_2_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_2_name, wallet_2_pass); + wallet_2_conf.conf["dbdir"] = mm_wallet_1.folder.join("DB").to_str().unwrap().into(); + + block_on(mm_wallet_1.stop()).unwrap(); + + let mm_wallet_2 = MarketMakerIt::start(wallet_2_conf.conf, wallet_2_conf.rpc_password, None).unwrap(); + + let get_wallet_names_2 = block_on(get_wallet_names(&mm_wallet_2)); + assert_eq!(get_wallet_names_2.wallet_names, vec![ + "active_wallet", + "wallet_to_be_deleted" + ]); + assert_eq!(get_wallet_names_2.activated_wallet.as_deref(), Some(wallet_2_name)); + + // Try to delete the active wallet - should fail + let (status, body, _) = block_on(delete_wallet(&mm_wallet_2, wallet_2_name, wallet_2_pass)); + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(body.contains("Cannot delete wallet 'active_wallet' as it is currently active.")); + + // Try to delete with the wrong password - should fail + let (status, body, _) = block_on(delete_wallet(&mm_wallet_2, wallet_1_name, "wrong_pass")); + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(body.contains("Invalid password")); + + // Try to delete a non-existent wallet - should fail + let (status, body, _) = block_on(delete_wallet(&mm_wallet_2, "non_existent_wallet", "any_pass")); + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(body.contains("Wallet 'non_existent_wallet' not found.")); + + // Delete the inactive wallet with the correct password - should succeed + let (status, body, _) = block_on(delete_wallet(&mm_wallet_2, wallet_1_name, wallet_1_pass)); + assert_eq!(status, StatusCode::OK, "Body: {}", body); + + // Verify the wallet is deleted + let get_wallet_names_3 = block_on(get_wallet_names(&mm_wallet_2)); + assert_eq!(get_wallet_names_3.wallet_names, vec![wallet_2_name]); + assert_eq!(get_wallet_names_3.activated_wallet.as_deref(), Some(wallet_2_name)); + + block_on(mm_wallet_2.stop()).unwrap(); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_delete_wallet_in_no_login_mode() { + // 0. Setup a seednode to be able to run a no-login node. + let seednode_conf = Mm2TestConf::seednode_with_wallet_name(&json!([]), "seednode_wallet", "seednode_pass"); + let mm_seednode = MarketMakerIt::start(seednode_conf.conf, seednode_conf.rpc_password, None).unwrap(); + let seednode_ip = mm_seednode.ip.to_string(); + + // 1. Setup: Create a wallet to be deleted later. + let wallet_to_delete_name = "wallet_for_no_login_test"; + let wallet_to_delete_pass = "password123"; + let coins = json!([]); + + let wallet_conf = Mm2TestConf::seednode_with_wallet_name(&coins, wallet_to_delete_name, wallet_to_delete_pass); + let mm_setup = MarketMakerIt::start(wallet_conf.conf.clone(), wallet_conf.rpc_password, None).unwrap(); + + let wallet_names_before = block_on(get_wallet_names(&mm_setup)); + assert_eq!(wallet_names_before.wallet_names, vec![wallet_to_delete_name]); + let db_dir = mm_setup.folder.join("DB"); + block_on(mm_setup.stop()).unwrap(); + + // 2. Execution: Start in no-login mode, connecting to the seednode. + let mut no_login_conf = Mm2TestConf::no_login_node(&coins, &[&seednode_ip]); + no_login_conf.conf["dbdir"] = db_dir.to_str().unwrap().into(); + + let mm_no_login = MarketMakerIt::start(no_login_conf.conf, no_login_conf.rpc_password, None).unwrap(); + + let wallet_names_no_login = block_on(get_wallet_names(&mm_no_login)); + assert!(wallet_names_no_login + .wallet_names + .contains(&wallet_to_delete_name.to_string())); + + let (status, body, _) = block_on(delete_wallet( + &mm_no_login, + wallet_to_delete_name, + wallet_to_delete_pass, + )); + assert_eq!(status, StatusCode::OK, "Delete failed with body: {}", body); + + block_on(mm_no_login.stop()).unwrap(); + + // 3. Verification: Start another instance to check if the wallet is gone. + let mut verification_conf = Mm2TestConf::seednode_with_wallet_name(&coins, "verification_wallet", "pass"); + verification_conf.conf["dbdir"] = db_dir.to_str().unwrap().into(); + let mm_verify = MarketMakerIt::start(verification_conf.conf, verification_conf.rpc_password, None).unwrap(); + + let wallet_names_after = block_on(get_wallet_names(&mm_verify)); + assert!(!wallet_names_after + .wallet_names + .contains(&wallet_to_delete_name.to_string())); + + block_on(mm_verify.stop()).unwrap(); + + // 4. Teardown: Stop the seednode. + block_on(mm_seednode.stop()).unwrap(); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_raw_transaction_rick() { @@ -6052,6 +6620,7 @@ fn test_connection_healthcheck_rpc() { thread::sleep(Duration::from_secs(2)); let mut alice_conf = Mm2TestConf::seednode(ALICE_SEED, &json!([])); + alice_conf.conf["is_bootstrap_node"] = json!(false); alice_conf.conf["seednodes"] = json!([bob_mm.my_seed_addr()]); alice_conf.conf["skip_startup_checks"] = json!(true); let alice_mm = MarketMakerIt::start(alice_conf.conf, alice_conf.rpc_password, None).unwrap(); @@ -6102,8 +6671,8 @@ mod trezor_tests { eth_testnet_conf_trezor, init_trezor_rpc, init_trezor_status_rpc, init_trezor_user_action_rpc, init_withdraw, jst_sepolia_trezor_conf, mm_ctx_with_custom_db_with_conf, tbtc_legacy_conf, tbtc_segwit_conf, - withdraw_status, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_NODES, - ETH_SEPOLIA_SWAP_CONTRACT}; + withdraw_status, MarketMakerIt, Mm2TestConf, ETH_SEPOLIA_CHAIN_ID, + ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use mm2_test_helpers::structs::{InitTaskResult, RpcV2Response, TransactionDetails, WithdrawStatus}; use rpc_task::{rpc_common::RpcTaskStatusRequest, RpcInitReq, RpcTaskStatus}; use serde_json::{self as json, json, Value as Json}; @@ -6190,7 +6759,7 @@ mod trezor_tests { "coin": "ETH", "urls": ETH_SEPOLIA_NODES, "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, - "priv_key_policy": "Trezor", + "priv_key_policy": { "type": "Trezor" }, }); let mut eth_conf = eth_sepolia_trezor_firmware_compat_conf(); @@ -6207,7 +6776,9 @@ mod trezor_tests { "ETH", ð_conf, &req, - CoinProtocol::ETH, + CoinProtocol::ETH { + chain_id: ETH_SEPOLIA_CHAIN_ID, + }, priv_key_policy, )) .unwrap(); @@ -6237,7 +6808,7 @@ mod trezor_tests { "method": "electrum", "coin": ticker, "servers": tbtc_electrums(), - "priv_key_policy": "Trezor", + "priv_key_policy": { "type": "Trezor" }, }); let activation_params = UtxoActivationParams::from_legacy_req(&enable_req).unwrap(); let request: InitStandaloneCoinReq = json::from_value(json!({ @@ -6441,7 +7012,7 @@ mod trezor_tests { ], "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, "erc20_tokens_requests": [{"ticker": ticker_token}], - "priv_key_policy": "Trezor" + "priv_key_policy": { "type": "Trezor" } })) .unwrap(), )) @@ -6559,7 +7130,7 @@ mod trezor_tests { ], "swap_contract_address": ETH_SEPOLIA_SWAP_CONTRACT, "erc20_tokens_requests": [], - "priv_key_policy": "Trezor" + "priv_key_policy": { "type": "Trezor" } })) .unwrap(), )) diff --git a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs index 187404a580..832f23cfae 100644 --- a/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/orderbook_sync_tests.rs @@ -36,6 +36,7 @@ fn alice_can_see_the_active_order_after_connection() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -254,6 +255,7 @@ fn alice_can_see_the_active_order_after_orderbook_sync_segwit() { "coins": bob_coins_config, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -427,6 +429,7 @@ fn test_orderbook_segwit() { "coins": bob_coins_config, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -540,6 +543,7 @@ fn test_get_orderbook_with_same_orderbook_ticker() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -586,6 +590,7 @@ fn test_conf_settings_in_orderbook() { "rpc_password": "password", "coins": coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -708,6 +713,7 @@ fn alice_can_see_confs_in_orderbook_after_sync() { "rpc_password": "password", "coins": bob_coins, "i_am_seed": true, + "is_bootstrap_node": true }), "password".into(), None, @@ -848,6 +854,7 @@ fn orderbook_extended_data() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -960,6 +967,7 @@ fn orderbook_should_display_base_rel_volumes() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1058,6 +1066,7 @@ fn orderbook_should_work_without_coins_activation() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1139,6 +1148,7 @@ fn test_all_orders_per_pair_per_node_must_be_displayed_in_orderbook() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, @@ -1210,6 +1220,7 @@ fn setprice_min_volume_should_be_displayed_in_orderbook() { "coins": coins, "rpc_password": "pass", "i_am_seed": true, + "is_bootstrap_node": true }), "pass".into(), None, diff --git a/mm2src/mm2_metamask/Cargo.toml b/mm2src/mm2_metamask/Cargo.toml index e26be8c434..925f424236 100644 --- a/mm2src/mm2_metamask/Cargo.toml +++ b/mm2src/mm2_metamask/Cargo.toml @@ -4,20 +4,20 @@ version = "0.1.0" edition = "2021" [target.'cfg(target_arch = "wasm32")'.dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures" } -itertools = "0.10" -js-sys = { version = "0.3.27" } -jsonrpc-core = "18.0.0" # Same version as `web3` depends on. -lazy_static = "1.4" +derive_more.workspace = true +futures = { workspace = true } +itertools.workspace = true +js-sys.workspace = true +jsonrpc-core.workspace = true +lazy_static.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } mm2_eth = { path = "../mm2_eth" } -parking_lot = { version = "0.12.0", features = ["nightly"] } -serde = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -serde_derive = "1.0" -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["eip-1193"] } +parking_lot = { workspace = true, features = ["nightly"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +serde_derive.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +web3 = { workspace = true, features = ["eip-1193"] } diff --git a/mm2src/mm2_metrics/Cargo.toml b/mm2src/mm2_metrics/Cargo.toml index f7a5fabc52..4a30e7f0b3 100644 --- a/mm2src/mm2_metrics/Cargo.toml +++ b/mm2src/mm2_metrics/Cargo.toml @@ -7,21 +7,21 @@ edition = "2021" doctest = false [dependencies] -base64 = "0.21.2" +base64.workspace = true common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -itertools = "0.10" -metrics = { version = "0.21" } -metrics-util = { version = "0.15" } +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +itertools.workspace = true +metrics.workspace = true +metrics-util.workspace = true mm2_err_handle = { path = "../mm2_err_handle" } -serde = "1" -serde_derive = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } +hyper = { workspace = true, features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features -hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "webpki-tokio"] } -metrics-exporter-prometheus = "0.12.1" +hyper-rustls = { workspace = true, default-features = false, features = ["http1", "http2", "webpki-tokio"] } +metrics-exporter-prometheus.workspace = true diff --git a/mm2src/mm2_net/Cargo.toml b/mm2src/mm2_net/Cargo.toml index a720327d96..3512d89e9b 100644 --- a/mm2src/mm2_net/Cargo.toml +++ b/mm2src/mm2_net/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mm2_net" version = "0.1.0" -edition = "2018" +edition = "2021" [lib] doctest = false @@ -9,48 +9,48 @@ doctest = false [features] [dependencies] -async-stream = { version = "0.3" } -async-trait = "0.1" -bytes = "1.1" -cfg-if = "1.0" +async-stream.workspace = true +async-trait.workspace = true +bytes.workspace = true +cfg-if.workspace = true common = { path = "../common" } -derive_more = "0.99" -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } -gstuff = "0.7" -http = "0.2" -lazy_static = "1.4" +derive_more.workspace = true +ethkey.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"] } +gstuff.workspace = true +http.workspace = true +lazy_static.workspace = true mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_number = { path = "../mm2_number" } -prost = "0.12" -rand = { version = "0.7", features = ["std", "small_rng", "wasm-bindgen"] } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -thiserror = "1.0.30" +prost.workspace = true +rand.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +thiserror.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -base64 = "0.21.7" -futures-util = "0.3" +base64.workspace = true +futures-util.workspace = true mm2_state_machine = { path = "../mm2_state_machine"} -http-body = "0.4" -httparse = "1.8.0" -js-sys = "0.3.27" -pin-project = "1.1.2" -tonic = { version = "0.10", default-features = false, features = ["prost", "codegen"] } -tower-service = "0.3" -wasm-bindgen = "0.2.86" -wasm-bindgen-test = { version = "0.3.2" } -wasm-bindgen-futures = "0.4.21" -web-sys = { version = "0.3.55", features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", +http-body.workspace = true +httparse.workspace = true +js-sys.workspace = true +pin-project.workspace = true +tonic = { workspace = true, default-features = false, features = ["prost", "codegen"] } +tower-service.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-test.workspace = true +wasm-bindgen-futures.workspace = true +web-sys = { workspace = true, features = ["console", "CloseEvent", "DomException", "ErrorEvent", "IdbDatabase", "IdbCursor", "IdbCursorWithValue", "IdbFactory", "IdbIndex", "IdbIndexParameters", "IdbObjectStore", "IdbObjectStoreParameters", "IdbOpenDbRequest", "IdbKeyRange", "IdbTransaction", "IdbTransactionMode", "IdbVersionChangeEvent", "MessageEvent", "MessagePort", "ReadableStreamDefaultReader", "ReadableStream", "SharedWorker", "Url", "WebSocket"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -futures-util = { version = "0.3" } -hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp", "stream"] } -rustls = { version = "0.21", default-features = false } -tokio = { version = "1.20" } -tokio-rustls = { version = "0.24", default-features = false } +futures-util.workspace = true +hyper = { workspace = true, features = ["client", "http2", "server", "tcp", "stream"] } +rustls.workspace = true +tokio.workspace = true +tokio-rustls = { workspace = true, default-features = false } diff --git a/mm2src/mm2_net/src/ip_addr.rs b/mm2src/mm2_net/src/ip_addr.rs index 4c4c77f448..ec3d66f54e 100644 --- a/mm2src/mm2_net/src/ip_addr.rs +++ b/mm2src/mm2_net/src/ip_addr.rs @@ -11,7 +11,7 @@ use std::io::Read; use std::net::{IpAddr, Ipv4Addr}; use std::path::Path; -use mm2_err_handle::prelude::MmError; +use mm2_err_handle::prelude::{MapToMmResult, MmError}; use std::net::ToSocketAddrs; const IP_PROVIDERS: [&str; 2] = ["http://checkip.amazonaws.com/", "http://api.ipify.org"]; @@ -172,12 +172,10 @@ pub async fn myipaddr(ctx: MmArc) -> Result { #[derive(Debug, Display)] pub enum ParseAddressError { - #[display(fmt = "Address/Seed {} resolved to IPv6 which is not supported", _0)] - UnsupportedIPv6Address(String), - #[display(fmt = "Address/Seed {} to_socket_addrs empty iter", _0)] - EmptyIterator(String), - #[display(fmt = "Couldn't resolve '{}' Address/Seed: {}", _0, _1)] - UnresolvedAddress(String, String), + #[display(fmt = "Address '{address}' cannot be resolved to IPv4.")] + CannotResolveIPv4 { address: String }, + #[display(fmt = "Couldn't resolve any IP on '{}' address. {}", address, reason)] + UnresolvedAddress { address: String, reason: String }, } pub fn addr_to_ipv4_string(address: &str) -> Result> { @@ -189,28 +187,34 @@ pub fn addr_to_ipv4_string(address: &str) -> Result match iter.next() { - Some(addr) => { - if addr.is_ipv4() { - Ok(addr.ip().to_string()) - } else { - log::warn!( - "Address/Seed {} resolved to IPv6 {} which is not supported", - address, - addr - ); - MmError::err(ParseAddressError::UnsupportedIPv6Address(address.into())) - } - }, - None => { - log::warn!("Address/Seed {} to_socket_addrs empty iter", address); - MmError::err(ParseAddressError::EmptyIterator(address.into())) - }, - }, - Err(e) => { - log::error!("Couldn't resolve '{}' seed: {}", address, e); - MmError::err(ParseAddressError::UnresolvedAddress(address.into(), e.to_string())) - }, + let iter = address_with_port.as_str().to_socket_addrs().map_to_mm(|e| { + log::error!("Couldn't resolve '{}' seed: {}", address, e); + ParseAddressError::UnresolvedAddress { + address: address.to_owned(), + reason: e.to_string(), + } + })?; + + if iter.len() == 0 { + return MmError::err(ParseAddressError::UnresolvedAddress { + address: address.to_owned(), + reason: "Empty DNS result.".to_owned(), + }); } + + for resolved in iter { + if resolved.is_ipv4() { + return Ok(resolved.ip().to_string()); + } else { + log::warn!( + "Address/Seed {} resolved to IPv6 {} which is not supported", + address, + resolved + ); + } + } + + MmError::err(ParseAddressError::CannotResolveIPv4 { + address: address.to_owned(), + }) } diff --git a/mm2src/mm2_number/Cargo.toml b/mm2src/mm2_number/Cargo.toml index 0303fd20d8..83ca5ed5bc 100644 --- a/mm2src/mm2_number/Cargo.toml +++ b/mm2src/mm2_number/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" doctest = false [dependencies] -bigdecimal = { version = "0.3", features = ["serde"] } -num-bigint = { version = "0.4", features = ["serde", "std"] } -num-rational = { version = "0.4", features = ["serde"] } -num-traits = "0.2" -paste = "1.0" -serde = { version = "1", features = ["serde_derive"] } -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +bigdecimal.workspace = true +num-bigint.workspace = true +num-rational.workspace = true +num-traits.workspace = true +paste.workspace = true +serde = { workspace = true, features = ["serde_derive"] } +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 6cc38db4d6..c0d9a3761f 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -11,44 +11,43 @@ application = ["dep:mm2_number"] doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } -compatible-time = { version = "1.1.0", package = "web-time" } -derive_more = "0.99" -futures = { version = "0.3.1", default-features = false } -futures-ticker = "0.0.3" -hex = "0.4.2" -lazy_static = "1.4" -log = "0.4" +compatible-time.workspace = true +derive_more.workspace = true +futures.workspace = true +futures-ticker.workspace = true +hex.workspace = true +lazy_static.workspace = true +log.workspace = true mm2_core = { path = "../mm2_core" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number", optional = true } -parking_lot = { version = "0.12.0", features = ["nightly"] } -rand = { version = "0.7", default-features = false, features = ["wasm-bindgen"] } -regex = "1" -rmp-serde = "0.14.3" -secp256k1 = { version = "0.20", features = ["rand"] } -serde = { version = "1.0", default-features = false } -serde_bytes = "0.11.5" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -sha2 = "0.10" -smallvec = "1.6.1" -syn = "2.0.18" -void = "1.0" +parking_lot = { workspace = true, features = ["nightly"] } +rand = { workspace = true, features = ["wasm-bindgen"] } +regex.workspace = true +rmp-serde.workspace = true +secp256k1 = { workspace = true, features = ["rand"] } +serde = { workspace = true, default-features = false } +serde_bytes.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +sha2.workspace = true +smallvec.workspace = true +void.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -futures-rustls = "0.24" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } -timed-map = { version = "1.3", features = ["rustc-hash"] } -tokio = { version = "1.20", default-features = false } +futures-rustls.workspace = true +libp2p = { workspace = true, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } +timed-map = { workspace = true, features = ["rustc-hash"] } +tokio.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] futures-rustls = "0.22" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } -timed-map = { version = "1.3", features = ["rustc-hash", "wasm"] } +libp2p = { workspace = true, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } +timed-map = { workspace = true, features = ["rustc-hash", "wasm"] } [dev-dependencies] -async-std = "1.6.2" -env_logger = "0.9.3" +async-std.workspace = true +env_logger.workspace = true common = { path = "../common", features = ["for-tests"] } diff --git a/mm2src/mm2_p2p/src/application/network_event.rs b/mm2src/mm2_p2p/src/application/network_event.rs index fa152469d1..bdab12f1dc 100644 --- a/mm2src/mm2_p2p/src/application/network_event.rs +++ b/mm2src/mm2_p2p/src/application/network_event.rs @@ -1,6 +1,6 @@ use common::executor::Timer; use mm2_core::mm_ctx::MmArc; -use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput}; +use mm2_event_stream::{Broadcaster, Event, EventStreamer, NoDataIn, StreamHandlerInput, StreamerId}; use async_trait::async_trait; use futures::channel::oneshot; @@ -38,7 +38,7 @@ impl NetworkEvent { impl EventStreamer for NetworkEvent { type DataInType = NoDataIn; - fn streamer_id(&self) -> String { "NETWORK".to_string() } + fn streamer_id(&self) -> StreamerId { StreamerId::Network } async fn handle( self, diff --git a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs index ad649328b4..5d5806582c 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -22,7 +22,6 @@ use rand::seq::SliceRandom; use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; use std::hash::{Hash, Hasher}; -use std::iter; use std::net::IpAddr; use std::sync::{Mutex, MutexGuard}; use std::task::{Context, Poll}; @@ -34,7 +33,6 @@ use super::request_response::{build_request_response_behaviour, PeerRequest, Pee RequestResponseSender}; use crate::application::request_response::network_info::NetworkInfoRequest; use crate::application::request_response::P2PRequest; -use crate::network::{get_all_network_seednodes, DEFAULT_NETID}; use crate::relay_address::{RelayAddress, RelayAddressError}; use crate::swarm_runtime::SwarmRuntime; use crate::{decode_message, encode_message, NetworkInfo, NetworkPorts, RequestResponseBehaviourEvent}; @@ -54,6 +52,7 @@ const CONNECTED_RELAYS_CHECK_INTERVAL: Duration = Duration::from_secs(30); const ANNOUNCE_INTERVAL: Duration = Duration::from_secs(600); const ANNOUNCE_INITIAL_DELAY: Duration = Duration::from_secs(60); const CHANNEL_BUF_SIZE: usize = 1024 * 8; +const DEFAULT_NETID: u16 = 8762; /// Used in time validation logic for each peer which runs immediately after the /// `ConnectionEstablished` event. @@ -714,23 +713,12 @@ fn start_gossipsub( .map_err(|e| AdexBehaviourError::InitializationError(e.to_owned()))?; // build a gossipsub network behaviour - let mut gossipsub = Gossipsub::new(MessageAuthenticity::Author(local_peer_id), gossipsub_config) + let gossipsub = Gossipsub::new(MessageAuthenticity::Author(local_peer_id), gossipsub_config) .map_err(|e| AdexBehaviourError::InitializationError(e.to_owned()))?; let floodsub = Floodsub::new(local_peer_id, config.netid != DEFAULT_NETID); - let mut peers_exchange = PeersExchange::new(network_info); - if !network_info.in_memory() { - // Please note WASM nodes don't support `PeersExchange` currently, - // so `get_all_network_seednodes` returns an empty list. - for (peer_id, addr, _domain) in get_all_network_seednodes(config.netid) { - let multiaddr = addr.try_to_multiaddr(network_info)?; - peers_exchange.add_peer_addresses_to_known_peers(&peer_id, iter::once(multiaddr).collect()); - if peer_id != local_peer_id { - gossipsub.add_explicit_relay(peer_id); - } - } - } + let peers_exchange = PeersExchange::new(network_info); // build a request-response network behaviour let request_response = build_request_response_behaviour(); @@ -795,6 +783,14 @@ fn start_gossipsub( Err(e) => error!("Dial {:?} failed: {:?}", relay, e), } } + + // All currently connected peers come from the config file (because we didn't connect any other + // ones yet), so it's safe to treat them as trusted nodes. + let peers: Vec<_> = libp2p::Swarm::connected_peers(&swarm).cloned().collect(); + for peer in peers { + swarm.behaviour_mut().core.gossipsub.add_explicit_peer(&peer); + } + drop(recently_dialed_peers); let mut check_connected_relays_interval = @@ -817,6 +813,7 @@ fn start_gossipsub( if swarm.disconnect_peer_id(peer_id).is_err() { error!("Disconnection from `{peer_id}` failed unexpectedly, which should never happen."); } + swarm.behaviour_mut().core.gossipsub.remove_explicit_peer(&peer_id); } loop { diff --git a/mm2src/mm2_p2p/src/lib.rs b/mm2src/mm2_p2p/src/lib.rs index b1d0283be0..ff05a6d4f6 100644 --- a/mm2src/mm2_p2p/src/lib.rs +++ b/mm2src/mm2_p2p/src/lib.rs @@ -2,7 +2,6 @@ pub mod behaviours; -mod network; mod relay_address; mod swarm_runtime; @@ -39,7 +38,6 @@ pub use libp2p::identity::{secp256k1::PublicKey as Libp2pSecpPublic, Keypair, Pu pub use libp2p::{Multiaddr, PeerId}; // relay-address related re-exports -pub use network::SeedNodeInfo; pub use relay_address::RelayAddress; pub use relay_address::RelayAddressError; diff --git a/mm2src/mm2_p2p/src/network.rs b/mm2src/mm2_p2p/src/network.rs deleted file mode 100644 index 6d5524a31e..0000000000 --- a/mm2src/mm2_p2p/src/network.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::relay_address::RelayAddress; -use libp2p::PeerId; - -pub const DEFAULT_NETID: u16 = 8762; - -pub struct SeedNodeInfo { - pub id: &'static str, - pub domain: &'static str, -} - -impl SeedNodeInfo { - pub const fn new(id: &'static str, domain: &'static str) -> Self { Self { id, domain } } -} - -#[cfg_attr(target_arch = "wasm32", allow(dead_code))] -const ALL_DEFAULT_NETID_SEEDNODES: &[SeedNodeInfo] = &[ - SeedNodeInfo::new( - "12D3KooWHKkHiNhZtKceQehHhPqwU5W1jXpoVBgS1qst899GjvTm", - "viserion.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWAToxtunEBWCoAHjefSv74Nsmxranw8juy3eKEdrQyGRF", - "rhaegal.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWSmEi8ypaVzFA1AGde2RjxNW5Pvxw3qa2fVe48PjNs63R", - "drogon.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWMrjLmrv8hNgAoVf1RfumfjyPStzd4nv5XL47zN4ZKisb", - "falkor.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWEWzbYcosK2JK9XpFXzumfgsWJW1F7BZS15yLTrhfjX2Z", - "smaug.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWJWBnkVsVNjiqUEPjLyHpiSmQVAJ5t6qt1Txv5ctJi9Xd", - "balerion.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWPR2RoPi19vQtLugjCdvVmCcGLP2iXAzbDfP3tp81ZL4d", - "kalessin.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWJDoV9vJdy6PnzwVETZ3fWGMhV41VhSbocR1h2geFqq9Y", - "icefyre.dragon-seed.com", - ), - SeedNodeInfo::new( - "12D3KooWEaZpH61H4yuQkaNG5AsyGdpBhKRppaLdAY52a774ab5u", - "seed01.kmdefi.net", - ), - SeedNodeInfo::new( - "12D3KooWAd5gPXwX7eDvKWwkr2FZGfoJceKDCA53SHmTFFVkrN7Q", - "seed02.kmdefi.net", - ), -]; - -#[cfg(target_arch = "wasm32")] -pub fn get_all_network_seednodes(_netid: u16) -> Vec<(PeerId, RelayAddress, String)> { Vec::new() } - -#[cfg(not(target_arch = "wasm32"))] -pub fn get_all_network_seednodes(netid: u16) -> Vec<(PeerId, RelayAddress, String)> { - use std::str::FromStr; - - if netid != DEFAULT_NETID { - return Vec::new(); - } - ALL_DEFAULT_NETID_SEEDNODES - .iter() - .map(|SeedNodeInfo { id, domain }| { - let peer_id = PeerId::from_str(id).unwrap_or_else(|e| panic!("Valid peer id {id}: {e}")); - let ip = - mm2_net::ip_addr::addr_to_ipv4_string(domain).unwrap_or_else(|e| panic!("Valid domain {domain}: {e}")); - let address = RelayAddress::IPv4(ip); - let domain = domain.to_string(); - (peer_id, address, domain) - }) - .collect() -} diff --git a/mm2src/mm2_rpc/Cargo.toml b/mm2src/mm2_rpc/Cargo.toml index 6f84bb201a..d82cc9dd81 100644 --- a/mm2src/mm2_rpc/Cargo.toml +++ b/mm2src/mm2_rpc/Cargo.toml @@ -8,18 +8,18 @@ doctest = false [dependencies] common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"], optional = true } -gstuff = { version = "0.7", features = ["nightly"], optional = true} -http = {version = "0.2", optional = true} +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await", "thread-pool"], optional = true } +gstuff = { workspace = true, optional = true} +http = { workspace = true, optional = true} mm2_err_handle = { path = "../mm2_err_handle", optional = true } mm2_number = { path = "../mm2_number" } rpc = { path = "../mm2_bitcoin/rpc" } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } ser_error = { path = "../derives/ser_error", optional = true} ser_error_derive = { path = "../derives/ser_error_derive", optional=true } -uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } +uuid.workspace = true [features] default = [] diff --git a/mm2src/mm2_state_machine/Cargo.toml b/mm2src/mm2_state_machine/Cargo.toml index 9683850ba0..f6a03acb9b 100644 --- a/mm2src/mm2_state_machine/Cargo.toml +++ b/mm2src/mm2_state_machine/Cargo.toml @@ -9,8 +9,8 @@ edition = "2021" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true [dev-dependencies] common = { path = "../common" } -futures = { version = "0.3" } \ No newline at end of file +futures.workspace = true diff --git a/mm2src/mm2_test_helpers/src/electrums.rs b/mm2src/mm2_test_helpers/src/electrums.rs index 03eaac51e6..a410daf1c2 100644 --- a/mm2src/mm2_test_helpers/src/electrums.rs +++ b/mm2src/mm2_test_helpers/src/electrums.rs @@ -73,11 +73,7 @@ pub fn tbtc_electrums() -> Vec { } #[cfg(target_arch = "wasm32")] -pub fn tqtum_electrums() -> Vec { - vec![ - json!({ "url": "electrum3.cipig.net:30071", "protocol": "WSS" }), - ] -} +pub fn tqtum_electrums() -> Vec { vec![json!({ "url": "electrum3.cipig.net:30071", "protocol": "WSS" })] } #[cfg(not(target_arch = "wasm32"))] pub fn tqtum_electrums() -> Vec { diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 9285768bc1..f98fe3041c 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -186,6 +186,8 @@ pub const DOC_ELECTRUM_ADDRS: &[&str] = &[ "electrum2.cipig.net:10020", "electrum3.cipig.net:10020", ]; + +/// NOTE: These are websocket servers. #[cfg(target_arch = "wasm32")] pub const DOC_ELECTRUM_ADDRS: &[&str] = &[ "electrum1.cipig.net:30020", @@ -275,6 +277,7 @@ impl Mm2TestConf { "coins": coins, "rpc_password": DEFAULT_RPC_PASSWORD, "i_am_seed": true, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -291,6 +294,7 @@ impl Mm2TestConf { "rpc_password": DEFAULT_RPC_PASSWORD, "i_am_seed": true, "use_trading_proto_v2": true, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -306,6 +310,7 @@ impl Mm2TestConf { "rpc_password": DEFAULT_RPC_PASSWORD, "i_am_seed": true, "enable_hd": true, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -322,6 +327,7 @@ impl Mm2TestConf { "i_am_seed": true, "enable_hd": true, "use_trading_proto_v2": true, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -337,6 +343,7 @@ impl Mm2TestConf { "i_am_seed": true, "wallet_name": wallet_name, "wallet_password": wallet_password, + "is_bootstrap_node": true }), rpc_password: DEFAULT_RPC_PASSWORD.into(), } @@ -420,6 +427,11 @@ impl Mm2TestConf { } pub fn no_login_node(coins: &Json, seednodes: &[&str]) -> Self { + assert!( + !seednodes.is_empty(), + "Invalid Test Setup: A no-login node requires at least one seednode." + ); + Mm2TestConf { conf: json!({ "gui": "nogui", @@ -473,11 +485,11 @@ pub enum Mm2InitPrivKeyPolicy { GlobalHDAccount, } -pub fn zombie_conf() -> Json { zombie_conf_inner(None) } +pub fn zombie_conf() -> Json { zombie_conf_inner(None, 0) } -pub fn zombie_conf_for_docker() -> Json { zombie_conf_inner(Some(10)) } +pub fn zombie_conf_for_docker() -> Json { zombie_conf_inner(Some(10), 1) } -pub fn zombie_conf_inner(custom_blocktime: Option) -> Json { +pub fn zombie_conf_inner(custom_blocktime: Option, required_confirmations: u8) -> Json { json!({ "coin":"ZOMBIE", "asset":"ZOMBIE", @@ -504,7 +516,7 @@ pub fn zombie_conf_inner(custom_blocktime: Option) -> Json { "z_derivation_path": "m/32'/133'", } }, - "required_confirmations":0, + "required_confirmations": required_confirmations, "derivation_path": "m/44'/133'", }) } @@ -549,6 +561,7 @@ pub fn rick_conf() -> Json { "txversion":4, "overwintered":1, "derivation_path": "m/44'/141'", + "sign_message_prefix": "Komodo Signed Message:\n", "protocol":{ "type":"UTXO" } @@ -822,11 +835,13 @@ pub fn eth_testnet_conf_trezor() -> Json { "coin": "ETH", "name": "ethereum", "mm2": 1, - "chain_id": 1337, "max_eth_tx_type": 2, "derivation_path": "m/44'/1'", // Trezor uses coin type 1 for testnet "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": 1337, + } }, "trezor_coin": "Ethereum" }) @@ -842,10 +857,12 @@ fn eth_conf(coin: &str) -> Json { "coin": coin, "name": "ethereum", "mm2": 1, - "chain_id": 1337, "derivation_path": "m/44'/60'", "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": 1337, + } }, "max_eth_tx_type": 2 }) @@ -856,7 +873,6 @@ pub fn erc20_dev_conf(contract_address: &str) -> Json { json!({ "coin": "ERC20DEV", "name": "erc20dev", - "chain_id": 1337, "mm2": 1, "derivation_path": "m/44'/60'", "protocol": { @@ -882,7 +898,6 @@ pub fn nft_dev_conf() -> Json { json!({ "coin": "NFT_ETH", "name": "nftdev", - "chain_id": 1337, "mm2": 1, "derivation_path": "m/44'/60'", "protocol": { @@ -902,9 +917,11 @@ pub fn eth_sepolia_conf() -> Json { "coin": "ETH", "name": "ethereum", "derivation_path": "m/44'/60'", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID, + } }, "max_eth_tx_type": 2, "trezor_coin": "Ethereum" @@ -916,9 +933,11 @@ pub fn eth_sepolia_trezor_firmware_compat_conf() -> Json { "coin": "tETH", "name": "ethereum", "derivation_path": "m/44'/1'", // Note: trezor uses coin type 1' for eth for testnet (SLIP44_TESTNET) - "chain_id": ETH_SEPOLIA_CHAIN_ID, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": ETH_SEPOLIA_CHAIN_ID, + } }, "max_eth_tx_type": 2, "trezor_coin": "tETH" @@ -929,7 +948,6 @@ pub fn eth_jst_testnet_conf() -> Json { json!({ "coin": "JST", "name": "jst", - "chain_id": 1337, "derivation_path": "m/44'/60'", "protocol": { "type": "ERC20", @@ -946,12 +964,10 @@ pub fn jst_sepolia_conf() -> Json { json!({ "coin": "JST", "name": "jst", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "protocol": { "type": "ERC20", "protocol_data": { "platform": "ETH", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "contract_address": ETH_SEPOLIA_TOKEN_CONTRACT } }, @@ -963,14 +979,12 @@ pub fn jst_sepolia_trezor_conf() -> Json { json!({ "coin": "tJST", "name": "tjst", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "derivation_path": "m/44'/1'", // Note: Trezor uses 1' coin type for all testnets "trezor_coin": "tETH", "protocol": { "type": "ERC20", "protocol_data": { "platform": "ETH", - "chain_id": ETH_SEPOLIA_CHAIN_ID, "contract_address": ETH_SEPOLIA_TOKEN_CONTRACT } } @@ -1095,11 +1109,13 @@ pub fn tbnb_conf() -> Json { "coin": "tBNB", "name": "binancesmartchaintest", "avg_blocktime": 0.25, - "chain_id": 97, "mm2": 1, "required_confirmations": 0, "protocol": { - "type": "ETH" + "type": "ETH", + "protocol_data": { + "chain_id": 97 + } } }) } @@ -1182,6 +1198,9 @@ pub async fn mm_ctx_with_custom_async_db() -> MmArc { ctx } +#[cfg(target_arch = "wasm32")] +pub async fn mm_ctx_with_custom_async_db() -> MmArc { MmCtxBuilder::new().with_test_db_namespace().into_mm_arc() } + /// Automatically kill a wrapped process. pub struct RaiiKill { pub handle: Child, @@ -1860,6 +1879,7 @@ pub fn mm_spat() -> (&'static str, MarketMakerIt, RaiiDump, RaiiDump) { ], "i_am_seed": true, "rpc_password": "pass", + "is_bootstrap_node": true, }), "pass".into(), None, @@ -2730,7 +2750,7 @@ pub async fn withdraw_v1( pub async fn ibc_withdraw( mm: &MarketMakerIt, - source_channel: &str, + source_channel: u16, coin: &str, to: &str, amount: &str, @@ -2870,7 +2890,7 @@ pub async fn init_z_coin_status(mm: &MarketMakerIt, task_id: u64) -> Json { json::from_str(&request.1).unwrap() } -pub async fn sign_message(mm: &MarketMakerIt, coin: &str) -> Json { +pub async fn sign_message(mm: &MarketMakerIt, coin: &str, derivation_path: Option) -> Json { let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -2879,7 +2899,8 @@ pub async fn sign_message(mm: &MarketMakerIt, coin: &str) -> Json { "id": 0, "params":{ "coin": coin, - "message":"test" + "message": "test", + "address": derivation_path } })) .await @@ -2969,6 +2990,20 @@ pub async fn get_wallet_names(mm: &MarketMakerIt) -> GetWalletNamesResult { res.result } +pub async fn delete_wallet(mm: &MarketMakerIt, wallet_name: &str, password: &str) -> (StatusCode, String, HeaderMap) { + mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "delete_wallet", + "mmrpc": "2.0", + "params": { + "wallet_name": wallet_name, + "password": password, + } + })) + .await + .unwrap() +} + pub async fn max_maker_vol(mm: &MarketMakerIt, coin: &str) -> RpcResponse { let rc = mm .rpc(&json!({ @@ -3318,13 +3353,15 @@ pub async fn init_utxo_electrum( "rpc": "Electrum", "rpc_data": { "servers": servers, - "path_to_address": path_to_address, } } }); if let Some(priv_key_policy) = priv_key_policy { activation_params["priv_key_policy"] = priv_key_policy.into(); } + if let Some(path_to_address) = path_to_address { + activation_params["path_to_address"] = json!(path_to_address); + } let request = mm .rpc(&json!({ "userpass": mm.userpass, @@ -3719,6 +3756,8 @@ pub async fn test_qrc20_history_impl(local_start: Option) { "coins": coins, "rpc_password": "pass", "metrics_interval": 30., + "disable_p2p": true, + "p2p_in_memory": false }), "pass".into(), local_start, diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index b5d3be28f5..fac660af8d 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -746,7 +746,7 @@ pub enum CreateNewAccountStatus { #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] #[serde(untagged)] -pub enum WithdrawFrom { +pub enum HDAddressSelector { AddressId(HDAccountAddressId), DerivationPath { derivation_path: String }, } diff --git a/mm2src/proxy_signature/Cargo.toml b/mm2src/proxy_signature/Cargo.toml index 260aa1a568..524dd1d39e 100644 --- a/mm2src/proxy_signature/Cargo.toml +++ b/mm2src/proxy_signature/Cargo.toml @@ -4,11 +4,11 @@ version = "0.1.0" edition = "2018" [dependencies] -chrono = "0.4" -http = "0.2" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.12", default-features = false, features = ["identify"] } -serde = "1" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +chrono.workspace = true +http.workspace = true +libp2p = { workspace = true, features = ["identify"] } +serde.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } [dev-dependencies] -rand = { version = "0.7", features = ["std", "small_rng"] } +rand = { workspace = true, features = ["std", "small_rng"] } diff --git a/mm2src/rpc_task/Cargo.toml b/mm2src/rpc_task/Cargo.toml index c542159f25..ebe7f1b4b3 100644 --- a/mm2src/rpc_task/Cargo.toml +++ b/mm2src/rpc_task/Cargo.toml @@ -7,14 +7,14 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" +async-trait.workspace = true common = { path = "../common" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } -derive_more = "0.99" -futures = "0.3" +derive_more.workspace = true +futures.workspace = true ser_error = { path = "../derives/ser_error" } ser_error_derive = { path = "../derives/ser_error_derive" } -serde = "1" -serde_derive = "1" -serde_json = "1" +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true diff --git a/mm2src/rpc_task/src/manager.rs b/mm2src/rpc_task/src/manager.rs index b5c43a04b6..232c8efaef 100644 --- a/mm2src/rpc_task/src/manager.rs +++ b/mm2src/rpc_task/src/manager.rs @@ -6,7 +6,7 @@ use common::log::{debug, info, trace, warn}; use futures::channel::oneshot; use futures::future::{select, Either}; use mm2_err_handle::prelude::*; -use mm2_event_stream::{Event, StreamingManager, StreamingManagerError}; +use mm2_event_stream::{Event, StreamerId, StreamingManager, StreamingManagerError}; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::atomic::Ordering; @@ -192,7 +192,7 @@ impl RpcTaskManager { // Note that this should really always be `Some`, since we updated the status *successfully*. if let Some(new_status) = self.task_status(task_id, false) { let event = Event::new( - format!("TASK:{task_id}"), + StreamerId::Task { task_id }, serde_json::to_value(new_status).expect("Serialization shouldn't fail."), ); if let Err(e) = self.streaming_manager.broadcast_to(event, client_id) { diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 4f714ed34d..bc30874dfc 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -11,19 +11,18 @@ mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } -mocktopus = { version = "0.8.0", optional = true } - -derive_more = "0.99" -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -lazy_static = "1.4" -serde = "1.0" -serde_derive = "1.0" -serde_json = { version = "1", features = ["preserve_order", "raw_value"] } -url = { version = "2.2.2", features = ["serde"] } +mocktopus = { workspace = true, optional = true } +derive_more.workspace = true +ethereum-types.workspace = true +lazy_static.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json = { workspace = true, features = ["preserve_order", "raw_value"] } +url.workspace = true [features] test-ext-api = [] # use test config to connect to an external api for-tests = ["dep:mocktopus"] [dev-dependencies] -mocktopus = { version = "0.8.0" } \ No newline at end of file +mocktopus.workspace = true diff --git a/mm2src/trezor/Cargo.toml b/mm2src/trezor/Cargo.toml index 36e5abb0ec..55f40b5d3d 100644 --- a/mm2src/trezor/Cargo.toml +++ b/mm2src/trezor/Cargo.toml @@ -7,34 +7,34 @@ edition = "2018" doctest = false [dependencies] -async-trait = "0.1" -byteorder = "1.3.2" +async-trait.workspace = true +byteorder.workspace = true common = { path = "../common" } -derive_more = "0.99" -futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } +derive_more.workspace = true +futures = { workspace = true, features = ["compat", "async-await"] } hw_common = { path = "../hw_common" } mm2_err_handle = { path = "../mm2_err_handle" } -prost = "0.12" -rand = { version = "0.7", features = ["std", "wasm-bindgen"] } +prost.workspace = true +rand = { workspace = true, features = ["std", "wasm-bindgen"] } rpc_task = { path = "../rpc_task" } -serde = "1.0" -serde_derive = "1.0" -ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -lazy_static = "1.4" +serde.workspace = true +serde_derive.workspace = true +ethcore-transaction.workspace = true +ethereum-types.workspace = true +ethkey.workspace = true +bip32.workspace = true +lazy_static.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } -async-std = { version = "1.5" } +bip32.workspace = true +async-std.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = { version = "0.3.27" } -wasm-bindgen = "0.2.86" -wasm-bindgen-futures = { version = "0.4.1" } -wasm-bindgen-test = { version = "0.3.1" } -web-sys = { version = "0.3.55" } +js-sys.workspace = true +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +wasm-bindgen-test.workspace = true +web-sys.workspace = true [features] trezor-udp = [] # use for tests to connect to trezor emulator over udp