diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4885c152ec..264eb83a11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: branches: - master schedule: - - cron: '15 0 * * *' # every day at 00:15 UTC + - cron: "15 0 * * *" # every day at 00:15 UTC env: CARGO_TERM_COLOR: always @@ -28,14 +28,14 @@ jobs: - name: Install rust run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/cargo-info-extractor/extract.py --rust-version) - name: Build - run: cargo build --release --locked --features trezor + run: cargo build --release --locked --features trezor,ledger - name: Run tests - run: cargo test --release --workspace --features trezor + run: cargo test --release --workspace --features trezor,ledger - name: Run doc tests - run: cargo test --release --doc --features trezor + run: cargo test --release --doc --features trezor,ledger # This test is ignored, so it needs to run separately. - name: Run mixed_sighash_types test - run: cargo test --release mixed_sighash_types --features trezor + run: cargo test --release mixed_sighash_types --features trezor,ledger # This test is ignored, so it needs to run separately. - name: Run test_4opc_sequences test run: cargo test --release test_4opc_sequences -- --ignored @@ -59,18 +59,18 @@ jobs: - name: Update local dependency repositories run: sudo apt-get update - name: Install build dependencies - run: sudo apt-get install -yqq --no-install-recommends build-essential python3 python3-toml podman pkg-config libssl-dev + run: sudo apt-get install -yqq --no-install-recommends build-essential python3 python3-toml podman pkg-config libssl-dev libdbus-1-dev libusb-1.0-0-dev - name: Install rust run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/cargo-info-extractor/extract.py --rust-version) - name: Build - run: cargo build --release --locked --features trezor + run: cargo build --release --locked --features trezor,ledger - name: Run tests - run: cargo test --release --workspace --features trezor + run: cargo test --release --workspace --features trezor,ledger - name: Run doc tests - run: cargo test --release --doc --features trezor + run: cargo test --release --doc --features trezor,ledger # This test is ignored, so it needs to run separately. - name: Run mixed_sighash_types test - run: cargo test --release mixed_sighash_types --features trezor + run: cargo test --release mixed_sighash_types --features trezor,ledger # This test is ignored, so it needs to run separately. - name: Run test_4opc_sequences test run: cargo test --release test_4opc_sequences @@ -94,14 +94,14 @@ jobs: - name: Install rust run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/cargo-info-extractor/extract.py --rust-version) - name: Build - run: cargo build --release --locked --features trezor + run: cargo build --release --locked --features trezor,ledger - name: Run tests - run: cargo test --release --workspace --features trezor + run: cargo test --release --workspace --features trezor,ledger - name: Run doc tests - run: cargo test --release --doc --features trezor + run: cargo test --release --doc --features trezor,ledger # This test is ignored, so it needs to run separately. - name: Run mixed_sighash_types test - run: cargo test --release mixed_sighash_types --features trezor + run: cargo test --release mixed_sighash_types --features trezor,ledger # This test is ignored, so it needs to run separately. - name: Run test_4opc_sequences test run: cargo test --release test_4opc_sequences @@ -117,9 +117,9 @@ jobs: run_tests_on_trezor_preparation: runs-on: ubuntu-latest steps: - # Note: we need to mimic the directory structure of the run_tests_on_trezor job, otherwise nextest - # will fail to execute archived tests. So we checkout the source code to "./mintlayer-core". - # (Also note that because of this the resulting path of the source dir will be "/.../mintlayer-core/mintlayer-core/mintlayer-core") + # Note: we need to mimic the directory structure of the run_tests_on_trezor job, otherwise nextest + # will fail to execute archived tests. So we checkout the source code to "./mintlayer-core". + # (Also note that because of this the resulting path of the source dir will be "/.../mintlayer-core/mintlayer-core/mintlayer-core") - name: Checkout the core repository uses: actions/checkout@v4 with: @@ -130,7 +130,7 @@ jobs: run: sudo apt-get update - name: Install build dependencies - run: sudo apt-get install -yqq --no-install-recommends build-essential python3 python3-toml pkg-config libssl-dev + run: sudo apt-get install -yqq --no-install-recommends build-essential python3 python3-toml pkg-config libssl-dev libdbus-1-dev libusb-1.0-0-dev - name: Extract required info from Cargo.toml id: extract_cargo_info @@ -227,13 +227,91 @@ jobs: # Note: since we haven't installed Cargo in this job, we have to execute "cargo-nextest nextest" # instead of "cargo nextest". - name: Run tests in the emulator - run: - nix-shell --run " - poetry run core/emu.py - --headless --quiet --temporary-profile - --mnemonic \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\" - --command env --chdir ../mintlayer-core - cargo-nextest nextest run --archive-file tests.tar.zst -j1 trezor_signer + run: nix-shell --run " + poetry run core/emu.py + --headless --quiet --temporary-profile + --mnemonic \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\" + --command env --chdir ../mintlayer-core + cargo-nextest nextest run --archive-file tests.tar.zst -j1 trezor_signer " working-directory: ./mintlayer-trezor-firmware timeout-minutes: 10 + + # Build Ledger-specific tests and archive them + run_tests_on_ledger_preparation: + runs-on: ubuntu-latest + steps: + - name: Checkout the core repository + uses: actions/checkout@v4 + with: + submodules: recursive + path: ./mintlayer-core + - name: Update local dependency repositories + run: sudo apt-get update + - name: Install build dependencies + run: sudo apt-get install -yqq --no-install-recommends build-essential pkg-config libdbus-1-dev libusb-1.0-0-dev + - name: Install rust + run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + - name: Build and archive the tests + run: cargo nextest archive --release --locked -p wallet --features enable-ledger-device-tests --archive-file ledger-tests.tar.zst + working-directory: ./mintlayer-core + - name: Upload archived tests + uses: actions/upload-artifact@v4 + with: + name: archived-ledger-tests + path: ./mintlayer-core/ledger-tests.tar.zst + retention-days: 1 + + # Run Ledger-specific tests on an emulator + run_tests_on_ledger: + needs: run_tests_on_ledger_preparation + runs-on: ubuntu-latest + steps: + - name: Checkout the core repository + uses: actions/checkout@v4 + with: + submodules: recursive + path: ./mintlayer-core + - name: Checkout mintlayer-ledger-app repository + uses: actions/checkout@v4 + with: + repository: mintlayer/mintlayer-ledger-app + ref: feature/mintlayer-app + path: ./mintlayer-ledger-app + - name: Download archived tests + uses: actions/download-artifact@v4 + with: + name: archived-ledger-tests + path: ./mintlayer-core + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + - name: Build Ledger app in container + run: | + sudo docker run --rm \ + -v "$(realpath ./mintlayer-ledger-app):/app" \ + ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \ + sh -c 'cd /app && cargo ledger build nanosplus' + - name: Run Ledger emulator and execute tests + run: | + set -e + + sudo docker run -d --rm --name ledger-emulator \ + -v "$(realpath ./mintlayer-ledger-app):/app" \ + --publish 5001:5001 --publish 9999:9999 \ + ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \ + sh -c 'cd /app && speculos --apdu-port 9999 --api-port 5001 --display headless --model nanosp -s "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" target/nanosplus/release/mintlayer-app' + + echo "--- Waiting for emulator to initialize ---" + sleep 15 + + # Set up a trap to ensure the container is stopped even if tests fail or the job is cancelled + trap "echo '--- Dumping Ledger emulator logs ---'; sudo docker logs ledger-emulator; echo '--- Stopping Ledger emulator ---'; sudo docker stop ledger-emulator" EXIT + + echo "--- Running Ledger device tests on the host ---" + cd ./mintlayer-core + cargo-nextest nextest run --archive-file ledger-tests.tar.zst -j1 ledger_signer || test_exit_code=$? + + exit $test_exit_code + timeout-minutes: 15 diff --git a/.github/workflows/code_checks.yml b/.github/workflows/code_checks.yml index 54370e9e48..7b15f35608 100644 --- a/.github/workflows/code_checks.yml +++ b/.github/workflows/code_checks.yml @@ -25,7 +25,7 @@ jobs: - name: Update local dependency repositories run: sudo apt-get update - name: Install dependencies - run: sudo apt-get install -yqq --no-install-recommends build-essential python3 python3-toml + run: sudo apt-get install -yqq --no-install-recommends build-essential python3 python3-toml libdbus-1-dev libusb-1.0-0-dev - name: Install rust run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/cargo-info-extractor/extract.py --rust-version) diff --git a/Cargo.lock b/Cargo.lock index 1a05d92b35..c6348518a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ name = "accounting" version = "1.1.0" dependencies = [ "common", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "proptest", "rstest", "serialization", @@ -146,7 +146,7 @@ dependencies = [ "bitflags 2.9.1", "cc", "cesu8", - "jni", + "jni 0.21.1", "jni-sys", "libc", "log", @@ -327,7 +327,7 @@ dependencies = [ "logging", "mempool", "orders-accounting", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "pos-accounting", "rstest", "serialization", @@ -358,7 +358,7 @@ dependencies = [ "node-comm", "pos-accounting", "randomness", - "reqwest", + "reqwest 0.11.27", "rstest", "serde", "serde_json", @@ -1023,7 +1023,7 @@ dependencies = [ "mockall", "mocks", "p2p", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "pos-accounting", "randomness", "rayon", @@ -1041,6 +1041,35 @@ dependencies = [ "utils", ] +[[package]] +name = "bluez-async" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" +dependencies = [ + "bitflags 2.9.1", + "bluez-generated", + "dbus", + "dbus-tokio", + "futures", + "itertools 0.14.0", + "log", + "serde", + "serde-xml-rs", + "thiserror 2.0.12", + "tokio", + "uuid", +] + +[[package]] +name = "bluez-generated" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" +dependencies = [ + "dbus", +] + [[package]] name = "borsh" version = "1.5.7" @@ -1075,6 +1104,34 @@ dependencies = [ "serde", ] +[[package]] +name = "btleplug" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" +dependencies = [ + "async-trait", + "bitflags 2.9.1", + "bluez-async", + "dashmap 6.1.0", + "dbus", + "futures", + "jni 0.19.0", + "jni-utils", + "log", + "objc2 0.5.2", + "objc2-core-bluetooth", + "objc2-foundation 0.2.2", + "once_cell", + "static_assertions", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "uuid", + "windows 0.61.3", + "windows-future", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -1271,7 +1328,7 @@ dependencies = [ "num", "oneshot", "orders-accounting", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "pos-accounting", "randomness", "rpc", @@ -1347,7 +1404,7 @@ dependencies = [ "mockall", "num-traits", "orders-accounting", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "pos-accounting", "randomness", "rstest", @@ -1433,7 +1490,7 @@ dependencies = [ "logging", "num-derive", "num-traits", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "pos-accounting", "serialization", "static_assertions", @@ -1513,7 +1570,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -1652,7 +1709,7 @@ dependencies = [ "merkletree-mintlayer", "num", "once_cell", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "paste", "proptest", "randomness", @@ -1696,7 +1753,7 @@ dependencies = [ "itertools 0.14.0", "logging", "num", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "pos-accounting", "randomness", "rstest", @@ -2008,7 +2065,7 @@ dependencies = [ "num", "num-derive", "num-traits", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand_chacha 0.3.1", "randomness", "ripemd", @@ -2144,14 +2201,38 @@ dependencies = [ "zbus 4.4.0", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + [[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", ] [[package]] @@ -2164,27 +2245,89 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.101", ] +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", "quote", "syn 2.0.101", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.11", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.11", +] + [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-tokio" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" +dependencies = [ + "dbus", + "libc", + "tokio", +] + [[package]] name = "dconf_rs" version = "0.3.0" @@ -2396,7 +2539,7 @@ dependencies = [ "networking", "p2p", "p2p-test-utils", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "randomness", "rstest", "serialization", @@ -2485,6 +2628,41 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encdec" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7aafa197dadfa18575eb62d05adaec89237d0fb0663baf2e141529b3a20b0" +dependencies = [ + "encdec-base", + "encdec-macros", +] + +[[package]] +name = "encdec-base" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5abc9d559c177b2a75892e92c1812216e5cec7e1a14e0682b25ed6f5c0bd78a2" +dependencies = [ + "byteorder", + "heapless", + "num-traits", + "thiserror 2.0.12", +] + +[[package]] +name = "encdec-macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace893fa216d3c4cd14239cf2807b48c1ad8abcf583d779a29f410cd35cff413" +dependencies = [ + "darling 0.14.4", + "encdec-base", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -3226,6 +3404,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.9.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.6.0" @@ -3236,6 +3433,15 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3288,6 +3494,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -3408,6 +3624,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "hidapi" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "pkg-config", + "windows-sys 0.48.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3507,7 +3736,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -3530,6 +3759,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -3538,6 +3768,7 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", ] [[package]] @@ -3556,6 +3787,22 @@ dependencies = [ "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "rustls 0.23.32", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.3", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -3569,20 +3816,46 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ + "base64 0.22.1", "bytes", + "futures-channel", "futures-core", + "futures-util", "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration 0.6.1", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -3991,6 +4264,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -4041,6 +4324,20 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] + [[package]] name = "jni" version = "0.21.1" @@ -4063,6 +4360,21 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +dependencies = [ + "dashmap 5.5.3", + "futures", + "jni 0.19.0", + "log", + "once_cell", + "static_assertions", + "uuid", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -4153,7 +4465,7 @@ checksum = "1ccf93fc4a0bfe05d851d37d7c32b7f370fe94336b52a2f0efc5f1981895c2e5" dependencies = [ "async-trait", "hyper 0.14.32", - "hyper-rustls", + "hyper-rustls 0.24.2", "jsonrpsee-core", "jsonrpsee-types", "serde", @@ -4270,12 +4582,54 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "ledger-lib" +version = "0.1.0" +source = "git+https://github.com/ledger-community/rust-ledger.git?rev=510bb3ca30639af4bdb12a918b6bbbdb75fa5f52#510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" +dependencies = [ + "async-trait", + "btleplug", + "displaydoc", + "encdec", + "futures", + "hidapi", + "ledger-proto", + "once_cell", + "strum", + "thiserror 2.0.12", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "ledger-proto" +version = "0.1.0" +source = "git+https://github.com/ledger-community/rust-ledger.git?rev=510bb3ca30639af4bdb12a918b6bbbdb75fa5f52#510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" +dependencies = [ + "bitflags 2.9.1", + "displaydoc", + "encdec", + "num_enum", + "thiserror 2.0.12", +] + [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -4624,7 +4978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b69f92eb22368186aa4d25e71d2263f822b137ed3c022356acf37634c1ddda0" dependencies = [ "itertools 0.12.1", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.69", ] @@ -4640,6 +4994,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "messages" +version = "0.1.0" +source = "git+https://github.com/mintlayer/mintlayer-ledger-app?rev=78bd2e2514be3a6db83e7493eaaa03c40acc5409#78bd2e2514be3a6db83e7493eaaa03c40acc5409" +dependencies = [ + "num_enum", + "parity-scale-codec 3.7.5 (git+https://github.com/OBorce/parity-scale-codec.git?branch=fix%2Fmissing-target-atomic-ptr)", + "trezor-common 1.1.0 (git+https://github.com/mintlayer/mintlayer-core?branch=feature%2Fhardware-wallet-common2)", +] + [[package]] name = "metal" version = "0.27.0" @@ -4874,7 +5238,7 @@ dependencies = [ "futures", "logging", "once_cell", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "randomness", "rstest", "serde", @@ -5194,18 +5558,19 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -5300,6 +5665,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-bluetooth" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -5604,7 +5980,7 @@ dependencies = [ "common", "crypto", "logging", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "randomness", "rstest", "serialization", @@ -5678,7 +6054,7 @@ dependencies = [ "p2p-backend-test-suite", "p2p-test-utils", "p2p-types", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "pos-accounting", "randomness", "rpc", @@ -5746,7 +6122,7 @@ name = "p2p-types" version = "1.1.0" dependencies = [ "common", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "rpc-description", "serde", "serialization", @@ -5787,11 +6163,24 @@ dependencies = [ "byte-slice-cast", "const_format", "impl-trait-for-tuples", - "parity-scale-codec-derive", + "parity-scale-codec-derive 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "rustversion", "serde", ] +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "git+https://github.com/OBorce/parity-scale-codec.git?branch=fix%2Fmissing-target-atomic-ptr#b6aafd677f9b83d8bcd0d1a614ed115f7e43568a" +dependencies = [ + "arrayvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive 3.7.5 (git+https://github.com/OBorce/parity-scale-codec.git?branch=fix%2Fmissing-target-atomic-ptr)", + "rustversion", +] + [[package]] name = "parity-scale-codec-derive" version = "3.7.5" @@ -5804,6 +6193,17 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "git+https://github.com/OBorce/parity-scale-codec.git?branch=fix%2Fmissing-target-atomic-ptr#b6aafd677f9b83d8bcd0d1a614ed115f7e43568a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "parking" version = "2.2.1" @@ -6064,7 +6464,7 @@ dependencies = [ "accounting", "common", "crypto", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "randomness", "rstest", "serialization", @@ -6665,11 +7065,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -6683,7 +7083,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tower-service", @@ -6694,6 +7094,46 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls 0.27.7", + "hyper-tls 0.6.0", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tower 0.5.2", + "tower-http 0.6.6", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rfd" version = "0.15.3" @@ -6881,6 +7321,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rstest_reuse" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" +dependencies = [ + "quote", + "rand 0.8.5", + "syn 2.0.101", +] + [[package]] name = "rusb" version = "0.9.4" @@ -7016,6 +7467,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.6", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -7089,6 +7553,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.103.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -7197,7 +7672,7 @@ dependencies = [ "hex", "hex-literal", "logging", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "proptest", "serde", "serde_json", @@ -7317,6 +7792,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde-xml-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53630160a98edebde0123eb4dfd0fce6adff091b2305db3154a9e920206eb510" +dependencies = [ + "log", + "serde", + "thiserror 1.0.69", + "xml-rs", +] + [[package]] name = "serde_bytes" version = "0.11.17" @@ -7435,7 +7922,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.101", @@ -7485,7 +7972,7 @@ version = "1.1.0" dependencies = [ "arraytools", "hex-literal", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.8.5", ] @@ -7493,7 +7980,7 @@ dependencies = [ name = "serialization-tagged" version = "1.1.0" dependencies = [ - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "proptest", "serialization", "serialization-core", @@ -7854,7 +8341,7 @@ version = "1.1.0" dependencies = [ "libtest-mimic", "logging", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "proptest", "serialization", "storage", @@ -7953,6 +8440,12 @@ dependencies = [ "vte", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -8054,6 +8547,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -8083,7 +8579,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -8096,6 +8603,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -8153,7 +8670,7 @@ dependencies = [ "futures", "hex", "jsonrpsee", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "randomness", "rpc", "serde", @@ -8355,7 +8872,7 @@ dependencies = [ "common", "crypto", "logging", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "randomness", "rstest", "serialization", @@ -8452,6 +8969,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +dependencies = [ + "rustls 0.23.32", + "tokio", +] + [[package]] name = "tokio-socks" version = "0.5.2" @@ -8607,6 +9134,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -8717,7 +9262,7 @@ dependencies = [ "crypto", "num-derive", "num-traits", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "rstest", "serialization", "strum", @@ -8725,6 +9270,17 @@ dependencies = [ "trezor-client", ] +[[package]] +name = "trezor-common" +version = "1.1.0" +source = "git+https://github.com/mintlayer/mintlayer-core?branch=feature%2Fhardware-wallet-common2#5a2106e456ec767053dc81bbdacc33e8e7537d38" +dependencies = [ + "num-derive", + "num-traits", + "parity-scale-codec 3.7.5 (git+https://github.com/OBorce/parity-scale-codec.git?branch=fix%2Fmissing-target-atomic-ptr)", + "strum", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -8978,7 +9534,7 @@ dependencies = [ "logging", "loom", "num-traits", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "probabilistic-collections", "qrcodegen", "randomness", @@ -9018,7 +9574,7 @@ dependencies = [ "crypto", "itertools 0.14.0", "logging", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "randomness", "rstest", "serialization", @@ -9086,25 +9642,32 @@ dependencies = [ name = "wallet" version = "1.1.0" dependencies = [ + "anyhow", "async-trait", "bip39", "chainstate", "chainstate-test-framework", + "clap", "common", "consensus", "crypto", "ctor", + "derive_more", "hex", "itertools 0.14.0", "lazy_static", + "ledger-lib", "logging", "mempool", + "messages", "orders-accounting", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "pos-accounting", "randomness", + "reqwest 0.12.23", "rpc-description", "rstest", + "rstest_reuse", "secp256k1", "semver", "serde", @@ -9112,10 +9675,12 @@ dependencies = [ "serial_test", "serialization", "storage", + "strum", "tempfile", "test-utils", "thiserror 1.0.69", "tokio", + "tracing", "trezor-client", "tx-verifier", "utils", @@ -9442,10 +10007,11 @@ dependencies = [ "bip39", "common", "crypto", + "derive_more", "hex", "itertools 0.14.0", "logging", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "randomness", "rpc-description", "rstest", @@ -9968,7 +10534,7 @@ dependencies = [ "windows-interface", "windows-link", "windows-result", - "windows-strings", + "windows-strings 0.4.2", ] [[package]] @@ -10020,6 +10586,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.2", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -10029,6 +10606,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 91594e484b..655bb39dcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,81 +6,85 @@ repository = "https://github.com/mintlayer/mintlayer-core" readme = "README.md" license = "MIT" version = "1.1.0" -authors = ["Samer Afach ", "Ben Marsh ", "Enrico Rubboli "] +authors = [ + "Samer Afach ", + "Ben Marsh ", + "Enrico Rubboli ", +] edition = "2021" [workspace] members = [ - "accounting", # Accounting and balances abstractions. - "api-server/api-server-common", # API server, for light-wallets and block explorers: common between web-server and scanner. - "api-server/storage-test-suite", # Test suite for the abstract storage layer of the API server to ensure consistent behavior. - "api-server/scanner-daemon", # API server, for light-wallets and block explorers: blockchain scanner daemon. - "api-server/scanner-lib", # API server, for light-wallets and block explorers: blockchain scanner library. - "api-server/stack-test-suite", # API server, for light-wallets and block explorers: testing the scanner and web-server. - "api-server/web-server", # API server, for light-wallets and block explorers: web-server. - "blockprod", # Block production with whatever consensus algorithm. - "chainstate", # Code on chainstate of blocks and transactions. - "chainstate/db-dumper", # A tool for dumping the contents of the chainstate db. - "chainstate/test-suite", # Tests for the chainstate, separated to make use of the chainstate test framework. - "common", # Everything else, until it's moved to another crate. - "consensus", # Consensus related logic. - "crypto", # Cryptographic primitives and their interfaces. - "dns-server", # DNS-server. - "logging", # Logging engine and its interfaces. - "mempool", # Mempool interface and implementation. - "mempool/types", # Common mempool types. - "mintscript", # Basic scripting language for validating transactions. - "mocks", # Mock implementations of our traits (used for testing). - "node-daemon", # Node terminal binary. - "networking", # Pure networking implementations - "node-gui", # Node GUI binary. - "node-gui/backend", # Node GUI backend, common logic for implementing GUI clients. - "node-lib", # Node lib; the common library between daemon, tui and gui node executables. - "orders-accounting", # Orders accounting - "p2p", # P2p communication interfaces and protocols. - "p2p/backend-test-suite", # P2p backend agnostic tests. - "p2p/types", # P2p support types with minimal dependencies. - "pos-accounting", # PoS accounting and balances abstractions. - "randomness", # A wrapper around all randomness functionality to make audits easier - "rpc", # Rpc abstraction and implementation. - "rpc/description", # Data types describing an RPC interface. - "rpc/description-macro", # Macro to generate rpc interface description. - "rpc/types", # Support types for use in RPC interfaces. - "script", # Bitcoin script and its interfaces. - "serialization", # Full featured serialization interfaces and implementations. - "serialization/core", # Serialization core tools. - "serialization/tagged", # Serialization for direct/tagged encoding style. - "serialization/tagged/derive", # direct/tagged encoding style derive macros. - "storage", # storage abstraction layer and its implementation. - "storage/backend-test-suite", # Tests for validating storage backend implementations. - "storage/core", # Core backend-agnostic storage abstraction. - "storage/failing", # Storage adapter to occasionally fail certain operations, for testing. - "storage/inmemory", # In-memory storage backend implementation. - "storage/lmdb", # LMDB-based persistent storage backend implementation. - "storage/sqlite", # SQLite-based persistent storage backend implementation. - "subsystem", # Utilities for working with concurrent subsystems. - "test", # Integration tests. - "test-rpc-functions", # RPC functions specifically for tests. - "test-utils", # Various utilities for tests. - "tokens-accounting", # Tokens accounting - "utils", # Various utilities. - "utils/networking", # Various async/tokio utilities. - "utxo", # Utxo and related utilities (cache, undo, etc.). - "trezor-common", # Code used by Trezor firmware repository. - "wallet", # Wallet primitives. - "wallet/wallet-cli", # Wallet CLI/REPL binary. - "wallet/wallet-cli-lib", # Wallet CLI/REPL lib. - "wallet/wallet-cli-commands", # Wallet CLI/REPL commands. - "wallet/wallet-controller", # Common code for wallet UI applications. - "wallet/wallet-node-client", # Wallet-to-node communication tools. - "wallet/wallet-address-generator", # Wallet address generator binary. - "wallet/wallet-address-generator-lib",# Wallet address generator lib. - "wallet/wallet-rpc-client", # Wallet RPC communication. - "wallet/wallet-rpc-daemon", # Wallet RPC daemon binary. - "wallet/wallet-rpc-lib", # Wallet RPC definitions library. - "wallet/wallet-test-node", # Node for wallet testing as a library. - "wasm-wrappers", # WASM wrappers for various components. - "wasm-wrappers/wasm-doc-gen", # WASM wrappers documentation generator. + "accounting", # Accounting and balances abstractions. + "api-server/api-server-common", # API server, for light-wallets and block explorers: common between web-server and scanner. + "api-server/storage-test-suite", # Test suite for the abstract storage layer of the API server to ensure consistent behavior. + "api-server/scanner-daemon", # API server, for light-wallets and block explorers: blockchain scanner daemon. + "api-server/scanner-lib", # API server, for light-wallets and block explorers: blockchain scanner library. + "api-server/stack-test-suite", # API server, for light-wallets and block explorers: testing the scanner and web-server. + "api-server/web-server", # API server, for light-wallets and block explorers: web-server. + "blockprod", # Block production with whatever consensus algorithm. + "chainstate", # Code on chainstate of blocks and transactions. + "chainstate/db-dumper", # A tool for dumping the contents of the chainstate db. + "chainstate/test-suite", # Tests for the chainstate, separated to make use of the chainstate test framework. + "common", # Everything else, until it's moved to another crate. + "consensus", # Consensus related logic. + "crypto", # Cryptographic primitives and their interfaces. + "dns-server", # DNS-server. + "logging", # Logging engine and its interfaces. + "mempool", # Mempool interface and implementation. + "mempool/types", # Common mempool types. + "mintscript", # Basic scripting language for validating transactions. + "mocks", # Mock implementations of our traits (used for testing). + "node-daemon", # Node terminal binary. + "networking", # Pure networking implementations + "node-gui", # Node GUI binary. + "node-gui/backend", # Node GUI backend, common logic for implementing GUI clients. + "node-lib", # Node lib; the common library between daemon, tui and gui node executables. + "orders-accounting", # Orders accounting + "p2p", # P2p communication interfaces and protocols. + "p2p/backend-test-suite", # P2p backend agnostic tests. + "p2p/types", # P2p support types with minimal dependencies. + "pos-accounting", # PoS accounting and balances abstractions. + "randomness", # A wrapper around all randomness functionality to make audits easier + "rpc", # Rpc abstraction and implementation. + "rpc/description", # Data types describing an RPC interface. + "rpc/description-macro", # Macro to generate rpc interface description. + "rpc/types", # Support types for use in RPC interfaces. + "script", # Bitcoin script and its interfaces. + "serialization", # Full featured serialization interfaces and implementations. + "serialization/core", # Serialization core tools. + "serialization/tagged", # Serialization for direct/tagged encoding style. + "serialization/tagged/derive", # direct/tagged encoding style derive macros. + "storage", # storage abstraction layer and its implementation. + "storage/backend-test-suite", # Tests for validating storage backend implementations. + "storage/core", # Core backend-agnostic storage abstraction. + "storage/failing", # Storage adapter to occasionally fail certain operations, for testing. + "storage/inmemory", # In-memory storage backend implementation. + "storage/lmdb", # LMDB-based persistent storage backend implementation. + "storage/sqlite", # SQLite-based persistent storage backend implementation. + "subsystem", # Utilities for working with concurrent subsystems. + "test", # Integration tests. + "test-rpc-functions", # RPC functions specifically for tests. + "test-utils", # Various utilities for tests. + "tokens-accounting", # Tokens accounting + "utils", # Various utilities. + "utils/networking", # Various async/tokio utilities. + "utxo", # Utxo and related utilities (cache, undo, etc.). + "trezor-common", # Code used by Trezor firmware repository. + "wallet", # Wallet primitives. + "wallet/wallet-cli", # Wallet CLI/REPL binary. + "wallet/wallet-cli-lib", # Wallet CLI/REPL lib. + "wallet/wallet-cli-commands", # Wallet CLI/REPL commands. + "wallet/wallet-controller", # Common code for wallet UI applications. + "wallet/wallet-node-client", # Wallet-to-node communication tools. + "wallet/wallet-address-generator", # Wallet address generator binary. + "wallet/wallet-address-generator-lib", # Wallet address generator lib. + "wallet/wallet-rpc-client", # Wallet RPC communication. + "wallet/wallet-rpc-daemon", # Wallet RPC daemon binary. + "wallet/wallet-rpc-lib", # Wallet RPC definitions library. + "wallet/wallet-test-node", # Node for wallet testing as a library. + "wasm-wrappers", # WASM wrappers for various components. + "wasm-wrappers/wasm-doc-gen", # WASM wrappers documentation generator. ] default-members = [ @@ -214,10 +218,12 @@ reedline = "0.38" ref-cast = "1.0" regex = "1.10" replace_with = "0.1" +reqwest = "0.12" rfd = { version = "0.15", default-features = false } ripemd = "0.1" rlimit = "0.10" rstest = "0.24" +rstest_reuse = "0.7" rusqlite = "0.33" schnorrkel = "0.11" secp256k1 = { version = "0.29", default-features = false } @@ -245,7 +251,11 @@ tempfile = "3.3" testing_logger = "0.1" thiserror = "1.0" tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["registry", "env-filter", "json"] } +tracing-subscriber = { version = "0.3", features = [ + "registry", + "env-filter", + "json", +] } tokio = { version = "1.27", default-features = false } tokio-socks = "0.5" tokio-stream = "0.1" @@ -256,6 +266,16 @@ tower-http-axum = { package = "tower-http", version = "0.5" } x25519-dalek = "2.0" zeroize = "1.5" +[workspace.dependencies.ledger-lib] +git = "https://github.com/ledger-community/rust-ledger.git" +rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" + +[workspace.dependencies.mintlayer-ledger-messages] +git = "https://github.com/mintlayer/mintlayer-ledger-app" +# The commit "Fix comments" +rev = "78bd2e2514be3a6db83e7493eaaa03c40acc5409" +package = "messages" + [workspace.dependencies.trezor-client] git = "https://github.com/mintlayer/mintlayer-trezor-firmware" # The commit "Merge pull request #16 from mintlayer/sign_message_convert_to_hex_if_non_ascii" @@ -295,7 +315,8 @@ opt-level = 2 [features] trezor = [] -default = ["trezor"] +ledger = [] +default = ["trezor", "ledger"] [patch.crates-io] # Using fontconfig-parser v0.5.8 completely breaks UI in node-gui on Linux - most of the text disappears and the few @@ -307,3 +328,6 @@ default = ["trezor"] # is fontconfig-parser <- fontdb <- cosmic-text <- various "iced" crates. # TODO: investigate this further. fontconfig-parser = { git = "https://github.com/Riey/fontconfig-parser", rev = "f7d13a779e6ee282ce75acbc00a1270c0350e0c2" } +# This patch is needed because there is no release of the library and because ledger-lib depends on ledger-proto, so this is the only way to make the former find the latter. +# Note that the revision specified here must be the same as the one used in the workspace.dependencies section +ledger-proto = { git = "https://github.com/ledger-community/rust-ledger.git", rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" } diff --git a/node-gui/Cargo.toml b/node-gui/Cargo.toml index 261a702d98..77c630848b 100644 --- a/node-gui/Cargo.toml +++ b/node-gui/Cargo.toml @@ -5,7 +5,11 @@ license.workspace = true version.workspace = true edition.workspace = true rust-version.workspace = true -authors = ["Samer Afach ", "Ben Marsh ", "Enrico Rubboli "] +authors = [ + "Samer Afach ", + "Ben Marsh ", + "Enrico Rubboli ", +] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -23,7 +27,7 @@ utils = { path = "../utils" } wallet = { path = "../wallet" } wallet-controller = { path = "../wallet/wallet-controller" } wallet-types = { path = "../wallet/types" } -wallet-cli-commands = { path = "../wallet/wallet-cli-commands"} +wallet-cli-commands = { path = "../wallet/wallet-cli-commands" } wallet-storage = { path = "../wallet/storage" } anyhow.workspace = true @@ -42,5 +46,16 @@ tokio.workspace = true winres = "0.1" [features] -trezor = ["wallet-controller/trezor", "wallet-types/trezor", "wallet-cli-commands/trezor", "node-gui-backend/trezor"] -default = ["trezor"] +trezor = [ + "wallet-controller/trezor", + "wallet-types/trezor", + "wallet-cli-commands/trezor", + "node-gui-backend/trezor", +] +ledger = [ + "wallet-controller/ledger", + "wallet-types/ledger", + "wallet-cli-commands/ledger", + "node-gui-backend/ledger", +] +default = ["trezor", "ledger"] diff --git a/node-gui/backend/Cargo.toml b/node-gui/backend/Cargo.toml index 0d27d83476..a94afe1352 100644 --- a/node-gui/backend/Cargo.toml +++ b/node-gui/backend/Cargo.toml @@ -5,7 +5,11 @@ license.workspace = true version.workspace = true edition.workspace = true rust-version.workspace = true -authors = ["Samer Afach ", "Ben Marsh ", "Enrico Rubboli "] +authors = [ + "Samer Afach ", + "Ben Marsh ", + "Enrico Rubboli ", +] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -24,9 +28,9 @@ utils = { path = "../../utils" } wallet = { path = "../../wallet" } wallet-controller = { path = "../../wallet/wallet-controller" } wallet-types = { path = "../../wallet/types" } -wallet-rpc-lib = { path = "../../wallet/wallet-rpc-lib"} -wallet-rpc-client = { path = "../../wallet/wallet-rpc-client"} -wallet-cli-commands = { path = "../../wallet/wallet-cli-commands"} +wallet-rpc-lib = { path = "../../wallet/wallet-rpc-lib" } +wallet-rpc-client = { path = "../../wallet/wallet-rpc-client" } +wallet-cli-commands = { path = "../../wallet/wallet-cli-commands" } anyhow.workspace = true chrono.workspace = true @@ -45,4 +49,20 @@ test-utils = { path = "../../test-utils" } rstest.workspace = true [features] -trezor = ["wallet/trezor", "wallet-controller/trezor", "wallet-types/trezor", "wallet-rpc-lib/trezor", "wallet-rpc-client/trezor", "wallet-cli-commands/trezor"] +trezor = [ + "wallet/trezor", + "wallet-controller/trezor", + "wallet-types/trezor", + "wallet-rpc-lib/trezor", + "wallet-rpc-client/trezor", + "wallet-cli-commands/trezor", +] +ledger = [ + "wallet/ledger", + "wallet-controller/ledger", + "wallet-types/ledger", + "wallet-rpc-lib/ledger", + "wallet-rpc-client/ledger", + "wallet-cli-commands/ledger", +] +default = ["trezor", "ledger"] diff --git a/node-gui/backend/src/backend_impl.rs b/node-gui/backend/src/backend_impl.rs index d2d1f0709d..22ec60b70d 100644 --- a/node-gui/backend/src/backend_impl.rs +++ b/node-gui/backend/src/backend_impl.rs @@ -354,9 +354,43 @@ impl Backend { (wallet_data, accounts_info, best_block) } + #[cfg(feature = "ledger")] + (WalletType::Ledger, ColdHotNodeController::Hot(controller)) => { + let handles_client = WalletHandlesClient::new( + controller.chainstate.clone(), + controller.mempool.clone(), + controller.block_prod.clone(), + controller.p2p.clone(), + ) + .await + .map_err(|e| BackendError::WalletError(e.to_string()))?; + + let (wallet_rpc, command_handler, best_block, accounts_info, accounts_data) = self + .create_wallet( + handles_client, + file_path.clone(), + wallet_args, + import, + wallet_events, + ) + .await?; + + let wallet_data = WalletData { + controller: GuiHotColdController::Hot(wallet_rpc, command_handler), + accounts: accounts_data, + best_block, + updated: false, + }; + + (wallet_data, accounts_info, best_block) + } #[cfg(feature = "trezor")] (WalletType::Trezor, ColdHotNodeController::Cold) => { - return Err(BackendError::ColdTrezorNotSupported) + return Err(BackendError::HardwareWalletNotSupportedInColdMode) + } + #[cfg(feature = "ledger")] + (WalletType::Ledger, ColdHotNodeController::Cold) => { + return Err(BackendError::HardwareWalletNotSupportedInColdMode) } (WalletType::Hot, ColdHotNodeController::Cold) => { return Err(BackendError::HotNotSupported) @@ -593,9 +627,49 @@ impl Backend { (wallet_data, accounts_info, best_block, encryption_state) } + #[cfg(feature = "ledger")] + (WalletType::Ledger, ColdHotNodeController::Hot(controller)) => { + let handles_client = WalletHandlesClient::new( + controller.chainstate.clone(), + controller.mempool.clone(), + controller.block_prod.clone(), + controller.p2p.clone(), + ) + .await + .map_err(|e| BackendError::WalletError(e.to_string()))?; + + let ( + wallet_rpc, + command_handler, + encryption_state, + best_block, + accounts_info, + accounts_data, + ) = self + .open_wallet( + handles_client, + file_path.clone(), + wallet_events, + Some(HardwareWalletType::Ledger), + ) + .await?; + + let wallet_data = WalletData { + controller: GuiHotColdController::Hot(wallet_rpc, command_handler), + accounts: accounts_data, + best_block, + updated: false, + }; + + (wallet_data, accounts_info, best_block, encryption_state) + } #[cfg(feature = "trezor")] (WalletType::Trezor, ColdHotNodeController::Cold) => { - return Err(BackendError::ColdTrezorNotSupported) + return Err(BackendError::HardwareWalletNotSupportedInColdMode) + } + #[cfg(feature = "ledger")] + (WalletType::Ledger, ColdHotNodeController::Cold) => { + return Err(BackendError::HardwareWalletNotSupportedInColdMode) } (WalletType::Hot, ColdHotNodeController::Cold) => { return Err(BackendError::HotNotSupported) diff --git a/node-gui/backend/src/error.rs b/node-gui/backend/src/error.rs index 4d170094d1..42fc4d9525 100644 --- a/node-gui/backend/src/error.rs +++ b/node-gui/backend/src/error.rs @@ -46,8 +46,8 @@ pub enum BackendError { ColdWallet, #[error("Cannot interact with a hot wallet when in Cold wallet mode")] HotNotSupported, - #[error("Cannot use a Trezor wallet in a Cold wallet mode")] - ColdTrezorNotSupported, + #[error("Cannot use a Hardware wallet in a Cold wallet mode")] + HardwareWalletNotSupportedInColdMode, #[error("Invalid console command: {0}")] InvalidConsoleCommand(String), #[error("Empty console command")] diff --git a/node-gui/src/main_window/main_menu.rs b/node-gui/src/main_window/main_menu.rs index 113be444ad..1600ffc23d 100644 --- a/node-gui/src/main_window/main_menu.rs +++ b/node-gui/src/main_window/main_menu.rs @@ -125,6 +125,27 @@ fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, i }, ), ]; + #[cfg(feature = "ledger")] + { + menu.push(menu_item( + "(Beta) Create new Ledger wallet", + MenuMessage::CreateNewWallet { + wallet_type: WalletType::Ledger, + }, + )); + menu.push(menu_item( + "(Beta) Recover from Ledger wallet", + MenuMessage::RecoverWallet { + wallet_type: WalletType::Ledger, + }, + )); + menu.push(menu_item( + "(Beta) Open Ledger wallet", + MenuMessage::OpenWallet { + wallet_type: WalletType::Ledger, + }, + )); + } #[cfg(feature = "trezor")] { menu.push(menu_item( @@ -146,6 +167,7 @@ fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, i }, )); } + // TODO: enable setting when needed // menu.push(menu_item("Settings", MenuMessage::NoOp)); menu.push(menu_item("Exit", MenuMessage::Exit)); diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs b/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs index a00d1cf3ad..757339ca19 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs @@ -118,17 +118,48 @@ pub fn view_left_panel( .on_press(WalletMessage::SelectPanel(panel)) .padding(panel_button_row_padding) }; + let is_cold_wallet = wallet_info.wallet_type == WalletType::Cold; // `next_height` is used to prevent flickering when a new block is found - let show_scan_progress = match wallet_info.wallet_type { - WalletType::Cold => false, - #[cfg(feature = "trezor")] - WalletType::Trezor => { - wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height - } - WalletType::Hot => { - wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height - } + let show_scan_progress = if is_cold_wallet { + false + } else { + wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height + }; + + let hardware_wallet_panels = || { + column![ + panel_button( + "Transactions", + SelectedPanel::Transactions, + selected_panel, + TRANSACTIONS_TOOLTIP_TEXT + ), + panel_button( + "Addresses", + SelectedPanel::Addresses, + selected_panel, + ADDRESSES_TOOLTIP_TEXT + ), + panel_button( + "Send", + SelectedPanel::Send, + selected_panel, + SEND_TOOLTIP_TEXT + ), + panel_button( + "Delegation", + SelectedPanel::Delegation, + selected_panel, + DELEGATION_TOOLTIP_TEXT + ), + panel_button( + "Console", + SelectedPanel::Console, + selected_panel, + CONSOLE_TOOLTIP_TEXT, + ) + ] }; let scan_progress_widget = if show_scan_progress { @@ -188,38 +219,11 @@ pub fn view_left_panel( match wallet_info.wallet_type { #[cfg(feature = "trezor")] WalletType::Trezor => { - column![ - panel_button( - "Transactions", - SelectedPanel::Transactions, - selected_panel, - TRANSACTIONS_TOOLTIP_TEXT - ), - panel_button( - "Addresses", - SelectedPanel::Addresses, - selected_panel, - ADDRESSES_TOOLTIP_TEXT - ), - panel_button( - "Send", - SelectedPanel::Send, - selected_panel, - SEND_TOOLTIP_TEXT - ), - panel_button( - "Delegation", - SelectedPanel::Delegation, - selected_panel, - DELEGATION_TOOLTIP_TEXT - ), - panel_button( - "Console", - SelectedPanel::Console, - selected_panel, - CONSOLE_TOOLTIP_TEXT, - ) - ] + hardware_wallet_panels() + } + #[cfg(feature = "ledger")] + WalletType::Ledger => { + hardware_wallet_panels() } WalletType::Cold => { column![ diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs b/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs index ff73d1152f..e88d8c7521 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs @@ -183,6 +183,8 @@ impl WalletTab { WalletType::Cold => SelectedPanel::Addresses, #[cfg(feature = "trezor")] WalletType::Trezor => SelectedPanel::Transactions, + #[cfg(feature = "ledger")] + WalletType::Ledger => SelectedPanel::Transactions, }; WalletTab { @@ -471,6 +473,7 @@ impl Tab for WalletTab { Some(wallet_info) => match wallet_info.extra_info { wallet_controller::types::WalletExtraInfo::SoftwareWallet => "Software wallet", wallet_controller::types::WalletExtraInfo::TrezorWallet { .. } => "Trezor wallet", + wallet_controller::types::WalletExtraInfo::LedgerWallet { .. } => "Ledger wallet", }, None => "No wallet", }; @@ -489,15 +492,12 @@ impl Tab for WalletTab { .get(&self.selected_account) .expect("selected account must be known"); - let still_syncing = match wallet_info.wallet_type { - WalletType::Cold => false, - #[cfg(feature = "trezor")] - WalletType::Trezor => { - wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height - } - WalletType::Hot => { - wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height - } + let is_cold_wallet = wallet_info.wallet_type == WalletType::Cold; + + let still_syncing = if is_cold_wallet { + false + } else { + wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height } .then_some(WalletMessage::StillSyncing); diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/status_bar.rs b/node-gui/src/main_window/main_widget/tabs/wallet/status_bar.rs index d94fe3c5aa..fabca6abe1 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/status_bar.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/status_bar.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(any(feature = "trezor", feature = "ledger"))] +use iced::widget::{rich_text, span}; use iced::{ font, widget::{container, row, Container}, @@ -31,7 +33,7 @@ const HORIZONTAL_PADDING: f32 = 10.; pub fn estimate_status_bar_height(wallet_info: &WalletExtraInfo) -> f32 { match wallet_info { WalletExtraInfo::SoftwareWallet => 0., - WalletExtraInfo::TrezorWallet { .. } => { + WalletExtraInfo::TrezorWallet { .. } | WalletExtraInfo::LedgerWallet { .. } => { TEXT_SIZE + 2. * VERTICAL_PADDING // For some reason, the status bar gets a bit of additional height. + 4. @@ -55,8 +57,6 @@ pub fn view_status_bar(wallet_info: &WalletExtraInfo) -> Option { - use iced::widget::{rich_text, span}; - row![ rich_text([span("Device name: ").font(bold_font), span(device_name.clone())]) .size(TEXT_SIZE), @@ -67,6 +67,11 @@ pub fn view_status_bar(wallet_info: &WalletExtraInfo) -> Option { + row![rich_text([span("App version: ").font(bold_font), span(app_version.clone())]) + .size(TEXT_SIZE),] + } }; let status_bar = Container::new( diff --git a/node-gui/src/main_window/mod.rs b/node-gui/src/main_window/mod.rs index f8561a16c9..8109f3476f 100644 --- a/node-gui/src/main_window/mod.rs +++ b/node-gui/src/main_window/mod.rs @@ -38,7 +38,7 @@ use wallet_cli_commands::ConsoleCommand; use wallet_controller::types::WalletTypeArgs; use wallet_types::{seed_phrase::StoreSeedPhrase, wallet_type::WalletType, ImportOrCreate}; -#[cfg(feature = "trezor")] +#[cfg(any(feature = "trezor", feature = "ledger"))] use crate::widgets::create_hw_wallet::hw_wallet_create_dialog; use crate::{ main_window::{main_menu::MenuMessage, main_widget::MainWidgetMessage}, @@ -148,6 +148,8 @@ pub enum WalletArgs { }, #[cfg(feature = "trezor")] Trezor, + #[cfg(feature = "ledger")] + Ledger, } impl From<&WalletArgs> for WalletType { @@ -165,6 +167,8 @@ impl From<&WalletArgs> for WalletType { } #[cfg(feature = "trezor")] WalletArgs::Trezor => WalletType::Trezor, + #[cfg(feature = "ledger")] + WalletArgs::Ledger => WalletType::Ledger, } } } @@ -291,6 +295,8 @@ impl MainWindow { }, #[cfg(feature = "trezor")] WalletType::Trezor => WalletArgs::Trezor, + #[cfg(feature = "ledger")] + WalletType::Ledger => WalletArgs::Ledger, }; self.active_dialog = ActiveDialog::WalletCreate { wallet_args }; Task::none() @@ -733,6 +739,8 @@ impl MainWindow { } #[cfg(feature = "trezor")] WalletArgs::Trezor => WalletTypeArgs::Trezor { device_id: None }, + #[cfg(feature = "ledger")] + WalletArgs::Ledger => WalletTypeArgs::Ledger, }; self.file_dialog_active = true; @@ -864,6 +872,16 @@ impl MainWindow { ImportOrCreate::Create, ) .into(), + #[cfg(feature = "ledger")] + WalletArgs::Ledger => hw_wallet_create_dialog( + Box::new(move || MainWindowMessage::ImportWalletMnemonic { + args: WalletArgs::Ledger, + import: ImportOrCreate::Create, + }), + Box::new(|| MainWindowMessage::CloseDialog), + ImportOrCreate::Create, + ) + .into(), }, ActiveDialog::WalletRecover { wallet_type } => { let is_cold = *wallet_type == WalletType::Cold; @@ -888,6 +906,16 @@ impl MainWindow { ImportOrCreate::Import, ) .into(), + #[cfg(feature = "ledger")] + WalletType::Ledger => hw_wallet_create_dialog( + Box::new(move || MainWindowMessage::ImportWalletMnemonic { + args: WalletArgs::Ledger, + import: ImportOrCreate::Create, + }), + Box::new(|| MainWindowMessage::CloseDialog), + ImportOrCreate::Import, + ) + .into(), } } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index d14cb2ea00..5b94332adc 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -32,39 +32,60 @@ bip39 = { workspace = true, default-features = false, features = [ "std", "zeroize", ] } +derive_more.workspace = true hex.workspace = true itertools.workspace = true parity-scale-codec.workspace = true semver.workspace = true serde.workspace = true thiserror.workspace = true -trezor-client = { workspace = true, optional = true } -zeroize.workspace = true - -[dev-dependencies] -chainstate-test-framework = { path = "../chainstate/test-framework" } -test-utils = { path = "../test-utils" } tokio = { workspace = true, default-features = false, features = [ "io-util", "macros", - "net", "rt", "sync", ] } +trezor-client = { workspace = true, optional = true } +ledger-lib = { workspace = true, optional = true } +mintlayer-ledger-messages = { workspace = true, optional = true } +zeroize.workspace = true + +[dev-dependencies] +chainstate-test-framework = { path = "../chainstate/test-framework" } +test-utils = { path = "../test-utils" } +anyhow.workspace = true +clap.workspace = true ctor.workspace = true lazy_static.workspace = true +reqwest = { workspace = true, features = ["json"] } rstest.workspace = true +rstest_reuse.workspace = true secp256k1 = { workspace = true, default-features = false } serde_json.workspace = true serial_test.workspace = true +strum.workspace = true tempfile.workspace = true +tokio = { workspace = true, default-features = false, features = [ + "io-util", + "macros", + "net", + "rt", + "sync", +] } +tracing = { workspace = true } [features] trezor = ["dep:trezor-client", "wallet-types/trezor"] enable-trezor-device-tests = [] +enable-ledger-device-tests = [] # Note: currently this is used in certain external tests (in particular, in the bridge), so we only # allow it for regtest. TODO: it's better to have some regtest-specific options for the wallet, # similar to what we have for the node. use-deterministic-signatures-in-software-signer-for-regtest = [] -default = ["trezor"] +ledger = [ + "dep:ledger-lib", + "dep:mintlayer-ledger-messages", + "wallet-types/ledger", +] +default = ["trezor", "ledger"] diff --git a/wallet/src/key_chain/master_key_chain/mod.rs b/wallet/src/key_chain/master_key_chain/mod.rs index a93eb60b92..5eb67449c0 100644 --- a/wallet/src/key_chain/master_key_chain/mod.rs +++ b/wallet/src/key_chain/master_key_chain/mod.rs @@ -21,7 +21,7 @@ use crypto::key::hdkd::u31::U31; use crypto::vrf::ExtendedVRFPrivateKey; use std::sync::Arc; use wallet_storage::{ - StoreTxRwUnlocked, WalletStorageReadLocked, WalletStorageReadUnlocked, + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteLocked, WalletStorageWriteUnlocked, }; use wallet_types::seed_phrase::{SerializableSeedPhrase, StoreSeedPhrase}; @@ -59,9 +59,9 @@ impl MasterKeyChain { )) } - pub fn new_from_mnemonic( + pub fn new_from_mnemonic( chain_config: Arc, - db_tx: &mut StoreTxRwUnlocked, + db_tx: &mut impl WalletStorageWriteUnlocked, mnemonic_str: &str, passphrase: Option<&str>, save_seed_phrase: StoreSeedPhrase, @@ -80,9 +80,9 @@ impl MasterKeyChain { ) } - fn new_from_root_key( + fn new_from_root_key( chain_config: Arc, - db_tx: &mut StoreTxRwUnlocked, + db_tx: &mut impl WalletStorageWriteUnlocked, root_key: ExtendedPrivateKey, root_vrf_key: ExtendedVRFPrivateKey, seed_phrase: Option, @@ -131,7 +131,7 @@ impl MasterKeyChain { pub fn create_account_key_chain( &self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut (impl WalletStorageWriteLocked + WalletStorageReadUnlocked), account_index: U31, lookahead_size: u32, ) -> KeyChainResult { diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs new file mode 100644 index 0000000000..3457bbbe76 --- /dev/null +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -0,0 +1,304 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::BTreeMap, mem::size_of_val, time::Duration}; + +use crate::signer::{ledger_signer::LedgerError, SignerError, SignerResult}; +use common::{ + chain::{self}, + primitives, +}; +use crypto::key::{ + extended::ExtendedPublicKey, + hdkd::{chain_code::ChainCode, derivation_path::DerivationPath}, + secp256k1::{extended_keys::Secp256k1ExtendedPublicKey, Secp256k1PublicKey}, +}; +use serialization::Encode; +use utils::ensure; +use wallet_types::hw_data::LedgerFullInfo; + +use ledger_lib::Exchange; +use mintlayer_ledger_messages::{ + decode_all as ledger_decode_all, encode as ledger_encode, AddrType, Amount as LAmount, + Bip32Path as LedgerBip32Path, CoinType, GetPublicKeyRespones, GetVersionRespones, + InputAdditionalInfoReq, Ins, MsgSignature, OutputValue as LOutputValue, P1SignTx, PubKeyP1, + PublicKeyReq, SignMessageReq, SignTxReq, Signature as LedgerSignature, TxInput as LTxInput, + TxInputReq, TxMetadataReq, TxOutput as LTxOutput, TxOutputReq, APDU_CLASS, H256 as LH256, + P1_APP_NAME, P1_GET_VERSION, P1_SIGN_NEXT, P1_SIGN_START, P2_DONE, P2_SIGN_MORE, +}; + +const MAX_ADPU_LEN: usize = (u8::MAX - 5) as usize; // 4 bytes for the header + 1 for len +const TIMEOUT_DUR: Duration = Duration::from_secs(100); +const OK_RESPONSE: u16 = 0x9000; +const TX_VERSION: u8 = 1; + +struct SignatureResult { + sig: LedgerSignature, + input_idx: usize, + has_more_signatures: bool, +} + +/// Check that the response ends with the OK status code and return the rest of the response back +pub fn ok_response(mut resp: Vec) -> SignerResult> { + let (_, status_code) = resp.split_last_chunk().ok_or(LedgerError::InvalidResponse)?; + let response_status = u16::from_be_bytes(*status_code); + + ensure!( + response_status == OK_RESPONSE, + LedgerError::ErrorResponse(response_status) + ); + + resp.truncate(resp.len() - size_of_val(&response_status)); + Ok(resp) +} + +/// Send a message to the Ledger and check the response status code is ok +async fn exchange_message( + ledger: &mut L, + msg_buf: &[u8], +) -> Result, SignerError> { + let resp = ledger + .exchange(msg_buf, TIMEOUT_DUR) + .await + .map_err(|err| LedgerError::DeviceError(err.to_string()))?; + ok_response(resp) +} + +/// Send a message in chunks to the ledger as the max size of a message can be 255 bytes +async fn send_chunked( + ledger: &mut L, + ins: u8, + p1: u8, + message: &[u8], +) -> Result, SignerError> { + let mut msg_buf = vec![]; + let mut chunks = message.chunks(MAX_ADPU_LEN).peekable(); + let mut resp = vec![]; + while let Some(chunk) = chunks.next() { + msg_buf.clear(); + + let p2 = if chunks.peek().is_some() { + P2_SIGN_MORE + } else { + P2_DONE + }; + + msg_buf.extend([APDU_CLASS, ins, p1, p2]); + msg_buf.push(chunk.len() as u8); + msg_buf.extend(chunk); + resp = exchange_message(ledger, &msg_buf).await?; + } + + Ok(resp) +} + +pub async fn sign_challenge( + ledger: &mut L, + coin: CoinType, + path: LedgerBip32Path, + addr_type: AddrType, + message: &[u8], +) -> SignerResult> { + let req = SignMessageReq { + coin, + addr_type, + path, + }; + + send_chunked(ledger, Ins::SIGN_MSG, P1_SIGN_START, &ledger_encode(req)).await?; + + let resp = send_chunked(ledger, Ins::SIGN_MSG, P1_SIGN_NEXT, message).await?; + + let sig: MsgSignature = ledger_decode_all(&resp).ok_or(LedgerError::InvalidResponse)?; + + Ok(sig.signature.to_vec()) +} + +pub async fn get_app_name(ledger: &mut L) -> Result, ledger_lib::Error> { + let msg_buf = [APDU_CLASS, Ins::APP_NAME, P1_APP_NAME, P2_DONE]; + ledger.exchange(&msg_buf, Duration::from_millis(500)).await +} + +async fn get_app_version(ledger: &mut L) -> Result, ledger_lib::Error> { + let msg_buf = [APDU_CLASS, Ins::GET_VERSION, P1_GET_VERSION, P2_DONE]; + ledger.exchange(&msg_buf, Duration::from_millis(500)).await +} + +pub async fn check_current_app(ledger: &mut L) -> SignerResult { + let resp = get_app_name(ledger) + .await + .map_err(|err| LedgerError::DeviceError(err.to_string()))?; + let resp = ok_response(resp)?; + let name = String::from_utf8(resp).map_err(|_| LedgerError::InvalidResponse)?; + + ensure!( + name == "mintlayer-app", + LedgerError::DifferentActiveApp(name) + ); + + let resp = get_app_version(ledger) + .await + .map_err(|err| LedgerError::DeviceError(err.to_string()))?; + let ver = ok_response(resp)?; + let app_version_resp: GetVersionRespones = + ledger_decode_all(&ver).ok_or(LedgerError::InvalidResponse)?; + let app_version = common::primitives::semver::SemVer { + major: app_version_resp.major, + minor: app_version_resp.minor, + patch: app_version_resp.patch as u16, + }; + + Ok(LedgerFullInfo { app_version }) +} + +pub async fn get_extended_public_key( + ledger: &mut L, + coin_type: CoinType, + derivation_path: DerivationPath, +) -> SignerResult { + let path = LedgerBip32Path( + derivation_path.as_slice().iter().map(|c| c.into_encoded_index()).collect(), + ); + let req = PublicKeyReq { coin_type, path }; + + let resp = send_chunked( + ledger, + Ins::PUB_KEY, + PubKeyP1::NoDisplayAddress.into(), + &ledger_encode(req), + ) + .await?; + + let resp: GetPublicKeyRespones = + ledger_decode_all(&resp).ok_or(LedgerError::InvalidResponse)?; + + let extended_public_key = Secp256k1ExtendedPublicKey::new_unchecked( + derivation_path, + ChainCode::from(resp.chain_code), + Secp256k1PublicKey::from_bytes(&resp.public_key).map_err(|_| LedgerError::InvalidKey)?, + ); + + Ok(ExtendedPublicKey::new(extended_public_key)) +} + +pub async fn sign_tx( + ledger: &mut L, + chain_type: CoinType, + inputs: Vec, + input_additional_infos: Vec, + outputs: Vec, +) -> SignerResult>> { + let metadata = ledger_encode(TxMetadataReq { + coin: chain_type, + version: TX_VERSION, + num_inputs: inputs.len() as u32, + num_outputs: outputs.len() as u32, + }); + send_chunked(ledger, Ins::SIGN_TX, P1SignTx::Metadata.into(), &metadata).await?; + + for inp in inputs { + send_chunked( + ledger, + Ins::SIGN_TX, + P1SignTx::Input.into(), + &ledger_encode(SignTxReq::Input(inp)), + ) + .await?; + } + + for info in input_additional_infos { + send_chunked( + ledger, + Ins::SIGN_TX, + P1SignTx::InputAdditionalInfo.into(), + &ledger_encode(SignTxReq::InputAdditionalInfo(info)), + ) + .await?; + } + + // the response from the last output will have the first signature returned + let mut resp = vec![]; + for o in outputs { + resp = send_chunked( + ledger, + Ins::SIGN_TX, + P1SignTx::Output.into(), + &ledger_encode(SignTxReq::Output(o)), + ) + .await?; + } + + let mut signatures: BTreeMap<_, Vec<_>> = BTreeMap::new(); + + let next_sig = ledger_encode(SignTxReq::NextSignature); + let mut msg_buf = vec![APDU_CLASS, Ins::SIGN_TX, P1SignTx::NextSignature.into(), P2_DONE]; + msg_buf.push(next_sig.len() as u8); + msg_buf.extend(next_sig); + loop { + let SignatureResult { + sig, + input_idx, + has_more_signatures, + } = decode_signature_response(&resp)?; + + signatures.entry(input_idx).or_default().push(sig); + + if !has_more_signatures { + break; + } + + resp = exchange_message(ledger, &msg_buf).await?; + } + + Ok(signatures) +} + +fn decode_signature_response(resp: &[u8]) -> Result { + let input_idx = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize; + let has_more_signatures = *resp.last().ok_or(LedgerError::InvalidResponse)? == P2_SIGN_MORE; + + let sig: LedgerSignature = + ledger_decode_all(&resp[..resp.len() - 1][1..]).ok_or(LedgerError::InvalidResponse)?; + + Ok(SignatureResult { + sig, + input_idx, + has_more_signatures, + }) +} + +pub fn to_ledger_tx_output(value: &chain::TxOutput) -> LTxOutput { + ledger_decode_all(value.encode().as_slice()).expect("ok") +} + +pub fn to_ledger_tx_input(value: &chain::TxInput) -> LTxInput { + ledger_decode_all(value.encode().as_slice()).expect("ok") +} + +pub fn to_ledger_amount(value: &primitives::Amount) -> LAmount { + LAmount::from_atoms(value.into_atoms()) +} + +pub fn to_ledger_output_value(value: &chain::output_value::OutputValue) -> LOutputValue { + match value { + chain::output_value::OutputValue::Coin(amount) => { + LOutputValue::Coin(to_ledger_amount(amount)) + } + chain::output_value::OutputValue::TokenV0(_) => panic!("unsupported V0"), + chain::output_value::OutputValue::TokenV1(token_id, amount) => { + LOutputValue::TokenV1(LH256(token_id.to_hash().into()), to_ledger_amount(amount)) + } + } +} diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs new file mode 100644 index 0000000000..06e8b59457 --- /dev/null +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -0,0 +1,1230 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod ledger_messages; + +use std::{collections::BTreeMap, sync::Arc}; + +use crate::{ + key_chain::{make_account_path, AccountKeyChainImplHardware, AccountKeyChains, FoundPubKey}, + signer::{ + ledger_signer::ledger_messages::{ + check_current_app, get_app_name, get_extended_public_key, sign_challenge, sign_tx, + to_ledger_amount, to_ledger_output_value, to_ledger_tx_input, to_ledger_tx_output, + }, + utils::{is_htlc_utxo, produce_uniparty_signature_for_input}, + Signer, SignerError, SignerProvider, SignerResult, + }, + Account, WalletResult, +}; +use common::{ + chain::{ + config::ChainType, + signature::{ + inputsig::{ + arbitrary_message::ArbitraryMessageSignature, + authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, + authorize_pubkey_spend::AuthorizedPublicKeySpend, + authorize_pubkeyhash_spend::AuthorizedPublicKeyHashSpend, + classical_multisig::{ + authorize_classical_multisig::{ + sign_classical_multisig_spending, AuthorizedClassicalMultisigSpend, + ClassicalMultisigCompletionStatus, + }, + multisig_partial_signature::{self, PartiallySignedMultisigChallenge}, + }, + standard_signature::StandardInputSignature, + InputWitness, + }, + sighash::{sighashtype::SigHashType, signature_hash}, + DestinationSigError, + }, + AccountCommand, ChainConfig, Destination, OrderAccountCommand, SignedTransactionIntent, + Transaction, TxInput, TxOutput, + }, + primitives::{BlockHeight, Idable, H256}, +}; +use crypto::key::{ + extended::ExtendedPublicKey, + hdkd::{derivable::Derivable, u31::U31}, + signature::SignatureKind, + PrivateKey, SigAuxDataProvider, Signature, SignatureError, +}; +use serialization::Encode; +use utils::ensure; +use wallet_storage::{ + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteUnlocked, +}; +use wallet_types::{ + account_info::DEFAULT_ACCOUNT_INDEX, + hw_data::{HardwareWalletFullInfo, LedgerData, LedgerFullInfo}, + partially_signed_transaction::{PartiallySignedTransaction, TokensAdditionalInfo}, + signature_status::SignatureStatus, + AccountId, +}; + +use async_trait::async_trait; +use itertools::{izip, Itertools}; +use ledger_lib::{Exchange, Filters, LedgerHandle, LedgerProvider, Transport}; +use mintlayer_ledger_messages::{ + AddrType, Bip32Path as LedgerBip32Path, CoinType, InputAdditionalInfoReq, + InputAddressPath as LedgerInputAddressPath, Signature as LedgerSignature, TxInputReq, + TxOutputReq, +}; +use randomness::make_true_rng; +use tokio::sync::Mutex; + +/// Signer errors +#[derive(thiserror::Error, Debug, Eq, PartialEq)] +pub enum LedgerError { + #[error("No connected Ledger device found")] + NoDeviceFound, + #[error("Connected to an unknown Ledger device model")] + UnknownModel, + #[error("A different app is currently open on your Ledger device: \"{0}\". Please close it and open the Mintlayer app instead.")] + DifferentActiveApp(String), + #[error("Received an invalid response from the Ledger device")] + InvalidResponse, + #[error("Received an error response from the Ledger device: {0}")] + ErrorResponse(u16), + #[error("Device error: {0}")] + DeviceError(String), + #[error("Missing hardware wallet data in database")] + MissingHardwareWalletData, + #[error("Derivation path is too long to send to Ledger")] + PathToLong, + #[error("Invalid public key returned from Ledger")] + InvalidKey, + #[error("The file being loaded is a software wallet and does not correspond to the connected hardware wallet")] + WalletFileIsSoftwareWallet, + #[error("Public keys mismatch - wrong device or passphrase")] + HardwareWalletDifferentMnemonicOrPassphrase, + #[error("A multisig signature was returned for a single address from Device")] + MultisigSignatureReturned, + #[error("Multiple signatures returned for a single address from Device")] + MultipleSignaturesReturned, + #[error("Missing multisig index for signature returned from Device")] + MissingMultisigIndexForSignature, + #[error("Signature error: {0}")] + SignatureError(#[from] SignatureError), +} + +struct StandaloneInput { + multisig_idx: Option, + private_key: PrivateKey, +} + +type StandaloneInputs = BTreeMap>; + +#[async_trait] +pub trait LedgerFinder { + type Ledger; + + async fn find_ledger_device_from_db( + &self, + db_tx: &mut T, + chain_config: Arc, + ) -> SignerResult<(Self::Ledger, LedgerData)>; +} + +pub struct LedgerSigner { + chain_config: Arc, + client: Arc>, + sig_aux_data_provider: std::sync::Mutex>, + provider: P, +} + +impl LedgerSigner +where + L: Exchange + Send, + P: LedgerFinder, +{ + pub fn new(chain_config: Arc, client: Arc>, provider: P) -> Self { + Self::new_with_sig_aux_data_provider( + chain_config, + client, + Box::new(make_true_rng()), + provider, + ) + } + + pub fn new_with_sig_aux_data_provider( + chain_config: Arc, + client: Arc>, + sig_aux_data_provider: Box, + provider: P, + ) -> Self { + Self { + chain_config, + client, + sig_aux_data_provider: std::sync::Mutex::new(sig_aux_data_provider), + provider, + } + } + + /// Calls initialize on the device with the current session_id. + /// + /// If the operation fails due to an USB error (which may indicate a lost connection to the device), + /// the function will attempt to reconnect to the Ledger device once before returning an error. + async fn check_session( + &mut self, + db_tx: &mut T, + key_chain: &impl AccountKeyChains, + ) -> SignerResult<()> { + let mut client = self.client.lock().await; + + loop { + match get_app_name(&mut *client).await { + Ok(_) => return Ok(()), + Err(ledger_lib::Error::Timeout) => { + continue; + } + // In case of a communication error try to reconnect, and try again + Err( + ledger_lib::Error::Hid(_) + | ledger_lib::Error::Tcp(_) + | ledger_lib::Error::Ble(_), + ) => { + let (mut new_client, _data) = self + .provider + .find_ledger_device_from_db(db_tx, self.chain_config.clone()) + .await?; + + check_public_keys_against_key_chain( + db_tx, + &mut new_client, + key_chain, + &self.chain_config, + ) + .await?; + + *client = new_client; + return Ok(()); + } + Err(err) => { + return Err(SignerError::LedgerError(LedgerError::DeviceError( + err.to_string(), + ))) + } + } + } + } + + /// Attempts to perform an operation on the Ledger client. + /// + /// If the operation fails due to an USB error (which may indicate a lost connection to the device), + /// the function will attempt to reconnect to the Ledger device once before returning an error. + async fn perform_ledger_operation( + &mut self, + operation: F, + db_tx: &mut T, + key_chain: &impl AccountKeyChains, + ) -> SignerResult + where + F: AsyncFnOnce(&mut L) -> Result, + { + self.check_session(db_tx, key_chain).await?; + + let mut client = self.client.lock().await; + operation(&mut client) + .await + .map_err(|e| LedgerError::DeviceError(e.to_string()).into()) + } + + #[allow(clippy::too_many_arguments)] + fn make_signature<'a, 'b, MakeWitnessFn, StandaloneSignerFn>( + &self, + signatures: &[LedgerSignature], + standalone_inputs: &'a [StandaloneInput], + destination: &'b Destination, + sighash_type: SigHashType, + sighash: H256, + key_chain: &impl AccountKeyChains, + make_witness: MakeWitnessFn, + sign_with_standalone_private_key: StandaloneSignerFn, + ) -> SignerResult<(Option, SignatureStatus)> + where + MakeWitnessFn: Fn(StandardInputSignature) -> InputWitness, + StandaloneSignerFn: Fn(&'a StandaloneInput, &'b Destination) -> SignerResult, + { + match destination { + Destination::AnyoneCanSpend => Ok(( + Some(InputWitness::NoSignature(None)), + SignatureStatus::FullySigned, + )), + Destination::PublicKeyHash(_) => { + if let Some(signature) = single_signature(signatures)? { + let pk = key_chain + .find_public_key(destination) + .ok_or(SignerError::DestinationNotFromThisWallet)? + .into_public_key(); + let sig = Signature::from_raw_data( + signature.signature, + SignatureKind::Secp256k1Schnorr, + ) + .map_err(LedgerError::SignatureError)?; + let sig = AuthorizedPublicKeyHashSpend::new(pk, sig); + StandardInputSignature::new(sighash_type, sig.encode()).verify_signature( + &self.chain_config, + destination, + &sighash, + )?; + let sig = make_witness(StandardInputSignature::new(sighash_type, sig.encode())); + + Ok((Some(sig), SignatureStatus::FullySigned)) + } else { + let standalone = match standalone_inputs { + [] => return Ok((None, SignatureStatus::NotSigned)), + [standalone] => standalone, + _ => return Err(LedgerError::MultisigSignatureReturned.into()), + }; + + let sig = sign_with_standalone_private_key(standalone, destination)?; + Ok((Some(sig), SignatureStatus::FullySigned)) + } + } + Destination::PublicKey(_) => { + if let Some(signature) = single_signature(signatures)? { + let sig = Signature::from_raw_data( + signature.signature, + SignatureKind::Secp256k1Schnorr, + ) + .map_err(LedgerError::SignatureError)?; + let sig = AuthorizedPublicKeySpend::new(sig); + StandardInputSignature::new(sighash_type, sig.encode()).verify_signature( + &self.chain_config, + destination, + &sighash, + )?; + let sig = make_witness(StandardInputSignature::new(sighash_type, sig.encode())); + + Ok((Some(sig), SignatureStatus::FullySigned)) + } else { + let standalone = match standalone_inputs { + [] => return Ok((None, SignatureStatus::NotSigned)), + [standalone] => standalone, + _ => return Err(LedgerError::MultisigSignatureReturned.into()), + }; + + let sig = sign_with_standalone_private_key(standalone, destination)?; + Ok((Some(sig), SignatureStatus::FullySigned)) + } + } + Destination::ClassicMultisig(_) => { + if let Some(challenge) = key_chain.find_multisig_challenge(destination) { + let (current_signatures, status) = self.update_and_check_multisig( + signatures, + AuthorizedClassicalMultisigSpend::new_empty(challenge.clone()), + sighash, + )?; + + let (current_signatures, status) = self.sign_with_standalone_private_keys( + current_signatures, + standalone_inputs, + status, + sighash, + )?; + + let sig = make_witness(StandardInputSignature::new( + sighash_type, + current_signatures.encode(), + )); + return Ok((Some(sig), status)); + } + + Ok((None, SignatureStatus::NotSigned)) + } + Destination::ScriptHash(_) => Ok((None, SignatureStatus::NotSigned)), + } + } + + fn to_ledger_output_msgs(&self, ptx: &PartiallySignedTransaction) -> Vec { + ptx.tx() + .outputs() + .iter() + .map(|out| TxOutputReq { + out: to_ledger_tx_output(out), + }) + .collect() + } + + fn check_multisig_signature_status( + &self, + sighash: H256, + current_signatures: &AuthorizedClassicalMultisigSpend, + ) -> Result { + let msg = sighash.encode(); + let verifier = PartiallySignedMultisigChallenge::from_partial( + &self.chain_config, + &msg, + current_signatures, + )?; + let status = match verifier.verify_signatures(&self.chain_config)? { + multisig_partial_signature::SigsVerifyResult::CompleteAndValid => { + SignatureStatus::FullySigned + } + multisig_partial_signature::SigsVerifyResult::Incomplete => { + let challenge = current_signatures.challenge(); + SignatureStatus::PartialMultisig { + required_signatures: challenge.min_required_signatures(), + num_signatures: current_signatures.signatures().len() as u8, + } + } + multisig_partial_signature::SigsVerifyResult::Invalid => { + SignatureStatus::InvalidSignature + } + }; + Ok(status) + } + + fn update_and_check_multisig( + &self, + signatures: &[LedgerSignature], + mut current_signatures: AuthorizedClassicalMultisigSpend, + sighash: H256, + ) -> SignerResult<(AuthorizedClassicalMultisigSpend, SignatureStatus)> { + for sig in signatures { + let idx = sig.multisig_idx.ok_or(LedgerError::MissingMultisigIndexForSignature)?; + let sig = Signature::from_raw_data(sig.signature, SignatureKind::Secp256k1Schnorr) + .map_err(LedgerError::SignatureError)?; + current_signatures.add_signature(idx as u8, sig); + } + + let status = self.check_multisig_signature_status(sighash, ¤t_signatures)?; + + Ok((current_signatures, status)) + } + + fn sign_with_standalone_private_keys( + &self, + current_signatures: AuthorizedClassicalMultisigSpend, + standalone_inputs: &[StandaloneInput], + new_status: SignatureStatus, + sighash: H256, + ) -> SignerResult<(AuthorizedClassicalMultisigSpend, SignatureStatus)> { + let challenge = current_signatures.challenge().clone(); + + standalone_inputs.iter().try_fold( + (current_signatures, new_status), + |(mut current_signatures, mut status), inp| -> SignerResult<_> { + if status == SignatureStatus::FullySigned { + return Ok((current_signatures, status)); + } + + let key_index = + inp.multisig_idx.ok_or(LedgerError::MissingMultisigIndexForSignature)?; + let res = sign_classical_multisig_spending( + &self.chain_config, + key_index as u8, + &inp.private_key, + &challenge, + &sighash, + current_signatures, + self.sig_aux_data_provider.lock().expect("poisoned mutex").as_mut(), + ) + .map_err(DestinationSigError::ClassicalMultisigSigningFailed)?; + + match res { + ClassicalMultisigCompletionStatus::Complete(signatures) => { + current_signatures = signatures; + status = SignatureStatus::FullySigned; + } + ClassicalMultisigCompletionStatus::Incomplete(signatures) => { + current_signatures = signatures; + status = SignatureStatus::PartialMultisig { + required_signatures: challenge.min_required_signatures(), + num_signatures: current_signatures.signatures().len() as u8, + }; + } + }; + + Ok((current_signatures, status)) + }, + ) + } +} + +#[async_trait] +impl Signer for LedgerSigner +where + L: Exchange + Send, + P: Send + Sync + LedgerFinder, +{ + async fn sign_tx( + &mut self, + ptx: PartiallySignedTransaction, + _tokens_additional_info: &TokensAdditionalInfo, + key_chain: &(impl AccountKeyChains + Sync), + mut db_tx: impl WalletStorageReadUnlocked + Send, + block_height: BlockHeight, + ) -> SignerResult<( + PartiallySignedTransaction, + Vec, + Vec, + )> { + let (inputs, standalone_inputs) = to_ledger_input_msgs(&ptx, key_chain, &db_tx)?; + let outputs = self.to_ledger_output_msgs(&ptx); + let input_additional_infos = to_ledger_input_additional_info_reqs(&ptx)?; + let coin_type = to_ledger_chain_type(&self.chain_config); + + let input_commitment_version = self + .chain_config + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .sighash_input_commitment_version(); + // input_commitments V0 is not implemented as it will likely not be needed by the time + // Ledger support is released + ensure!( + input_commitment_version == common::chain::SighashInputCommitmentVersion::V1, + LedgerError::MultisigSignatureReturned + ); + + let new_signatures = self + .perform_ledger_operation( + async move |client| { + sign_tx(client, coin_type, inputs, input_additional_infos, outputs).await + }, + &mut db_tx, + key_chain, + ) + .await?; + + let input_commitments = + ptx.make_sighash_input_commitments_at_height(&self.chain_config, block_height)?; + + let (witnesses, prev_statuses, new_statuses) = itertools::process_results( + izip!( + ptx.witnesses(), + ptx.input_utxos(), + ptx.destinations(), + ptx.htlc_secrets() + ) + .enumerate() + .map(|(input_index, (witness, input_utxo, destination, htlc_secret))| -> SignerResult<_> { + let is_htlc_input = input_utxo.as_ref().is_some_and(is_htlc_utxo); + let make_witness = |sig: StandardInputSignature| { + let sig = if is_htlc_input { + let sighash_type = sig.sighash_type(); + let spend = if let Some(htlc_secret) = htlc_secret { + AuthorizedHashedTimelockContractSpend::Spend( + htlc_secret.clone(), + sig.into_raw_signature(), + ) + } else { + AuthorizedHashedTimelockContractSpend::Refund(sig.into_raw_signature()) + }; + + let serialized_spend = spend.encode(); + StandardInputSignature::new(sighash_type, serialized_spend) + } + else { + sig + }; + + InputWitness::Standard(sig) + }; + + let sign_with_standalone_private_key = |standalone: &StandaloneInput, destination: &Destination| { + produce_uniparty_signature_for_input( + is_htlc_input, + htlc_secret.clone(), + &standalone.private_key, + destination.clone(), + ptx.tx(), + &input_commitments, + input_index, + self.sig_aux_data_provider.lock().expect("poisoned mutex").as_mut() + ) + }; + + let input_utxo = &ptx.input_utxos()[input_index]; + + match witness { + Some(w) => match w { + InputWitness::NoSignature(_) => Ok(( + Some(w.clone()), + SignatureStatus::FullySigned, + SignatureStatus::FullySigned, + )), + InputWitness::Standard(sig) => match destination { + Some(destination) => { + if tx_verifier::input_check::signature_only_check::verify_tx_signature( + &self.chain_config, + destination, + &ptx, + &input_commitments, + input_index, + input_utxo.clone() + ) + .is_ok() + { + Ok(( + Some(w.clone()), + SignatureStatus::FullySigned, + SignatureStatus::FullySigned, + )) + } else if let Destination::ClassicMultisig(_) = destination { + let sighash = signature_hash( + sig.sighash_type(), + ptx.tx(), + &input_commitments, + input_index, + )?; + + let current_signatures = if is_htlc_input { + let htlc_spend = AuthorizedHashedTimelockContractSpend::from_data(sig.raw_signature())?; + match htlc_spend { + AuthorizedHashedTimelockContractSpend::Spend(_, _) => { + return Err(SignerError::HtlcRefundExpectedForMultisig); + }, + AuthorizedHashedTimelockContractSpend::Refund(raw_sig) => { + AuthorizedClassicalMultisigSpend::from_data(&raw_sig)? + }, + } + } else { + AuthorizedClassicalMultisigSpend::from_data(sig.raw_signature())? + }; + + let previous_status = SignatureStatus::PartialMultisig { + required_signatures: current_signatures + .challenge() + .min_required_signatures(), + num_signatures: current_signatures.signatures().len() as u8, + }; + + let (current_signatures, new_status) = if let Some(signatures) = new_signatures.get(&input_index) + { + self.update_and_check_multisig(signatures, current_signatures, sighash)? + } else { + (current_signatures, previous_status) + }; + + let (current_signatures, new_status) = + self.sign_with_standalone_private_keys( + current_signatures, + standalone_inputs.get(&(input_index as u32)).map_or(&[], |x| x.as_slice()), + new_status, + sighash + )?; + + let sighash_type = SigHashType::all(); + let sig = make_witness(StandardInputSignature::new( + sighash_type, + current_signatures.encode(), + )); + + Ok((Some(sig), previous_status, new_status)) + } else { + Ok(( + None, + SignatureStatus::InvalidSignature, + SignatureStatus::NotSigned, + )) + } + } + None => Ok(( + Some(w.clone()), + SignatureStatus::UnknownSignature, + SignatureStatus::UnknownSignature, + )), + }, + }, + None => match (destination, new_signatures.get(&input_index)) { + (Some(destination), Some(sig)) => { + let sighash_type = SigHashType::all(); + let sighash = signature_hash(sighash_type, ptx.tx(), &input_commitments, input_index)?; + let (sig, status) = self.make_signature( + sig, + standalone_inputs.get(&(input_index as u32)).map_or(&[], |x| x.as_slice()), + destination, + sighash_type, + sighash, + key_chain, + make_witness, + sign_with_standalone_private_key, + )?; + + Ok((sig, SignatureStatus::NotSigned, status)) + } + (Some(Destination::AnyoneCanSpend), None) => { + Ok(( + Some(InputWitness::NoSignature(None)), + SignatureStatus::NotSigned, + SignatureStatus::FullySigned, + )) + } + (Some(destination), None) => { + let standalone = match standalone_inputs.get(&(input_index as u32)).map(|x| x.as_slice()) { + Some([standalone]) => standalone, + Some([]) | None => return Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)), + Some(_) => return Err(LedgerError::MultisigSignatureReturned.into()), + }; + + let sig = produce_uniparty_signature_for_input( + is_htlc_input, + htlc_secret.clone(), + &standalone.private_key, + destination.clone(), + ptx.tx(), + &input_commitments, + input_index, + self.sig_aux_data_provider.lock().expect("poisoned mutex").as_mut() + )?; + + + Ok((Some(sig), SignatureStatus::NotSigned, SignatureStatus::FullySigned)) + } + (None, _) => { + Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)) + } + }, + } + }), + |iter| iter.multiunzip() + )?; + + Ok((ptx.with_witnesses(witnesses)?, prev_statuses, new_statuses)) + } + + async fn sign_challenge( + &mut self, + message: &[u8], + destination: &Destination, + key_chain: &(impl AccountKeyChains + Sync), + mut db_tx: impl WalletStorageReadUnlocked + Send, + ) -> SignerResult { + let data = match key_chain.find_public_key(destination) { + Some(FoundPubKey::Hierarchy(xpub)) => { + let address_n = LedgerBip32Path( + xpub.get_derivation_path() + .as_slice() + .iter() + .map(|c| c.into_encoded_index()) + .collect(), + ); + + let addr_type = match destination { + Destination::PublicKey(_) => AddrType::PublicKey, + Destination::PublicKeyHash(_) => AddrType::PublicKeyHash, + Destination::AnyoneCanSpend => { + return Err(SignerError::SigningError( + DestinationSigError::AttemptedToProduceSignatureForAnyoneCanSpend, + )) + } + Destination::ClassicMultisig(_) => { + return Err(SignerError::SigningError( + DestinationSigError::AttemptedToProduceClassicalMultisigSignatureInUnipartySignatureCode, + )) + } + Destination::ScriptHash(_) => { + return Err(SignerError::SigningError( + DestinationSigError::Unsupported, + )) + } + }; + + let coin_type = to_ledger_chain_type(&self.chain_config); + let message = message.to_vec(); + let sig = self + .perform_ledger_operation( + async move |client| { + sign_challenge(client, coin_type, address_n, addr_type, &message).await + }, + &mut db_tx, + key_chain, + ) + .await?; + + let signature = Signature::from_raw_data(&sig, SignatureKind::Secp256k1Schnorr) + .map_err(LedgerError::SignatureError)?; + + match &destination { + Destination::PublicKey(_) => Ok(AuthorizedPublicKeySpend::new(signature).encode()), + Destination::PublicKeyHash(_) => { + Ok(AuthorizedPublicKeyHashSpend::new(xpub.into_public_key(), signature) + .encode()) + } + Destination::AnyoneCanSpend => { + Err(SignerError::SigningError( + DestinationSigError::AttemptedToProduceSignatureForAnyoneCanSpend, + )) + } + Destination::ClassicMultisig(_) => { + Err(SignerError::SigningError( + DestinationSigError::AttemptedToProduceClassicalMultisigSignatureInUnipartySignatureCode, + )) + } + Destination::ScriptHash(_) => { + Err(SignerError::SigningError( + DestinationSigError::Unsupported, + )) + } + }? + } + Some(FoundPubKey::Standalone(acc_public_key)) => { + let standalone_pk = db_tx + .get_account_standalone_private_key(&acc_public_key)? + .ok_or(SignerError::DestinationNotFromThisWallet)?; + + let sig = ArbitraryMessageSignature::produce_uniparty_signature( + &standalone_pk, + destination, + message, + self.sig_aux_data_provider.lock().expect("poisoned mutex").as_mut(), + )?; + return Ok(sig); + } + None => return Err(SignerError::DestinationNotFromThisWallet), + }; + + let sig = ArbitraryMessageSignature::from_data(data); + Ok(sig) + } + + async fn sign_transaction_intent( + &mut self, + transaction: &Transaction, + input_destinations: &[Destination], + intent: &str, + key_chain: &(impl AccountKeyChains + Sync), + mut db_tx: impl WalletStorageReadUnlocked + Send, + ) -> SignerResult { + let tx_id = transaction.get_id(); + let message_to_sign = SignedTransactionIntent::get_message_to_sign(intent, &tx_id); + + let mut signatures = Vec::with_capacity(input_destinations.len()); + for dest in input_destinations { + let dest = SignedTransactionIntent::normalize_destination(dest); + let sig = self + .sign_challenge(message_to_sign.as_bytes(), &dest, key_chain, &mut db_tx) + .await?; + + signatures.push(sig.into_raw()); + } + + SignedTransactionIntent::from_components( + message_to_sign, + signatures, + input_destinations, + &self.chain_config, + ) + .map_err(Into::into) + } +} + +fn to_ledger_input_msgs( + ptx: &PartiallySignedTransaction, + key_chain: &impl AccountKeyChains, + db_tx: &impl WalletStorageReadUnlocked, +) -> SignerResult<(Vec, StandaloneInputs)> { + let res: (Vec<_>, BTreeMap<_, _>) = itertools::process_results( + ptx.tx().inputs().iter().zip(ptx.destinations()).enumerate().map( + |(idx, (inp, dest))| -> SignerResult<_> { + let (address_paths, standalone_inputs) = + dest.as_ref().map_or(Ok((vec![], vec![])), |dest| { + destination_to_address_paths(key_chain, dest, db_tx) + })?; + + let input = TxInputReq { + inp: to_ledger_tx_input(inp), + addresses: address_paths, + }; + + Ok((input, (idx as u32, standalone_inputs))) + }, + ), + |iter| iter.unzip(), + )?; + + Ok(res) +} + +/// Find the derivation paths to the key in the destination, or multiple in the case of a multisig +fn destination_to_address_paths( + key_chain: &impl AccountKeyChains, + dest: &Destination, + db_tx: &impl WalletStorageReadUnlocked, +) -> SignerResult<(Vec, Vec)> { + destination_to_address_paths_impl(key_chain, dest, None, db_tx) +} + +fn destination_to_address_paths_impl( + key_chain: &impl AccountKeyChains, + dest: &Destination, + multisig_idx: Option, + db_tx: &impl WalletStorageReadUnlocked, +) -> SignerResult<(Vec, Vec)> { + match key_chain.find_public_key(dest) { + Some(FoundPubKey::Hierarchy(xpub)) => { + let address_n = xpub + .get_derivation_path() + .as_slice() + .iter() + .map(|c| c.into_encoded_index()) + .collect(); + Ok(( + vec![LedgerInputAddressPath { + path: LedgerBip32Path(address_n), + multisig_idx, + }], + vec![], + )) + } + Some(FoundPubKey::Standalone(acc_public_key)) => { + let standalone_input = + db_tx.get_account_standalone_private_key(&acc_public_key)?.map(|private_key| { + StandaloneInput { + multisig_idx, + private_key, + } + }); + Ok((vec![], standalone_input.into_iter().collect())) + } + None if multisig_idx.is_none() => { + if let Some(challenge) = key_chain.find_multisig_challenge(dest) { + let (x, y): (Vec<_>, Vec<_>) = itertools::process_results( + challenge.public_keys().iter().enumerate().map(|(idx, pk)| { + destination_to_address_paths_impl( + key_chain, + &Destination::PublicKey(pk.clone()), + Some(idx as u32), + db_tx, + ) + }), + |iter| iter.unzip(), + )?; + + Ok(( + x.into_iter().flatten().collect(), + y.into_iter().flatten().collect(), + )) + } else { + Ok((vec![], vec![])) + } + } + None => Ok((vec![], vec![])), + } +} + +fn to_ledger_input_additional_info_reqs( + ptx: &PartiallySignedTransaction, +) -> SignerResult> { + ptx.input_utxos() + .iter() + .zip(ptx.tx().inputs()) + .map(|(utxo, inp)| { + let additional_info = match inp { + TxInput::Utxo(_) => { + let utxo = utxo.as_ref().ok_or(SignerError::MissingUtxo)?; + match utxo { + TxOutput::ProduceBlockFromStake(_, pool_id) => { + let pool_info = ptx + .additional_info() + .get_pool_info(pool_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + InputAdditionalInfoReq::PoolInfo { + utxo: to_ledger_tx_output(utxo), + staker_balance: to_ledger_amount(&pool_info.staker_balance), + } + } + _ => InputAdditionalInfoReq::Utxo { + utxo: to_ledger_tx_output(utxo), + }, + } + } + TxInput::Account(_) => InputAdditionalInfoReq::None, + TxInput::AccountCommand(_, cmd) => match cmd { + AccountCommand::MintTokens(_, _) + | AccountCommand::UnmintTokens(_) + | AccountCommand::LockTokenSupply(_) + | AccountCommand::FreezeToken(_, _) + | AccountCommand::UnfreezeToken(_) + | AccountCommand::ChangeTokenAuthority(_, _) + | AccountCommand::ChangeTokenMetadataUri(_, _) => InputAdditionalInfoReq::None, + AccountCommand::FillOrder(order_id, _, _) => { + let order_info = ptx + .additional_info() + .get_order_info(order_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + InputAdditionalInfoReq::OrderInfo { + initially_asked: to_ledger_output_value(&order_info.initially_asked), + initially_given: to_ledger_output_value(&order_info.initially_given), + ask_balance: to_ledger_amount(&order_info.ask_balance), + give_balance: to_ledger_amount(&order_info.give_balance), + } + } + AccountCommand::ConcludeOrder(order_id) => { + let order_info = ptx + .additional_info() + .get_order_info(order_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + InputAdditionalInfoReq::OrderInfo { + initially_asked: to_ledger_output_value(&order_info.initially_asked), + initially_given: to_ledger_output_value(&order_info.initially_given), + ask_balance: to_ledger_amount(&order_info.ask_balance), + give_balance: to_ledger_amount(&order_info.give_balance), + } + } + }, + | TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _) => { + let order_info = ptx + .additional_info() + .get_order_info(order_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + InputAdditionalInfoReq::OrderInfo { + initially_asked: to_ledger_output_value(&order_info.initially_asked), + initially_given: to_ledger_output_value(&order_info.initially_given), + ask_balance: to_ledger_amount(&order_info.ask_balance), + give_balance: to_ledger_amount(&order_info.give_balance), + } + } + OrderAccountCommand::ConcludeOrder(order_id) => { + let order_info = ptx + .additional_info() + .get_order_info(order_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + InputAdditionalInfoReq::OrderInfo { + initially_asked: to_ledger_output_value(&order_info.initially_asked), + initially_given: to_ledger_output_value(&order_info.initially_given), + ask_balance: to_ledger_amount(&order_info.ask_balance), + give_balance: to_ledger_amount(&order_info.give_balance), + } + } + OrderAccountCommand::FreezeOrder(_) => InputAdditionalInfoReq::None, + }, + }; + Ok(additional_info) + }) + .collect() +} + +fn to_ledger_chain_type(chain_config: &ChainConfig) -> CoinType { + match chain_config.chain_type() { + ChainType::Mainnet => CoinType::Mainnet, + ChainType::Testnet => CoinType::Testnet, + ChainType::Signet => CoinType::Regtest, + ChainType::Regtest => CoinType::Signet, + } +} + +async fn find_ledger_device() -> SignerResult<(LedgerHandle, LedgerFullInfo)> { + let mut provider = LedgerProvider::init().await; + let mut devices = provider + .list(Filters::Any) + .await + .map_err(|err| LedgerError::DeviceError(err.to_string()))?; + + let device = devices.pop().ok_or(LedgerError::NoDeviceFound)?; + + let mut handle = provider + .connect(device) + .await + .map_err(|err| LedgerError::DeviceError(err.to_string()))?; + + let full_info = check_current_app(&mut handle).await?; + + Ok((handle, full_info)) +} + +/// Check that the public keys in the provided key chain are the same as the ones from the +/// connected hardware wallet +async fn check_public_keys_against_key_chain( + db_tx: &mut T, + client: &mut L, + key_chain: &impl AccountKeyChains, + chain_config: &ChainConfig, +) -> SignerResult<()> { + let expected_pk = + fetch_extended_pub_key(client, chain_config, key_chain.account_index()).await?; + + if key_chain.account_public_key() == &expected_pk { + return Ok(()); + } + + if let Ok(Some(_data)) = db_tx.get_hardware_wallet_data() { + // Data is empty there is nothing to compare + return Err(LedgerError::HardwareWalletDifferentMnemonicOrPassphrase.into()); + } + + Err(LedgerError::WalletFileIsSoftwareWallet.into()) +} + +/// Check that the public keys in the DB are the same as the ones from the connected hardware +/// wallet +async fn check_public_keys_against_db( + db_tx: &mut T, + client: &mut LedgerHandle, + chain_config: Arc, +) -> SignerResult<()> { + let (id, first_acc) = db_tx + .get_accounts_info()? + .iter() + .find_map(|(id, info)| { + (info.account_index() == DEFAULT_ACCOUNT_INDEX).then_some((id.clone(), info.clone())) + }) + .ok_or(SignerError::WalletNotInitialized)?; + + let loaded_acc = AccountKeyChainImplHardware::load_from_database( + chain_config.clone(), + db_tx, + &id, + &first_acc, + )?; + + check_public_keys_against_key_chain(db_tx, client, &loaded_acc, &chain_config).await +} + +async fn fetch_extended_pub_key( + client: &mut L, + chain_config: &ChainConfig, + account_index: U31, +) -> Result { + let derivation_path = make_account_path(chain_config, account_index); + let coin_type = to_ledger_chain_type(chain_config); + + get_extended_public_key(client, coin_type, derivation_path) + .await + .map_err(|e| LedgerError::DeviceError(e.to_string())) +} + +fn single_signature( + signatures: &[LedgerSignature], +) -> Result, LedgerError> { + match signatures { + [] => Ok(None), + [single] => { + ensure!( + single.multisig_idx.is_none(), + LedgerError::MultisigSignatureReturned + ); + Ok(Some(single)) + } + _ => Err(LedgerError::MultipleSignaturesReturned), + } +} + +#[derive(Clone, derive_more::Debug)] +pub struct LedgerSignerProvider { + #[debug(skip)] + client: Arc>, + info: LedgerFullInfo, +} + +#[async_trait] +impl LedgerFinder for LedgerSignerProvider { + type Ledger = LedgerHandle; + + async fn find_ledger_device_from_db( + &self, + db_tx: &mut T, + chain_config: Arc, + ) -> SignerResult<(Self::Ledger, LedgerData)> { + let (mut client, info) = find_ledger_device().await?; + + check_public_keys_against_db(db_tx, &mut client, chain_config).await?; + + Ok((client, info.into())) + } +} + +impl LedgerSignerProvider { + pub async fn new() -> SignerResult { + let (client, info) = find_ledger_device().await?; + + Ok(Self { + client: Arc::new(Mutex::new(client)), + info, + }) + } + + pub async fn load_from_database( + chain_config: Arc, + db_tx: &mut T, + ) -> WalletResult { + let (mut client, info) = find_ledger_device().await?; + + check_public_keys_against_db(db_tx, &mut client, chain_config).await?; + + Ok(Self { + client: Arc::new(Mutex::new(client)), + info, + }) + } + + async fn fetch_extended_pub_key( + &self, + chain_config: &Arc, + account_index: U31, + ) -> SignerResult { + fetch_extended_pub_key(&mut *self.client.lock().await, chain_config, account_index) + .await + .map_err(SignerError::LedgerError) + } +} + +#[async_trait] +impl SignerProvider for LedgerSignerProvider { + type S = LedgerSigner; + type K = AccountKeyChainImplHardware; + + fn provide(&mut self, chain_config: Arc, _account_index: U31) -> Self::S { + LedgerSigner::new(chain_config, self.client.clone(), self.clone()) + } + + async fn make_new_account( + &mut self, + chain_config: Arc, + account_index: U31, + name: Option, + db_tx: &mut T, + ) -> WalletResult> { + let account_pubkey = self.fetch_extended_pub_key(&chain_config, account_index).await?; + + let lookahead_size = db_tx.get_lookahead_size()?; + + let key_chain = AccountKeyChainImplHardware::new_from_hardware_key( + chain_config.clone(), + db_tx, + account_pubkey, + account_index, + lookahead_size, + )?; + + Account::new(chain_config, db_tx, key_chain, name) + } + + fn load_account_from_database( + &self, + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + id: &AccountId, + ) -> WalletResult> { + Account::load_from_database(chain_config, db_tx, id) + } + + fn get_hardware_wallet_info(&self) -> Option { + Some(HardwareWalletFullInfo::Ledger(self.info.clone())) + } +} + +#[cfg(feature = "enable-ledger-device-tests")] +#[cfg(test)] +mod tests; + +#[cfg(feature = "enable-ledger-device-tests")] +#[cfg(test)] +mod speculos; diff --git a/wallet/src/signer/ledger_signer/speculos/handle.rs b/wallet/src/signer/ledger_signer/speculos/handle.rs new file mode 100644 index 0000000000..e3b8a74d16 --- /dev/null +++ b/wallet/src/signer/ledger_signer/speculos/handle.rs @@ -0,0 +1,130 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Speculos runtime handle, provides out-of-band interaction with a simulator instance +//! via the +//! [HTTP API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/LedgerHQ/speculos/master/speculos/api/static/swagger/swagger.json) +//! to allow button pushes and screenshots when executing integration tests. +//! +//! + +use std::net::SocketAddr; + +use async_trait::async_trait; +use logging::log; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumIter}; + +/// Button enumeration +#[derive(Clone, Copy, PartialEq, Debug, Display, EnumIter)] +#[strum(serialize_all = "kebab-case")] +pub enum Button { + Left, + Right, + Both, +} + +/// Button actions +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Display, EnumIter)] +#[serde(rename_all = "kebab-case")] +pub enum Action { + Press, + Release, + PressAndRelease, +} + +/// Button action object for serialization and use with the HTTP API +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +struct ButtonAction { + action: Action, +} + +/// [Handle] trait for interacting with speculos +#[async_trait] +pub trait Handle { + /// Get speculos HTTP address + fn addr(&self) -> SocketAddr; + + /// Send a button action to the simulator + async fn button(&self, button: Button, action: Action) -> anyhow::Result<()> { + log::debug!("Sending button request: {}:{}", button, action); + + // Post action to HTTP API + let r = Client::new() + .post(format!("http://{}/button/{}", self.addr(), button)) + .json(&ButtonAction { action }) + .send() + .await?; + + log::debug!("Button request complete: {}", r.status()); + + Ok(()) + } +} + +/// Handle to a Speculos instance running under Podman +#[derive(Debug)] +pub struct PodmanHandle { + addr: SocketAddr, +} + +impl PodmanHandle { + pub fn new(addr: SocketAddr) -> Self { + Self { addr } + } +} + +#[async_trait] +impl Handle for PodmanHandle { + fn addr(&self) -> SocketAddr { + self.addr + } +} + +#[cfg(test)] +mod tests { + use super::*; + use strum::IntoEnumIterator; + + /// Check button string encoding + #[test] + fn button_encoding() { + for button in Button::iter() { + let expected = match button { + Button::Left => "left", + Button::Right => "right", + Button::Both => "both", + }; + assert_eq!(&button.to_string(), expected); + } + } + + /// Check button action encoding + #[test] + fn action_encoding() { + for action in Action::iter() { + let expected = match action { + Action::Press => r#"{"action":"press"}"#, + Action::Release => r#"{"action":"release"}"#, + Action::PressAndRelease => r#"{"action":"press-and-release"}"#, + }; + assert_eq!( + &serde_json::to_string(&ButtonAction { action }).unwrap(), + expected + ); + } + } +} diff --git a/wallet/src/signer/ledger_signer/speculos/mod.rs b/wallet/src/signer/ledger_signer/speculos/mod.rs new file mode 100644 index 0000000000..0d0085567a --- /dev/null +++ b/wallet/src/signer/ledger_signer/speculos/mod.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Rust wrapper for executing Speculos via podman, +//! provided to simplify CI/CD with ledger applications. + +mod handle; +pub use handle::*; diff --git a/wallet/src/signer/ledger_signer/tests.rs b/wallet/src/signer/ledger_signer/tests.rs new file mode 100644 index 0000000000..b773994b51 --- /dev/null +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -0,0 +1,414 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, + sync::Arc, + time::Duration, +}; + +use async_trait::async_trait; +use ledger_lib::{ + transport::{TcpDevice, TcpInfo, TcpTransport}, + Device, Transport, +}; +use mintlayer_ledger_messages::CoinType; +use randomness::make_true_rng; +use rstest::rstest; +use serialization::hex::HexEncode; +use test_utils::random::{make_seedable_rng, Seed}; +use tokio::{ + sync::{ + mpsc::{self, Sender}, + Mutex, + }, + time::sleep, +}; + +use crate::signer::{ + ledger_signer::{ + ledger_messages::{check_current_app, get_app_name, get_extended_public_key, ok_response}, + speculos::{Action, Button, Handle, PodmanHandle}, + LedgerError, LedgerFinder, LedgerSigner, + }, + tests::{ + generic_tests::{ + sign_message_test_params, test_sign_transaction_generic, + test_sign_transaction_intent_generic, MessageToSign, + }, + no_another_signer, + }, + SignerError, SignerResult, +}; +use common::chain::{config::create_mainnet, ChainConfig, SighashInputCommitmentVersion}; +use crypto::key::{ + hdkd::{derivation_path::DerivationPath, u31::U31}, + PredefinedSigAuxDataProvider, SigAuxDataProvider, +}; +use logging::log; +use wallet_storage::WalletStorageReadLocked; +use wallet_types::hw_data::LedgerData; + +#[derive(Debug)] +enum ControlMessage { + Finish, +} + +async fn auto_confirmer(mut control_msg_rx: mpsc::Receiver, handle: PodmanHandle) { + loop { + tokio::select! { + _ = sleep(Duration::from_millis(100)) => { + // As we don't know how many screens will be shown just go 1 right and try to confirm + handle.button(Button::Right, Action::PressAndRelease).await.unwrap(); + handle.button(Button::Both, Action::PressAndRelease).await.unwrap(); + handle.button(Button::Both, Action::PressAndRelease).await.unwrap(); + } + msg = control_msg_rx.recv() => { + match msg { + Some(ControlMessage::Finish) => { + eprintln!("Received finish signal."); + break; + } + None => { + eprintln!("Channel closed."); + break; + } + } + } + } + } + + println!("Auto-confirmer task finished."); +} + +struct DummyProvider; + +#[async_trait] +impl LedgerFinder for DummyProvider { + type Ledger = TcpDevice; + + async fn find_ledger_device_from_db( + &self, + _db_tx: &mut T, + _chain_config: Arc, + ) -> SignerResult<(Self::Ledger, LedgerData)> { + Err(SignerError::LedgerError(LedgerError::NoDeviceFound)) + } +} + +async fn setup( + deterministic_aux: bool, +) -> ( + tokio::task::JoinHandle<()>, + Sender, + impl Fn(Arc, U31) -> LedgerSigner, +) { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5001); + let handle = PodmanHandle::new(addr); + + let mut transport = TcpTransport::new().unwrap(); + let mut device = transport + .connect(TcpInfo { + addr: SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9999), + }) + .await + .unwrap(); + + let mut tries = 0; + loop { + match get_app_name(&mut device).await { + Ok(_) => break, + Err(_) => { + tries += 1; + if tries > 10 { + break; + } + sleep(Duration::from_millis(100)).await; + } + } + } + + let device = Arc::new(Mutex::new(device)); + + let (control_msg_tx, control_msg_rx) = mpsc::channel(1); + let auto_clicker = tokio::spawn(auto_confirmer(control_msg_rx, handle)); + + (auto_clicker, control_msg_tx, move |chain_config, _| { + let aux_provider: Box = if deterministic_aux { + Box::new(PredefinedSigAuxDataProvider) + } else { + Box::new(make_true_rng()) + }; + + LedgerSigner::new_with_sig_aux_data_provider( + chain_config, + device.clone(), + aux_provider, + DummyProvider {}, + ) + }) +} + +#[rstest] +#[trace] +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_app_name() { + let mut transport = TcpTransport::new().unwrap(); + let mut device = transport + .connect(TcpInfo { + addr: SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9999), + }) + .await + .unwrap(); + + let mut tries = 0; + loop { + match get_app_name(&mut device).await { + Ok(_) => break, + Err(_) => { + tries += 1; + if tries > 10 { + break; + } + sleep(Duration::from_millis(100)).await; + } + } + } + + let resp = get_app_name(&mut device).await.unwrap(); + let resp = ok_response(resp).unwrap(); + let name = String::from_utf8(resp).unwrap(); + + assert_eq!(name, "mintlayer-app"); + + let info = device.app_info(Duration::from_millis(100)).await.unwrap(); + eprintln!("info: {info:?}"); + + let info = check_current_app(&mut device).await.unwrap(); + eprintln!("info: {info:?}"); + + assert_eq!(info.app_version.to_string(), "0.1.0"); +} + +#[rstest] +#[trace] +#[serial_test::serial] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_account_extended_public_key() { + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(false).await; + + let signer = make_ledger_signer(Arc::new(create_mainnet()), U31::ZERO); + + let derivation_path = DerivationPath::from_str("m/44h/19788h/0h").unwrap(); + let (public_key, chain_code) = get_extended_public_key( + &mut *signer.client.lock().await, + CoinType::Mainnet, + derivation_path, + ) + .await + .unwrap() + .into_public_key_and_chain_code(); + + let expected_pk = "029103888be8638b733d54eba6c5a96ae12583881dfab4b9585366548b54e3f8fd"; + assert_eq!( + expected_pk, + public_key.hex_encode().strip_prefix("00").unwrap() + ); + + let expected_chain_code = "0b71f99e82c97a4c8f75d8d215e7260bcf9e964d437ec252af26877adf7e8683"; + assert_eq!(expected_chain_code, chain_code.hex_encode()); + + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest_reuse::apply(sign_message_test_params)] +#[rstest] +#[trace] +#[serial_test::serial] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_sign_message(#[case] seed: Seed, message_to_sign: MessageToSign) { + use crate::signer::tests::generic_tests::test_sign_message_generic; + + log::debug!("test_sign_transaction_intent, seed = {seed:?}"); + + let mut rng = make_seedable_rng(seed); + + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(false).await; + + test_sign_message_generic( + &mut rng, + message_to_sign, + make_ledger_signer, + no_another_signer(), + ) + .await; + + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[serial_test::serial] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_sign_transaction_intent(#[case] seed: Seed) { + log::debug!("test_sign_transaction_intent, seed = {seed:?}"); + + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(false).await; + + let mut rng = make_seedable_rng(seed); + + test_sign_transaction_intent_generic(&mut rng, make_ledger_signer, no_another_signer()).await; + + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[serial_test::serial] +#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_sign_transaction( + #[case] seed: Seed, + #[case] input_commitments_version: SighashInputCommitmentVersion, +) { + log::debug!("test_sign_transaction, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); + + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(false).await; + + let mut rng = make_seedable_rng(seed); + + test_sign_transaction_generic( + &mut rng, + input_commitments_version, + make_ledger_signer, + no_another_signer(), + ) + .await; + + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[serial_test::serial] +#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_fixed_signatures2( + #[case] seed: Seed, + #[case] input_commitments_version: SighashInputCommitmentVersion, +) { + use crate::signer::tests::generic_fixed_signature_tests::test_fixed_signatures_generic2; + + log::debug!("test_fixed_signatures2, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); + + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; + + let mut rng = make_seedable_rng(seed); + + test_fixed_signatures_generic2(&mut rng, input_commitments_version, make_ledger_signer).await; + + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[serial_test::serial] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_sign_message_sig_consistency(#[case] seed: Seed) { + use crate::signer::tests::{ + generic_tests::test_sign_message_generic, make_deterministic_software_signer, + }; + + log::debug!("test_sign_message_sig_consistency, seed = {seed:?}"); + + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; + + let mut rng = make_seedable_rng(seed); + + test_sign_message_generic( + &mut rng, + MessageToSign::Random, + make_ledger_signer, + Some(make_deterministic_software_signer), + ) + .await; + + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[serial_test::serial] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_sign_transaction_intent_sig_consistency(#[case] seed: Seed) { + use crate::signer::tests::make_deterministic_software_signer; + + log::debug!("test_sign_transaction_intent_sig_consistency, seed = {seed:?}"); + + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; + + let mut rng = make_seedable_rng(seed); + + test_sign_transaction_intent_generic( + &mut rng, + make_ledger_signer, + Some(make_deterministic_software_signer), + ) + .await; + + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[serial_test::serial] +#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_sign_transaction_sig_consistency( + #[case] seed: Seed, + #[case] input_commitments_version: SighashInputCommitmentVersion, +) { + use crate::signer::tests::make_deterministic_software_signer; + + log::debug!("test_sign_transaction_sig_consistency, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); + + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; + + let mut rng = make_seedable_rng(seed); + + test_sign_transaction_generic( + &mut rng, + input_commitments_version, + make_ledger_signer, + Some(make_deterministic_software_signer), + ) + .await; + + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index c3375c3862..931ba2dfb4 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -48,6 +48,8 @@ use wallet_types::{ AccountId, }; +#[cfg(feature = "ledger")] +use crate::signer::ledger_signer::LedgerError; use crate::{ key_chain::{AccountKeyChains, KeyChainError}, Account, WalletResult, @@ -61,6 +63,9 @@ pub mod utils; #[cfg(feature = "trezor")] use self::trezor_signer::TrezorError; +#[cfg(feature = "ledger")] +pub mod ledger_signer; + /// Signer errors #[derive(thiserror::Error, Debug, Eq, PartialEq)] pub enum SignerError { @@ -87,6 +92,9 @@ pub enum SignerError { #[cfg(feature = "trezor")] #[error("Trezor error: {0}")] TrezorError(#[from] TrezorError), + #[cfg(feature = "ledger")] + #[error("Ledger error: {0}")] + LedgerError(#[from] LedgerError), #[error("Partially signed tx is missing input's destination")] MissingDestinationInTransaction, #[error("Partially signed tx is missing UTXO type input's UTXO")] @@ -107,6 +115,8 @@ pub enum SignerError { PartiallySignedTransactionError(#[from] PartiallySignedTransactionError), #[error("Duplicate UTXO input: {0:?}")] DuplicateUtxoInput(UtxoOutPoint), + #[error("Wallet not initialized")] + WalletNotInitialized, } type SignerResult = Result; @@ -150,18 +160,19 @@ pub trait Signer { ) -> SignerResult; } -pub trait SignerProvider { +#[async_trait] +pub trait SignerProvider: Send { type S: Signer + Send; type K: AccountKeyChains + Sync + Send; fn provide(&mut self, chain_config: Arc, account_index: U31) -> Self::S; - fn make_new_account( + async fn make_new_account( &mut self, chain_config: Arc, account_index: U31, name: Option, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut T, ) -> WalletResult>; fn load_account_from_database( diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index e8f58f05f4..63eb52b0ee 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -50,8 +50,7 @@ use crypto::key::{ }; use randomness::make_true_rng; use wallet_storage::{ - StoreTxRwUnlocked, WalletStorageReadLocked, WalletStorageReadUnlocked, - WalletStorageWriteUnlocked, + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteUnlocked, }; use wallet_types::{ hw_data::HardwareWalletFullInfo, @@ -453,9 +452,9 @@ pub struct SoftwareSignerProvider { } impl SoftwareSignerProvider { - pub fn new_from_mnemonic( + pub fn new_from_mnemonic( chain_config: Arc, - db_tx: &mut StoreTxRwUnlocked, + db_tx: &mut impl WalletStorageWriteUnlocked, mnemonic_str: &str, passphrase: Option<&str>, save_seed_phrase: StoreSeedPhrase, @@ -480,6 +479,7 @@ impl SoftwareSignerProvider { } } +#[async_trait] impl SignerProvider for SoftwareSignerProvider { type S = SoftwareSigner; type K = AccountKeyChainImplSoftware; @@ -488,12 +488,12 @@ impl SignerProvider for SoftwareSignerProvider { SoftwareSigner::new(chain_config, account_index) } - fn make_new_account( + async fn make_new_account( &mut self, chain_config: Arc, next_account_index: U31, name: Option, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut T, ) -> WalletResult> { let lookahead_size = db_tx.get_lookahead_size()?; let account_key_chain = self.master_key_chain.create_account_key_chain( diff --git a/wallet/src/signer/tests/generic_tests.rs b/wallet/src/signer/tests/generic_tests.rs index 12515d07c2..3e5531530b 100644 --- a/wallet/src/signer/tests/generic_tests.rs +++ b/wallet/src/signer/tests/generic_tests.rs @@ -85,6 +85,18 @@ pub enum MessageToSign { Predefined(Vec), } +#[rstest_reuse::template] +pub fn sign_message_test_params( + #[values( + MessageToSign::Random, + // Special case: an "overlong" utf-8 string (basically, the letter 'K' encoded with 2 bytes + // instead of 1). Trezor firmware used to have troubles with this. + MessageToSign::Predefined(vec![193, 139]) + )] + message_to_sign: MessageToSign, +) { +} + pub async fn test_sign_message_generic( rng: &mut (impl Rng + CryptoRng), message_to_sign: MessageToSign, diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index 4d2e241a4d..35834f8f10 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -149,8 +149,10 @@ pub enum TrezorError { MultipleSignaturesReturned, #[error("A multisig signature was returned for a single address from Device")] MultisigSignatureReturned, + #[error("The file being loaded is a ledger wallet and does not correspond to the connected hardware wallet")] + LedgerWalletDifferentFile, #[error("The file being loaded is a software wallet and does not correspond to the connected hardware wallet")] - HardwareWalletDifferentFile, + WalletFileIsSoftwareWallet, #[error( "Public keys mismatch - wrong device or passphrase.\n\ Last used device id: \"{file_device_id}\", connected device id: \"{connected_device_id}\".\n\ @@ -1588,19 +1590,14 @@ fn to_trezor_output_lock(lock: &OutputTimeLock) -> trezor_client::protos::Mintla lock_req } -#[derive(Clone)] +#[derive(Clone, derive_more::Debug)] pub struct TrezorSignerProvider { + #[debug(skip)] client: Arc>, info: TrezorFullInfo, session_id: Vec, } -impl std::fmt::Debug for TrezorSignerProvider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("TrezorSignerProvider") - } -} - impl TrezorSignerProvider { pub fn new(selected: Option) -> Result { let (client, info, session_id) = find_trezor_device(selected)?; @@ -1685,10 +1682,14 @@ fn check_public_keys_against_key_chain( .into()); } } + #[cfg(feature = "ledger")] + HardwareWalletData::Ledger(_) => { + return Err(TrezorError::LedgerWalletDifferentFile.into()); + } } } - Err(TrezorError::HardwareWalletDifferentFile)? + Err(TrezorError::WalletFileIsSoftwareWallet)? } fn fetch_extended_pub_key( @@ -1851,6 +1852,7 @@ fn get_checked_firmware_version(client: &mut Trezor) -> Result( &mut self, chain_config: Arc, account_index: U31, name: Option, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut T, ) -> WalletResult> { let account_pubkey = self.fetch_extended_pub_key(&chain_config, account_index)?; diff --git a/wallet/src/signer/trezor_signer/tests.rs b/wallet/src/signer/trezor_signer/tests.rs index 402f72b753..62c6f602ac 100644 --- a/wallet/src/signer/trezor_signer/tests.rs +++ b/wallet/src/signer/trezor_signer/tests.rs @@ -30,7 +30,7 @@ use crate::signer::{ test_fixed_signatures_generic_htlc_refunding, }, generic_tests::{ - test_sign_message_generic, test_sign_transaction_generic, + sign_message_test_params, test_sign_message_generic, test_sign_transaction_generic, test_sign_transaction_intent_generic, MessageToSign, }, make_deterministic_software_signer, no_another_signer, @@ -72,21 +72,13 @@ pub fn make_deterministic_trezor_signer( // the rng seed that caused the panic won't be printed. So, in these tests we log the seed // manually at the start of each test. +#[rstest_reuse::apply(sign_message_test_params)] #[rstest] #[trace] #[serial] #[case(Seed::from_entropy())] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_sign_message( - #[case] seed: Seed, - #[values( - MessageToSign::Random, - // Special case: an "overlong" utf-8 string (basically, the letter 'K' encoded with 2 bytes - // instead of 1). The firmware used to have troubles with this. - MessageToSign::Predefined(vec![193, 139]) - )] - message_to_sign: MessageToSign, -) { +async fn test_sign_message(#[case] seed: Seed, message_to_sign: MessageToSign) { log::debug!("test_sign_message, seed = {seed:?}"); let _join_guard = maybe_spawn_auto_confirmer(); diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index cf95222bec..d0e595674f 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -269,8 +269,10 @@ pub enum WalletError { "A VRF public key must be specified when creating a staking pool using a hardware wallet" )] VrfKeyMustBeProvided, - #[error("Cannot change a Trezor wallet type")] - CannotChangeTrezorWalletType, + #[error("Cannot change a {from} wallet type to {to}")] + CannotChangeWalletType { from: WalletType, to: WalletType }, + #[error("Cannot change a Ledger wallet type")] + CannotChangeLedgerWalletType, #[error("Missing additional data for Pool {0}")] MissingPoolAdditionalData(PoolId), #[error("Missing additional data for Token {0}")] @@ -357,14 +359,14 @@ where B: storage::BackendWithSendableTransactions + 'static, P: SignerProvider, { - pub fn create_new_wallet) -> WalletResult

>( + pub async fn create_new_wallet) -> WalletResult

>( chain_config: Arc, db: Store, best_block: (BlockHeight, Id), wallet_type: WalletType, signer_provider: F, ) -> WalletResult> { - let mut wallet = Self::new_wallet(chain_config, db, wallet_type, signer_provider)?; + let mut wallet = Self::new_wallet(chain_config, db, wallet_type, signer_provider).await?; if let WalletCreation::Wallet(ref mut w) = wallet { w.set_best_block(best_block.0, best_block.1)?; @@ -373,16 +375,16 @@ where Ok(wallet) } - pub fn recover_wallet) -> WalletResult

>( + pub async fn recover_wallet) -> WalletResult

>( chain_config: Arc, db: Store, wallet_type: WalletType, signer_provider: F, ) -> WalletResult> { - Self::new_wallet(chain_config, db, wallet_type, signer_provider) + Self::new_wallet(chain_config, db, wallet_type, signer_provider).await } - fn new_wallet) -> WalletResult

>( + async fn new_wallet) -> WalletResult

>( chain_config: Arc, mut db: Store, wallet_type: WalletType, @@ -394,7 +396,7 @@ where db_tx.set_chain_info(&ChainInfo::new(chain_config.as_ref()))?; db_tx.set_lookahead_size(LOOKAHEAD_SIZE)?; db_tx.set_wallet_type(wallet_type)?; - let mut signer_provider = match signer_provider(&mut db_tx) { + let mut signer_provider = match signer_provider(&mut db_tx).await { Ok(x) => x, #[cfg(feature = "trezor")] Err(WalletError::SignerError(SignerError::TrezorError( @@ -413,7 +415,8 @@ where &mut db_tx, None, &mut signer_provider, - )?; + ) + .await?; let next_unused_account = Wallet::::create_next_unused_account( U31::ONE, @@ -421,7 +424,8 @@ where &mut db_tx, None, &mut signer_provider, - )?; + ) + .await?; db_tx.commit()?; @@ -441,7 +445,7 @@ where /// Migrate the wallet DB from version 1 to version 2 /// * save the chain info in the DB based on the chain type specified by the user /// * reset transactions - fn migration_v2( + async fn migration_v2( db: &mut Store, chain_config: Arc, signer_provider: &mut P, @@ -455,7 +459,7 @@ where Self::reset_wallet_transactions(chain_config.clone(), &mut db_tx)?; // Create the next unused account - Self::migrate_next_unused_account(chain_config, &mut db_tx, signer_provider)?; + Self::migrate_next_unused_account(chain_config, &mut db_tx, signer_provider).await?; db_tx.set_storage_version(WALLET_VERSION_V2)?; db_tx.commit()?; @@ -594,9 +598,9 @@ where } /// Check the wallet DB version and perform any migrations needed - fn check_and_migrate_db< + async fn check_and_migrate_db< F: Fn(u32) -> Result<(), WalletError>, - F2: FnOnce(&StoreTxRo) -> WalletResult

, + F2: AsyncFnOnce(StoreTxRo) -> WalletResult

, >( db: &mut Store, chain_config: Arc, @@ -609,7 +613,7 @@ where version != WALLET_VERSION_UNINITIALIZED, WalletError::WalletNotInitialized ); - let mut signer_provider = signer_provider(&db.transaction_ro()?)?; + let mut signer_provider = signer_provider(db.transaction_ro()?).await?; loop { let version = db.transaction_ro()?.get_storage_version()?; @@ -621,7 +625,7 @@ where } WALLET_VERSION_V1 => { pre_migration(WALLET_VERSION_V1)?; - Self::migration_v2(db, chain_config.clone(), &mut signer_provider)?; + Self::migration_v2(db, chain_config.clone(), &mut signer_provider).await?; } WALLET_VERSION_V2 => { pre_migration(WALLET_VERSION_V2)?; @@ -705,12 +709,32 @@ where #[cfg(feature = "trezor")] (WalletType::Cold | WalletType::Hot, WalletType::Trezor) | (WalletType::Trezor, WalletType::Hot | WalletType::Cold) => { - return Err(WalletError::CannotChangeTrezorWalletType) + return Err(WalletError::CannotChangeWalletType { + from: current_wallet_type, + to: wallet_type, + }) + } + #[cfg(all(feature = "trezor", feature = "ledger"))] + (WalletType::Ledger, WalletType::Trezor) | (WalletType::Trezor, WalletType::Ledger) => { + return Err(WalletError::CannotChangeWalletType { + from: current_wallet_type, + to: wallet_type, + }) + } + #[cfg(feature = "ledger")] + (WalletType::Cold | WalletType::Hot, WalletType::Ledger) + | (WalletType::Ledger, WalletType::Hot | WalletType::Cold) => { + return Err(WalletError::CannotChangeWalletType { + from: current_wallet_type, + to: wallet_type, + }) } (WalletType::Cold, WalletType::Cold) => {} (WalletType::Hot, WalletType::Hot) => {} #[cfg(feature = "trezor")] (WalletType::Trezor, WalletType::Trezor) => {} + #[cfg(feature = "trezor")] + (WalletType::Ledger, WalletType::Ledger) => {} } Ok(()) } @@ -786,11 +810,11 @@ where .collect() } - fn migrate_next_unused_account( + async fn migrate_next_unused_account( chain_config: Arc, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut T, signer_provider: &mut P, - ) -> Result<(), WalletError> { + ) -> WalletResult<()> { let accounts_info = db_tx.get_accounts_info()?; let mut accounts: BTreeMap> = accounts_info .keys() @@ -802,23 +826,27 @@ where .map(|account| (account.account_index(), account)) .collect(); let last_account = accounts.pop_last().ok_or(WalletError::WalletNotInitialized)?; + let next_account_index = last_account .0 .plus_one() .map_err(|_| WalletError::AbsoluteMaxNumAccountsExceeded(last_account.0))?; + Wallet::::create_next_unused_account( next_account_index, chain_config.clone(), db_tx, None, signer_provider, - )?; + ) + .await?; + Ok(()) } - pub fn load_wallet< + pub async fn load_wallet< F: Fn(u32) -> WalletResult<()>, - F2: FnOnce(&StoreTxRo) -> WalletResult

, + F2: AsyncFnOnce(StoreTxRo) -> WalletResult

, >( chain_config: Arc, mut db: Store, @@ -838,7 +866,9 @@ where pre_migration, controller_mode, signer_provider, - ) { + ) + .await + { Ok(x) => x, #[cfg(feature = "trezor")] Err(WalletError::SignerError(SignerError::TrezorError( @@ -985,10 +1015,10 @@ where self.signer_provider.get_hardware_wallet_info() } - fn create_next_unused_account( + async fn create_next_unused_account( next_account_index: U31, chain_config: Arc, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut T, name: Option, signer_provider: &mut P, ) -> WalletResult<(U31, Account)> { @@ -997,19 +1027,16 @@ where WalletError::EmptyAccountName ); - let account = signer_provider.make_new_account( - chain_config.clone(), - next_account_index, - name, - db_tx, - )?; + let account = signer_provider + .make_new_account(chain_config.clone(), next_account_index, name, db_tx) + .await?; Ok((next_account_index, account)) } /// Promotes the unused account into the used accounts and creates a new unused account /// Returns the new index and optional name if provided - pub fn create_next_account( + pub async fn create_next_account( &mut self, name: Option, ) -> WalletResult<(U31, Option)> { @@ -1039,7 +1066,8 @@ where &mut db_tx, None, &mut self.signer_provider, - )?; + ) + .await?; self.next_unused_account.1.set_name(name.clone(), &mut db_tx)?; std::mem::swap(&mut self.next_unused_account, &mut next_unused_account); @@ -2509,7 +2537,7 @@ where /// If `common_block_height` is zero, only the genesis block is considered common. /// If a new transaction is recognized for the unused account, it is transferred to the used /// accounts and a new unused account is created. - pub fn scan_new_blocks_unused_account( + pub async fn scan_new_blocks_unused_account( &mut self, common_block_height: BlockHeight, blocks: Vec, @@ -2527,7 +2555,7 @@ where db_tx.commit()?; if added_new_tx_in_unused_acc { - self.create_next_account(None)?; + self.create_next_account(None).await?; } else { break; } diff --git a/wallet/src/wallet/test_helpers.rs b/wallet/src/wallet/test_helpers.rs index 4a3f8de5c5..dc3b90cf59 100644 --- a/wallet/src/wallet/test_helpers.rs +++ b/wallet/src/wallet/test_helpers.rs @@ -34,7 +34,7 @@ use crate::{ DefaultWallet, Wallet, }; -pub fn create_wallet_with_mnemonic( +pub async fn create_wallet_with_mnemonic( chain_config: Arc, mnemonic: &str, ) -> DefaultWallet { @@ -45,7 +45,7 @@ pub fn create_wallet_with_mnemonic( db, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, - |db_tx| { + async |db_tx| { Ok(SoftwareSignerProvider::new_from_mnemonic( chain_config, db_tx, @@ -55,6 +55,7 @@ pub fn create_wallet_with_mnemonic( )?) }, ) + .await .unwrap() .wallet() .unwrap() @@ -68,7 +69,7 @@ pub fn create_named_in_memory_store(db_name: &str) -> Store { Store::new(create_named_in_memory_backend(db_name)).unwrap() } -pub fn create_wallet_with_mnemonic_and_named_db( +pub async fn create_wallet_with_mnemonic_and_named_db( chain_config: Arc, mnemonic: &str, db_name: &str, @@ -80,22 +81,24 @@ pub fn create_wallet_with_mnemonic_and_named_db( db, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, - |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( + async |db_tx| { + SoftwareSignerProvider::new_from_mnemonic( chain_config, db_tx, mnemonic, None, StoreSeedPhrase::DoNotStore, - )?) + ) + .map_err(Into::into) }, ) + .await .unwrap() .wallet() .unwrap() } -pub fn scan_wallet(wallet: &mut Wallet, height: BlockHeight, blocks: Vec) +pub async fn scan_wallet(wallet: &mut Wallet, height: BlockHeight, blocks: Vec) where B: storage::BackendWithSendableTransactions + 'static, P: SignerProvider, @@ -108,5 +111,6 @@ where wallet .scan_new_blocks_unused_account(height, blocks, &WalletEventsNoOp) + .await .unwrap(); } diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 74a44202cc..3633ded5fa 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -262,8 +262,7 @@ where (coins, token_balances) } -#[track_caller] -fn verify_wallet_balance( +async fn verify_wallet_balance( chain_config: &Arc, wallet: &Wallet, expected_balance: Amount, @@ -285,8 +284,9 @@ fn verify_wallet_balance( |_| Ok(()), WalletControllerMode::Hot, false, - |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), + async |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), &db_tx), ) + .await .unwrap() .wallet() .unwrap(); @@ -296,13 +296,11 @@ fn verify_wallet_balance( assert_eq!(coin_balance, expected_balance); } -#[track_caller] -fn create_wallet(chain_config: Arc) -> DefaultWallet { - create_wallet_with_mnemonic(chain_config, MNEMONIC) +async fn create_wallet(chain_config: Arc) -> DefaultWallet { + create_wallet_with_mnemonic(chain_config, MNEMONIC).await } -#[track_caller] -fn create_block_with_reward_address( +async fn create_block_with_reward_address( chain_config: &Arc, wallet: &mut Wallet, transactions: Vec, @@ -323,12 +321,11 @@ where ) .unwrap(); - scan_wallet(wallet, BlockHeight::new(block_height), vec![block1.clone()]); + scan_wallet(wallet, BlockHeight::new(block_height), vec![block1.clone()]).await; block1 } -#[track_caller] -fn create_block( +async fn create_block( chain_config: &Arc, wallet: &mut Wallet, transactions: Vec, @@ -347,12 +344,12 @@ where reward, block_height, address.clone().into_object(), - ); + ) + .await; (address, block) } -#[track_caller] -fn test_balance_from_genesis( +async fn test_balance_from_genesis( chain_type: ChainType, utxos: Vec, expected_balance: Amount, @@ -372,15 +369,17 @@ fn test_balance_from_genesis( ); let db_name = random_ascii_alphanumeric_string(rng, 10..20); - let wallet = create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name); + let wallet = + create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name).await; verify_wallet_balance(&chain_config, &wallet, expected_balance, || { create_named_in_memory_store(&db_name) - }); + }) + .await; } -#[test] -fn wallet_creation_in_memory() { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_creation_in_memory() { let chain_config = Arc::new(create_regtest()); let empty_db = create_wallet_in_memory().unwrap(); let chain_config2 = chain_config.clone(); @@ -393,14 +392,16 @@ fn wallet_creation_in_memory() { |_| Ok(()), WalletControllerMode::Hot, false, - |db_tx| SoftwareSignerProvider::load_from_database(chain_config2, db_tx), - ) { + async |db_tx| SoftwareSignerProvider::load_from_database(chain_config2, &db_tx), + ) + .await + { Ok(_) => panic!("Wallet loading should fail"), Err(err) => assert_eq!(err, WalletError::WalletNotInitialized), } // initialize a new wallet with mnemonic - let wallet = create_wallet(chain_config.clone()); + let wallet = create_wallet(chain_config.clone()).await; let initialized_db = wallet.db; // successfully load a wallet from initialized db @@ -411,15 +412,17 @@ fn wallet_creation_in_memory() { |_| Ok(()), WalletControllerMode::Hot, false, - |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), + async |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), &db_tx), ) + .await .unwrap(); } #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_migration_to_v2(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_migration_to_v2(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let address = get_address( &create_regtest(), @@ -444,23 +447,26 @@ fn wallet_migration_to_v2(#[case] seed: Seed) { db, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, - |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( + async |db_tx| { + SoftwareSignerProvider::new_from_mnemonic( chain_config.clone(), db_tx, MNEMONIC, None, StoreSeedPhrase::DoNotStore, - )?) + ) + .map_err(Into::into) }, ) + .await .unwrap() .wallet() .unwrap(); verify_wallet_balance(&chain_config, &wallet, genesis_amount, || { create_named_in_memory_store(&db_name) - }); + }) + .await; let password = Some("password".into()); wallet.encrypt_wallet(&password).unwrap(); @@ -510,8 +516,9 @@ fn wallet_migration_to_v2(#[case] seed: Seed) { |_| Ok(()), WalletControllerMode::Hot, false, - |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), + async |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), &db_tx), ) + .await .unwrap() .wallet() .unwrap(); @@ -529,13 +536,15 @@ fn wallet_migration_to_v2(#[case] seed: Seed) { ); verify_wallet_balance(&chain_config, &wallet, genesis_amount, || { create_named_in_memory_store(&new_db_name) - }); + }) + .await; } #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_seed_phrase_retrieval(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_seed_phrase_retrieval(#[case] seed: Seed) { use wallet_types::seed_phrase::SeedPhraseLanguage; let mut rng = make_seedable_rng(seed); @@ -543,7 +552,7 @@ fn wallet_seed_phrase_retrieval(#[case] seed: Seed) { // create wallet without saving the seed phrase { - let wallet = create_wallet(chain_config.clone()); + let wallet = create_wallet(chain_config.clone()).await; let seed_phrase = wallet.seed_phrase().unwrap(); assert!(seed_phrase.is_none()); } @@ -562,16 +571,18 @@ fn wallet_seed_phrase_retrieval(#[case] seed: Seed) { db, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, - |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( + async |db_tx| { + SoftwareSignerProvider::new_from_mnemonic( chain_config.clone(), db_tx, MNEMONIC, wallet_passphrase.as_ref().map(|p| p.as_ref()), StoreSeedPhrase::Store, - )?) + ) + .map_err(Into::into) }, ) + .await .unwrap() .wallet() .unwrap(); @@ -646,8 +657,8 @@ fn wallet_seed_phrase_retrieval(#[case] seed: Seed) { assert!(seed_phrase.is_none()); } -#[test] -fn wallet_seed_phrase_check_address() { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_seed_phrase_check_address() { let chain_config = Arc::new(create_mainnet()); // create wallet with saving the seed phrase @@ -659,16 +670,18 @@ fn wallet_seed_phrase_check_address() { db, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, - |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( + async |db_tx| { + SoftwareSignerProvider::new_from_mnemonic( chain_config.clone(), db_tx, MNEMONIC, wallet_passphrase.as_ref().map(|p| p.as_ref()), StoreSeedPhrase::Store, - )?) + ) + .map_err(Into::into) }, ) + .await .unwrap() .wallet() .unwrap(); @@ -702,16 +715,18 @@ fn wallet_seed_phrase_check_address() { db, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, - |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( + async |db_tx| { + SoftwareSignerProvider::new_from_mnemonic( chain_config.clone(), db_tx, MNEMONIC, wallet_passphrase.as_ref().map(|p| p.as_ref()), StoreSeedPhrase::Store, - )?) + ) + .map_err(Into::into) }, ) + .await .unwrap() .wallet() .unwrap(); @@ -735,7 +750,8 @@ fn wallet_seed_phrase_check_address() { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_balance_genesis(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_balance_genesis(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_type = ChainType::Mainnet; @@ -758,7 +774,8 @@ fn wallet_balance_genesis(#[case] seed: Seed) { vec![genesis_output.clone()], genesis_amount, &mut rng, - ); + ) + .await; let genesis_amount_2 = Amount::from_atoms(54321); let genesis_output_2 = TxOutput::LockThenTransfer( @@ -772,7 +789,8 @@ fn wallet_balance_genesis(#[case] seed: Seed) { vec![genesis_output, genesis_output_2], (genesis_amount + genesis_amount_2).unwrap(), &mut rng, - ); + ) + .await; let address_indexes = [0, LOOKAHEAD_SIZE - 1, LOOKAHEAD_SIZE]; for purpose in KeyPurpose::ALL { @@ -790,14 +808,16 @@ fn wallet_balance_genesis(#[case] seed: Seed) { let genesis_output = make_address_output(address.into_object(), genesis_amount); if address_index.into_u32() == LOOKAHEAD_SIZE { - test_balance_from_genesis(chain_type, vec![genesis_output], Amount::ZERO, &mut rng); + test_balance_from_genesis(chain_type, vec![genesis_output], Amount::ZERO, &mut rng) + .await; } else { test_balance_from_genesis( chain_type, vec![genesis_output], genesis_amount, &mut rng, - ); + ) + .await; } } } @@ -806,7 +826,8 @@ fn wallet_balance_genesis(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn locked_wallet_balance_works(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn locked_wallet_balance_works(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_type = ChainType::Mainnet; let genesis_amount = Amount::from_atoms(rng.gen_range(1..10000)); @@ -833,7 +854,7 @@ fn locked_wallet_balance_works(#[case] seed: Seed) { .build(), ); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, genesis_amount); @@ -849,13 +870,14 @@ fn locked_wallet_balance_works(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_balance_block_reward(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_balance_block_reward(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); let db_name = random_ascii_alphanumeric_string(&mut rng, 10..20); let mut wallet = - create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name); + create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -865,7 +887,7 @@ fn wallet_balance_block_reward(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(10000); - let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; // Verify that the first block reward has been received let (best_block_id, best_block_height) = get_best_block(&wallet); @@ -873,7 +895,8 @@ fn wallet_balance_block_reward(#[case] seed: Seed) { assert_eq!(best_block_height, BlockHeight::new(1)); verify_wallet_balance(&chain_config, &wallet, block1_amount, || { create_named_in_memory_store(&db_name) - }); + }) + .await; // Create the second block that sends the reward to the wallet let block2_amount = Amount::from_atoms(20000); @@ -896,7 +919,7 @@ fn wallet_balance_block_reward(#[case] seed: Seed) { ) .unwrap(); let block2_id = block2.header().block_id(); - scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2]); + scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2]).await; // Verify that the second block reward is also received let (best_block_id, best_block_height) = get_best_block(&wallet); @@ -907,7 +930,8 @@ fn wallet_balance_block_reward(#[case] seed: Seed) { &wallet, (block1_amount + block2_amount).unwrap(), || create_named_in_memory_store(&db_name), - ); + ) + .await; // Create a new block to replace the second block let block2_amount_new = Amount::from_atoms(30000); @@ -930,7 +954,7 @@ fn wallet_balance_block_reward(#[case] seed: Seed) { ) .unwrap(); let block2_new_id = block2_new.header().block_id(); - scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2_new]); + scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2_new]).await; // Verify that the balance includes outputs from block1 and block2_new, but not block2 let (best_block_id, best_block_height) = get_best_block(&wallet); @@ -941,19 +965,21 @@ fn wallet_balance_block_reward(#[case] seed: Seed) { &wallet, (block1_amount + block2_amount_new).unwrap(), || create_named_in_memory_store(&db_name), - ); + ) + .await; } #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_balance_block_transactions(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_balance_block_transactions(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); let db_name = random_ascii_alphanumeric_string(&mut rng, 10..20); let mut wallet = - create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name); + create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name).await; let tx_amount1 = Amount::from_atoms(10000); let address = get_address( @@ -977,24 +1003,27 @@ fn wallet_balance_block_transactions(#[case] seed: Seed) { vec![signed_transaction1], Amount::ZERO, 0, - ); + ) + .await; verify_wallet_balance(&chain_config, &wallet, tx_amount1, || { create_named_in_memory_store(&db_name) - }); + }) + .await; } // Verify that outputs can be created and consumed in the same block #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_balance_parent_child_transactions(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_balance_parent_child_transactions(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); let db_name = random_ascii_alphanumeric_string(&mut rng, 10..20); let mut wallet = - create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name); + create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name).await; let tx_amount1 = Amount::from_atoms(20000); let tx_amount2 = Amount::from_atoms(10000); @@ -1038,15 +1067,16 @@ fn wallet_balance_parent_child_transactions(#[case] seed: Seed) { vec![signed_transaction1, signed_transaction2], Amount::ZERO, 0, - ); + ) + .await; verify_wallet_balance(&chain_config, &wallet, tx_amount2, || { create_named_in_memory_store(&db_name) - }); + }) + .await; } -#[track_caller] -fn test_wallet_accounts( +async fn test_wallet_accounts( chain_config: &Arc, wallet: &Wallet, expected_accounts: Vec, @@ -1067,8 +1097,9 @@ fn test_wallet_accounts( |_| Ok(()), WalletControllerMode::Hot, false, - |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), + async |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), &db_tx), ) + .await .unwrap() .wallet() .unwrap(); @@ -1086,11 +1117,12 @@ async fn wallet_accounts_creation(#[case] seed: Seed) { let db_name = random_ascii_alphanumeric_string(&mut rng, 10..20); let mut wallet = - create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name); + create_wallet_with_mnemonic_and_named_db(chain_config.clone(), MNEMONIC, &db_name).await; test_wallet_accounts(&chain_config, &wallet, vec![DEFAULT_ACCOUNT_INDEX], || { create_named_in_memory_store(&db_name) - }); + }) + .await; // DEFAULT_ACCOUNT_INDEX now has 1 transaction so next account can be created let _ = create_block( &chain_config, @@ -1098,13 +1130,14 @@ async fn wallet_accounts_creation(#[case] seed: Seed) { vec![], Amount::from_atoms(100), 0, - ); + ) + .await; - let res = wallet.create_next_account(Some("name".into())).unwrap(); + let res = wallet.create_next_account(Some("name".into())).await.unwrap(); assert_eq!(res, (U31::from_u32(1).unwrap(), Some("name".into()))); // but we cannot create a third account as the new one has no transactions - let error = wallet.create_next_account(None).err().unwrap(); + let error = wallet.create_next_account(None).await.err().unwrap(); assert_eq!(error, WalletError::EmptyLastAccount); let acc1_pk = wallet.get_new_address(res.0).unwrap().1; @@ -1127,33 +1160,34 @@ async fn wallet_accounts_creation(#[case] seed: Seed) { // even with an unconfirmed transaction we cannot create a new account wallet.add_unconfirmed_tx(tx.clone(), &WalletEventsNoOp).unwrap(); - let error = wallet.create_next_account(None).err().unwrap(); + let error = wallet.create_next_account(None).await.err().unwrap(); assert_eq!(error, WalletError::EmptyLastAccount); // after getting a confirmed transaction we can create a new account - let _ = create_block(&chain_config, &mut wallet, vec![tx], Amount::ZERO, 1); - let res = wallet.create_next_account(Some("name2".into())).unwrap(); + let _ = create_block(&chain_config, &mut wallet, vec![tx], Amount::ZERO, 1).await; + let res = wallet.create_next_account(Some("name2".into())).await.unwrap(); assert_eq!(res, (U31::from_u32(2).unwrap(), Some("name2".into()))); } #[rstest] #[trace] #[case(Seed::from_entropy())] -fn locked_wallet_accounts_creation_fail(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn locked_wallet_accounts_creation_fail(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; // Need at least one address used from the previous account in order to create a new account // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 1..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let password = Some(gen_random_password(&mut rng)); wallet.encrypt_wallet(&password).unwrap(); wallet.lock_wallet().unwrap(); - let err = wallet.create_next_account(None); + let err = wallet.create_next_account(None).await; assert_eq!( err, Err(WalletError::DatabaseError( @@ -1166,10 +1200,11 @@ fn locked_wallet_accounts_creation_fail(#[case] seed: Seed) { // success after unlock wallet.unlock_wallet(&password.unwrap()).unwrap(); if name.is_empty() { - let err = wallet.create_next_account(Some(name)); + let err = wallet.create_next_account(Some(name)).await; assert_eq!(err, Err(WalletError::EmptyAccountName)); } else { - let (new_account_index, new_name) = wallet.create_next_account(Some(name.clone())).unwrap(); + let (new_account_index, new_name) = + wallet.create_next_account(Some(name.clone())).await.unwrap(); assert_ne!(new_account_index, DEFAULT_ACCOUNT_INDEX); assert_eq!(new_name.unwrap(), name); assert_eq!(wallet.number_of_accounts(), 2); @@ -1179,48 +1214,49 @@ fn locked_wallet_accounts_creation_fail(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_recover_new_account(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_recover_new_account(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; - let err = wallet.create_next_account(None).err().unwrap(); + let err = wallet.create_next_account(None).await.err().unwrap(); assert_eq!(err, WalletError::EmptyLastAccount); let mut total_amounts = BTreeMap::new(); let mut last_account_index = DEFAULT_ACCOUNT_INDEX; - let blocks = (0..rng.gen_range(1..100)) - .map(|idx| { - let tx_amount1 = Amount::from_atoms(rng.gen_range(1..10)); - total_amounts - .entry(last_account_index) - .and_modify(|amount: &mut Amount| *amount = (*amount + tx_amount1).unwrap()) - .or_insert(tx_amount1); + let mut blocks = vec![]; + for idx in 0..rng.gen_range(1..100) { + let tx_amount1 = Amount::from_atoms(rng.gen_range(1..10)); + total_amounts + .entry(last_account_index) + .and_modify(|amount: &mut Amount| *amount = (*amount + tx_amount1).unwrap()) + .or_insert(tx_amount1); - let address = wallet.get_new_address(last_account_index).unwrap().1; + let address = wallet.get_new_address(last_account_index).unwrap().1; - let transaction1 = Transaction::new( - 0, - Vec::new(), - vec![make_address_output(address.into_object(), tx_amount1)], - ) - .unwrap(); - let signed_transaction1 = SignedTransaction::new(transaction1, Vec::new()).unwrap(); - let (_, block) = create_block( - &chain_config, - &mut wallet, - vec![signed_transaction1], - Amount::ZERO, - idx, - ); + let transaction1 = Transaction::new( + 0, + Vec::new(), + vec![make_address_output(address.into_object(), tx_amount1)], + ) + .unwrap(); + let signed_transaction1 = SignedTransaction::new(transaction1, Vec::new()).unwrap(); + let (_, block) = create_block( + &chain_config, + &mut wallet, + vec![signed_transaction1], + Amount::ZERO, + idx, + ) + .await; - if rng.gen_bool(0.2) { - last_account_index = wallet.create_next_account(None).unwrap().0; - } - block - }) - .collect_vec(); + if rng.gen_bool(0.2) { + last_account_index = wallet.create_next_account(None).await.unwrap().0; + } + blocks.push(block); + } // verify all accounts have the expected balances for (acc_idx, expected_balance) in total_amounts.iter() { @@ -1229,9 +1265,9 @@ fn wallet_recover_new_account(#[case] seed: Seed) { } // Create a new wallet with the same mnemonic - let mut wallet = create_wallet(chain_config); + let mut wallet = create_wallet(chain_config).await; // scan the blocks again - scan_wallet(&mut wallet, BlockHeight::new(0), blocks.clone()); + scan_wallet(&mut wallet, BlockHeight::new(0), blocks.clone()).await; // verify the wallet has recovered all of the accounts assert_eq!(wallet.number_of_accounts(), total_amounts.len(),); @@ -1251,14 +1287,14 @@ async fn locked_wallet_cant_sign_transaction(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 1..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let password = Some(gen_random_password(&mut rng)); wallet.encrypt_wallet(&password).unwrap(); @@ -1352,7 +1388,7 @@ async fn locked_wallet_standalone_keys( let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -1396,7 +1432,8 @@ async fn locked_wallet_standalone_keys( block1_amount, 0, standalone_destination, - ); + ) + .await; } else { // test that wallet will recognise a destination belonging to a standalone key in a // transaction @@ -1414,7 +1451,7 @@ async fn locked_wallet_standalone_keys( ) .unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]); + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]).await; // check the transaction has been added to the wallet let tx_data = wallet @@ -1491,10 +1528,10 @@ async fn wallet_get_transaction(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(100000..1000000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -1534,7 +1571,8 @@ async fn wallet_get_transaction(#[case] seed: Seed) { vec![tx.clone()], Amount::ZERO, 1, - ); + ) + .await; let found_tx = wallet.get_transaction(DEFAULT_ACCOUNT_INDEX, tx_id).unwrap(); assert_eq!( @@ -1552,10 +1590,10 @@ async fn wallet_list_mainchain_transactions(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(100000..1000000)); - let (addr, _) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let (addr, _) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let dest = addr.into_object(); let coin_balance = get_coin_balance(&wallet); @@ -1585,7 +1623,8 @@ async fn wallet_list_mainchain_transactions(#[case] seed: Seed) { vec![tx.clone()], Amount::ZERO, 1, - ); + ) + .await; let tx = wallet .create_transaction_to_addresses( @@ -1608,7 +1647,8 @@ async fn wallet_list_mainchain_transactions(#[case] seed: Seed) { vec![tx.clone()], Amount::ZERO, 2, - ); + ) + .await; let txs = wallet.mainchain_transactions(DEFAULT_ACCOUNT_INDEX, Some(dest), 100).unwrap(); // should have 2 txs the send to and the spent from @@ -1637,14 +1677,14 @@ async fn wallet_transactions_with_fees(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(30000000..50000000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -1755,7 +1795,7 @@ async fn wallet_transactions_with_fees(#[case] seed: Seed) { // make sure we have selected all of the previously created outputs assert!(selected_utxos.len() >= num_outputs as usize); - let account1 = wallet.create_next_account(None).unwrap().0; + let account1 = wallet.create_next_account(None).await.unwrap().0; let address2 = wallet.get_new_address(account1).unwrap().1.into_object(); let feerate = FeeRate::from_amount_per_kb(Amount::from_atoms(rng.gen_range(1..1000))); let SignedTxWithFees { tx, fees } = wallet @@ -1802,11 +1842,11 @@ async fn wallet_transactions_with_fees(#[case] seed: Seed) { assert_eq!(*exact_fee, *fees.get(&Currency::Coin).unwrap()); } -#[test] -fn lock_wallet_fail_empty_password() { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lock_wallet_fail_empty_password() { let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config); + let mut wallet = create_wallet(chain_config).await; let empty_password = Some(String::new()); assert_eq!( wallet.encrypt_wallet(&empty_password), @@ -1824,7 +1864,7 @@ async fn spend_from_user_specified_utxos(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; // Generate a new block which sends reward to the wallet let utxo_amount = Amount::from_atoms(rng.gen_range(100..10000)); @@ -1848,7 +1888,7 @@ async fn spend_from_user_specified_utxos(#[case] seed: Seed) { BlockReward::new(reward_outputs), ) .unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]); + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]).await; let utxos = wallet .get_utxos( @@ -1959,14 +1999,14 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -2026,7 +2066,8 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -2088,7 +2129,7 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { ) .unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(2), vec![block3.clone()]); + scan_wallet(&mut wallet, BlockHeight::new(2), vec![block3.clone()]).await; let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -2099,7 +2140,7 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { ); // do a reorg back to block 2 - scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2.clone()]); + scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2.clone()]).await; let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let (pool_id, pool_data) = pool_ids.first().unwrap(); @@ -2126,7 +2167,8 @@ async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { vec![decommission_tx], Amount::ZERO, 2, - ); + ) + .await; let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); @@ -2153,8 +2195,8 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2); + let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2).await; let coin_balance1 = get_coin_balance(&wallet1); assert_eq!(coin_balance1, Amount::ZERO); @@ -2163,8 +2205,8 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); + let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0).await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]).await; let pool_ids1 = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids1.is_empty()); @@ -2262,8 +2304,9 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: vec![stake_pool_transaction], Amount::ZERO, 1, - ); - scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2.clone()]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2.clone()]).await; let coin_balance1 = get_coin_balance(&wallet1); assert_eq!(coin_balance1, Amount::ZERO); @@ -2348,8 +2391,8 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: ) .unwrap(); - scan_wallet(&mut wallet1, BlockHeight::new(2), vec![block3.clone()]); - scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3.clone()]); + scan_wallet(&mut wallet1, BlockHeight::new(2), vec![block3.clone()]).await; + scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3.clone()]).await; let pool_ids_for_staking1 = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); @@ -2402,8 +2445,9 @@ async fn create_stake_pool_for_different_wallet_and_list_pool_ids(#[case] seed: vec![decommission_tx], Amount::ZERO, 3, - ); - scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]).await; let pool_ids1 = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids1.is_empty()); @@ -2424,14 +2468,14 @@ async fn reset_keys_after_failed_transaction(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -2477,8 +2521,8 @@ async fn send_to_unknown_delegation(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2); + let mut wallet = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -2493,13 +2537,15 @@ async fn send_to_unknown_delegation(#[case] seed: Seed) { vec![], block1_amount, block_height, - ); + ) + .await; scan_wallet( &mut wallet2, BlockHeight::new(block_height), vec![block.clone()], - ); + ) + .await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, delegation_amount); @@ -2513,13 +2559,15 @@ async fn send_to_unknown_delegation(#[case] seed: Seed) { vec![], block1_amount, block_height, - ); + ) + .await; scan_wallet( &mut wallet, BlockHeight::new(block_height), vec![block.clone()], - ); + ) + .await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, delegation_amount); @@ -2549,12 +2597,14 @@ async fn send_to_unknown_delegation(#[case] seed: Seed) { vec![delegation_tx], Amount::ZERO, block_height, - ); + ) + .await; scan_wallet( &mut wallet2, BlockHeight::new(block_height), vec![block.clone()], - ); + ) + .await; let delegation_data = wallet2.get_delegation(DEFAULT_ACCOUNT_INDEX, wallet2_delegation_id).unwrap(); @@ -2584,12 +2634,14 @@ async fn send_to_unknown_delegation(#[case] seed: Seed) { vec![delegation_stake_tx], block1_amount, block_height, - ); + ) + .await; scan_wallet( &mut wallet2, BlockHeight::new(block_height), vec![block.clone()], - ); + ) + .await; // Wallet2 should see the transaction and know that someone has staked to the delegation let delegation_data = @@ -2601,7 +2653,7 @@ async fn send_to_unknown_delegation(#[case] seed: Seed) { let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); - let (other_acc_idx, _) = wallet.create_next_account(None).unwrap(); + let (other_acc_idx, _) = wallet.create_next_account(None).await.unwrap(); let address = wallet.get_new_address(other_acc_idx).unwrap().1; let unknown_pool_id = PoolId::new(H256::zero()); @@ -2625,7 +2677,8 @@ async fn send_to_unknown_delegation(#[case] seed: Seed) { vec![delegation_tx], Amount::ZERO, 2, - ); + ) + .await; // the new delegation even though created from DEFAULT_ACCOUNT_INDEX is not theirs assert_eq!( @@ -2646,7 +2699,7 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -2654,7 +2707,7 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let delegation_amount = Amount::from_atoms(rng.gen_range(2..100)); let block1_amount = (chain_config.min_stake_pool_pledge() + delegation_amount).unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -2688,7 +2741,8 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); @@ -2717,7 +2771,8 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { vec![delegation_tx], Amount::ZERO, 2, - ); + ) + .await; let mut delegations = wallet.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); assert_eq!(delegations.len(), 1); @@ -2748,7 +2803,8 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { vec![delegation_stake_tx], Amount::ZERO, 3, - ); + ) + .await; let delegation_tx1 = wallet .create_transaction_to_addresses_from_delegation( @@ -2781,7 +2837,7 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { // Send delegation to account 1 // test that account 1 will receive the money but not register the delegation id as theirs - let (other_acc_idx, _) = wallet.create_next_account(None).unwrap(); + let (other_acc_idx, _) = wallet.create_next_account(None).await.unwrap(); let address = wallet.get_new_address(other_acc_idx).unwrap().1; let delegation_tx2 = wallet @@ -2811,7 +2867,8 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { assert_eq!(*deleg_id, delegation_id); assert_eq!(deleg_data.last_nonce, Some(AccountNonce::new(1))); - let (_, block5) = create_block(&chain_config, &mut wallet, delegation_tx1, Amount::ZERO, 4); + let (_, block5) = + create_block(&chain_config, &mut wallet, delegation_tx1, Amount::ZERO, 4).await; let _ = create_block( &chain_config, @@ -2819,7 +2876,8 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { vec![delegation_tx2.clone()], Amount::ZERO, 5, - ); + ) + .await; // Check delegation balance after confirmed tx status let mut delegations = wallet.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); @@ -2840,7 +2898,7 @@ async fn create_spend_from_delegations(#[case] seed: Seed) { assert!(delegations.is_empty()); // roll back the delegation tx to test removal code - scan_wallet(&mut wallet, BlockHeight::new(4), vec![block5]); + scan_wallet(&mut wallet, BlockHeight::new(4), vec![block5]).await; let coin_balance = wallet .get_balance(other_acc_idx, UtxoState::Confirmed.into(), WithLocked::Any) @@ -2899,8 +2957,8 @@ async fn issue_and_transfer_tokens(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2); + let mut wallet = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -2941,8 +2999,9 @@ async fn issue_and_transfer_tokens(#[case] seed: Seed) { vec![], block1_amount, 0, - ); - scan_wallet(other_wallet, BlockHeight::new(0), vec![block.clone()]); + ) + .await; + scan_wallet(other_wallet, BlockHeight::new(0), vec![block.clone()]).await; let coin_balance = get_coin_balance(random_issuing_wallet); assert_eq!(coin_balance, block1_amount); @@ -3099,7 +3158,8 @@ async fn issue_and_transfer_tokens(#[case] seed: Seed) { token_issuance_transactions, block1_amount, 1, - ); + ) + .await; let (coin_balance, token_balances) = get_currency_balances(&wallet); @@ -3159,7 +3219,8 @@ async fn issue_and_transfer_tokens(#[case] seed: Seed) { vec![transfer_tokens_transaction], block1_amount, 2, - ); + ) + .await; let (coin_balance, token_balances) = get_currency_balances(&wallet); let mut expected_amount = ((block1_amount * 3).unwrap() - issuance_fee).unwrap(); @@ -3226,7 +3287,7 @@ async fn check_tokens_v0_are_ignored(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -3236,7 +3297,7 @@ async fn check_tokens_v0_are_ignored(#[case] seed: Seed) { + chain_config.fungible_token_issuance_fee()) .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -3287,7 +3348,7 @@ async fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -3297,7 +3358,7 @@ async fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { + (chain_config.fungible_token_issuance_fee() * 4).unwrap()) .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -3331,7 +3392,8 @@ async fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { vec![token_issuance_transaction], block1_amount, 1, - ); + ) + .await; let freezable = token_issuance.is_freezable.as_bool(); let token_info = RPCFungibleTokenInfo::new( @@ -3364,7 +3426,7 @@ async fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { .unwrap() .tx; - let _ = create_block(&chain_config, &mut wallet, vec![mint_tx], block2_amount, 2); + let _ = create_block(&chain_config, &mut wallet, vec![mint_tx], block2_amount, 2).await; let unconfirmed_token_info = wallet .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) @@ -3455,7 +3517,8 @@ async fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { vec![freeze_tx, unfreeze_tx], block2_amount, 3, - ); + ) + .await; let unconfirmed_token_info = wallet .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) @@ -3568,7 +3631,8 @@ async fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { vec![freeze_tx], block2_amount, 4, - ); + ) + .await; // now the transfer tx should be conflicting let pending_txs = wallet.pending_transactions(DEFAULT_ACCOUNT_INDEX).unwrap(); @@ -3601,7 +3665,7 @@ async fn change_token_supply_fixed(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -3611,7 +3675,7 @@ async fn change_token_supply_fixed(#[case] seed: Seed) { + chain_config.fungible_token_issuance_fee()) .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -3644,7 +3708,8 @@ async fn change_token_supply_fixed(#[case] seed: Seed) { vec![token_issuance_transaction], block1_amount, 1, - ); + ) + .await; let freezable = token_issuance.is_freezable.as_bool(); let mut token_info = RPCFungibleTokenInfo::new( @@ -3763,7 +3828,8 @@ async fn change_token_supply_fixed(#[case] seed: Seed) { vec![mint_transaction], block2_amount, 2, - ); + ) + .await; token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); let unconfirmed_token_info = wallet @@ -3824,7 +3890,8 @@ async fn change_token_supply_fixed(#[case] seed: Seed) { vec![unmint_transaction], block2_amount, 3, - ); + ) + .await; token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); let unconfirmed_token_info = wallet @@ -3865,7 +3932,7 @@ async fn change_token_supply_unlimited(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -3875,7 +3942,7 @@ async fn change_token_supply_unlimited(#[case] seed: Seed) { + chain_config.fungible_token_issuance_fee()) .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -3908,7 +3975,8 @@ async fn change_token_supply_unlimited(#[case] seed: Seed) { vec![token_issuance_transaction], block2_amount, 1, - ); + ) + .await; let freezable = token_issuance.is_freezable.as_bool(); let mut token_info = RPCFungibleTokenInfo::new( @@ -3967,7 +4035,8 @@ async fn change_token_supply_unlimited(#[case] seed: Seed) { vec![mint_transaction], block2_amount, 2, - ); + ) + .await; token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); let unconfirmed_token_info = wallet @@ -4027,7 +4096,8 @@ async fn change_token_supply_unlimited(#[case] seed: Seed) { vec![unmint_transaction], block2_amount, 3, - ); + ) + .await; token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); let unconfirmed_token_info = wallet @@ -4068,7 +4138,7 @@ async fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -4078,7 +4148,7 @@ async fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { + chain_config.fungible_token_issuance_fee()) .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -4111,7 +4181,8 @@ async fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { vec![token_issuance_transaction], block2_amount, 1, - ); + ) + .await; let freezable = token_issuance.is_freezable.as_bool(); let mut token_info = RPCFungibleTokenInfo::new( @@ -4169,7 +4240,8 @@ async fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { vec![mint_transaction], block2_amount, 2, - ); + ) + .await; token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); let unconfirmed_token_info = wallet @@ -4230,7 +4302,8 @@ async fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { vec![unmint_transaction], block2_amount, 3, - ); + ) + .await; token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); let unconfirmed_token_info = wallet @@ -4265,7 +4338,8 @@ async fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { vec![lock_transaction], block2_amount, 4, - ); + ) + .await; token_info.is_locked = true; let unconfirmed_token_info = wallet @@ -4331,7 +4405,7 @@ async fn lock_then_transfer(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -4364,7 +4438,7 @@ async fn lock_then_transfer(#[case] seed: Seed) { // not important that it is not the actual median wallet.set_median_time(timestamp).unwrap(); let timestamp = block1.timestamp().add_int_seconds(seconds_between_blocks).unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]); + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]).await; // check balance let coin_balance = get_coin_balance(&wallet); @@ -4419,7 +4493,7 @@ async fn lock_then_transfer(#[case] seed: Seed) { // not important that it is not the actual median wallet.set_median_time(timestamp).unwrap(); let mut timestamp = block2.timestamp().add_int_seconds(seconds_between_blocks).unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2]); + scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2]).await; // check balance let balance_without_locked_transfer = @@ -4458,7 +4532,7 @@ async fn lock_then_transfer(#[case] seed: Seed) { // not important that it is not the actual median wallet.set_median_time(timestamp).unwrap(); timestamp = new_block.timestamp().add_int_seconds(seconds_between_blocks).unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(2 + idx), vec![new_block]); + scan_wallet(&mut wallet, BlockHeight::new(2 + idx), vec![new_block]).await; } // check that after block_count_lock, the amount is included @@ -4477,7 +4551,7 @@ async fn wallet_multiple_transactions_in_single_block(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let blocks_to_add = rng.gen_range(1..10); @@ -4485,7 +4559,7 @@ async fn wallet_multiple_transactions_in_single_block(#[case] seed: Seed) { for i in 0..blocks_to_add { // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 1..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, i as u64); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, i as u64).await; amounts.push(block1_amount); } @@ -4541,7 +4615,8 @@ async fn wallet_multiple_transactions_in_single_block(#[case] seed: Seed) { transactions, Amount::ZERO, blocks_to_add as u64, - ); + ) + .await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, total_change); @@ -4555,7 +4630,7 @@ async fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -4570,7 +4645,7 @@ async fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { (NETWORK_FEE + 1) * (total_num_transactions as u128) ..=(NETWORK_FEE + 1) * (total_num_transactions as u128) + 10000, )); - let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -4663,10 +4738,10 @@ async fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { wallet.scan_mempool(transactions.as_slice(), &WalletEventsNoOp).unwrap(); // create new wallet - let mut wallet = create_wallet(chain_config); + let mut wallet = create_wallet(chain_config).await; // scan the first block - scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]); + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]).await; // scan mempool transaction in random order transactions.shuffle(&mut rng); @@ -4746,7 +4821,7 @@ async fn wallet_abandon_transactions(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -4759,7 +4834,7 @@ async fn wallet_abandon_transactions(#[case] seed: Seed) { (NETWORK_FEE + 1) * (total_num_transactions as u128) ..=(NETWORK_FEE + 1) * (total_num_transactions as u128) + 10000, )); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -4882,10 +4957,11 @@ async fn wallet_abandon_transactions(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_address_usage(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_address_usage(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let usage = wallet .get_addresses_usage(DEFAULT_ACCOUNT_INDEX, KeyPurpose::ReceiveFunds) @@ -4909,7 +4985,7 @@ fn wallet_address_usage(#[case] seed: Seed) { ); let block1_amount = Amount::from_atoms(10000); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let last_used = addresses_to_issue + 1; let usage = wallet @@ -4922,10 +4998,11 @@ fn wallet_address_usage(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_set_lookahead_size(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_set_lookahead_size(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let usage = wallet .get_addresses_usage(DEFAULT_ACCOUNT_INDEX, KeyPurpose::ReceiveFunds) @@ -4949,7 +5026,7 @@ fn wallet_set_lookahead_size(#[case] seed: Seed) { ); let block1_amount = Amount::from_atoms(10000); - let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let last_used = addresses_to_issue + 1; let usage = wallet @@ -4970,7 +5047,7 @@ fn wallet_set_lookahead_size(#[case] seed: Seed) { wallet.set_lookahead_size(less_than_last_used, true).unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]); + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]).await; let coins = get_coin_balance_for_acc(&wallet, DEFAULT_ACCOUNT_INDEX); assert_eq!(coins, Amount::ZERO); let usage = wallet @@ -4982,7 +5059,7 @@ fn wallet_set_lookahead_size(#[case] seed: Seed) { let more_than_last_used = rng.gen_range(last_used + 1..100); wallet.set_lookahead_size(more_than_last_used, false).unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]); + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]).await; let coins = get_coin_balance_for_acc(&wallet, DEFAULT_ACCOUNT_INDEX); assert_eq!(coins, block1_amount); let usage = wallet @@ -5003,14 +5080,14 @@ async fn decommission_pool_wrong_account(#[case] seed: Seed) { let acc_0_index = DEFAULT_ACCOUNT_INDEX; let acc_1_index = U31::ONE; - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -5020,7 +5097,7 @@ async fn decommission_pool_wrong_account(#[case] seed: Seed) { let pool_amount = block1_amount; - let res = wallet.create_next_account(Some("name".into())).unwrap(); + let res = wallet.create_next_account(Some("name".into())).await.unwrap(); assert_eq!(res, (U31::from_u32(1).unwrap(), Some("name".into()))); let decommission_key = wallet.get_new_address(acc_1_index).unwrap().1; @@ -5048,7 +5125,8 @@ async fn decommission_pool_wrong_account(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -5088,7 +5166,8 @@ async fn decommission_pool_wrong_account(#[case] seed: Seed) { vec![decommission_tx], Amount::ZERO, 2, - ); + ) + .await; let coin_balance = get_coin_balance_for_acc(&wallet, acc_1_index); assert_eq!(coin_balance, pool_amount); @@ -5105,14 +5184,14 @@ async fn decommission_pool_request_wrong_account(#[case] seed: Seed) { let acc_0_index = DEFAULT_ACCOUNT_INDEX; let acc_1_index = U31::ONE; - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -5122,7 +5201,7 @@ async fn decommission_pool_request_wrong_account(#[case] seed: Seed) { let pool_amount = block1_amount; - let res = wallet.create_next_account(Some("name".into())).unwrap(); + let res = wallet.create_next_account(Some("name".into())).await.unwrap(); assert_eq!(res, (U31::from_u32(1).unwrap(), Some("name".into()))); let decommission_key = wallet.get_new_address(acc_1_index).unwrap().1; @@ -5150,7 +5229,8 @@ async fn decommission_pool_request_wrong_account(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -5199,14 +5279,14 @@ async fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { let acc_0_index = DEFAULT_ACCOUNT_INDEX; let acc_1_index = U31::ONE; - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let (addr, _) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let (addr, _) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let utxo = make_address_output(addr.clone().into_object(), block1_amount); let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); @@ -5217,7 +5297,7 @@ async fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { let pool_amount = block1_amount; - let res = wallet.create_next_account(Some("name".into())).unwrap(); + let res = wallet.create_next_account(Some("name".into())).await.unwrap(); assert_eq!(res, (U31::from_u32(1).unwrap(), Some("name".into()))); let decommission_key = wallet.get_new_address(acc_1_index).unwrap().1; @@ -5266,7 +5346,8 @@ async fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; assert_eq!(get_coin_balance(&wallet), Amount::ZERO); @@ -5311,7 +5392,7 @@ async fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { .into_signed_tx() .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![signed_tx], Amount::ZERO, 2); + let _ = create_block(&chain_config, &mut wallet, vec![signed_tx], Amount::ZERO, 2).await; // the pool amount is back after decommission assert_eq!(get_coin_balance(&wallet), pool_amount); @@ -5326,12 +5407,12 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut hot_wallet = create_wallet(chain_config.clone()); + let mut hot_wallet = create_wallet(chain_config.clone()).await; // create cold wallet that is not synced and only contains decommission key let another_mnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow"; - let mut cold_wallet = create_wallet_with_mnemonic(chain_config.clone(), another_mnemonic); + let mut cold_wallet = create_wallet_with_mnemonic(chain_config.clone(), another_mnemonic).await; let decommission_key = cold_wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; let coin_balance = get_coin_balance(&hot_wallet); @@ -5339,7 +5420,7 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut hot_wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut hot_wallet, vec![], block1_amount, 0).await; let pool_ids = hot_wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -5349,7 +5430,7 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { let pool_amount = block1_amount; - let res = hot_wallet.create_next_account(Some("name".into())).unwrap(); + let res = hot_wallet.create_next_account(Some("name".into())).await.unwrap(); assert_eq!(res, (U31::from_u32(1).unwrap(), Some("name".into()))); let stake_pool_transaction = hot_wallet @@ -5375,7 +5456,8 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; let pool_ids = hot_wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -5426,7 +5508,8 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { vec![signed_tx], Amount::ZERO, 2, - ); + ) + .await; let coin_balance = get_coin_balance(&hot_wallet); assert_eq!(coin_balance, pool_amount,); @@ -5440,12 +5523,12 @@ async fn filter_pools(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet1 = create_wallet(chain_config.clone()); + let mut wallet1 = create_wallet(chain_config.clone()).await; // create another wallet to store decommission key let another_mnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow"; - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), another_mnemonic); + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), another_mnemonic).await; let decommission_key = wallet2.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; let coin_balance = get_coin_balance(&wallet1); @@ -5453,8 +5536,8 @@ async fn filter_pools(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); - let _ = create_block(&chain_config, &mut wallet2, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0).await; + let _ = create_block(&chain_config, &mut wallet2, vec![], block1_amount, 0).await; let pool_ids = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -5488,7 +5571,8 @@ async fn filter_pools(#[case] seed: Seed) { vec![stake_pool_transaction.clone()], Amount::ZERO, 1, - ); + ) + .await; // sync for wallet2 let _ = create_block( &chain_config, @@ -5496,7 +5580,8 @@ async fn filter_pools(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; // check wallet1 filter let pool_ids = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); @@ -5531,12 +5616,12 @@ async fn sign_send_request_cold_wallet(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut hot_wallet = create_wallet(chain_config.clone()); + let mut hot_wallet = create_wallet(chain_config.clone()).await; // create cold wallet that is not synced let another_mnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow"; - let mut cold_wallet = create_wallet_with_mnemonic(chain_config.clone(), another_mnemonic); + let mut cold_wallet = create_wallet_with_mnemonic(chain_config.clone(), another_mnemonic).await; let cold_wallet_address = cold_wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; let coin_balance = get_coin_balance(&hot_wallet); @@ -5555,7 +5640,7 @@ async fn sign_send_request_cold_wallet(#[case] seed: Seed) { ) .unwrap(); - scan_wallet(&mut hot_wallet, BlockHeight::new(0), vec![block1.clone()]); + scan_wallet(&mut hot_wallet, BlockHeight::new(0), vec![block1.clone()]).await; // hot wallet has 0 balance let coin_balance = get_coin_balance(&hot_wallet); @@ -5608,7 +5693,8 @@ async fn sign_send_request_cold_wallet(#[case] seed: Seed) { vec![signed_tx], Amount::ZERO, 1, - ); + ) + .await; let coin_balance = get_coin_balance(&hot_wallet); assert_eq!(coin_balance, to_send,); @@ -5650,7 +5736,7 @@ async fn test_not_exhaustion_of_keys(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -5668,7 +5754,7 @@ async fn test_not_exhaustion_of_keys(#[case] seed: Seed) { ) .unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]); + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1.clone()]).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -5701,8 +5787,8 @@ async fn test_add_standalone_multisig(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2); + let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, Amount::ZERO); @@ -5747,7 +5833,7 @@ async fn test_add_standalone_multisig(#[case] seed: Seed) { ) .unwrap(); - scan_wallet(&mut wallet1, BlockHeight::new(0), vec![block1.clone()]); + scan_wallet(&mut wallet1, BlockHeight::new(0), vec![block1.clone()]).await; // Check amount is still zero let coin_balance = get_coin_balance(&wallet1); @@ -5835,15 +5921,15 @@ async fn create_htlc_and_spend(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2); + let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, block1_amount); @@ -5899,8 +5985,9 @@ async fn create_htlc_and_spend(#[case] seed: Seed) { vec![create_htlc_tx.clone()], Amount::ZERO, 1, - ); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block2]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block2]).await; // Htlc is not accounted in balance assert_eq!(get_coin_balance(&wallet1), Amount::ZERO); @@ -5961,8 +6048,9 @@ async fn create_htlc_and_spend(#[case] seed: Seed) { let spend_tx = spend_ptx.into_signed_tx().unwrap(); - let (_, block2) = create_block(&chain_config, &mut wallet2, vec![spend_tx], Amount::ZERO, 1); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block2]); + let (_, block2) = + create_block(&chain_config, &mut wallet2, vec![spend_tx], Amount::ZERO, 1).await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block2]).await; // Coins from htlc successfully transferred assert_eq!(get_coin_balance(&wallet1), Amount::ZERO); @@ -5979,15 +6067,15 @@ async fn create_htlc_and_refund(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2); + let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, block1_amount); @@ -6063,8 +6151,9 @@ async fn create_htlc_and_refund(#[case] seed: Seed) { vec![create_htlc_tx], Amount::ZERO, 1, - ); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block2]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block2]).await; // Htlc is not accounted in balance assert_eq!(get_coin_balance(&wallet1), Amount::ZERO); @@ -6126,8 +6215,9 @@ async fn create_htlc_and_refund(#[case] seed: Seed) { vec![refund_tx], Amount::ZERO, 2, - ); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block3]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block3]).await; // Refund can be seen in the wallet balance assert_eq!(get_coin_balance(&wallet1), coin_balance); @@ -6142,7 +6232,7 @@ async fn create_order(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -6152,7 +6242,7 @@ async fn create_order(#[case] seed: Seed) { + chain_config.fungible_token_issuance_fee()) .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -6184,7 +6274,8 @@ async fn create_order(#[case] seed: Seed) { vec![token_issuance_transaction], block2_amount, 1, - ); + ) + .await; // Mint some tokens let freezable = token_issuance.is_freezable.as_bool(); @@ -6224,7 +6315,8 @@ async fn create_order(#[case] seed: Seed) { vec![mint_transaction], Amount::ZERO, 2, - ); + ) + .await; let expected_balance = (block1_amount - chain_config.fungible_token_issuance_fee()).unwrap(); let (coin_balance, token_balances) = get_currency_balances(&wallet); @@ -6264,7 +6356,8 @@ async fn create_order(#[case] seed: Seed) { vec![create_order_tx], Amount::ZERO, 3, - ); + ) + .await; let (coin_balance, token_balances) = get_currency_balances(&wallet); assert_eq!(coin_balance, expected_balance); @@ -6279,7 +6372,7 @@ async fn create_order_and_conclude(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -6289,7 +6382,7 @@ async fn create_order_and_conclude(#[case] seed: Seed) { + chain_config.fungible_token_issuance_fee()) .unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, block1_amount); @@ -6321,7 +6414,8 @@ async fn create_order_and_conclude(#[case] seed: Seed) { vec![token_issuance_transaction], block2_amount, 1, - ); + ) + .await; // Mint some tokens let freezable = token_issuance.is_freezable.as_bool(); @@ -6361,7 +6455,8 @@ async fn create_order_and_conclude(#[case] seed: Seed) { vec![mint_transaction], Amount::ZERO, 2, - ); + ) + .await; let expected_balance = (block1_amount - chain_config.fungible_token_issuance_fee()).unwrap(); let (coin_balance, token_balances) = get_currency_balances(&wallet); @@ -6414,7 +6509,8 @@ async fn create_order_and_conclude(#[case] seed: Seed) { vec![create_order_tx], Amount::ZERO, 3, - ); + ) + .await; let (coin_balance, token_balances) = get_currency_balances(&wallet); assert_eq!(coin_balance, expected_balance); @@ -6457,7 +6553,8 @@ async fn create_order_and_conclude(#[case] seed: Seed) { vec![conclude_order_tx], Amount::ZERO, 4, - ); + ) + .await; let (coin_balance, token_balances) = get_currency_balances(&wallet); assert_eq!(coin_balance, expected_balance); @@ -6475,8 +6572,8 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); - let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2); + let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2).await; assert_eq!(get_coin_balance(&wallet1), Amount::ZERO); assert_eq!(get_coin_balance(&wallet2), Amount::ZERO); @@ -6486,8 +6583,8 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { + chain_config.fungible_token_issuance_fee()) .unwrap(); - let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); + let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0).await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, block1_amount); @@ -6519,8 +6616,9 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { vec![token_issuance_transaction], block2_amount, 1, - ); - scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]).await; // Mint some tokens let address2 = wallet2.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; @@ -6562,8 +6660,9 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { vec![mint_transaction], Amount::from_atoms(NETWORK_FEE), 2, - ); - scan_wallet(&mut wallet1, BlockHeight::new(2), vec![block3]); + ) + .await; + scan_wallet(&mut wallet1, BlockHeight::new(2), vec![block3]).await; { let expected_balance = @@ -6626,8 +6725,9 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { vec![create_order_tx], Amount::ZERO, 3, - ); - scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]).await; { let expected_balance = @@ -6686,8 +6786,9 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { vec![fill_order_tx_1], Amount::ZERO, 4, - ); - scan_wallet(&mut wallet1, BlockHeight::new(4), vec![block5]); + ) + .await; + scan_wallet(&mut wallet1, BlockHeight::new(4), vec![block5]).await; { let expected_balance = @@ -6762,8 +6863,9 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { vec![fill_order_tx_2], Amount::ZERO, 5, - ); - scan_wallet(&mut wallet1, BlockHeight::new(5), vec![block6]); + ) + .await; + scan_wallet(&mut wallet1, BlockHeight::new(5), vec![block6]).await; { let expected_balance = @@ -6830,8 +6932,9 @@ async fn create_order_fill_completely_conclude(#[case] seed: Seed) { vec![conclude_order_tx], Amount::ZERO, 6, - ); - scan_wallet(&mut wallet2, BlockHeight::new(6), vec![block7]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(6), vec![block7]).await; { let expected_balance = @@ -6859,8 +6962,8 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); - let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); - let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2); + let mut wallet1 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; + let mut wallet2 = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC2).await; assert_eq!(get_coin_balance(&wallet1), Amount::ZERO); assert_eq!(get_coin_balance(&wallet2), Amount::ZERO); @@ -6870,8 +6973,8 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { + chain_config.fungible_token_issuance_fee()) .unwrap(); - let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); + let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0).await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, block1_amount); @@ -6903,8 +7006,9 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { vec![token_issuance_transaction], block2_amount, 1, - ); - scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]).await; // Mint some tokens let address2 = wallet2.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; @@ -6946,8 +7050,9 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { vec![mint_transaction], Amount::from_atoms(NETWORK_FEE), 2, - ); - scan_wallet(&mut wallet1, BlockHeight::new(2), vec![block3]); + ) + .await; + scan_wallet(&mut wallet1, BlockHeight::new(2), vec![block3]).await; { let expected_balance = @@ -7010,8 +7115,9 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { vec![create_order_tx], Amount::ZERO, 3, - ); - scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]).await; { let expected_balance = @@ -7070,8 +7176,9 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { vec![fill_order_tx_1], Amount::ZERO, 4, - ); - scan_wallet(&mut wallet1, BlockHeight::new(4), vec![block5]); + ) + .await; + scan_wallet(&mut wallet1, BlockHeight::new(4), vec![block5]).await; { let expected_balance = @@ -7145,8 +7252,9 @@ async fn create_order_fill_partially_conclude(#[case] seed: Seed) { vec![conclude_order_tx], Amount::ZERO, 5, - ); - scan_wallet(&mut wallet2, BlockHeight::new(5), vec![block6]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(5), vec![block6]).await; { let expected_balance = ((block1_amount - chain_config.fungible_token_issuance_fee()) @@ -7187,8 +7295,8 @@ async fn conflicting_delegation_account_nonce(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); - let mut wallet1 = create_wallet(chain_config.clone()); - let mut wallet2 = create_wallet(chain_config.clone()); + let mut wallet1 = create_wallet(chain_config.clone()).await; + let mut wallet2 = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, Amount::ZERO); @@ -7196,8 +7304,8 @@ async fn conflicting_delegation_account_nonce(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let delegation_amount = Amount::from_atoms(rng.gen_range(10..100)); let block1_amount = (chain_config.min_stake_pool_pledge() + delegation_amount).unwrap(); - let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); + let (_, block1) = create_block(&chain_config, &mut wallet1, vec![], block1_amount, 0).await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]).await; let pool_ids = wallet1.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -7232,8 +7340,9 @@ async fn conflicting_delegation_account_nonce(#[case] seed: Seed) { vec![stake_pool_transaction.clone()], Amount::ZERO, 1, - ); - scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]).await; let coin_balance = get_coin_balance(&wallet1); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); @@ -7263,8 +7372,9 @@ async fn conflicting_delegation_account_nonce(#[case] seed: Seed) { vec![delegation_tx], Amount::ZERO, 2, - ); - scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3]).await; let mut delegations = wallet1.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); assert_eq!(delegations.len(), 1); @@ -7296,8 +7406,9 @@ async fn conflicting_delegation_account_nonce(#[case] seed: Seed) { vec![delegation_stake_tx], Amount::ZERO, 3, - ); - scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]).await; let coin_balance_after_delegating = get_coin_balance(&wallet1); assert_eq!( @@ -7381,9 +7492,10 @@ async fn conflicting_delegation_account_nonce(#[case] seed: Seed) { vec![spend_from_delegation_tx_3], Amount::ZERO, 4, - ); + ) + .await; let block5_id = block5.get_id(); - scan_wallet(&mut wallet1, BlockHeight::new(4), vec![block5]); + scan_wallet(&mut wallet1, BlockHeight::new(4), vec![block5]).await; // if confirmed tx is added conflicting txs must be removed from the output cache let mut delegations = wallet1.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); @@ -7489,7 +7601,7 @@ async fn conflicting_delegation_account_nonce_same_wallet(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -7497,7 +7609,7 @@ async fn conflicting_delegation_account_nonce_same_wallet(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let delegation_amount = Amount::from_atoms(rng.gen_range(2..100)); let block1_amount = (chain_config.min_stake_pool_pledge() + delegation_amount).unwrap(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; // Create a pool let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); @@ -7532,7 +7644,8 @@ async fn conflicting_delegation_account_nonce_same_wallet(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); @@ -7562,7 +7675,8 @@ async fn conflicting_delegation_account_nonce_same_wallet(#[case] seed: Seed) { vec![delegation_tx], Amount::ZERO, 2, - ); + ) + .await; let mut delegations = wallet.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); assert_eq!(delegations.len(), 1); @@ -7594,7 +7708,8 @@ async fn conflicting_delegation_account_nonce_same_wallet(#[case] seed: Seed) { vec![delegation_stake_tx], Amount::ZERO, 3, - ); + ) + .await; let coin_balance_after_delegating = get_coin_balance(&wallet); assert_eq!( @@ -7683,7 +7798,8 @@ async fn conflicting_delegation_account_nonce_same_wallet(#[case] seed: Seed) { vec![spend_from_delegation_tx_1], Amount::ZERO, 4, - ); + ) + .await; // Confirmed tx should replace the first one leaving the second one as descendant let mut delegations = wallet.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); @@ -7741,14 +7857,14 @@ async fn conflicting_order_account_nonce(#[case] seed: Seed) { .build(); let chain_config = Arc::new(chain_config); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet let block1_amount = chain_config.fungible_token_issuance_fee(); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; // Issue a token let address2 = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; @@ -7777,7 +7893,8 @@ async fn conflicting_order_account_nonce(#[case] seed: Seed) { vec![token_issuance_transaction], block2_amount, 1, - ); + ) + .await; // Mint some tokens let freezable = token_issuance.is_freezable.as_bool(); @@ -7821,7 +7938,8 @@ async fn conflicting_order_account_nonce(#[case] seed: Seed) { vec![mint_transaction], reward_to_spend_on_orders, 2, - ); + ) + .await; // Create an order selling tokens for coins let buy_amount = reward_to_spend_on_orders; @@ -7849,7 +7967,8 @@ async fn conflicting_order_account_nonce(#[case] seed: Seed) { vec![create_order_tx], Amount::ZERO, 3, - ); + ) + .await; let (coin_balance_after_create_order, token_balance_after_create_order) = get_currency_balances(&wallet); @@ -7957,7 +8076,8 @@ async fn conflicting_order_account_nonce(#[case] seed: Seed) { vec![fill_order_tx_1], Amount::ZERO, 4, - ); + ) + .await; // if confirmed tx is added conflicting txs must be replaced in the output cache, leaving descendants intact let mut orders = wallet.get_orders(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); @@ -8024,8 +8144,8 @@ async fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); - let mut wallet = create_wallet(chain_config.clone()); - let mut wallet2 = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; + let mut wallet2 = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -8033,8 +8153,8 @@ async fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed // Generate a new block which sends reward to the wallet let delegation_amount = Amount::from_atoms(rng.gen_range(10..100)); let block1_amount = (chain_config.min_stake_pool_pledge() + delegation_amount).unwrap(); - let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); + let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]).await; let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -8069,8 +8189,9 @@ async fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed vec![stake_pool_transaction.clone()], Amount::ZERO, 1, - ); - scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); @@ -8100,8 +8221,9 @@ async fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed vec![delegation_tx], Amount::ZERO, 2, - ); - scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3]).await; let mut delegations = wallet.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); assert_eq!(delegations.len(), 1); @@ -8134,8 +8256,9 @@ async fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed vec![delegation_stake_tx], Amount::ZERO, 3, - ); - scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4]).await; let coin_balance_after_delegating = get_coin_balance(&wallet); assert_eq!( @@ -8226,9 +8349,10 @@ async fn conflicting_delegation_account_nonce_multiple_inputs(#[case] seed: Seed vec![spend_from_delegation_tx_confirmed], Amount::ZERO, 4, - ); + ) + .await; let block5_id = block5.get_id(); - scan_wallet(&mut wallet, BlockHeight::new(4), vec![block5]); + scan_wallet(&mut wallet, BlockHeight::new(4), vec![block5]).await; // if confirmed tx is added conflicting txs must be removed from the output cache let mut delegations = wallet.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); @@ -8310,8 +8434,8 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); - let mut wallet = create_wallet(chain_config.clone()); - let mut wallet2 = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; + let mut wallet2 = create_wallet(chain_config.clone()).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -8319,8 +8443,8 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let delegation_amount = Amount::from_atoms(rng.gen_range(10..100)); let block1_amount = (chain_config.min_stake_pool_pledge() + delegation_amount).unwrap(); - let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); - scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]); + let (_, block1) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0).await; + scan_wallet(&mut wallet2, BlockHeight::new(0), vec![block1]).await; let pool_ids = wallet.get_pool_ids(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -8355,8 +8479,9 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { vec![stake_pool_transaction.clone()], Amount::ZERO, 1, - ); - scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(1), vec![block2]).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, (block1_amount - pool_amount).unwrap(),); @@ -8386,8 +8511,9 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { vec![delegation_tx], Amount::ZERO, 2, - ); - scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(2), vec![block3]).await; let mut delegations = wallet.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); assert_eq!(delegations.len(), 1); @@ -8419,8 +8545,9 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { vec![delegation_stake_tx], Amount::ZERO, 3, - ); - scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4.clone()]); + ) + .await; + scan_wallet(&mut wallet2, BlockHeight::new(3), vec![block4.clone()]).await; let coin_balance_after_delegating = get_coin_balance(&wallet); assert_eq!( @@ -8429,7 +8556,7 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { ); // Create an empty block to disconnect later and trigger unconfirmed tx removal - let (_, _) = create_block(&chain_config, &mut wallet, vec![], Amount::ZERO, 4); + let (_, _) = create_block(&chain_config, &mut wallet, vec![], Amount::ZERO, 4).await; // Add unconfirmed tx that spends from delegations let spend_from_delegation_tx_1 = wallet @@ -8462,7 +8589,7 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { assert_eq!(deleg_data.last_nonce, Some(AccountNonce::new(0))); // Reset empty block and unconfirmed tx - scan_wallet(&mut wallet, BlockHeight::new(3), vec![block4]); + scan_wallet(&mut wallet, BlockHeight::new(3), vec![block4]).await; // Create and submit tx with different tx id let withdraw_amount_2 = Amount::from_atoms(5); @@ -8486,9 +8613,10 @@ async fn conflicting_delegation_account_with_reorg(#[case] seed: Seed) { vec![spend_from_delegation_tx_2], Amount::ZERO, 4, - ); + ) + .await; let block5_id = block5.get_id(); - scan_wallet(&mut wallet, BlockHeight::new(4), vec![block5]); + scan_wallet(&mut wallet, BlockHeight::new(4), vec![block5]).await; // if confirmed tx is added, conflicting txs must be removed from the output cache let mut delegations = wallet.get_delegations(DEFAULT_ACCOUNT_INDEX).unwrap().collect_vec(); @@ -8548,7 +8676,7 @@ async fn rollback_utxos_after_abandon(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); - let mut wallet = create_wallet(chain_config.clone()); + let mut wallet = create_wallet(chain_config.clone()).await; // Generate a new block which sends reward to the wallet let utxo_amount = Amount::from_atoms(rng.gen_range(100..10000)); @@ -8572,7 +8700,7 @@ async fn rollback_utxos_after_abandon(#[case] seed: Seed) { BlockReward::new(reward_outputs), ) .unwrap(); - scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]); + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]).await; let utxos = wallet .get_utxos( @@ -8675,7 +8803,7 @@ async fn token_id_generation_v1_uses_first_tx_input(#[case] seed: Seed) { .build(), ); - let mut wallet = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC); + let mut wallet = create_wallet_with_mnemonic(chain_config.clone(), MNEMONIC).await; let coin_balance = get_coin_balance(&wallet); assert_eq!(coin_balance, Amount::ZERO); @@ -8702,7 +8830,8 @@ async fn token_id_generation_v1_uses_first_tx_input(#[case] seed: Seed) { vec![], block_amount, generated_blocks_count, - ); + ) + .await; cur_balance = (cur_balance + block_amount).unwrap(); generated_blocks_count += 1; } diff --git a/wallet/src/wallet_events.rs b/wallet/src/wallet_events.rs index 9d96bef170..7ed5000d5e 100644 --- a/wallet/src/wallet_events.rs +++ b/wallet/src/wallet_events.rs @@ -20,7 +20,7 @@ use wallet_types::WalletTx; /// Callbacks that are called when the database is updated and the UI should be re-rendered. /// For example, when a new wallet is imported and the wallet scan is in progress, /// the wallet balance and address/transaction lists should be updated after these callbacks. -pub trait WalletEvents { +pub trait WalletEvents: Sync { /// New block is scanned fn new_block(&self); diff --git a/wallet/storage/Cargo.toml b/wallet/storage/Cargo.toml index 6c28bd9e19..7fe01f88c4 100644 --- a/wallet/storage/Cargo.toml +++ b/wallet/storage/Cargo.toml @@ -28,4 +28,5 @@ rstest.workspace = true [features] trezor = ["wallet-types/trezor"] -default = ["trezor"] +ledger = ["wallet-types/ledger"] +default = ["trezor", "ledger"] diff --git a/wallet/types/Cargo.toml b/wallet/types/Cargo.toml index 7c8fac78c5..ba232458f1 100644 --- a/wallet/types/Cargo.toml +++ b/wallet/types/Cargo.toml @@ -18,7 +18,11 @@ serialization = { path = "../../serialization" } storage = { path = "../../storage" } utils = { path = "../../utils" } -bip39 = { workspace = true, default-features = false, features = ["std", "zeroize"] } +bip39 = { workspace = true, default-features = false, features = [ + "std", + "zeroize", +] } +derive_more.workspace = true hex.workspace = true itertools.workspace = true parity-scale-codec.workspace = true @@ -34,4 +38,5 @@ rstest.workspace = true [features] trezor = [] -default = ["trezor"] +ledger = [] +default = ["trezor", "ledger"] diff --git a/wallet/types/src/hw_data.rs b/wallet/types/src/hw_data.rs index 02fc47b8fd..3ace5d596e 100644 --- a/wallet/types/src/hw_data.rs +++ b/wallet/types/src/hw_data.rs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use common::primitives::semver::SemVer; use serialization::{Decode, Encode}; /// This is the data that will be stored in the wallet db. @@ -32,6 +33,7 @@ pub struct TrezorFullInfo { pub firmware_version: semver::Version, } +#[cfg(feature = "trezor")] impl From for TrezorData { fn from(info: TrezorFullInfo) -> Self { Self { @@ -41,12 +43,34 @@ impl From for TrezorData { } } +/// This is the data that will be stored in the wallet db. +#[cfg(feature = "ledger")] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct LedgerData {} + +/// All the info we may want to know about a Ledger device. +#[cfg(feature = "ledger")] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct LedgerFullInfo { + pub app_version: SemVer, +} + +#[cfg(feature = "ledger")] +impl From for LedgerData { + fn from(_value: LedgerFullInfo) -> Self { + Self {} + } +} + /// This is the data that will be stored in the wallet db. #[derive(Debug, Clone, Encode, Decode)] pub enum HardwareWalletData { #[cfg(feature = "trezor")] #[codec(index = 0)] Trezor(TrezorData), + #[cfg(feature = "ledger")] + #[codec(index = 1)] + Ledger(LedgerData), } /// All the info we may want to know about a hardware wallet. @@ -54,6 +78,8 @@ pub enum HardwareWalletData { pub enum HardwareWalletFullInfo { #[cfg(feature = "trezor")] Trezor(TrezorFullInfo), + #[cfg(feature = "ledger")] + Ledger(LedgerFullInfo), } impl From for HardwareWalletData { @@ -61,6 +87,8 @@ impl From for HardwareWalletData { match info { #[cfg(feature = "trezor")] HardwareWalletFullInfo::Trezor(trezor_data) => Self::Trezor(trezor_data.into()), + #[cfg(feature = "ledger")] + HardwareWalletFullInfo::Ledger(ledger_data) => Self::Ledger(ledger_data.into()), } } } diff --git a/wallet/types/src/wallet_type.rs b/wallet/types/src/wallet_type.rs index 2b84a0a3b5..d46fda23bc 100644 --- a/wallet/types/src/wallet_type.rs +++ b/wallet/types/src/wallet_type.rs @@ -25,6 +25,9 @@ pub enum WalletType { #[cfg(feature = "trezor")] #[codec(index = 2)] Trezor, + #[cfg(feature = "ledger")] + #[codec(index = 3)] + Ledger, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -56,6 +59,10 @@ impl WalletType { (Self::Trezor, WalletControllerMode::Hot) => true, #[cfg(feature = "trezor")] (Self::Trezor, WalletControllerMode::Cold) => false, + #[cfg(feature = "ledger")] + (Self::Ledger, WalletControllerMode::Hot) => true, + #[cfg(feature = "ledger")] + (Self::Ledger, WalletControllerMode::Cold) => false, } } } @@ -76,6 +83,8 @@ impl Display for WalletType { Self::Cold => write!(f, "Cold"), #[cfg(feature = "trezor")] Self::Trezor => write!(f, "Trezor"), + #[cfg(feature = "ledger")] + Self::Ledger => write!(f, "Ledger"), } } } diff --git a/wallet/wallet-cli-commands/Cargo.toml b/wallet/wallet-cli-commands/Cargo.toml index 55134d5354..aad5989a70 100644 --- a/wallet/wallet-cli-commands/Cargo.toml +++ b/wallet/wallet-cli-commands/Cargo.toml @@ -44,7 +44,13 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true shlex.workspace = true thiserror.workspace = true -tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { workspace = true, default-features = false, features = [ + "io-util", + "macros", + "net", + "rt", + "sync", +] } futures.workspace = true prettytable-rs = "0.10" @@ -65,4 +71,5 @@ rstest.workspace = true [features] trezor = ["wallet-types/trezor", "wallet-rpc-lib/trezor"] -default = ["trezor"] +ledger = ["wallet-types/ledger", "wallet-rpc-lib/ledger"] +default = ["trezor", "ledger"] diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index d7b333cca0..1815b477cf 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -253,6 +253,15 @@ where false, Some(HardwareWalletType::Trezor { device_id }), ), + OpenWalletSubCommand::Ledger { + wallet_path, + encryption_password, + } => ( + wallet_path, + encryption_password, + false, + Some(HardwareWalletType::Ledger), + ), }; let response = self @@ -335,6 +344,10 @@ where device_name, device_id, firmware_version ) } + WalletExtraInfo::LedgerWallet { app_version } => format!( + "This is a ledger wallet, running app version {}", + app_version + ), }; let account_names = info .account_names diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index 59672720cf..46009caa26 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -94,6 +94,18 @@ pub enum CreateWalletSubCommand { #[arg(long)] device_id: Option, }, + /// (Beta) Create a wallet using a connected Ledger wallet. + /// + /// Only the public keys will be kept in the wallet file. + /// + /// Cannot specify a mnemonic or passphrase here, both are managed on the device. + /// Depending on its configuration, the passphrase may need to be entered manually + /// each time or may be applied automatically after unlocking with a secondary PIN. + #[command()] + Ledger { + /// File path of the wallet file + wallet_path: PathBuf, + }, } impl CreateWalletSubCommand { @@ -120,6 +132,8 @@ impl CreateWalletSubCommand { wallet_path, device_id, } => (wallet_path, WalletTypeArgs::Trezor { device_id }), + + Self::Ledger { wallet_path } => (wallet_path, WalletTypeArgs::Ledger), } } } @@ -169,6 +183,18 @@ pub enum RecoverWalletSubCommand { #[arg(long)] device_id: Option, }, + /// (Beta) Recover a wallet using a connected Ledger hardware wallet. + /// + /// Only the public keys will be kept in the wallet file. + /// + /// Cannot specify a mnemonic or passphrase here, both are managed on the device. + /// Depending on its configuration, the passphrase may need to be entered manually + /// each time or may be applied automatically after unlocking with a secondary PIN. + #[command()] + Ledger { + /// File path of the wallet file + wallet_path: PathBuf, + }, } impl RecoverWalletSubCommand { @@ -195,6 +221,8 @@ impl RecoverWalletSubCommand { wallet_path, device_id, } => (wallet_path, WalletTypeArgs::Trezor { device_id }), + + Self::Ledger { wallet_path } => (wallet_path, WalletTypeArgs::Ledger), } } } @@ -227,6 +255,15 @@ pub enum OpenWalletSubCommand { #[arg(long)] device_id: Option, }, + + /// (Beta) Open a wallet file that is connected to a Ledger hardware wallet. + #[command()] + Ledger { + /// File path of the wallet file + wallet_path: PathBuf, + /// The existing password, if the wallet is encrypted. + encryption_password: Option, + }, } #[derive(Debug, Parser)] diff --git a/wallet/wallet-cli-lib/Cargo.toml b/wallet/wallet-cli-lib/Cargo.toml index 0c7836a6a6..a08641f07d 100644 --- a/wallet/wallet-cli-lib/Cargo.toml +++ b/wallet/wallet-cli-lib/Cargo.toml @@ -40,7 +40,13 @@ reedline = { workspace = true, features = ["external_printer"] } serde_json.workspace = true shlex.workspace = true thiserror.workspace = true -tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { workspace = true, default-features = false, features = [ + "io-util", + "macros", + "net", + "rt", + "sync", +] } futures.workspace = true prettytable-rs = "0.10" @@ -59,5 +65,18 @@ wallet-test-node = { path = "../wallet-test-node" } rstest.workspace = true [features] -trezor = ["wallet/trezor", "wallet-cli-commands/trezor", "wallet-types/trezor", "wallet-rpc-lib/trezor", "wallet-rpc-client/trezor"] -default = ["trezor"] +trezor = [ + "wallet/trezor", + "wallet-cli-commands/trezor", + "wallet-types/trezor", + "wallet-rpc-lib/trezor", + "wallet-rpc-client/trezor", +] +ledger = [ + "wallet/ledger", + "wallet-cli-commands/ledger", + "wallet-types/ledger", + "wallet-rpc-lib/ledger", + "wallet-rpc-client/ledger", +] +default = ["trezor", "ledger"] diff --git a/wallet/wallet-cli/Cargo.toml b/wallet/wallet-cli/Cargo.toml index b1ca0315ad..4d35b41a9d 100644 --- a/wallet/wallet-cli/Cargo.toml +++ b/wallet/wallet-cli/Cargo.toml @@ -4,7 +4,11 @@ license.workspace = true edition.workspace = true version.workspace = true rust-version.workspace = true -authors = ["Samer Afach ", "Ben Marsh ", "Enrico Rubboli "] +authors = [ + "Samer Afach ", + "Ben Marsh ", + "Enrico Rubboli ", +] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -13,8 +17,15 @@ utils = { path = "../../utils" } wallet-cli-lib = { path = "../wallet-cli-lib" } clap = { workspace = true, features = ["derive"] } -tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { workspace = true, default-features = false, features = [ + "io-util", + "macros", + "net", + "rt", + "sync", +] } [features] trezor = ["wallet-cli-lib/trezor"] -default = ["trezor"] +ledger = ["wallet-cli-lib/ledger"] +default = ["trezor", "ledger"] diff --git a/wallet/wallet-controller/Cargo.toml b/wallet/wallet-controller/Cargo.toml index 8daced808c..ca1b880809 100644 --- a/wallet/wallet-controller/Cargo.toml +++ b/wallet/wallet-controller/Cargo.toml @@ -28,7 +28,10 @@ wallet-storage = { path = "../storage" } wallet-types = { path = "../types" } async-trait.workspace = true -bip39 = { workspace = true, default-features = false, features = ["std", "zeroize"] } +bip39 = { workspace = true, default-features = false, features = [ + "std", + "zeroize", +] } ctor.workspace = true derive_more.workspace = true futures = { workspace = true, default-features = false } @@ -36,7 +39,13 @@ itertools.workspace = true serde.workspace = true strum.workspace = true thiserror.workspace = true -tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { workspace = true, default-features = false, features = [ + "io-util", + "macros", + "net", + "rt", + "sync", +] } zeroize.workspace = true [dev-dependencies] @@ -52,4 +61,5 @@ rstest.workspace = true [features] trezor = ["wallet-types/trezor"] -default = ["trezor"] +ledger = ["wallet-types/ledger"] +default = ["trezor", "ledger"] diff --git a/wallet/wallet-controller/src/helpers/tests.rs b/wallet/wallet-controller/src/helpers/tests.rs index eaad6fe667..049e114641 100644 --- a/wallet/wallet-controller/src/helpers/tests.rs +++ b/wallet/wallet-controller/src/helpers/tests.rs @@ -93,7 +93,7 @@ mod tx_to_partially_signed_tx_general_test { let chain_config = Arc::new(create_regtest()); let block_timestamp = chain_config.genesis_block().timestamp(); - let mut wallet = create_wallet_with_mnemonic(Arc::clone(&chain_config), MNEMONIC); + let mut wallet = create_wallet_with_mnemonic(Arc::clone(&chain_config), MNEMONIC).await; // Transfer to a destination belonging to the wallet. let token0_transfer_utxo_dest = wallet_new_dest(&mut wallet); @@ -235,7 +235,7 @@ mod tx_to_partially_signed_tx_general_test { let known_create_pool_outpoint = UtxoOutPoint::new(last_block_id.into(), 1); let last_height = blocks.len() as u64 + 1; - scan_wallet(&mut wallet, BlockHeight::new(0), blocks); + scan_wallet(&mut wallet, BlockHeight::new(0), blocks).await; let htlc_spend_key = Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)); let htlc_refund_key = Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)); @@ -853,7 +853,7 @@ async fn tx_to_partially_signed_tx_htlc_input_with_known_utxo_test( let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet_with_mnemonic(Arc::clone(&chain_config), MNEMONIC); + let mut wallet = create_wallet_with_mnemonic(Arc::clone(&chain_config), MNEMONIC).await; let token_id = TokenId::random_using(&mut rng); @@ -903,7 +903,8 @@ async fn tx_to_partially_signed_tx_htlc_input_with_known_utxo_test( Amount::from_atoms(rng.gen()), Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), 0, - ); + ) + .await; let last_height = 1; let node_mock = { diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index e1fb90375e..953f0810a8 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -86,6 +86,8 @@ pub use node_comm::{ rpc_client::NodeRpcClient, }; use randomness::{make_pseudo_rng, make_true_rng, Rng}; +#[cfg(feature = "ledger")] +use wallet::signer::ledger_signer::LedgerSignerProvider; #[cfg(feature = "trezor")] use wallet::signer::trezor_signer::TrezorSignerProvider; #[cfg(feature = "trezor")] @@ -120,7 +122,7 @@ use wallet_types::{ Currency, }; -#[cfg(feature = "trezor")] +#[cfg(any(feature = "trezor", feature = "ledger"))] use crate::types::WalletExtraInfo; // Note: the standard `Debug` macro is not smart enough and requires N to implement the `Debug` @@ -219,7 +221,7 @@ pub type ColdController = Controller Controller where N: NodeInterface + Clone + Send + Sync + 'static, - W: WalletEvents, + W: WalletEvents + Send + 'static, B: storage::BackendWithSendableTransactions + 'static, { pub async fn new( @@ -257,7 +259,7 @@ where } } - pub fn create_wallet( + pub async fn create_wallet( chain_config: Arc, file_path: impl AsRef, args: WalletTypeArgsComputed, @@ -288,16 +290,18 @@ where db, best_block, wallet_type, - |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( + async |db_tx| { + SoftwareSignerProvider::new_from_mnemonic( chain_config.clone(), db_tx, &mnemonic.to_string(), passphrase_ref, store_seed_phrase, - )?) + ) + .map_err(Into::into) }, ) + .await .map_err(ControllerError::WalletError) .map(|w| w.map_wallet(RuntimeWallet::Software)) } @@ -307,22 +311,35 @@ where db, best_block, wallet_type, - |_db_tx| { - Ok(TrezorSignerProvider::new( + async |_db_tx| { + TrezorSignerProvider::new( device_id.map(|device_id| SelectedDevice { device_id }), ) - .map_err(SignerError::TrezorError)?) + .map_err(SignerError::TrezorError) + .map_err(Into::into) }, ) + .await .map_err(ControllerError::WalletError) .map(|w| w.map_wallet(RuntimeWallet::Trezor)), + #[cfg(feature = "ledger")] + WalletTypeArgsComputed::Ledger => wallet::Wallet::create_new_wallet( + Arc::clone(&chain_config), + db, + best_block, + wallet_type, + async |_db_tx| LedgerSignerProvider::new().await.map_err(Into::into), + ) + .await + .map_err(ControllerError::WalletError) + .map(|w| w.map_wallet(RuntimeWallet::Ledger)), }; Self::delete_wallet_file_on_wallet_creation_failure(&res, file_path); res } - pub fn recover_wallet( + pub async fn recover_wallet( chain_config: Arc, file_path: impl AsRef, args: WalletTypeArgsComputed, @@ -351,16 +368,18 @@ where Arc::clone(&chain_config), db, wallet_type, - |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( + async |db_tx| { + SoftwareSignerProvider::new_from_mnemonic( chain_config.clone(), db_tx, &mnemonic.to_string(), passphrase_ref, store_seed_phrase, - )?) + ) + .map_err(Into::into) }, ) + .await .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Software)) } @@ -370,16 +389,30 @@ where Arc::clone(&chain_config), db, wallet_type, - |_db_tx| { - Ok(TrezorSignerProvider::new( + async |_db_tx| { + TrezorSignerProvider::new( device_id.map(|device_id| SelectedDevice { device_id }), ) - .map_err(SignerError::TrezorError)?) + .map_err(SignerError::TrezorError) + .map_err(Into::into) }, ) + .await .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Trezor)) } + #[cfg(feature = "ledger")] + WalletTypeArgsComputed::Ledger => { + let wallet = wallet::Wallet::recover_wallet( + Arc::clone(&chain_config), + db, + wallet_type, + async |_db_tx| LedgerSignerProvider::new().await.map_err(Into::into), + ) + .await + .map_err(ControllerError::WalletError)?; + Ok(wallet.map_wallet(RuntimeWallet::Ledger)) + } }; Self::delete_wallet_file_on_wallet_creation_failure(&res, file_path); @@ -435,7 +468,7 @@ where Ok(()) } - pub fn open_wallet( + pub async fn open_wallet( chain_config: Arc, file_path: impl AsRef, password: Option, @@ -464,8 +497,11 @@ where |version| Self::make_backup_wallet_file(file_path.as_ref(), version), current_controller_mode, force_change_wallet_type, - |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), + async |db_tx| { + SoftwareSignerProvider::load_from_database(chain_config.clone(), &db_tx) + }, ) + .await .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Software)) } @@ -478,17 +514,36 @@ where |version| Self::make_backup_wallet_file(file_path.as_ref(), version), current_controller_mode, force_change_wallet_type, - |db_tx| { + async |db_tx| { TrezorSignerProvider::load_from_database( chain_config.clone(), - db_tx, + &db_tx, device_id, ) }, ) + .await .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Trezor)) } + #[cfg(feature = "ledger")] + WalletType::Ledger => { + let wallet = wallet::Wallet::load_wallet( + Arc::clone(&chain_config), + db, + password, + |version| Self::make_backup_wallet_file(file_path.as_ref(), version), + current_controller_mode, + force_change_wallet_type, + async |mut db_tx| { + LedgerSignerProvider::load_from_database(chain_config.clone(), &mut db_tx) + .await + }, + ) + .await + .map_err(ControllerError::WalletError)?; + Ok(wallet.map_wallet(RuntimeWallet::Ledger)) + } } } @@ -580,6 +635,10 @@ where device_id: trezor_info.device_id, firmware_version: trezor_info.firmware_version.to_string(), }, + #[cfg(feature = "ledger")] + HardwareWalletFullInfo::Ledger(ledger_data) => WalletExtraInfo::LedgerWallet { + app_version: ledger_data.app_version.to_string(), + }, }, None => WalletExtraInfo::SoftwareWallet, }; @@ -747,11 +806,14 @@ where .map_err(|err| ControllerError::SearchForTimestampsFailed(err)) } - pub fn create_account( + pub async fn create_account( &mut self, name: Option, ) -> Result<(U31, Option), ControllerError> { - self.wallet.create_next_account(name).map_err(ControllerError::WalletError) + self.wallet + .create_next_account(name) + .await + .map_err(ControllerError::WalletError) } pub fn update_account_name( @@ -835,6 +897,10 @@ where RuntimeWallet::Trezor(w) => { sync::sync_once(&self.chain_config, &self.rpc_client, w, &self.wallet_events).await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + sync::sync_once(&self.chain_config, &self.rpc_client, w, &self.wallet_events).await + } }?; match res { @@ -854,6 +920,11 @@ where sync::sync_once(&self.chain_config, &self.rpc_client, w, &self.wallet_events) .await?; } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + sync::sync_once(&self.chain_config, &self.rpc_client, w, &self.wallet_events) + .await?; + } } Ok(()) diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index 4b91db6617..54435898c3 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -64,6 +64,8 @@ use wallet_types::{ Currency, KeyPurpose, KeychainUsageState, SignedTxWithFees, }; +#[cfg(feature = "ledger")] +use wallet::signer::ledger_signer::LedgerSignerProvider; #[cfg(feature = "trezor")] use wallet::signer::trezor_signer::TrezorSignerProvider; @@ -72,6 +74,8 @@ pub enum RuntimeWallet { Software(Wallet), #[cfg(feature = "trezor")] Trezor(Wallet), + #[cfg(feature = "ledger")] + Ledger(Wallet), } impl RuntimeWallet @@ -91,6 +95,10 @@ where RuntimeWallet::Trezor(w) => { w.find_unspent_utxo_and_destination(input, htlc_spending_condition) } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.find_unspent_utxo_and_destination(input, htlc_spending_condition) + } } } @@ -99,6 +107,8 @@ where RuntimeWallet::Software(w) => w.find_account_destination(acc_outpoint), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.find_account_destination(acc_outpoint), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.find_account_destination(acc_outpoint), } } @@ -107,6 +117,8 @@ where RuntimeWallet::Software(w) => w.find_account_command_destination(cmd), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.find_account_command_destination(cmd), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.find_account_command_destination(cmd), } } @@ -118,6 +130,8 @@ where RuntimeWallet::Software(w) => w.find_order_account_command_destination(cmd), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.find_order_account_command_destination(cmd), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.find_order_account_command_destination(cmd), } } @@ -126,6 +140,8 @@ where RuntimeWallet::Software(w) => w.seed_phrase(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.seed_phrase(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.seed_phrase(), } } @@ -134,6 +150,8 @@ where RuntimeWallet::Software(w) => w.delete_seed_phrase(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.delete_seed_phrase(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.delete_seed_phrase(), } } @@ -142,6 +160,8 @@ where RuntimeWallet::Software(w) => w.reset_wallet_to_genesis(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.reset_wallet_to_genesis(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.reset_wallet_to_genesis(), } } @@ -150,6 +170,8 @@ where RuntimeWallet::Software(w) => w.encrypt_wallet(password), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.encrypt_wallet(password), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.encrypt_wallet(password), } } @@ -158,6 +180,8 @@ where RuntimeWallet::Software(w) => w.unlock_wallet(password), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.unlock_wallet(password), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.unlock_wallet(password), } } @@ -166,6 +190,8 @@ where RuntimeWallet::Software(w) => w.lock_wallet(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.lock_wallet(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.lock_wallet(), } } @@ -178,6 +204,8 @@ where RuntimeWallet::Software(w) => w.set_lookahead_size(lookahead_size, force_reduce), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.set_lookahead_size(lookahead_size, force_reduce), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.set_lookahead_size(lookahead_size, force_reduce), } } @@ -186,6 +214,8 @@ where RuntimeWallet::Software(w) => w.wallet_info(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.wallet_info(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.wallet_info(), } } @@ -194,17 +224,21 @@ where RuntimeWallet::Software(w) => w.hardware_wallet_info(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.hardware_wallet_info(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.hardware_wallet_info(), } } - pub fn create_next_account( + pub async fn create_next_account( &mut self, name: Option, ) -> Result<(U31, Option), WalletError> { match self { - RuntimeWallet::Software(w) => w.create_next_account(name), + RuntimeWallet::Software(w) => w.create_next_account(name).await, #[cfg(feature = "trezor")] - RuntimeWallet::Trezor(w) => w.create_next_account(name), + RuntimeWallet::Trezor(w) => w.create_next_account(name).await, + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.create_next_account(name).await, } } @@ -217,6 +251,8 @@ where RuntimeWallet::Software(w) => w.set_account_name(account_index, name), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.set_account_name(account_index, name), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.set_account_name(account_index, name), } } @@ -229,6 +265,8 @@ where RuntimeWallet::Software(w) => w.get_pos_gen_block_data(account_index, pool_id), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(_) => Err(WalletError::UnsupportedHardwareWalletOperation), } } @@ -240,6 +278,8 @@ where RuntimeWallet::Software(w) => w.get_pos_gen_block_data_by_pool_id(pool_id), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(_) => Err(WalletError::UnsupportedHardwareWalletOperation), } } @@ -252,6 +292,8 @@ where RuntimeWallet::Software(w) => w.get_pool_ids(account_index, filter), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_pool_ids(account_index, filter), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_pool_ids(account_index, filter), } } @@ -260,6 +302,8 @@ where RuntimeWallet::Software(w) => w.get_best_block(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_best_block(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_best_block(), } } @@ -271,6 +315,8 @@ where RuntimeWallet::Software(w) => w.get_best_block_for_account(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_best_block_for_account(account_index), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_best_block_for_account(account_index), } } @@ -279,6 +325,8 @@ where RuntimeWallet::Software(w) => w.is_locked(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.is_locked(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.is_locked(), } } @@ -297,6 +345,10 @@ where RuntimeWallet::Trezor(w) => { w.get_utxos(account_index, utxo_types, utxo_states, with_locked) } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.get_utxos(account_index, utxo_types, utxo_states, with_locked) + } } } @@ -307,6 +359,8 @@ where RuntimeWallet::Software(w) => w.get_transactions_to_be_broadcast(), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_transactions_to_be_broadcast(), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_transactions_to_be_broadcast(), } } @@ -320,6 +374,8 @@ where RuntimeWallet::Software(w) => w.get_balance(account_index, utxo_states, with_locked), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_balance(account_index, utxo_states, with_locked), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_balance(account_index, utxo_states, with_locked), } } @@ -338,6 +394,10 @@ where RuntimeWallet::Trezor(w) => { w.get_multisig_utxos(account_index, utxo_types, utxo_states, with_locked) } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.get_multisig_utxos(account_index, utxo_types, utxo_states, with_locked) + } } } @@ -349,6 +409,8 @@ where RuntimeWallet::Software(w) => w.pending_transactions(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.pending_transactions(account_index), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.pending_transactions(account_index), } } @@ -364,6 +426,8 @@ where } #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.mainchain_transactions(account_index, destination, limit), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.mainchain_transactions(account_index, destination, limit), } } @@ -377,6 +441,8 @@ where RuntimeWallet::Software(w) => w.get_transaction_list(account_index, skip, count), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_transaction_list(account_index, skip, count), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_transaction_list(account_index, skip, count), } } @@ -389,6 +455,8 @@ where RuntimeWallet::Software(w) => w.get_transaction(account_index, transaction_id), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_transaction(account_index, transaction_id), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_transaction(account_index, transaction_id), } } @@ -401,6 +469,8 @@ where RuntimeWallet::Software(w) => w.get_all_issued_addresses(account_index, key_purpose), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_all_issued_addresses(account_index, key_purpose), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_all_issued_addresses(account_index, key_purpose), } } @@ -420,6 +490,12 @@ where UtxoState::Confirmed.into(), WithLocked::Unlocked, ), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_address_coin_balances( + account_index, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ), } } @@ -431,6 +507,8 @@ where RuntimeWallet::Software(w) => w.get_all_issued_vrf_public_keys(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(_) => Err(WalletError::UnsupportedHardwareWalletOperation), } } @@ -442,6 +520,8 @@ where RuntimeWallet::Software(w) => w.get_legacy_vrf_public_key(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(_) => Err(WalletError::UnsupportedHardwareWalletOperation), } } @@ -454,6 +534,8 @@ where RuntimeWallet::Software(w) => w.get_addresses_usage(account_index, key_purpose), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_addresses_usage(account_index, key_purpose), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_addresses_usage(account_index, key_purpose), } } @@ -465,6 +547,8 @@ where RuntimeWallet::Software(w) => w.get_all_standalone_addresses(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_all_standalone_addresses(account_index), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_all_standalone_addresses(account_index), } } @@ -485,6 +569,10 @@ where RuntimeWallet::Trezor(w) => { w.get_all_standalone_address_details(account_index, address) } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.get_all_standalone_address_details(account_index, address) + } } } @@ -496,6 +584,8 @@ where RuntimeWallet::Software(w) => w.get_created_blocks(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_created_blocks(account_index), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_created_blocks(account_index), } } @@ -508,6 +598,8 @@ where RuntimeWallet::Software(w) => w.find_used_tokens(account_index, input_utxos), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.find_used_tokens(account_index, input_utxos), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.find_used_tokens(account_index, input_utxos), } } @@ -520,6 +612,8 @@ where RuntimeWallet::Software(w) => w.get_token_unconfirmed_info(account_index, token_info), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_token_unconfirmed_info(account_index, token_info), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_token_unconfirmed_info(account_index, token_info), } } @@ -532,6 +626,8 @@ where RuntimeWallet::Software(w) => w.abandon_transaction(account_index, tx_id), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.abandon_transaction(account_index, tx_id), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.abandon_transaction(account_index, tx_id), } } @@ -549,6 +645,10 @@ where RuntimeWallet::Trezor(w) => { w.standalone_address_label_rename(account_index, address, label) } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.standalone_address_label_rename(account_index, address, label) + } } } @@ -562,6 +662,8 @@ where RuntimeWallet::Software(w) => w.add_standalone_address(account_index, address, label), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.add_standalone_address(account_index, address, label), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.add_standalone_address(account_index, address, label), } } @@ -579,6 +681,10 @@ where RuntimeWallet::Trezor(w) => { w.add_standalone_private_key(account_index, private_key, label) } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.add_standalone_private_key(account_index, private_key, label) + } } } @@ -594,6 +700,8 @@ where } #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.add_standalone_multisig(account_index, challenge, label), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.add_standalone_multisig(account_index, challenge, label), } } @@ -605,6 +713,8 @@ where RuntimeWallet::Software(w) => w.get_new_address(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_new_address(account_index), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_new_address(account_index), } } @@ -617,6 +727,8 @@ where RuntimeWallet::Software(w) => w.find_public_key(account_index, address), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.find_public_key(account_index, address), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.find_public_key(account_index, address), } } @@ -628,6 +740,8 @@ where RuntimeWallet::Software(w) => w.account_extended_public_key(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.account_extended_public_key(account_index), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.account_extended_public_key(account_index), } } @@ -639,6 +753,8 @@ where RuntimeWallet::Software(w) => w.get_vrf_key(account_index), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(_) => Err(WalletError::UnsupportedHardwareWalletOperation), } } @@ -669,6 +785,16 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.issue_new_token( + account_index, + token_issuance, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -702,6 +828,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.issue_new_nft( + account_index, + address, + metadata, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -738,6 +875,18 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.mint_tokens( + account_index, + token_info, + amount, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -771,6 +920,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.unmint_tokens( + account_index, + token_info, + amount, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -801,6 +961,16 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.lock_token_supply( + account_index, + token_info, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -834,6 +1004,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.freeze_token( + account_index, + token_info, + is_token_unfreezable, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -864,6 +1045,16 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.unfreeze_token( + account_index, + token_info, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -897,6 +1088,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.change_token_authority( + account_index, + token_info, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -930,6 +1132,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.change_token_metadata_uri( + account_index, + token_info, + metadata_uri, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -970,6 +1183,19 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_transaction_to_addresses( + account_index, + outputs, + inputs, + change_addresses, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .await + } } } @@ -1003,6 +1229,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_sweep_transaction( + account_index, + destination_address, + filtered_inputs, + current_fee_rate, + additional_info, + ) + .await + } } } @@ -1015,6 +1252,8 @@ where RuntimeWallet::Software(w) => w.get_delegation(account_index, delegation_id), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_delegation(account_index, delegation_id), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_delegation(account_index, delegation_id), } } @@ -1048,6 +1287,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_sweep_from_delegation_transaction( + account_index, + destination_address, + delegation_id, + delegation_share, + current_fee_rate, + ) + .await + } } } @@ -1085,6 +1335,17 @@ where consolidate_fee_rate, ptx_additional_info, ), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.create_unsigned_transaction_to_addresses( + account_index, + outputs, + selected_inputs, + selection_algo, + change_addresses, + current_fee_rate, + consolidate_fee_rate, + ptx_additional_info, + ), } } @@ -1115,6 +1376,16 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_delegation( + account_index, + vec![output], + current_fee_rate, + consolidate_fee_rate, + ) + .await + } } } @@ -1151,6 +1422,18 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_transaction_to_addresses_from_delegation( + account_index, + address, + amount, + delegation_id, + delegation_share, + current_fee_rate, + ) + .await + } } } @@ -1181,6 +1464,16 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_stake_pool_with_vrf_key( + account_index, + current_fee_rate, + consolidate_fee_rate, + stake_pool_arguments, + ) + .await + } } } @@ -1214,6 +1507,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.decommission_stake_pool( + account_index, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ) + .await + } } } @@ -1247,6 +1551,17 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.decommission_stake_pool_request( + account_index, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ) + .await + } } } @@ -1283,6 +1598,18 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_htlc_tx( + account_index, + output_value, + htlc, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .await + } } } @@ -1323,6 +1650,19 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_order_tx( + account_index, + ask_value, + give_value, + conclude_key, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .await + } } } @@ -1363,6 +1703,19 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_conclude_order_tx( + account_index, + order_id, + order_info, + output_address, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .await + } } } @@ -1406,6 +1759,20 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_fill_order_tx( + account_index, + order_id, + order_info, + fill_amount_in_ask_currency, + output_address, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .await + } } } @@ -1442,6 +1809,18 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_freeze_order_tx( + account_index, + order_id, + order_info, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .await + } } } @@ -1463,6 +1842,10 @@ where RuntimeWallet::Trezor(w) => { w.sign_raw_transaction(account_index, ptx, tokens_additional_info).await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.sign_raw_transaction(account_index, ptx, tokens_additional_info).await + } } } @@ -1480,6 +1863,10 @@ where RuntimeWallet::Trezor(w) => { w.sign_challenge(account_index, challenge, destination).await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.sign_challenge(account_index, challenge, destination).await + } } } @@ -1523,6 +1910,20 @@ where ) .await } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.create_transaction_to_addresses_with_intent( + account_index, + outputs, + inputs, + change_addresses, + intent, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .await + } } } @@ -1535,6 +1936,8 @@ where RuntimeWallet::Software(w) => w.add_unconfirmed_tx(tx, wallet_events), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.add_unconfirmed_tx(tx, wallet_events), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.add_unconfirmed_tx(tx, wallet_events), } } @@ -1552,6 +1955,10 @@ where RuntimeWallet::Trezor(w) => { w.add_account_unconfirmed_tx(account_index, tx.clone(), wallet_events) } + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => { + w.add_account_unconfirmed_tx(account_index, tx.clone(), wallet_events) + } } } @@ -1567,6 +1974,10 @@ where RuntimeWallet::Trezor(w) => w .get_delegations(account_index) .map(|it| -> Box> { Box::new(it) }), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w + .get_delegations(account_index) + .map(|it| -> Box> { Box::new(it) }), } } } diff --git a/wallet/wallet-controller/src/sync/mod.rs b/wallet/wallet-controller/src/sync/mod.rs index bfe7c5af4b..67dc8adefc 100644 --- a/wallet/wallet-controller/src/sync/mod.rs +++ b/wallet/wallet-controller/src/sync/mod.rs @@ -15,6 +15,8 @@ use std::{cmp::Reverse, collections::BTreeMap, iter}; +use async_trait::async_trait; + use common::{ chain::{block::timestamp::BlockTimestamp, Block, ChainConfig, GenBlock}, primitives::{BlockHeight, Id}, @@ -32,6 +34,7 @@ use crate::ControllerError; const MAX_FETCH_BLOCK_COUNT: usize = 100; +#[async_trait] pub trait SyncingWallet { fn syncing_state(&self) -> WalletSyncingState; @@ -40,19 +43,20 @@ pub trait SyncingWallet { account: U31, common_block_height: BlockHeight, blocks: Vec, - wallet_events: &impl WalletEvents, + wallet_events: &(impl WalletEvents + Send), ) -> WalletResult<()>; - fn scan_blocks_for_unused_account( + async fn scan_blocks_for_unused_account( &mut self, common_block_height: BlockHeight, blocks: Vec, - wallet_events: &impl WalletEvents, + wallet_events: &(impl WalletEvents + Send), ) -> WalletResult<()>; fn update_median_time(&mut self, median_time: BlockTimestamp) -> WalletResult<()>; } +#[async_trait] impl SyncingWallet for Wallet where B: storage::BackendWithSendableTransactions + 'static, @@ -67,18 +71,19 @@ where account: U31, common_block_height: BlockHeight, blocks: Vec, - wallet_events: &impl WalletEvents, + wallet_events: &(impl WalletEvents + Send), ) -> WalletResult<()> { self.scan_new_blocks(account, common_block_height, blocks, wallet_events) } - fn scan_blocks_for_unused_account( + async fn scan_blocks_for_unused_account( &mut self, common_block_height: BlockHeight, blocks: Vec, - wallet_events: &impl WalletEvents, + wallet_events: &(impl WalletEvents + Send), ) -> WalletResult<()> { self.scan_new_blocks_unused_account(common_block_height, blocks, wallet_events) + .await } fn update_median_time(&mut self, median_time: BlockTimestamp) -> WalletResult<()> { @@ -122,7 +127,7 @@ pub async fn sync_once( chain_config: &ChainConfig, rpc_client: &T, wallet: &mut impl SyncingWallet, - wallet_events: &impl WalletEvents, + wallet_events: &(impl WalletEvents + Send + 'static), ) -> Result> { let mut print_flag = SetFlag::new(); let mut _log_on_exit = None; @@ -224,7 +229,7 @@ async fn fetch_and_sync_to_next_group( mut next_group_accounts: Vec, rpc_client: &T, wallet: &mut impl SyncingWallet, - wallet_events: &impl WalletEvents, + wallet_events: &(impl WalletEvents + Send + 'static), ) -> Result<(NextBlockInfo, Vec), ControllerError> { let block_to_fetch = (next_group_block_info.common_block_height - current.0.common_block_height) .expect("already sorted") @@ -241,7 +246,7 @@ async fn fetch_and_sync( block_to_fetch: usize, rpc_client: &T, wallet: &mut impl SyncingWallet, - wallet_events: &impl WalletEvents, + wallet_events: &(impl WalletEvents + Send + 'static), ) -> Result<(), ControllerError> { let FetchedBlocks { blocks, @@ -260,20 +265,21 @@ async fn fetch_and_sync( common_block_height, blocks.clone(), wallet_events, - )?; + ) + .await?; } Ok(()) } -fn scan_new_blocks( +async fn scan_new_blocks( acc: &AccountType, new_height: u64, block_id: Id, wallet: &mut impl SyncingWallet, common_block_height: BlockHeight, blocks: Vec, - wallet_events: &impl WalletEvents, + wallet_events: &(impl WalletEvents + Send + 'static), ) -> Result<(), ControllerError> { match acc { AccountType::Account(account) => { @@ -296,6 +302,7 @@ fn scan_new_blocks( wallet .scan_blocks_for_unused_account(common_block_height, blocks, wallet_events) + .await .map_err(ControllerError::WalletError)?; } } diff --git a/wallet/wallet-controller/src/sync/tests/mod.rs b/wallet/wallet-controller/src/sync/tests/mod.rs index 2119443cd5..a70e2c6aaa 100644 --- a/wallet/wallet-controller/src/sync/tests/mod.rs +++ b/wallet/wallet-controller/src/sync/tests/mod.rs @@ -19,6 +19,10 @@ use std::{ time::Duration, }; +use futures::executor::block_on; +use rstest::rstest; +use tokio::sync::mpsc; + use blockprod::TimestampSearchData; use chainstate::ChainInfo; use chainstate_test_framework::TestFramework; @@ -31,7 +35,6 @@ use common::{ }; use consensus::GenerateBlockInputData; use crypto::ephemeral_e2e::EndToEndPublicKey; -use futures::executor::block_on; use logging::log; use mempool::{tx_accumulator::PackingStrategy, FeeRate}; use mempool_types::tx_options::TxOptionsOverrides; @@ -41,9 +44,7 @@ use node_comm::{ }; use p2p_types::{bannable_address::BannableAddress, socket_address::SocketAddress}; use randomness::{seq::IteratorRandom, CryptoRng, Rng}; -use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; -use tokio::sync::mpsc; use utils_networking::IpOrSocketAddress; use wallet::wallet_events::WalletEventsNoOp; use wallet_types::{account_info::DEFAULT_ACCOUNT_INDEX, wallet_type::WalletControllerMode}; @@ -90,6 +91,7 @@ impl MockWallet { } } +#[async_trait::async_trait] impl SyncingWallet for MockWallet { fn syncing_state(&self) -> WalletSyncingState { WalletSyncingState { @@ -109,7 +111,7 @@ impl SyncingWallet for MockWallet { account: U31, common_block_height: BlockHeight, blocks: Vec, - _wallet_events: &impl WalletEvents, + _wallet_events: &(impl WalletEvents + Send), ) -> WalletResult<()> { assert!(account == DEFAULT_ACCOUNT_INDEX); assert!(!blocks.is_empty()); @@ -131,7 +133,7 @@ impl SyncingWallet for MockWallet { )) .await }) - .unwrap(); + .unwrap() } log::debug!( @@ -143,11 +145,11 @@ impl SyncingWallet for MockWallet { Ok(()) } - fn scan_blocks_for_unused_account( + async fn scan_blocks_for_unused_account( &mut self, common_block_height: BlockHeight, blocks: Vec, - _wallet_events: &impl WalletEvents, + _wallet_events: &(impl WalletEvents + Send), ) -> WalletResult<()> { assert!(!blocks.is_empty()); assert!( @@ -163,12 +165,10 @@ impl SyncingWallet for MockWallet { self.get_unused_acc_best_block_id() ); self.next_unused_blocks.push(block.header().block_id()); - block_on(async { - self.new_tip_tx - .send((AccountType::UnusedAccount, block.header().block_id())) - .await - }) - .unwrap(); + self.new_tip_tx + .send((AccountType::UnusedAccount, block.header().block_id())) + .await + .unwrap() } log::debug!( diff --git a/wallet/wallet-controller/src/tests/compose_transaction_tests.rs b/wallet/wallet-controller/src/tests/compose_transaction_tests.rs index 3dfe5fa7de..5f0fa536c0 100644 --- a/wallet/wallet-controller/src/tests/compose_transaction_tests.rs +++ b/wallet/wallet-controller/src/tests/compose_transaction_tests.rs @@ -67,7 +67,7 @@ async fn general_test(#[case] seed: Seed, #[case] use_htlc_secret: bool) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); - let mut wallet = create_wallet_with_mnemonic(Arc::clone(&chain_config), MNEMONIC); + let mut wallet = create_wallet_with_mnemonic(Arc::clone(&chain_config), MNEMONIC).await; let token1_id = TokenId::random_using(&mut rng); let token2_id = TokenId::random_using(&mut rng); @@ -98,7 +98,8 @@ async fn general_test(#[case] seed: Seed, #[case] use_htlc_secret: bool) { block_reward_amount, Destination::PublicKeyHash(PublicKeyHash::random_using(&mut rng)), 0, - ); + ) + .await; let last_height = 1; let token1_outpoint = UtxoOutPoint::new(tx_with_token1_id.into(), 0); diff --git a/wallet/wallet-controller/src/tests/test_utils.rs b/wallet/wallet-controller/src/tests/test_utils.rs index 9c1e4162a5..459701d833 100644 --- a/wallet/wallet-controller/src/tests/test_utils.rs +++ b/wallet/wallet-controller/src/tests/test_utils.rs @@ -162,7 +162,7 @@ pub fn tx_with_outputs(outputs: Vec) -> SignedTransaction { SignedTransaction::new(Transaction::new(0, vec![], outputs).unwrap(), Vec::new()).unwrap() } -pub fn create_block_scan_wallet( +pub async fn create_block_scan_wallet( chain_config: &ChainConfig, wallet: &mut Wallet, transactions: Vec, @@ -186,7 +186,7 @@ where ) .unwrap(); - scan_wallet(wallet, BlockHeight::new(block_height), vec![block.clone()]); + scan_wallet(wallet, BlockHeight::new(block_height), vec![block.clone()]).await; block } diff --git a/wallet/wallet-controller/src/types/mod.rs b/wallet/wallet-controller/src/types/mod.rs index 53e310f84c..a46113c5ff 100644 --- a/wallet/wallet-controller/src/types/mod.rs +++ b/wallet/wallet-controller/src/types/mod.rs @@ -68,6 +68,10 @@ pub enum WalletExtraInfo { // Note: semver::Version is not serializable, so we can't use it here. firmware_version: String, }, + #[cfg(feature = "ledger")] + LedgerWallet { + app_version: String, + }, } impl rpc_description::HasValueHint for WalletExtraInfo { @@ -192,6 +196,8 @@ pub enum WalletTypeArgs { }, #[cfg(feature = "trezor")] Trezor { device_id: Option }, + #[cfg(feature = "ledger")] + Ledger, } #[derive(Debug, Clone, Copy)] @@ -212,6 +218,8 @@ impl WalletTypeArgs { } => controller_mode.into(), #[cfg(feature = "trezor")] Self::Trezor { device_id: _ } => WalletType::Trezor, + #[cfg(feature = "trezor")] + Self::Ledger => WalletType::Ledger, } } @@ -254,6 +262,11 @@ impl WalletTypeArgs { WalletTypeArgsComputed::Trezor { device_id }, CreatedWallet::UserProvidedMnemonic, )), + #[cfg(feature = "ledger")] + Self::Ledger => Ok(( + WalletTypeArgsComputed::Ledger, + CreatedWallet::UserProvidedMnemonic, + )), } } } @@ -266,6 +279,8 @@ pub enum WalletTypeArgsComputed { }, #[cfg(feature = "trezor")] Trezor { device_id: Option }, + #[cfg(feature = "ledger")] + Ledger, } pub enum SweepFromAddresses { diff --git a/wallet/wallet-node-client/Cargo.toml b/wallet/wallet-node-client/Cargo.toml index 2bf1366b8f..afb2c84780 100644 --- a/wallet/wallet-node-client/Cargo.toml +++ b/wallet/wallet-node-client/Cargo.toml @@ -29,7 +29,13 @@ base64.workspace = true mockall.workspace = true serde_json.workspace = true thiserror.workspace = true -tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { workspace = true, default-features = false, features = [ + "io-util", + "macros", + "net", + "rt", + "sync", +] } tower.workspace = true [dev-dependencies] @@ -37,4 +43,5 @@ chainstate-storage = { path = "../../chainstate/storage" } [features] trezor = ["wallet-types/trezor"] -default = ["trezor"] +ledger = ["wallet-types/ledger"] +default = ["trezor", "ledger"] diff --git a/wallet/wallet-rpc-client/Cargo.toml b/wallet/wallet-rpc-client/Cargo.toml index 764ee2f1d2..f019a9a22f 100644 --- a/wallet/wallet-rpc-client/Cargo.toml +++ b/wallet/wallet-rpc-client/Cargo.toml @@ -36,8 +36,25 @@ tower.workspace = true [dev-dependencies] chainstate-storage = { path = "../../chainstate/storage" } -tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } +tokio = { workspace = true, default-features = false, features = [ + "io-util", + "macros", + "net", + "rt", + "sync", +] } [features] -trezor = ["wallet/trezor", "wallet-types/trezor", "wallet-rpc-lib/trezor", "wallet-controller/trezor"] -default = ["trezor"] +trezor = [ + "wallet/trezor", + "wallet-types/trezor", + "wallet-rpc-lib/trezor", + "wallet-controller/trezor", +] +ledger = [ + "wallet/ledger", + "wallet-types/ledger", + "wallet-rpc-lib/ledger", + "wallet-controller/ledger", +] +default = ["trezor", "ledger"] diff --git a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs index eaca9d165c..57122b7b99 100644 --- a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs @@ -103,6 +103,8 @@ impl WalletInterface for ClientWalletRpc { false, Some(HardwareWalletType::Trezor { device_id }), ), + #[cfg(feature = "ledger")] + WalletTypeArgs::Ledger => (None, None, false, Some(HardwareWalletType::Ledger)), }; ColdWalletRpcClient::create_wallet( @@ -135,6 +137,8 @@ impl WalletInterface for ClientWalletRpc { false, Some(HardwareWalletType::Trezor { device_id }), ), + #[cfg(feature = "ledger")] + WalletTypeArgs::Ledger => (None, None, false, Some(HardwareWalletType::Ledger)), }; ColdWalletRpcClient::recover_wallet( diff --git a/wallet/wallet-rpc-daemon/Cargo.toml b/wallet/wallet-rpc-daemon/Cargo.toml index 23f3126f7d..784b1b9797 100644 --- a/wallet/wallet-rpc-daemon/Cargo.toml +++ b/wallet/wallet-rpc-daemon/Cargo.toml @@ -4,7 +4,11 @@ license.workspace = true edition.workspace = true version.workspace = true rust-version.workspace = true -authors = ["Samer Afach ", "Ben Marsh ", "Enrico Rubboli "] +authors = [ + "Samer Afach ", + "Ben Marsh ", + "Enrico Rubboli ", +] [dependencies] @@ -26,4 +30,5 @@ expect-test.workspace = true [features] trezor = ["wallet-rpc-lib/trezor"] -default = ["trezor"] +ledger = ["wallet-rpc-lib/ledger"] +default = ["trezor", "ledger"] diff --git a/wallet/wallet-rpc-daemon/docs/RPC.md b/wallet/wallet-rpc-daemon/docs/RPC.md index 94461690f3..4da550f13a 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -3241,7 +3241,8 @@ Parameters: 1) string 2) null }, } - 2) null, + 2) { "type": "Ledger" } + 3) null, } ``` @@ -3291,7 +3292,8 @@ Parameters: 1) string 2) null }, } - 2) null, + 2) { "type": "Ledger" } + 3) null, } ``` @@ -3338,7 +3340,8 @@ Parameters: 1) string 2) null }, } - 2) null, + 2) { "type": "Ledger" } + 3) null, } ``` diff --git a/wallet/wallet-rpc-lib/Cargo.toml b/wallet/wallet-rpc-lib/Cargo.toml index 5419863cff..c01ae081d0 100644 --- a/wallet/wallet-rpc-lib/Cargo.toml +++ b/wallet/wallet-rpc-lib/Cargo.toml @@ -43,7 +43,7 @@ tokio.workspace = true consensus = { path = "../../consensus" } mempool = { path = "../../mempool" } -rpc = { path = "../../rpc", features = [ "test-support" ] } +rpc = { path = "../../rpc", features = ["test-support"] } subsystem = { path = "../../subsystem" } test-utils = { path = "../../test-utils" } wallet-test-node = { path = "../wallet-test-node" } @@ -53,4 +53,5 @@ rstest.workspace = true [features] trezor = ["wallet-types/trezor", "wallet/trezor", "wallet-controller/trezor"] -default = ["trezor"] +ledger = ["wallet-types/ledger", "wallet/ledger", "wallet-controller/ledger"] +default = ["trezor", "ledger"] diff --git a/wallet/wallet-rpc-lib/src/lib.rs b/wallet/wallet-rpc-lib/src/lib.rs index 74824e1bc3..54ef2bff95 100644 --- a/wallet/wallet-rpc-lib/src/lib.rs +++ b/wallet/wallet-rpc-lib/src/lib.rs @@ -18,7 +18,7 @@ pub mod config; mod rpc; mod service; -#[cfg(feature = "trezor")] +#[cfg(any(feature = "trezor", feature = "ledger"))] use rpc::types::HardwareWalletType; pub use rpc::{ types, ColdWalletRpcClient, ColdWalletRpcDescription, ColdWalletRpcServer, RpcCreds, RpcError, @@ -26,7 +26,7 @@ pub use rpc::{ }; pub use service::{Event, EventStream, TxState, WalletHandle, /* WalletResult, */ WalletService,}; use wallet_controller::{NodeInterface, NodeRpcClient}; -#[cfg(feature = "trezor")] +#[cfg(any(feature = "trezor", feature = "ledger"))] use wallet_types::wallet_type::WalletType; use std::{fmt::Debug, time::Duration}; @@ -108,6 +108,8 @@ where |hw| match hw { #[cfg(feature = "trezor")] HardwareWalletType::Trezor { device_id: _ } => WalletType::Trezor, + #[cfg(feature = "ledger")] + HardwareWalletType::Ledger => WalletType::Ledger, }, ); // Start the wallet service diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index bbe69eb854..2970df6fe5 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -89,7 +89,7 @@ use wallet_types::{ use crate::{WalletHandle, WalletRpcConfig}; -#[cfg(feature = "trezor")] +#[cfg(any(feature = "trezor", feature = "ledger"))] use wallet_types::wallet_type::WalletType; pub use self::types::RpcError; @@ -160,6 +160,8 @@ where match hw { #[cfg(feature = "trezor")] HardwareWalletType::Trezor { device_id } => (WalletType::Trezor, device_id), + #[cfg(feature = "ledger")] + HardwareWalletType::Ledger => (WalletType::Ledger, None), } }); Ok(self @@ -277,7 +279,10 @@ where } pub async fn create_account(&self, name: Option) -> WRpcResult { - let (num, name) = self.wallet.call(|w| w.create_account(name)).await??; + let (num, name) = self + .wallet + .call_async(move |w| Box::pin(async move { w.create_account(name).await })) + .await??; Ok(NewAccountInfo::new(num, name)) } diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 14003c39ac..215a5f1178 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -1048,6 +1048,8 @@ pub enum RpcCurrency { pub enum HardwareWalletType { #[cfg(feature = "trezor")] Trezor { device_id: Option }, + #[cfg(feature = "ledger")] + Ledger, } impl HardwareWalletType { @@ -1091,6 +1093,8 @@ impl HardwareWalletType { HardwareWalletType::Trezor { device_id } => { Ok(WalletTypeArgs::Trezor { device_id }) } + #[cfg(feature = "ledger")] + HardwareWalletType::Ledger => Ok(WalletTypeArgs::Ledger), } } } diff --git a/wallet/wallet-rpc-lib/src/service/mod.rs b/wallet/wallet-rpc-lib/src/service/mod.rs index 3bbafa4b5b..a6ecfa729b 100644 --- a/wallet/wallet-rpc-lib/src/service/mod.rs +++ b/wallet/wallet-rpc-lib/src/service/mod.rs @@ -77,7 +77,8 @@ where force_change_wallet_type, *open_as_wallet_type, None, - )? + ) + .await? .wallet()? }; diff --git a/wallet/wallet-rpc-lib/src/service/worker.rs b/wallet/wallet-rpc-lib/src/service/worker.rs index ba7f3c09cf..bc83f9af3f 100644 --- a/wallet/wallet-rpc-lib/src/service/worker.rs +++ b/wallet/wallet-rpc-lib/src/service/worker.rs @@ -172,7 +172,8 @@ where force_migrate_wallet_type, open_as_wallet_type, device_id, - )?; + ) + .await?; let wallet = match wallet { wallet::wallet::WalletCreation::Wallet(w) => w, @@ -227,6 +228,7 @@ where wallet_type, options.overwrite_wallet_file, ) + .await } else { WalletController::recover_wallet( self.chain_config.clone(), @@ -234,6 +236,7 @@ where computed_args, wallet_type, ) + .await } .map_err(RpcError::Controller)?; diff --git a/wallet/wallet-rpc-lib/tests/utils.rs b/wallet/wallet-rpc-lib/tests/utils.rs index d87b670239..ea7722cfa2 100644 --- a/wallet/wallet-rpc-lib/tests/utils.rs +++ b/wallet/wallet-rpc-lib/tests/utils.rs @@ -70,16 +70,18 @@ impl TestFramework { db, (BlockHeight::new(0), chain_config.genesis_block_id()), WalletType::Hot, - |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( + async |db_tx| { + SoftwareSignerProvider::new_from_mnemonic( chain_config.clone(), db_tx, wallet_test_node::MNEMONIC, None, StoreSeedPhrase::DoNotStore, - )?) + ) + .map_err(Into::into) }, ) + .await .unwrap(); wallet_path