From 72f898cc85af14c64bad3f8e82a62cc618ad80dd Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Wed, 23 Jul 2025 19:24:04 +0200 Subject: [PATCH 01/24] Add ledger signer --- .github/workflows/build.yml | 33 +- .github/workflows/code_checks.yml | 10 +- Cargo.lock | 2272 +++++++++++------ Cargo.toml | 164 +- wallet/Cargo.toml | 21 +- .../signer/ledger_signer/ledger_messages.rs | 319 +++ wallet/src/signer/ledger_signer/mod.rs | 1092 ++++++++ .../ledger_signer/speculus/drivers/mod.rs | 245 ++ .../signer/ledger_signer/speculus/handle.rs | 133 + .../src/signer/ledger_signer/speculus/mod.rs | 139 + wallet/src/signer/ledger_signer/tests.rs | 383 +++ wallet/src/signer/mod.rs | 8 + wallet/src/signer/trezor_signer/mod.rs | 6 + wallet/types/Cargo.toml | 8 +- wallet/types/src/hw_data.rs | 7 + 15 files changed, 3901 insertions(+), 939 deletions(-) create mode 100644 wallet/src/signer/ledger_signer/ledger_messages.rs create mode 100644 wallet/src/signer/ledger_signer/mod.rs create mode 100644 wallet/src/signer/ledger_signer/speculus/drivers/mod.rs create mode 100644 wallet/src/signer/ledger_signer/speculus/handle.rs create mode 100644 wallet/src/signer/ledger_signer/speculus/mod.rs create mode 100644 wallet/src/signer/ledger_signer/tests.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 231a81ecba..4259ecb08d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: branches: - "**" # target all branches schedule: - - cron: '15 0 * * *' # every day at 00:15 UTC + - cron: "15 0 * * *" # every day at 00:15 UTC env: CARGO_TERM_COLOR: always @@ -27,7 +27,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version-file: './build-tools/.python-version' + python-version-file: "./build-tools/.python-version" - name: Install Rust # Use bash to be able to escape the newline via '\'. @@ -76,12 +76,12 @@ jobs: run: sudo apt-get update - name: Install build dependencies - run: sudo apt-get install -yqq --no-install-recommends build-essential podman pkg-config libssl-dev + run: sudo apt-get install -yqq --no-install-recommends build-essential podman pkg-config libssl-dev libdbus-1-dev libusb-1.0-0-dev - name: Setup Python uses: actions/setup-python@v6 with: - python-version-file: './build-tools/.python-version' + python-version-file: "./build-tools/.python-version" - name: Install Rust run: | @@ -125,7 +125,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version-file: './build-tools/.python-version' + python-version-file: "./build-tools/.python-version" - name: Install Rust run: | @@ -162,9 +162,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@v5 with: @@ -180,7 +180,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version-file: './mintlayer-core/build-tools/.python-version' + python-version-file: "./mintlayer-core/build-tools/.python-version" - name: Extract required info from Cargo.toml id: extract_cargo_info @@ -240,7 +240,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version-file: './mintlayer-core/build-tools/.python-version' + python-version-file: "./mintlayer-core/build-tools/.python-version" - name: Extract required info from Cargo.toml id: extract_cargo_info @@ -281,13 +281,12 @@ 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 diff --git a/.github/workflows/code_checks.yml b/.github/workflows/code_checks.yml index b0276295fc..767b51b13e 100644 --- a/.github/workflows/code_checks.yml +++ b/.github/workflows/code_checks.yml @@ -8,7 +8,7 @@ on: branches: - "**" # target all branches 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,12 +28,12 @@ jobs: run: sudo apt-get update - name: Install build dependencies - run: sudo apt-get install -yqq --no-install-recommends build-essential + run: sudo apt-get install -yqq --no-install-recommends build-essential libdbus-1-dev libusb-1.0-0-dev - name: Setup Python uses: actions/setup-python@v6 with: - python-version-file: './build-tools/.python-version' + python-version-file: "./build-tools/.python-version" - name: Install Rust run: | @@ -69,7 +69,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version-file: './build-tools/.python-version' + python-version-file: "./build-tools/.python-version" - name: Install Rust # Use bash to be able to escape the newline via '\'. @@ -102,7 +102,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v6 with: - python-version-file: './build-tools/.python-version' + python-version-file: "./build-tools/.python-version" - name: Install Rust run: | diff --git a/Cargo.lock b/Cargo.lock index 5f096f5d5c..d3bb9a64f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -134,17 +134,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.4", + "bitflags 2.10.0", "cc", "cesu8", - "jni", + "jni 0.21.1", "jni-sys", "libc", "log", "ndk", "ndk-context", "ndk-sys 0.6.0+11769913", - "num_enum", + "num_enum 0.7.5", "thiserror 1.0.69", ] @@ -201,29 +201,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "api-blockchain-scanner-daemon" @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "ashpd" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" dependencies = [ "enumflags2", "futures-channel", @@ -451,18 +451,17 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-protocols", - "zbus 5.11.0", + "zbus 5.13.2", ] [[package]] name = "assert_cmd" -version = "2.0.17" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates", "predicates-core", @@ -532,16 +531,16 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.2", + "rustix 1.1.3", "slab", "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", @@ -563,7 +562,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.2", + "rustix 1.1.3", ] [[package]] @@ -574,7 +573,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -589,7 +588,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.2", + "rustix 1.1.3", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -609,7 +608,16 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", ] [[package]] @@ -634,10 +642,10 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "itoa", "matchit 0.7.3", @@ -667,8 +675,8 @@ dependencies = [ "axum-core 0.5.6", "bytes", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "itoa", "matchit 0.8.4", @@ -692,8 +700,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -712,8 +720,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -728,10 +736,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" dependencies = [ - "bitcoin-internals 0.3.0", - "bitcoin_hashes 0.14.0", + "bitcoin-internals", + "bitcoin_hashes", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -746,9 +760,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bb8" @@ -782,15 +796,15 @@ checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" [[package]] name = "bech32" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bigdecimal" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" dependencies = [ "autocfg", "libm", @@ -810,11 +824,11 @@ dependencies = [ [[package]] name = "bip39" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes", "serde", "unicode-normalization", "zeroize", @@ -852,17 +866,17 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitcoin" -version = "0.32.7" +version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda569d741b895131a88ee5589a467e73e9c4718e958ac9308e4f7dc44b6945" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", - "bech32 0.11.0", - "bitcoin-internals 0.3.0", + "bech32 0.11.1", + "bitcoin-internals", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes 0.14.0", - "hex-conservative 0.2.1", + "bitcoin_hashes", + "hex-conservative", "hex_lit", "secp256k1", ] @@ -876,12 +890,6 @@ dependencies = [ "bech32 0.9.1", ] -[[package]] -name = "bitcoin-internals" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" - [[package]] name = "bitcoin-internals" version = "0.3.0" @@ -890,9 +898,9 @@ checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-units" @@ -900,27 +908,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ - "bitcoin-internals 0.3.0", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" -dependencies = [ - "bitcoin-internals 0.2.0", - "hex-conservative 0.1.2", + "bitcoin-internals", ] [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", - "hex-conservative 0.2.1", + "hex-conservative", ] [[package]] @@ -931,11 +929,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1044,11 +1042,80 @@ dependencies = [ "utils", ] +[[package]] +name = "bluez-async" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce7d4413c940e8e3cb6afc122d3f4a07096aca259d286781128683fc9f39d9b" +dependencies = [ + "async-trait", + "bitflags 2.10.0", + "bluez-generated", + "dbus", + "dbus-tokio", + "futures", + "itertools 0.10.5", + "log", + "serde", + "serde-xml-rs", + "thiserror 1.0.69", + "tokio", + "uuid", +] + +[[package]] +name = "bluez-generated" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d1c659dbc82f0b8ca75606c91a371e763589b7f6acf36858eeed0c705afe367" +dependencies = [ + "dbus", +] + +[[package]] +name = "bollard" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af254ed2da4936ef73309e9597180558821cb16ae9bba4cb24ce6b612d8d80ed" +dependencies = [ + "base64 0.21.7", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http 0.2.12", + "hyper 0.14.32", + "hyperlocal", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.42.0-rc.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602bda35f33aeb571cef387dcd4042c643a8bf689d8aaac2cc47ea24cb7bc7e0" +dependencies = [ + "serde", + "serde_with 2.3.3", +] + [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", "cfg_aliases 0.2.1", @@ -1056,33 +1123,60 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", "serde", ] +[[package]] +name = "btleplug" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790e133b9d27f6dbd1289e046e735cef97fb0b4c840f18db52c959521dfb8145" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bluez-async", + "cocoa", + "dashmap", + "dbus", + "futures", + "jni 0.19.0", + "jni-utils", + "libc", + "log", + "objc", + "once_cell", + "static_assertions", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "uuid", + "windows 0.48.0", +] + [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "by_address" @@ -1098,11 +1192,12 @@ checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "byte-unit" -version = "5.1.6" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" dependencies = [ "rust_decimal", + "schemars 1.2.1", "serde", "utf8-width", ] @@ -1131,9 +1226,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -1146,7 +1241,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -1167,7 +1262,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "log", "polling", "rustix 0.38.44", @@ -1175,18 +1270,43 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "calloop" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +dependencies = [ + "bitflags 2.10.0", + "polling", + "rustix 1.1.3", + "slab", + "tracing", +] + [[package]] name = "calloop-wayland-source" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ - "calloop", + "calloop 0.13.0", "rustix 0.38.44", "wayland-backend", "wayland-client", ] +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.3", + "rustix 1.1.3", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cast" version = "0.3.0" @@ -1195,9 +1315,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.41" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -1448,16 +1568,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1500,9 +1620,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.49" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -1510,33 +1630,33 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.49" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "clipboard-win" @@ -1569,14 +1689,44 @@ dependencies = [ [[package]] name = "clipboard_x11" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" +checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" dependencies = [ - "thiserror 1.0.69", + "thiserror 2.0.18", "x11rb", ] +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types", + "libc", + "objc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1639,7 +1789,7 @@ name = "common" version = "1.2.0" dependencies = [ "anyhow", - "bech32 0.11.0", + "bech32 0.11.1", "bitcoin-bech32", "chrono", "clap", @@ -1671,7 +1821,7 @@ dependencies = [ "serde", "serde_json", "serde_test", - "serde_with", + "serde_with 3.16.1", "serial_test", "serialization", "smallvec", @@ -1829,26 +1979,26 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.23.2" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "core-graphics-types", + "foreign-types 0.3.2", "libc", ] [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.1", - "core-graphics-types 0.2.0", + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", "foreign-types 0.5.0", "libc", ] @@ -1864,24 +2014,13 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.1", - "libc", -] - [[package]] name = "cosmic-text" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "fontdb", "log", "rangemap", @@ -1952,6 +2091,12 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam" version = "0.8.4" @@ -2014,7 +2159,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crossterm_winapi", "mio", "parking_lot 0.12.5", @@ -2077,9 +2222,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -2088,21 +2233,21 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] @@ -2114,14 +2259,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "ctor-lite" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" +checksum = "b29fccfdaeb0f9bd90da5759b1d0eaa2f6cfcfe90960124cfc83141ed4e111fd" [[package]] name = "ctr" @@ -2162,7 +2307,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -2171,7 +2316,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libloading 0.8.9", "winapi", ] @@ -2192,14 +2337,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.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[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]] @@ -2212,8 +2381,19 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", - "syn 2.0.106", + "strsim 0.11.1", + "syn 2.0.114", +] + +[[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]] @@ -2222,16 +2402,53 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", "quote", - "syn 2.0.106", + "syn 2.0.114", +] + +[[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.12", ] [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "dbus" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[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" @@ -2241,9 +2458,9 @@ checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", "serde_core", @@ -2260,11 +2477,11 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ - "derive_more-impl 2.0.1", + "derive_more-impl 2.1.1", ] [[package]] @@ -2276,19 +2493,20 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "unicode-xid", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "rustc_version", + "syn 2.0.114", "unicode-xid", ] @@ -2389,7 +2607,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.6.2", "libc", "objc2 0.6.3", @@ -2403,7 +2621,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -2460,12 +2678,6 @@ dependencies = [ "utils-networking", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "downcast" version = "0.11.0" @@ -2486,22 +2698,23 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "drm" -version = "0.12.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "drm-ffi", "drm-fourcc", + "libc", "rustix 0.38.44", ] [[package]] name = "drm-ffi" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" dependencies = [ "drm-sys", "rustix 0.38.44", @@ -2515,9 +2728,9 @@ checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" [[package]] name = "drm-sys" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" dependencies = [ "libc", "linux-raw-sys 0.6.5", @@ -2535,6 +2748,51 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encdec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25de94e10baa85551f7c65730423239370ed5bed60bf8d2a9cbf2683327ba421" +dependencies = [ + "encdec-base 0.9.0", + "encdec-macros", +] + +[[package]] +name = "encdec-base" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8542ff2a35da7fc94ffcf280f35dc759219c4b48fa930e0a0f268220d7fb6a" +dependencies = [ + "byteorder", + "num-traits", +] + +[[package]] +name = "encdec-base" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516ae3c7d00515548bf26a6531883335ceac2e9cde4938e70feea7456569be09" +dependencies = [ + "byteorder", + "heapless", + "num-traits", + "thiserror 1.0.69", +] + +[[package]] +name = "encdec-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15497932aae6b53bf8548cc63c65929b4fab6be54e28709c80fc72f5707eeed" +dependencies = [ + "darling 0.14.4", + "encdec-base 0.8.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -2552,9 +2810,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "endian-type" @@ -2571,7 +2829,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -2591,7 +2849,7 @@ checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -2612,7 +2870,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -2655,9 +2913,9 @@ dependencies = [ [[package]] name = "euclid" -version = "0.22.11" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" dependencies = [ "num-traits", ] @@ -2730,7 +2988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.59.0", ] @@ -2759,11 +3017,22 @@ dependencies = [ "flate2", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fix-hidden-lifetime-bug" @@ -2799,9 +3068,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -2883,7 +3152,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -3009,7 +3278,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -3050,16 +3319,17 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" dependencies = [ "cc", "cfg-if", "libc", "log", "rustversion", - "windows 0.61.3", + "windows-link", + "windows-result", ] [[package]] @@ -3078,8 +3348,8 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.2", - "windows-link 0.2.1", + "rustix 1.1.3", + "windows-link", ] [[package]] @@ -3095,9 +3365,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -3201,7 +3471,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gpu-alloc-types", ] @@ -3211,7 +3481,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -3233,7 +3503,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gpu-descriptor-types", "hashbrown 0.14.5", ] @@ -3244,7 +3514,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -3259,17 +3529,36 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http", - "indexmap 2.11.4", + "http 1.4.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -3287,6 +3576,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3315,6 +3613,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" @@ -3330,7 +3634,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "com", "libc", "libloading 0.8.9", @@ -3352,6 +3656,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -3375,18 +3692,15 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" - -[[package]] -name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] @@ -3472,6 +3786,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "hidapi" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "565dd4c730b8f8b2c0fb36df6be12e5470ae10895ddcc4e9dcfbfb495de202b0" +dependencies = [ + "cc", + "cfg-if", + "libc", + "pkg-config", + "windows-sys 0.48.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3483,15 +3810,36 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", "itoa", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -3499,7 +3847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -3510,8 +3858,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -3535,17 +3883,41 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.7.0" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -3562,8 +3934,8 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", - "hyper", + "http 1.4.0", + "hyper 1.8.1", "hyper-util", "log", "rustls", @@ -3579,7 +3951,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -3594,7 +3966,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "native-tls", "tokio", @@ -3604,23 +3976,22 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", - "http", - "http-body", - "hyper", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.2", "system-configuration", "tokio", "tower-service", @@ -3628,11 +3999,24 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "hyperlocal" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" +dependencies = [ + "futures-util", + "hex", + "hyper 0.14.32", + "pin-project", + "tokio", +] + [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3687,7 +4071,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0013a238275494641bf8f1732a23a808196540dc67b22ff97099c044ae4c8a1c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "dark-light", "glam", @@ -3744,7 +4128,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba25a18cfa6d5cc160aca7e1b34f73ccdff21680fa8702168c09739767b6c66f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "cosmic-text", "half", @@ -3807,7 +4191,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15708887133671d2bcc6c1d01d1f176f43a64d6cdc3b2bf893396c3ee498295f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "futures", "glam", @@ -3860,9 +4244,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -3873,9 +4257,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -3886,11 +4270,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -3901,42 +4284,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -3979,7 +4358,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -3995,21 +4374,24 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", "serde_core", ] [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "inout" @@ -4037,9 +4419,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -4047,20 +4429,20 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -4091,9 +4473,23 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] [[package]] name = "jni" @@ -4117,6 +4513,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", + "futures", + "jni 0.19.0", + "log", + "once_cell", + "static_assertions", + "uuid", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4129,9 +4540,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -4161,14 +4572,14 @@ checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" dependencies = [ "base64 0.22.1", "futures-util", - "http", + "http 1.4.0", "jsonrpsee-core", "pin-project", "rustls", "rustls-pki-types", "rustls-platform-verifier", "soketto", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-rustls", "tokio-util", @@ -4186,8 +4597,8 @@ dependencies = [ "bytes", "futures-timer", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "jsonrpsee-types", "parking_lot 0.12.5", @@ -4196,7 +4607,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tower", @@ -4210,8 +4621,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" dependencies = [ "base64 0.22.1", - "http-body", - "hyper", + "http-body 1.0.1", + "hyper 1.8.1", "hyper-rustls", "hyper-util", "jsonrpsee-core", @@ -4220,7 +4631,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tower", "url", @@ -4236,7 +4647,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -4246,10 +4657,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" dependencies = [ "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -4258,7 +4669,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -4272,10 +4683,10 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" dependencies = [ - "http", + "http 1.4.0", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4284,7 +4695,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" dependencies = [ - "http", + "http 1.4.0", "jsonrpsee-client-transport", "jsonrpsee-core", "jsonrpsee-types", @@ -4334,11 +4745,55 @@ 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=4be10b810bb6500b7d8bc0b67e64fef24257e0e8#4be10b810bb6500b7d8bc0b67e64fef24257e0e8" +dependencies = [ + "async-trait", + "btleplug", + "displaydoc", + "encdec", + "futures", + "hidapi", + "ledger-proto", + "once_cell", + "strum 0.24.1", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "ledger-proto" +version = "0.1.0" +source = "git+https://github.com/ledger-community/rust-ledger.git?rev=4be10b810bb6500b7d8bc0b67e64fef24257e0e8#4be10b810bb6500b7d8bc0b67e64fef24257e0e8" +dependencies = [ + "bitflags 2.10.0", + "displaydoc", + "encdec", + "hex", + "num_enum 0.6.1", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] [[package]] name = "libloading" @@ -4357,24 +4812,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", - "redox_syscall 0.5.18", + "redox_syscall 0.7.0", ] [[package]] @@ -4432,9 +4887,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lmdb-mintlayer" @@ -4470,9 +4925,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "log_error" @@ -4482,7 +4937,7 @@ dependencies = [ "logging", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "tracing", "utils", ] @@ -4539,9 +4994,9 @@ dependencies = [ [[package]] name = "lyon_geom" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" +checksum = "e260b6de923e6e47adfedf6243013a7a874684165a6a277594ee3906021b2343" dependencies = [ "arrayvec", "euclid", @@ -4611,15 +5066,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -4715,9 +5170,9 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", - "core-graphics-types 0.1.3", + "core-graphics-types", "foreign-types 0.5.0", "log", "objc", @@ -4770,7 +5225,7 @@ name = "mintlayer-core-primitives" version = "1.0.0" source = "git+https://github.com/mintlayer/mintlayer-core-primitives?rev=4e21bf85e7fdca1577a49df7599b2b39ec913287#4e21bf85e7fdca1577a49df7599b2b39ec913287" dependencies = [ - "derive_more 2.0.1", + "derive_more 2.1.1", "fixed-hash", "parity-scale-codec", "strum 0.27.2", @@ -4826,14 +5281,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4859,7 +5314,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -4890,10 +5345,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" dependencies = [ "bit-set 0.5.3", - "bitflags 2.9.4", + "bitflags 2.10.0", "codespan-reporting", "hexf-parse", - "indexmap 2.11.4", + "indexmap 2.13.0", "log", "num-traits", "rustc-hash 1.1.0", @@ -4912,7 +5367,7 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", "security-framework 2.11.1", @@ -4926,11 +5381,11 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", - "num_enum", + "num_enum 0.7.5", "raw-window-handle", "thiserror 1.0.69", ] @@ -4999,20 +5454,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "cfg_aliases 0.2.1", - "libc", - "memoffset", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -5116,7 +5558,7 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_with", + "serde_with 3.16.1", "serialization", "subsystem", "test-utils", @@ -5221,9 +5663,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -5233,7 +5675,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -5299,24 +5741,44 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.7.5", "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -5360,14 +5822,14 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", "objc2-core-image", "objc2-foundation 0.2.2", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", ] [[package]] @@ -5376,7 +5838,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.6.2", "objc2 0.6.3", "objc2-foundation 0.3.2", @@ -5388,7 +5850,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -5412,7 +5874,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -5424,9 +5886,22 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", "dispatch2", "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -5465,7 +5940,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "dispatch", "libc", @@ -5478,7 +5953,18 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -5501,7 +5987,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -5513,13 +5999,25 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", ] +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-symbols" version = "0.2.2" @@ -5536,7 +6034,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -5545,7 +6043,7 @@ dependencies = [ "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -5568,7 +6066,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -5592,9 +6090,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oneshot" @@ -5616,11 +6114,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.74" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -5637,7 +6135,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -5646,11 +6144,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -5666,10 +6170,11 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.48" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" dependencies = [ + "libc", "libredox", ] @@ -5732,7 +6237,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -5784,7 +6289,7 @@ dependencies = [ "rstest", "serde", "serialization", - "siphasher 1.0.1", + "siphasher 1.0.2", "storage", "storage-inmemory", "subsystem", @@ -5871,7 +6376,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -5899,7 +6404,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -5953,7 +6458,7 @@ dependencies = [ "libc", "redox_syscall 0.5.18", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -6019,7 +6524,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -6028,7 +6533,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.1", + "siphasher 1.0.2", ] [[package]] @@ -6037,7 +6542,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 1.0.1", + "siphasher 1.0.2", ] [[package]] @@ -6057,7 +6562,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -6140,7 +6645,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -6193,9 +6698,9 @@ dependencies = [ [[package]] name = "postgres-protocol" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" dependencies = [ "base64 0.22.1", "byteorder", @@ -6211,9 +6716,9 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4605b7c057056dd35baeb6ac0c0338e4975b1f2bef0f65da953285eb007095" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" dependencies = [ "bytes", "fallible-iterator 0.2.0", @@ -6222,9 +6727,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -6310,14 +6815,14 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -6330,7 +6835,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "version_check", "yansi", ] @@ -6349,7 +6854,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.9.4", + "bitflags 2.10.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -6381,7 +6886,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -6415,9 +6920,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.150" +version = "2.1.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c569a9577fe28cc82ac9969ec31778511f1912b3468fe236c24393bebf8a571" +checksum = "b033d75bca9da25cfdcd9528de22ed7870d1695b9e1c3ce55b7127a4a2b16fac" dependencies = [ "psl-types", ] @@ -6462,18 +6967,18 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -6531,7 +7036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -6561,7 +7066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -6579,14 +7084,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -6633,9 +7138,9 @@ checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" [[package]] name = "rangemap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "raw-window-handle" @@ -6697,7 +7202,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", ] [[package]] @@ -6706,7 +7220,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] @@ -6749,14 +7263,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -6766,9 +7280,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -6777,9 +7291,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "relative-path" @@ -6804,19 +7318,19 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", - "h2", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-rustls", "hyper-tls", "hyper-util", @@ -6834,7 +7348,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tower", - "tower-http 0.6.7", + "tower-http 0.6.8", "tower-service", "url", "wasm-bindgen", @@ -6874,7 +7388,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -6891,9 +7405,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ "bitvec", "bytecheck", @@ -6909,9 +7423,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", @@ -6948,8 +7462,8 @@ dependencies = [ "base64 0.22.1", "crypto", "expect-test", - "http", - "hyper", + "http 1.4.0", + "hyper 1.8.1", "jsonrpsee", "logging", "randomness", @@ -6983,7 +7497,7 @@ version = "1.2.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -7025,7 +7539,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.106", + "syn 2.0.114", "unicode-ident", ] @@ -7045,7 +7559,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "fallible-iterator 0.3.0", "fallible-streaming-iterator", "hashlink", @@ -7065,9 +7579,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" dependencies = [ "arrayvec", "borsh", @@ -7112,7 +7626,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -7121,11 +7635,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -7134,9 +7648,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "log", "once_cell", @@ -7149,11 +7663,11 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -7161,9 +7675,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] @@ -7176,7 +7690,7 @@ checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", "log", "once_cell", "rustls", @@ -7197,9 +7711,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -7230,7 +7744,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "libm", "smallvec", @@ -7243,9 +7757,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -7288,9 +7802,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -7357,7 +7871,7 @@ dependencies = [ "ab_glyph", "log", "memmap2", - "smithay-client-toolkit", + "smithay-client-toolkit 0.19.2", "tiny-skia", ] @@ -7379,7 +7893,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.14.0", + "bitcoin_hashes", "rand 0.8.5", "secp256k1-sys", ] @@ -7399,7 +7913,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -7412,7 +7926,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -7431,9 +7945,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "semver" @@ -7462,6 +7976,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror 1.0.69", + "xml-rs", +] + [[package]] name = "serde_bytes" version = "0.11.19" @@ -7489,7 +8015,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -7500,20 +8026,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -7535,7 +8061,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -7570,17 +8096,32 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.0" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "indexmap 1.9.3", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "serde_with" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -7589,23 +8130,24 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "serial_test" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot 0.12.5", @@ -7615,13 +8157,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -7666,7 +8208,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -7739,9 +8281,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -7750,18 +8292,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simdutf8" @@ -7777,9 +8320,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skrifa" @@ -7793,9 +8336,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slave-pool" @@ -7808,9 +8351,9 @@ dependencies = [ [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "version_check", ] @@ -7827,9 +8370,9 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.4", - "calloop", - "calloop-wayland-source", + "bitflags 2.10.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", "cursor-icon", "libc", "log", @@ -7846,14 +8389,41 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.10.0", + "calloop 0.14.3", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.3", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + [[package]] name = "smithay-clipboard" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" dependencies = [ "libc", - "smithay-client-toolkit", + "smithay-client-toolkit 0.20.0", "wayland-backend", ] @@ -7910,9 +8480,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -7920,33 +8490,33 @@ dependencies = [ [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "as-raw-xcb-connection", "bytemuck", - "cfg_aliases 0.2.1", - "core-graphics 0.24.0", "drm", "fastrand", - "foreign-types 0.5.0", "js-sys", - "log", "memmap2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core", + "ndk", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", "raw-window-handle", "redox_syscall 0.5.18", - "rustix 0.38.44", + "rustix 1.1.3", "tiny-xlib", + "tracing", "wasm-bindgen", "wayland-backend", "wayland-client", "wayland-sys", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", "x11rb", ] @@ -7959,20 +8529,29 @@ dependencies = [ "base64 0.22.1", "bytes", "futures", - "http", + "http 1.4.0", "httparse", "log", "rand 0.8.5", "sha1", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -8105,12 +8684,27 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros 0.24.3", +] + [[package]] name = "strum" version = "0.26.3" @@ -8129,6 +8723,19 @@ dependencies = [ "strum_macros 0.27.2", ] +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "strum_macros" version = "0.26.4" @@ -8139,7 +8746,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -8151,7 +8758,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -8207,9 +8814,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -8233,7 +8840,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -8247,11 +8854,11 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -8272,16 +8879,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -8372,11 +8990,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -8387,18 +9005,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -8412,30 +9030,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -8482,9 +9100,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -8545,7 +9163,7 @@ dependencies = [ "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.2", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -8558,7 +9176,7 @@ source = "git+https://github.com/tokio-rs/tokio?rev=0d6c7af3e43457350bdc03a6dbca dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -8573,9 +9191,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b40d66d9b2cfe04b628173409368e58247e8eddbbd3b0e6c6ba1d09f20f6c9e" +checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" dependencies = [ "async-trait", "byteorder", @@ -8591,7 +9209,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.9.2", - "socket2 0.6.1", + "socket2 0.6.2", "tokio", "tokio-util", "whoami", @@ -8621,9 +9239,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -8633,9 +9251,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -8677,9 +9295,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -8690,7 +9308,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -8700,21 +9318,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.3", + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -8735,16 +9353,16 @@ dependencies = [ "axum 0.8.8", "base64 0.22.1", "bytes", - "h2", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-timeout", "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.1", + "socket2 0.6.2", "sync_wrapper", "tokio", "tokio-stream", @@ -8767,13 +9385,13 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.11.4", + "indexmap 2.13.0", "pin-project-lite", "slab", "sync_wrapper", @@ -8791,10 +9409,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "base64 0.21.7", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -8804,15 +9422,15 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "iri-string", "pin-project-lite", "tower", @@ -8834,9 +9452,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -8846,20 +9464,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -8888,9 +9506,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -8918,7 +9536,7 @@ dependencies = [ "mintlayer-firmware-deps", "protobuf", "rusb", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "unicode-normalization", ] @@ -8951,7 +9569,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -9014,7 +9632,7 @@ version = "1.2.0" dependencies = [ "itertools 0.14.0", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -9060,9 +9678,9 @@ checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-linebreak" @@ -9072,24 +9690,24 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-script" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" @@ -9127,14 +9745,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -9145,9 +9764,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf8-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" [[package]] name = "utf8_iter" @@ -9201,7 +9820,7 @@ dependencies = [ "itertools 0.14.0", "rpc-description", "serde_test", - "serde_with", + "serde_with 3.16.1", "test-utils", "thiserror 1.0.69", "tokio", @@ -9227,11 +9846,12 @@ dependencies = [ [[package]] name = "uuid" -version = "1.18.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "js-sys", + "serde_core", "wasm-bindgen", ] @@ -9285,10 +9905,14 @@ dependencies = [ name = "wallet" version = "1.2.0" dependencies = [ + "anyhow", "async-trait", "bip39", + "bollard", + "bytes", "chainstate", "chainstate-test-framework", + "clap", "common", "consensus", "crypto", @@ -9296,12 +9920,15 @@ dependencies = [ "hex", "itertools 0.14.0", "lazy_static", + "ledger-lib", + "ledger-proto", "logging", "mempool", "orders-accounting", "parity-scale-codec", "pos-accounting", "randomness", + "reqwest", "rpc-description", "rstest", "secp256k1", @@ -9312,10 +9939,12 @@ dependencies = [ "serialization", "storage", "strum 0.26.3", + "tar", "tempfile", "test-utils", "thiserror 1.0.69", "tokio", + "tracing", "trezor-client", "tx-verifier", "utils", @@ -9685,26 +10314,38 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasite" -version = "0.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -9713,27 +10354,14 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -9742,9 +10370,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9752,22 +10380,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -9778,7 +10406,7 @@ version = "1.2.0" dependencies = [ "anyhow", "clap", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -9805,7 +10433,7 @@ dependencies = [ "consensus", "crypto", "fixed-hash", - "getrandom 0.2.16", + "getrandom 0.2.17", "gloo-utils", "hex", "itertools 0.14.0", @@ -9826,13 +10454,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.2", + "rustix 1.1.3", "scoped-tls", "smallvec", "wayland-sys", @@ -9840,12 +10468,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ - "bitflags 2.9.4", - "rustix 1.1.2", + "bitflags 2.10.0", + "rustix 1.1.3", "wayland-backend", "wayland-scanner", ] @@ -9856,41 +10484,67 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" +checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", + "wayland-protocols", "wayland-scanner", ] [[package]] name = "wayland-protocols-plasma" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -9899,11 +10553,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -9912,9 +10566,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" dependencies = [ "proc-macro2", "quick-xml", @@ -9923,9 +10577,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" dependencies = [ "dlib", "log", @@ -9935,9 +10589,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -9959,14 +10613,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.4", + "webpki-root-certs 1.0.6", ] [[package]] name = "webpki-root-certs" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" dependencies = [ "rustls-pki-types", ] @@ -10004,10 +10658,10 @@ checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" dependencies = [ "arrayvec", "bit-vec 0.6.3", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases 0.1.1", "codespan-reporting", - "indexmap 2.11.4", + "indexmap 2.13.0", "log", "naga", "once_cell", @@ -10032,10 +10686,10 @@ dependencies = [ "arrayvec", "ash", "bit-set 0.5.3", - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "cfg_aliases 0.1.1", - "core-graphics-types 0.1.3", + "core-graphics-types", "d3d12", "glow", "glutin_wgl_sys", @@ -10073,16 +10727,16 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "js-sys", "web-sys", ] [[package]] name = "whoami" -version = "1.6.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +checksum = "8fae98cf96deed1b7572272dfc777713c249ae40aa1cf8862e091e8b745f5361" dependencies = [ "libredox", "wasite", @@ -10142,34 +10796,21 @@ dependencies = [ [[package]] name = "windows" -version = "0.52.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core 0.61.2", + "windows-core 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -10181,19 +10822,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -10202,20 +10830,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -10226,7 +10843,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -10237,49 +10854,24 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - [[package]] name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-result" -version = "0.3.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -10288,16 +10880,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -10306,7 +10889,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -10360,7 +10943,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -10415,7 +10998,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -10426,15 +11009,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -10624,10 +11198,10 @@ dependencies = [ "ahash 0.8.12", "android-activity", "atomic-waker", - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "bytemuck", - "calloop", + "calloop 0.13.0", "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation 0.9.4", @@ -10649,7 +11223,7 @@ dependencies = [ "redox_syscall 0.4.1", "rustix 0.38.44", "sctk-adwaita", - "smithay-client-toolkit", + "smithay-client-toolkit 0.19.2", "smol_str", "tracing", "unicode-segmentation", @@ -10669,9 +11243,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -10696,15 +11270,15 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -10737,7 +11311,7 @@ dependencies = [ "libc", "libloading 0.8.9", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "x11rb-protocol", ] @@ -10759,6 +11333,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.3", +] + [[package]] name = "xcursor" version = "0.3.10" @@ -10781,7 +11365,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dlib", "log", "once_cell", @@ -10796,9 +11380,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "yansi" @@ -10814,11 +11398,10 @@ checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -10826,13 +11409,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "synstructure", ] @@ -10858,7 +11441,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.29.0", + "nix", "ordered-stream", "rand 0.8.5", "serde", @@ -10876,9 +11459,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.11.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" dependencies = [ "async-broadcast", "async-recursion", @@ -10888,18 +11471,20 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix 0.30.1", + "libc", "ordered-stream", + "rustix 1.1.3", "serde", "serde_repr", "tokio", "tracing", "uds_windows", - "windows-sys 0.60.2", + "uuid", + "windows-sys 0.61.2", "winnow", - "zbus_macros 5.11.0", - "zbus_names 4.2.0", - "zvariant 5.7.0", + "zbus_macros 5.13.2", + "zbus_names 4.3.1", + "zvariant 5.9.2", ] [[package]] @@ -10911,23 +11496,23 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "zvariant_utils 2.1.0", ] [[package]] name = "zbus_macros" -version = "5.11.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", - "zbus_names 4.2.0", - "zvariant 5.7.0", - "zvariant_utils 3.2.1", + "syn 2.0.114", + "zbus_names 4.3.1", + "zvariant 5.9.2", + "zvariant_utils 3.3.0", ] [[package]] @@ -10943,14 +11528,13 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", "winnow", - "zvariant 5.7.0", + "zvariant 5.9.2", ] [[package]] @@ -10961,22 +11545,22 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] @@ -10996,7 +11580,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "synstructure", ] @@ -11011,20 +11595,20 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -11033,9 +11617,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -11044,15 +11628,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] +[[package]] +name = "zmij" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" + [[package]] name = "zvariant" version = "4.2.0" @@ -11068,17 +11658,17 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.7.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" dependencies = [ "endi", "enumflags2", "serde", "url", "winnow", - "zvariant_derive 5.7.0", - "zvariant_utils 3.2.1", + "zvariant_derive 5.9.2", + "zvariant_utils 3.3.0", ] [[package]] @@ -11090,21 +11680,21 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", "zvariant_utils 2.1.0", ] [[package]] name = "zvariant_derive" -version = "5.7.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", - "zvariant_utils 3.2.1", + "syn 2.0.114", + "zvariant_utils 3.3.0", ] [[package]] @@ -11115,18 +11705,18 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.114", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.114", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index e4eab96fe7..e2d7bbc16d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,80 +6,84 @@ repository = "https://github.com/mintlayer/mintlayer-core" readme = "README.md" license = "MIT" version = "1.2.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.). - "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.). + "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 = [ @@ -257,7 +261,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", +] } # Note: we use an unreleased version of Tokio, because we need a certain bugfix, see the `[patch.crates-io]` # section for additional details. tokio = { git = "https://github.com/tokio-rs/tokio", rev = "0d6c7af3e43457350bdc03a6dbcafa276fab7352", default-features = false } @@ -276,6 +284,14 @@ git = "https://github.com/mintlayer/mintlayer-core-primitives" rev = "4e21bf85e7fdca1577a49df7599b2b39ec913287" package = "mintlayer-core-primitives" +[workspace.dependencies.ledger-lib] +git = "https://github.com/ledger-community/rust-ledger.git" +rev = "4be10b810bb6500b7d8bc0b67e64fef24257e0e8" + +[workspace.dependencies.ledger-proto] +git = "https://github.com/ledger-community/rust-ledger.git" +rev = "4be10b810bb6500b7d8bc0b67e64fef24257e0e8" + [workspace.dependencies.trezor-client] git = "https://github.com/mintlayer/mintlayer-trezor-firmware" # The commit "Merge pull request #22 from mintlayer/specify_license_for_ml_reexports" @@ -292,7 +308,7 @@ features = ["bitcoin", "mintlayer"] panic = "abort" # prevent panic catching (mostly for the tokio runtime) [profile.release] -panic = "abort" # prevent panic catching (mostly for the tokio runtime) +panic = "abort" # prevent panic catching (mostly for the tokio runtime) overflow-checks = true # "Release" profile with debug info enabled. @@ -316,7 +332,8 @@ opt-level = 2 [features] tokio-console = [] 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 @@ -333,3 +350,4 @@ fontconfig-parser = { git = "https://github.com/Riey/fontconfig-parser", rev = " # subsystem (reproducible on a slow machine during initial sync with 30+ connected peers). # The PR was merged after 1.49, so it should probably be part of 1.50 when it comes out. tokio = { git = "https://github.com/tokio-rs/tokio", rev = "0d6c7af3e43457350bdc03a6dbcafa276fab7352" } +ledger-proto = { git = "https://github.com/ledger-community/rust-ledger.git", rev = "4be10b810bb6500b7d8bc0b67e64fef24257e0e8" } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 2c47110488..f88564185b 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -39,7 +39,15 @@ semver.workspace = true serde.workspace = true strum.workspace = true thiserror.workspace = true +tokio = { workspace = true, default-features = false, features = [ + "io-util", + "macros", + "rt", + "sync", +] } trezor-client = { workspace = true, optional = true } +ledger-lib = { workspace = true, optional = true } +ledger-proto = { workspace = true, optional = true } zeroize.workspace = true [dev-dependencies] @@ -52,6 +60,15 @@ tokio = { workspace = true, default-features = false, features = [ "rt", "sync", ] } +strum = { workspace = true } +reqwest = { workspace = true, features = ["json"] } +tracing = { workspace = true } +anyhow = { workspace = true } +clap = { workspace = true } +bollard = "0.14" +bytes = "1.10" +tar = "0.4" + ctor.workspace = true lazy_static.workspace = true @@ -64,8 +81,10 @@ tempfile.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:ledger-proto", "wallet-types/ledger"] +default = ["trezor", "ledger"] 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..90901b5ea9 --- /dev/null +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -0,0 +1,319 @@ +// 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, time::Duration}; + +use crypto::key::{ + extended::ExtendedPublicKey, + hdkd::{ + chain_code::{ChainCode, CHAINCODE_LENGTH}, + derivation_path::DerivationPath, + }, + secp256k1::{extended_keys::Secp256k1ExtendedPublicKey, Secp256k1PublicKey}, +}; +use ledger_lib::Exchange; +use serialization::{Decode, DecodeAll, Encode}; +use utils::ensure; + +use crate::signer::{ledger_signer::LedgerError, SignerError, SignerResult}; + +const MAX_MSG_SIZE: 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 CLA: u8 = 0xE0; +const TX_VERSION: u8 = 1; + +#[derive(Clone, Copy)] +pub enum LedgerAddrType { + PublicKey, + PublicKeyHash, +} + +impl From for u8 { + fn from(addr_type: LedgerAddrType) -> u8 { + match addr_type { + LedgerAddrType::PublicKey => 0, + LedgerAddrType::PublicKeyHash => 1, + } + } +} + +struct Ins {} + +impl Ins { + const APP_NAME: u8 = 0x04; + const PUB_KEY: u8 = 0x05; + const SIGN_TX: u8 = 0x06; + const SIGN_MSG: u8 = 0x07; +} + +struct P1 {} + +impl P1 { + const TX_META: u8 = 0; + const TX_INPUT: u8 = 1; + const TX_COMMITMENT: u8 = 2; + const TX_OUTPUT: u8 = 3; + const TX_SIG: u8 = 4; +} + +struct P2 {} + +impl P2 { + const DONE: u8 = 0x00; + const MORE: u8 = 0x80; +} + +#[derive(Encode)] +pub struct LedgerBip32Path(pub Vec); + +pub struct LedgerTxInput { + pub inp: Vec, + pub address_paths: Vec, +} + +pub struct LedgerTxInputCommitment { + pub commitment: Vec, +} + +pub struct LedgerTxOutput { + pub out: Vec, +} + +#[derive(Encode, Debug)] +pub struct LedgerInputAddressPath { + pub address_n: Vec, + pub multisig_idx: Option, +} + +#[derive(Decode)] +pub struct LedgerSignature { + pub signature: [u8; 64], + pub multisig_idx: Option, +} + +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 +fn ok_response(resp: Vec) -> SignerResult> { + let (resp, 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) + ); + + Ok(resp.to_vec()) +} + +/// send a message to the Ledger and check the respons status code is ok +async fn exchange_message( + ledger: &mut D, + 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, + step: u8, + message: &[u8], +) -> Result, SignerError> { + let mut msg_buf = vec![]; + let mut chunks = message.chunks(MAX_MSG_SIZE).peekable(); + let mut resp = vec![]; + while let Some(chunk) = chunks.next() { + msg_buf.clear(); + + let p2 = if chunks.peek().is_some() { + P2::MORE + } else { + P2::DONE + }; + + msg_buf.extend([CLA, ins, step, 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, + chain_type: u8, + path: &LedgerBip32Path, + addr_type: LedgerAddrType, + message: &[u8], +) -> SignerResult> { + let mut msg_buf = vec![]; + + msg_buf.extend([CLA, Ins::SIGN_MSG, 0, P2::MORE]); + let body = path.encode(); + msg_buf.push(body.len() as u8 + 2); + msg_buf.push(chain_type); + msg_buf.push(addr_type.into()); + msg_buf.extend(body); + + exchange_message(ledger, &msg_buf).await?; + + let resp = send_chunked(ledger, Ins::SIGN_MSG, 1, message).await?; + + let sig_len = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize; + let sig = resp.as_slice().get(1..1 + sig_len).ok_or(LedgerError::InvalidResponse)?; + + Ok(sig.to_vec()) +} + +pub async fn get_app_name(ledger: &mut L) -> Result, ledger_lib::Error> { + let msg_buf = [CLA, Ins::APP_NAME, 0, P2::DONE]; + ledger.exchange(&msg_buf, Duration::from_millis(100)).await +} + +#[allow(dead_code)] +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) + ); + + Ok(()) +} + +pub async fn get_extended_public_key( + ledger: &mut D, + chain_type: u8, + derivation_path: DerivationPath, +) -> SignerResult { + let address_n = LedgerBip32Path( + derivation_path.as_slice().iter().map(|c| c.into_encoded_index()).collect(), + ); + + let mut msg_buf = vec![]; + msg_buf.extend([CLA, Ins::PUB_KEY, 0, P2::DONE]); + let encoded_path = address_n.encode(); + let size: u8 = encoded_path.len().try_into().map_err(|_| LedgerError::PathToLong)?; + msg_buf.push(size + 1); + msg_buf.push(chain_type); + msg_buf.extend(encoded_path); + + let resp = exchange_message(ledger, &msg_buf).await?; + + let pk_len = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize; + let public_key = resp.as_slice().get(1..1 + pk_len).ok_or(LedgerError::InvalidResponse)?; + let chain_code_len = *resp.get(1 + pk_len).ok_or(LedgerError::InvalidResponse)? as usize; + let chain_code: [_; CHAINCODE_LENGTH] = resp + .as_slice() + .get(2 + pk_len..2 + pk_len + chain_code_len) + .ok_or(LedgerError::InvalidResponse)? + .try_into() + .map_err(|_| LedgerError::InvalidKey)?; + + let extended_public_key = Secp256k1ExtendedPublicKey::new_unchecked( + derivation_path, + ChainCode::from(chain_code), + Secp256k1PublicKey::from_bytes(public_key).map_err(|_| LedgerError::InvalidKey)?, + ); + + Ok(ExtendedPublicKey::new(extended_public_key)) +} + +pub async fn sign_tx( + ledger: &mut L, + chain_type: u8, + inputs: &[LedgerTxInput], + input_commitments: &[LedgerTxInputCommitment], + outputs: &[LedgerTxOutput], +) -> SignerResult>> { + let mut msg_buf = Vec::with_capacity(15); + + msg_buf.extend([CLA, Ins::SIGN_TX, P1::TX_META, P2::MORE]); + msg_buf.push(1 + 1 + 4 + 4); // data len coin + version + 2 u32 lens + msg_buf.push(chain_type); + msg_buf.push(TX_VERSION); + msg_buf.extend((inputs.len() as u32).to_be_bytes()); + msg_buf.extend((outputs.len() as u32).to_be_bytes()); + + exchange_message(ledger, &msg_buf).await?; + + for inp in inputs { + let paths = inp.address_paths.encode(); + send_chunked(ledger, Ins::SIGN_TX, P1::TX_INPUT, &paths).await?; + send_chunked(ledger, Ins::SIGN_TX, P1::TX_INPUT, &inp.inp).await?; + } + + for c in input_commitments { + send_chunked(ledger, Ins::SIGN_TX, P1::TX_COMMITMENT, &c.commitment).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, P1::TX_OUTPUT, &o.out).await?; + } + + let mut signatures: BTreeMap<_, Vec<_>> = BTreeMap::new(); + + let msg_buf = [CLA, Ins::SIGN_TX, P1::TX_SIG, P2::DONE, 0]; + 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::MORE; + + let sig = LedgerSignature::decode_all(&mut &resp[..resp.len() - 1][1..]) + .map_err(|_| LedgerError::InvalidResponse)?; + + Ok(SignatureResult { + sig, + input_idx, + has_more_signatures, + }) +} diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs new file mode 100644 index 0000000000..f7d27757e2 --- /dev/null +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -0,0 +1,1092 @@ +// 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::{borrow::Cow, collections::BTreeMap, sync::Arc}; + +use async_trait::async_trait; +use itertools::{izip, Itertools}; + +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::{ + input_commitments::SighashInputCommitment, 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 ledger_lib::{Exchange, Filters, LedgerHandle, LedgerProvider, Transport}; +use randomness::make_true_rng; +use serialization::Encode; +use tokio::sync::Mutex; +use utils::ensure; +use wallet_storage::{WalletStorageReadLocked, WalletStorageReadUnlocked}; +use wallet_types::{ + hw_data::LedgerData, + partially_signed_transaction::{PartiallySignedTransaction, TokensAdditionalInfo}, + signature_status::SignatureStatus, +}; + +use crate::{ + key_chain::{make_account_path, AccountKeyChains, FoundPubKey}, + signer::{ + ledger_signer::ledger_messages::{ + check_current_app, get_app_name, get_extended_public_key, sign_challenge, sign_tx, + LedgerAddrType, LedgerBip32Path, LedgerInputAddressPath, LedgerSignature, + LedgerTxInput, LedgerTxInputCommitment, LedgerTxOutput, + }, + utils::{is_htlc_utxo, produce_uniparty_signature_for_input}, + Signer, SignerError, SignerResult, + }, +}; + +/// Signer errors +#[derive(thiserror::Error, Debug, Eq, PartialEq)] +pub enum LedgerError { + #[error("No connected Ledger device found")] + NoDeviceFound, + #[error("Different active app: \"{0}\", opened on the Ledger. Please open the Mintlayer app.")] + 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 to 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")] + HardwareWalletDifferentFile, + #[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("Invalid Signature error: {0}")] + SignatureError(#[from] SignatureError), +} + +struct StandaloneInput { + multisig_idx: Option, + private_key: PrivateKey, +} + +type StandaloneInputs = BTreeMap>; + +#[async_trait] +pub trait LProvider { + type L; + + async fn find_ledger_device_from_db( + &self, + db_tx: &mut T, + ) -> SignerResult<(Self::L, 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: LProvider, +{ + 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 USB 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).await?; + + check_public_keys_against_key_chain( + db_tx, + &mut new_client, + &data, + 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: AsyncFn(&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, F, F2>( + &self, + signatures: &[LedgerSignature], + standalone_inputs: &'a [StandaloneInput], + destination: &'b Destination, + sighash_type: SigHashType, + sighash: H256, + key_chain: &impl AccountKeyChains, + make_witness: F, + sign_with_standalone_private_key: F2, + ) -> SignerResult<(Option, SignatureStatus)> + where + F: Fn(StandardInputSignature) -> InputWitness, + F2: 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| LedgerTxOutput { out: out.encode() }) + .collect() + } + + fn check_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_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 + LProvider, +{ + 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_commitments = to_ledger_commitments_msgs(&ptx)?; + let chain_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(); + let _input_commitment_version = match input_commitment_version { + common::chain::SighashInputCommitmentVersion::V0 => { + trezor_client::client::SighashInputCommitmentsVersion::V0 + } + common::chain::SighashInputCommitmentVersion::V1 => { + trezor_client::client::SighashInputCommitmentsVersion::V1 + } + }; + + let new_signatures = self + .perform_ledger_operation( + async move |client| { + sign_tx( + client, + chain_type, + &inputs, + &input_commitments, + &outputs, + // input_commitment_version, + ) + .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), None) => { + let standalone = match standalone_inputs.get(&(input_index as u32)).map(|x| x.as_slice()) { + Some([standalone]) => standalone, + Some(_) => return Err(LedgerError::MultisigSignatureReturned.into()), + None => return Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)) + }; + + 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(_) => LedgerAddrType::PublicKey, + Destination::PublicKeyHash(_) => LedgerAddrType::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 chain_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, chain_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 = LedgerTxInput { + inp: inp.encode(), + 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 { + 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_commitments_msgs( + ptx: &PartiallySignedTransaction, +) -> SignerResult> { + ptx.input_utxos() + .iter() + .zip(ptx.tx().inputs()) + .map(|(utxo, inp)| { + let commitment = 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)?; + SighashInputCommitment::ProduceBlockFromStakeUtxo { + utxo: Cow::Borrowed(utxo), + staker_balance: pool_info.staker_balance, + } + .encode() + } + _ => SighashInputCommitment::Utxo(Cow::Borrowed(utxo)).encode(), + } + } + TxInput::Account(_) => SighashInputCommitment::None.encode(), + TxInput::AccountCommand(_, cmd) => match cmd { + AccountCommand::MintTokens(_, _) + | AccountCommand::UnmintTokens(_) + | AccountCommand::LockTokenSupply(_) + | AccountCommand::FreezeToken(_, _) + | AccountCommand::UnfreezeToken(_) + | AccountCommand::ChangeTokenAuthority(_, _) + | AccountCommand::ChangeTokenMetadataUri(_, _) => { + SighashInputCommitment::None.encode() + } + AccountCommand::FillOrder(order_id, _, _) => { + let order_info = ptx + .additional_info() + .get_order_info(order_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + SighashInputCommitment::FillOrderAccountCommand { + initially_asked: order_info.initially_asked.clone(), + initially_given: order_info.initially_given.clone(), + } + .encode() + } + AccountCommand::ConcludeOrder(order_id) => { + let order_info = ptx + .additional_info() + .get_order_info(order_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + SighashInputCommitment::ConcludeOrderAccountCommand { + initially_asked: order_info.initially_asked.clone(), + initially_given: order_info.initially_given.clone(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + } + .encode() + } + }, + | TxInput::OrderAccountCommand(cmd) => match cmd { + OrderAccountCommand::FillOrder(order_id, _) => { + let order_info = ptx + .additional_info() + .get_order_info(order_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + SighashInputCommitment::FillOrderAccountCommand { + initially_asked: order_info.initially_asked.clone(), + initially_given: order_info.initially_given.clone(), + } + .encode() + } + OrderAccountCommand::ConcludeOrder(order_id) => { + let order_info = ptx + .additional_info() + .get_order_info(order_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + SighashInputCommitment::ConcludeOrderAccountCommand { + initially_asked: order_info.initially_asked.clone(), + initially_given: order_info.initially_given.clone(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + } + .encode() + } + OrderAccountCommand::FreezeOrder(_) => SighashInputCommitment::None.encode(), + }, + }; + Ok(LedgerTxInputCommitment { commitment }) + }) + .collect() +} + +fn to_ledger_chain_type(chain_config: &ChainConfig) -> u8 { + match chain_config.chain_type() { + ChainType::Mainnet => 0, + ChainType::Testnet => 1, + ChainType::Signet => 2, + ChainType::Regtest => 3, + } +} + +#[allow(dead_code)] +async fn find_ledger_device() -> SignerResult<(LedgerHandle, LedgerData)> { + 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()))?; + + check_current_app(&mut handle).await?; + + Ok((handle, LedgerData {})) +} + +/// 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, + _ledger_data: &LedgerData, + 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() { + // + } + + Err(LedgerError::HardwareWalletDifferentFile.into()) +} + +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 chain_type = to_ledger_chain_type(chain_config); + + get_extended_public_key(client, chain_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), + } +} + +#[cfg(feature = "enable-ledger-device-tests")] +#[cfg(test)] +mod tests; + +#[cfg(feature = "enable-ledger-device-tests")] +#[cfg(test)] +mod speculus; diff --git a/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs b/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs new file mode 100644 index 0000000000..e2a51c275f --- /dev/null +++ b/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs @@ -0,0 +1,245 @@ +// 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. + +//! Podman driver for speculos execution, runs a speculos instance within +//! a Podman container. + +use core::fmt::Debug; + +use async_trait::async_trait; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + process::Command as TokioCommand, + sync::oneshot::{channel, Sender}, +}; +use tracing::debug; + +use crate::signer::ledger_signer::speculus::{Handle, Options}; + +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::PathBuf, + process::Stdio, +}; + +/// [`Driver`] trait for speculos providers +#[async_trait] +pub trait Driver { + type Handle: Debug; + + /// Run speculos with the specified app and options + fn run(&self, app: &str, opts: Options) -> anyhow::Result; + + /// Exit task + fn exit(&self, handle: Self::Handle) -> anyhow::Result<()>; +} + +/// Podman-based Speculos driver +pub struct PodmanDriver; + +/// Handle to a Speculos instance running under Podman +#[derive(Debug)] +pub struct PodmanHandle { + name: String, + addr: SocketAddr, + // Sender to signal the log streaming task to shut down. + exit_tx: Sender<()>, +} + +impl PodmanDriver { + /// Create a new podman driver. + pub fn new() -> Result { + Ok(Self) + } + + /// Helper to run a synchronous std::process::Command in a non-blocking way. + fn run_command(mut command: std::process::Command) -> anyhow::Result { + let command_str = format!("{:?}", command); + let output = command.output().unwrap(); + + if !output.status.success() { + anyhow::bail!( + "Podman command failed: {}\nSTDOUT: {}\nSTDERR: {}", + command_str, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + debug!( + "Successfully ran podman command: {}\nSTDOUT: {}", + command_str, + String::from_utf8_lossy(&output.stdout) + ); + Ok(output) + } +} + +const DEFAULT_IMAGE: &str = "ghcr.io/ledgerhq/speculos"; + +#[async_trait] +impl Driver for PodmanDriver { + type Handle = PodmanHandle; + + fn run(&self, app: &str, opts: Options) -> anyhow::Result { + let name = format!("speculos-{}", opts.http_port); + + // Ensure any previous container with the same name is removed. + debug!("Force removing existing container '{}'", &name); + let mut cleanup_cmd = std::process::Command::new("podman"); + cleanup_cmd.args(["rm", "-f", &name]); + // We don't care if this fails (e.g., if the container didn't exist). + let _ = Self::run_command(cleanup_cmd); + + // Path to the application binary and its parent directory. + let app_path = PathBuf::from(app); + let app_file_name = app_path + .file_name() + .and_then(|n| n.to_str()) + .ok_or_else(|| anyhow::anyhow!("Invalid application path: {}", app))?; + let app_parent_dir = app_path + .parent() + .and_then(|p| p.to_str()) + .ok_or_else(|| anyhow::anyhow!("Could not get parent directory of app: {}", app))?; + + // Setup the command to run inside the container. + let mut speculos_cmd_args = opts.args(); + speculos_cmd_args.push(format!("/app/{}", app_file_name)); + + debug!("Container command: {}", speculos_cmd_args.join(" ")); + + // Build the `podman run` command. + let mut command = std::process::Command::new("podman"); + command.arg("run"); + // command.args(["--detach", "--name", &name]); + command.arg("--detach"); + command.arg("--name"); + command.arg(&name); + + command.arg("--log-driver=k8s-file"); + + // Map ports. + let mut ports = vec![opts.http_port]; + if let Some(p) = opts.apdu_port { + ports.push(p); + } + for port in ports { + command.arg("-p").arg(format!("{}:{}", port, port)); + } + + // Mount the app's directory as a volume instead of copying the file. + // This is simpler and more efficient than creating a tarball. + command.arg("-v").arg(format!("{}:/app:ro", app_parent_dir)); + + // Set image and the command to run. + command.arg(DEFAULT_IMAGE); + command.args(&speculos_cmd_args); + + // Create and start the container. + debug!("Creating and starting container '{}'", &name); + Self::run_command(command)?; + debug!("Container '{}' started", &name); + + let (exit_tx, mut exit_rx) = channel(); + + // Spawn a task to stream container logs. + let container_name_clone = name.clone(); + tokio::spawn(async move { + debug!( + "Starting log streaming for container '{}'", + container_name_clone + ); + let mut cmd = TokioCommand::new("podman"); + cmd.args(["logs", "--follow", &container_name_clone]); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + let mut child = match cmd.spawn() { + Ok(child) => child, + Err(e) => { + debug!("Failed to spawn podman logs: {}", e); + return; + } + }; + + let stdout = child.stdout.take().expect("Failed to open stdout"); + let stderr = child.stderr.take().expect("Failed to open stderr"); + + let mut stdout_reader = BufReader::new(stdout).lines(); + let mut stderr_reader = BufReader::new(stderr).lines(); + + loop { + tokio::select! { + // Check for exit signal + _ = &mut exit_rx => { + debug!("Received exit signal for log streaming. Killing process."); + let _ = child.kill().await; + break; + }, + // Read from stdout + Ok(Some(line)) = stdout_reader.next_line() => { + println!("[{}] {}", container_name_clone, line); + }, + // Read from stderr + Ok(Some(line)) = stderr_reader.next_line() => { + eprintln!("[{}] {}", container_name_clone, line); + }, + // Break if log stream ends + else => break, + } + } + debug!( + "Log streaming task for '{}' finished.", + container_name_clone + ); + }); + + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), opts.http_port); + Ok(PodmanHandle { + name, + addr, + exit_tx, + }) + } + + fn exit(&self, handle: Self::Handle) -> anyhow::Result<()> { + debug!("Stopping container {}", handle.name); + eprintln!("Stopping container {}", handle.name); + + // Signal the log streaming task to terminate. + let _ = handle.exit_tx.send(()); + + // Stop the container. + let mut stop_cmd = std::process::Command::new("podman"); + stop_cmd.args(["stop", &handle.name]); + // Ignore errors, as we will force remove it anyway. + let _ = Self::run_command(stop_cmd); + + // Remove the container. + debug!("Removing container {}", handle.name); + let mut rm_cmd = std::process::Command::new("podman"); + rm_cmd.args(["rm", "-f", &handle.name]); + Self::run_command(rm_cmd)?; + + debug!("Container {} removed", handle.name); + Ok(()) + } +} + +#[async_trait] +impl Handle for PodmanHandle { + fn addr(&self) -> SocketAddr { + self.addr + } +} diff --git a/wallet/src/signer/ledger_signer/speculus/handle.rs b/wallet/src/signer/ledger_signer/speculus/handle.rs new file mode 100644 index 0000000000..955c9e5319 --- /dev/null +++ b/wallet/src/signer/ledger_signer/speculus/handle.rs @@ -0,0 +1,133 @@ +// 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 image::{io::Reader as ImageReader, DynamicImage}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use strum::Display; +use tracing::debug; + +/// Button enumeration +#[derive(Clone, Copy, PartialEq, Debug, Display)] +#[strum(serialize_all = "kebab-case")] +pub enum Button { + Left, + Right, + Both, +} + +/// Button actions +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Display)] +#[serde(rename_all = "kebab-case")] +pub enum Action { + Press, + Release, + PressAndRelease, +} + +/// Button action object for serialisation and use with the HTTP API +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +struct ButtonAction { + pub 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<()> { + 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?; + + debug!("Button request complete: {}", r.status()); + + Ok(()) + } + + // /// Fetch a screenshot from the simulator + // async fn screenshot(&self) -> anyhow::Result { + // // Fetch screenshot from HTTP API + // let r = reqwest::get(format!("http://{}/screenshot", self.addr())).await?; + // + // // Read image bytes + // let b = r.bytes().await?; + // + // // Parse image object + // let i = ImageReader::new(Cursor::new(b)).with_guessed_format()?.decode()?; + // + // Ok(i) + // } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Check button string encoding + #[test] + fn button_encoding() { + let tests = &[(Button::Left, "left"), (Button::Right, "right"), (Button::Both, "both")]; + + for (v, s) in tests { + assert_eq!(&v.to_string(), s); + } + } + + /// Check button action encoding + #[test] + fn action_encoding() { + let tests = &[ + ( + ButtonAction { + action: Action::Press, + }, + r#"{"action":"press"}"#, + ), + ( + ButtonAction { + action: Action::Release, + }, + r#"{"action":"release"}"#, + ), + ( + ButtonAction { + action: Action::PressAndRelease, + }, + r#"{"action":"press-and-release"}"#, + ), + ]; + + for (v, s) in tests { + assert_eq!(&serde_json::to_string(v).unwrap(), s); + } + } +} diff --git a/wallet/src/signer/ledger_signer/speculus/mod.rs b/wallet/src/signer/ledger_signer/speculus/mod.rs new file mode 100644 index 0000000000..1e7c0518f6 --- /dev/null +++ b/wallet/src/signer/ledger_signer/speculus/mod.rs @@ -0,0 +1,139 @@ +// 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. + +use strum::{Display, EnumString, VariantNames}; + +mod drivers; +pub use drivers::*; + +mod handle; +pub use handle::*; + +/// Device model +#[derive(Copy, Clone, PartialEq, Debug, VariantNames, Display, EnumString)] +#[strum(serialize_all = "lowercase")] +pub enum Model { + /// Nano S + NanoS, + /// Nano S Plus + #[strum(serialize = "nanosplus", to_string = "nanosp")] + NanoSP, + /// Nano X + NanoX, +} + +/// Simulator display mode +#[derive(Copy, Clone, PartialEq, Debug, VariantNames, Display, EnumString)] +#[strum(serialize_all = "lowercase")] +pub enum Display { + /// Headless mode + Headless, + /// QT based rendering + Qt, + /// Text based (command line) rendering + Text, +} + +/// Simulator options +#[derive(Clone, PartialEq, Debug)] +pub struct Options { + /// Model to simulate + pub model: Model, + + /// Display mode + pub display: Display, + + /// SDK version override (defaults based on --model) + pub sdk: Option, + + /// API level override + pub api_level: Option, + + /// BIP39 seed for initialisation + pub seed: Option, + + /// Enable HTTP API port + pub http_port: u16, + + /// Enable APDU TCP port (usually 1237) + pub apdu_port: Option, + + /// Enable debugging and wait for GDB connection (port 1234) + pub debug: bool, + + /// Speculos root (used to configure python paths if set) + pub root: Option, + + /// Trace syscalls + pub trace: bool, +} + +impl Default for Options { + fn default() -> Self { + Self { + model: Model::NanoSP, + display: Display::Headless, + sdk: None, + api_level: None, + seed: None, + http_port: 5000, + apdu_port: None, + debug: false, + root: None, + trace: false, + } + } +} + +impl Options { + /// Build an argument list from [Options] + pub fn args(&self) -> Vec { + // Basic args + let mut args = vec![ + format!("--model={}", self.model), + format!("--display={}", self.display), + format!("--api-port={}", self.http_port), + ]; + + if let Some(seed) = &self.seed { + args.push(format!("--seed={seed}")); + } + + if let Some(apdu_port) = &self.apdu_port { + args.push(format!("--apdu-port={apdu_port}")); + } + + if let Some(sdk) = &self.sdk { + args.push(format!("--sdk={sdk}")); + } + + if let Some(api_level) = &self.api_level { + args.push(format!("--apiLevel={api_level}")); + } + + if self.debug { + args.push("--debug".to_string()); + } + + if self.trace { + args.push("-t".to_string()); + } + + args + } +} diff --git a/wallet/src/signer/ledger_signer/tests.rs b/wallet/src/signer/ledger_signer/tests.rs new file mode 100644 index 0000000000..467bd74bba --- /dev/null +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -0,0 +1,383 @@ +// 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::{Ipv4Addr, SocketAddr}, + str::FromStr, + sync::Arc, + time::Duration, +}; + +use async_trait::async_trait; +use common::chain::{config::create_mainnet, ChainConfig, SighashInputCommitmentVersion}; +use crypto::key::{ + hdkd::{derivation_path::DerivationPath, u31::U31}, + PredefinedSigAuxDataProvider, SigAuxDataProvider, +}; +use ledger_lib::{ + transport::{TcpDevice, TcpInfo, TcpTransport}, + Transport, +}; +use logging::log; +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 wallet_storage::WalletStorageReadLocked; +use wallet_types::hw_data::LedgerData; + +use crate::signer::{ + ledger_signer::{ + ledger_messages::get_extended_public_key, + speculus::{ + Action, Button, Display, Driver, Handle, Model, Options, PodmanDriver, PodmanHandle, + }, + LProvider, LedgerError, LedgerSigner, + }, + tests::{ + generic_tests::{ + test_sign_transaction_generic, test_sign_transaction_intent_generic, MessageToSign, + }, + no_another_signer, + }, + SignerError, SignerResult, +}; + +#[derive(Debug)] +enum ControlMessage { + Finish, +} + +async fn auto_confirmer( + mut finish_rx: mpsc::Receiver, + handle: PodmanHandle, + driver: PodmanDriver, +) { + loop { + tokio::select! { + _ = sleep(Duration::from_millis(100)) => { + handle.button(Button::Right, Action::PressAndRelease).await.unwrap(); + handle.button(Button::Both, Action::PressAndRelease).await.unwrap(); + } + msg = finish_rx.recv() => { + match msg { + Some(ControlMessage::Finish) => { + eprintln!("Received finish signal."); + // perform exit action + driver.exit(handle).unwrap(); + break; + } + None => { + eprintln!("Channel closed."); + break; + } + } + } + } + } + + println!("Auto-confirmer task finished."); +} + +struct DummyProvider; + +#[async_trait] +impl LProvider for DummyProvider { + type L = TcpDevice; + + async fn find_ledger_device_from_db( + &self, + _db_tx: &mut T, + ) -> SignerResult<(Self::L, LedgerData)> { + Err(SignerError::LedgerError(LedgerError::NoDeviceFound)) + } +} + +async fn setup( + offset: u16, + deterministic_aux: bool, +) -> ( + tokio::task::JoinHandle<()>, + Sender, + impl Fn(Arc, U31) -> LedgerSigner, +) { + let driver = PodmanDriver::new().unwrap(); + + let opts = Options { + model: Model::NanoSP, + api_level: Some("22".to_string()), + seed: Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string()), // Use a deterministic seed for tests + display: Display::Headless, + apdu_port: Some(1237 + offset), + http_port: 5001 + offset, + ..Default::default() + }; + + let app_path = format!( + "{}/../../ledger-mintlayer/target/nanosplus/release/mintlayer-app", + env!("CARGO_MANIFEST_DIR") + ); + let handle = driver.run(&app_path, opts.clone()).unwrap(); + + println!("Emulator is running..."); + + // TODO: instead of sleeping try to connect and wait for a successfull connection to the + // docker + sleep(Duration::from_secs(5)).await; + + let mut transport = TcpTransport::new().unwrap(); + let device = transport + .connect(TcpInfo { + addr: SocketAddr::new( + std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 1237 + offset, + ), + }) + .await + .unwrap(); + + let device = Arc::new(Mutex::new(device)); + + let (finish_tx, finish_rx) = mpsc::channel(1); + let auto_clicker = tokio::spawn(auto_confirmer(finish_rx, handle, driver)); + + (auto_clicker, finish_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] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_account_extended_public_key() { + let (auto_clicker, finish_tx, make_ledger_signer) = setup(1, 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, 0, 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()); + + finish_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[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, +) { + 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, finish_tx, make_ledger_signer) = setup(2, false).await; + + test_sign_message_generic( + &mut rng, + message_to_sign, + make_ledger_signer, + no_another_signer(), + ) + .await; + + finish_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[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, finish_tx, make_ledger_signer) = setup(3, false).await; + + let mut rng = make_seedable_rng(seed); + + test_sign_transaction_intent_generic(&mut rng, make_ledger_signer, no_another_signer()).await; + + finish_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[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, finish_tx, make_ledger_signer) = setup(4, 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; + + finish_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[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, finish_tx, make_ledger_signer) = setup(6, true).await; + + let mut rng = make_seedable_rng(seed); + + test_fixed_signatures_generic2(&mut rng, input_commitments_version, make_ledger_signer).await; + + finish_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[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, finish_tx, make_ledger_signer) = setup(7, 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; + + finish_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[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, finish_tx, make_ledger_signer) = setup(8, 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; + + finish_tx.send(ControlMessage::Finish).await.unwrap(); + auto_clicker.await.unwrap(); +} + +#[rstest] +#[trace] +#[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, finish_tx, make_ledger_signer) = setup(9, 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; + + finish_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..bfca2985ec 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")] diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index d1f4264ae8..38e2f73265 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -149,6 +149,8 @@ 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, #[error( @@ -1684,6 +1686,10 @@ fn check_public_keys_against_key_chain( .into()); } } + #[cfg(feature = "ledger")] + HardwareWalletData::Ledger(_) => { + return Err(TrezorError::LedgerWalletDifferentFile.into()); + } } } diff --git a/wallet/types/Cargo.toml b/wallet/types/Cargo.toml index ad2285a225..038a2616d0 100644 --- a/wallet/types/Cargo.toml +++ b/wallet/types/Cargo.toml @@ -18,7 +18,10 @@ 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", +] } hex.workspace = true itertools.workspace = true parity-scale-codec.workspace = true @@ -35,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..b7187e66f2 100644 --- a/wallet/types/src/hw_data.rs +++ b/wallet/types/src/hw_data.rs @@ -41,12 +41,19 @@ impl From for TrezorData { } } +#[cfg(feature = "ledger")] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct LedgerData {} + /// 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. From f1b9a6746f5f32f382d9db64294e08a267f83ea3 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 22 Aug 2025 20:48:18 +0200 Subject: [PATCH 02/24] fix ledger tests --- .github/workflows/build.yml | 8 ++++---- wallet/src/signer/ledger_signer/mod.rs | 7 +++++++ wallet/src/signer/ledger_signer/tests.rs | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4259ecb08d..0d91e19562 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -175,7 +175,7 @@ jobs: run: sudo apt-get update - name: Install build dependencies - run: sudo apt-get install -yqq --no-install-recommends build-essential pkg-config libssl-dev + run: sudo apt-get install -yqq --no-install-recommends build-essential pkg-config libssl-dev libdbus-1-dev libusb-1.0-0-dev - name: Setup Python uses: actions/setup-python@v6 @@ -283,9 +283,9 @@ jobs: - 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 + --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 diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs index f7d27757e2..a9b516c3fd 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -654,6 +654,13 @@ where 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, diff --git a/wallet/src/signer/ledger_signer/tests.rs b/wallet/src/signer/ledger_signer/tests.rs index 467bd74bba..0361509656 100644 --- a/wallet/src/signer/ledger_signer/tests.rs +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -124,7 +124,7 @@ async fn setup( let opts = Options { model: Model::NanoSP, - api_level: Some("22".to_string()), + api_level: Some("24".to_string()), seed: Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string()), // Use a deterministic seed for tests display: Display::Headless, apdu_port: Some(1237 + offset), From ebb748c290de60f9036f1e32a95cf79afefa4dc2 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Mon, 25 Aug 2025 14:43:02 +0200 Subject: [PATCH 03/24] ledger cleanup --- Cargo.lock | 275 ++++-------------- wallet/Cargo.toml | 28 +- .../signer/ledger_signer/ledger_messages.rs | 8 +- .../ledger_signer/speculus/drivers/mod.rs | 35 +-- wallet/src/signer/ledger_signer/tests.rs | 19 +- 5 files changed, 99 insertions(+), 266 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3bb9a64f0..4032c03fce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,10 +642,10 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "itoa", "matchit 0.7.3", @@ -675,8 +675,8 @@ dependencies = [ "axum-core 0.5.6", "bytes", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "itoa", "matchit 0.8.4", @@ -700,8 +700,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -720,8 +720,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -740,12 +740,6 @@ dependencies = [ "bitcoin_hashes", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -1072,45 +1066,6 @@ dependencies = [ "dbus", ] -[[package]] -name = "bollard" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af254ed2da4936ef73309e9597180558821cb16ae9bba4cb24ce6b612d8d80ed" -dependencies = [ - "base64 0.21.7", - "bollard-stubs", - "bytes", - "futures-core", - "futures-util", - "hex", - "http 0.2.12", - "hyper 0.14.32", - "hyperlocal", - "log", - "pin-project-lite", - "serde", - "serde_derive", - "serde_json", - "serde_repr", - "serde_urlencoded", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "url", - "winapi", -] - -[[package]] -name = "bollard-stubs" -version = "1.42.0-rc.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602bda35f33aeb571cef387dcd4042c643a8bf689d8aaac2cc47ea24cb7bc7e0" -dependencies = [ - "serde", - "serde_with 2.3.3", -] - [[package]] name = "borsh" version = "1.6.0" @@ -1821,7 +1776,7 @@ dependencies = [ "serde", "serde_json", "serde_test", - "serde_with 3.16.1", + "serde_with", "serial_test", "serialization", "smallvec", @@ -3017,17 +2972,6 @@ dependencies = [ "flate2", ] -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -3527,25 +3471,6 @@ dependencies = [ "svg_fmt", ] -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.13.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.13" @@ -3557,7 +3482,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http", "indexmap 2.13.0", "slab", "tokio", @@ -3808,17 +3733,6 @@ dependencies = [ "digest", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -3829,17 +3743,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -3847,7 +3750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -3858,8 +3761,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -3881,30 +3784,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.8.1" @@ -3915,9 +3794,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -3934,8 +3813,8 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", - "hyper 1.8.1", + "http", + "hyper", "hyper-util", "log", "rustls", @@ -3951,7 +3830,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.8.1", + "hyper", "hyper-util", "pin-project-lite", "tokio", @@ -3966,7 +3845,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "native-tls", "tokio", @@ -3984,9 +3863,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", @@ -3999,19 +3878,6 @@ dependencies = [ "windows-registry", ] -[[package]] -name = "hyperlocal" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" -dependencies = [ - "futures-util", - "hex", - "hyper 0.14.32", - "pin-project", - "tokio", -] - [[package]] name = "iana-time-zone" version = "0.1.65" @@ -4572,7 +4438,7 @@ checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" dependencies = [ "base64 0.22.1", "futures-util", - "http 1.4.0", + "http", "jsonrpsee-core", "pin-project", "rustls", @@ -4597,8 +4463,8 @@ dependencies = [ "bytes", "futures-timer", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "jsonrpsee-types", "parking_lot 0.12.5", @@ -4621,8 +4487,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" dependencies = [ "base64 0.22.1", - "http-body 1.0.1", - "hyper 1.8.1", + "http-body", + "hyper", "hyper-rustls", "hyper-util", "jsonrpsee-core", @@ -4657,10 +4523,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" dependencies = [ "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -4683,7 +4549,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" dependencies = [ - "http 1.4.0", + "http", "serde", "serde_json", "thiserror 2.0.18", @@ -4695,7 +4561,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" dependencies = [ - "http 1.4.0", + "http", "jsonrpsee-client-transport", "jsonrpsee-core", "jsonrpsee-types", @@ -5558,7 +5424,7 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_with 3.16.1", + "serde_with", "serialization", "subsystem", "test-utils", @@ -7326,11 +7192,11 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-rustls", "hyper-tls", "hyper-util", @@ -7462,8 +7328,8 @@ dependencies = [ "base64 0.22.1", "crypto", "expect-test", - "http 1.4.0", - "hyper 1.8.1", + "http", + "hyper", "jsonrpsee", "logging", "randomness", @@ -8094,21 +7960,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" -dependencies = [ - "base64 0.13.1", - "chrono", - "hex", - "indexmap 1.9.3", - "serde", - "serde_json", - "time", -] - [[package]] name = "serde_with" version = "3.16.1" @@ -8529,7 +8380,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures", - "http 1.4.0", + "http", "httparse", "log", "rand 0.8.5", @@ -8879,17 +8730,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.24.0" @@ -9353,11 +9193,11 @@ dependencies = [ "axum 0.8.8", "base64 0.22.1", "bytes", - "h2 0.4.13", - "http 1.4.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-timeout", "hyper-util", "percent-encoding", @@ -9411,8 +9251,8 @@ dependencies = [ "base64 0.21.7", "bitflags 2.10.0", "bytes", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", @@ -9429,8 +9269,8 @@ dependencies = [ "bitflags 2.10.0", "bytes", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "iri-string", "pin-project-lite", "tower", @@ -9820,7 +9660,7 @@ dependencies = [ "itertools 0.14.0", "rpc-description", "serde_test", - "serde_with 3.16.1", + "serde_with", "test-utils", "thiserror 1.0.69", "tokio", @@ -9908,8 +9748,6 @@ dependencies = [ "anyhow", "async-trait", "bip39", - "bollard", - "bytes", "chainstate", "chainstate-test-framework", "clap", @@ -9939,7 +9777,6 @@ dependencies = [ "serialization", "storage", "strum 0.26.3", - "tar", "tempfile", "test-utils", "thiserror 1.0.69", @@ -11333,16 +11170,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.3", -] - [[package]] name = "xcursor" version = "0.3.10" diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index f88564185b..5408452d78 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -53,30 +53,26 @@ 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", -] } -strum = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -tracing = { workspace = true } -anyhow = { workspace = true } -clap = { workspace = true } -bollard = "0.14" -bytes = "1.10" -tar = "0.4" - +anyhow.workspace = true +clap.workspace = true ctor.workspace = true lazy_static.workspace = true +reqwest = { workspace = true, features = ["json"] } rstest.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"] diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs index 90901b5ea9..ee73105254 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -124,8 +124,8 @@ fn ok_response(resp: Vec) -> SignerResult> { } /// send a message to the Ledger and check the respons status code is ok -async fn exchange_message( - ledger: &mut D, +async fn exchange_message( + ledger: &mut L, msg_buf: &[u8], ) -> Result, SignerError> { let resp = ledger @@ -210,8 +210,8 @@ pub async fn check_current_app(ledger: &mut L) -> SignerResult<()> Ok(()) } -pub async fn get_extended_public_key( - ledger: &mut D, +pub async fn get_extended_public_key( + ledger: &mut L, chain_type: u8, derivation_path: DerivationPath, ) -> SignerResult { diff --git a/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs b/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs index e2a51c275f..c10baaea77 100644 --- a/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs +++ b/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs @@ -24,7 +24,6 @@ use tokio::{ process::Command as TokioCommand, sync::oneshot::{channel, Sender}, }; -use tracing::debug; use crate::signer::ledger_signer::speculus::{Handle, Options}; @@ -64,7 +63,6 @@ impl PodmanDriver { Ok(Self) } - /// Helper to run a synchronous std::process::Command in a non-blocking way. fn run_command(mut command: std::process::Command) -> anyhow::Result { let command_str = format!("{:?}", command); let output = command.output().unwrap(); @@ -77,7 +75,7 @@ impl PodmanDriver { String::from_utf8_lossy(&output.stderr) ); } - debug!( + logging::log::debug!( "Successfully ran podman command: {}\nSTDOUT: {}", command_str, String::from_utf8_lossy(&output.stdout) @@ -96,7 +94,7 @@ impl Driver for PodmanDriver { let name = format!("speculos-{}", opts.http_port); // Ensure any previous container with the same name is removed. - debug!("Force removing existing container '{}'", &name); + logging::log::debug!("Force removing existing container '{}'", &name); let mut cleanup_cmd = std::process::Command::new("podman"); cleanup_cmd.args(["rm", "-f", &name]); // We don't care if this fails (e.g., if the container didn't exist). @@ -117,12 +115,11 @@ impl Driver for PodmanDriver { let mut speculos_cmd_args = opts.args(); speculos_cmd_args.push(format!("/app/{}", app_file_name)); - debug!("Container command: {}", speculos_cmd_args.join(" ")); + logging::log::debug!("Container command: {}", speculos_cmd_args.join(" ")); // Build the `podman run` command. let mut command = std::process::Command::new("podman"); command.arg("run"); - // command.args(["--detach", "--name", &name]); command.arg("--detach"); command.arg("--name"); command.arg(&name); @@ -138,8 +135,7 @@ impl Driver for PodmanDriver { command.arg("-p").arg(format!("{}:{}", port, port)); } - // Mount the app's directory as a volume instead of copying the file. - // This is simpler and more efficient than creating a tarball. + // Mount the app's directory as a volume command.arg("-v").arg(format!("{}:/app:ro", app_parent_dir)); // Set image and the command to run. @@ -147,16 +143,16 @@ impl Driver for PodmanDriver { command.args(&speculos_cmd_args); // Create and start the container. - debug!("Creating and starting container '{}'", &name); + logging::log::debug!("Creating and starting container '{}'", &name); Self::run_command(command)?; - debug!("Container '{}' started", &name); + logging::log::debug!("Container '{}' started", &name); let (exit_tx, mut exit_rx) = channel(); // Spawn a task to stream container logs. let container_name_clone = name.clone(); tokio::spawn(async move { - debug!( + logging::log::debug!( "Starting log streaming for container '{}'", container_name_clone ); @@ -168,7 +164,7 @@ impl Driver for PodmanDriver { let mut child = match cmd.spawn() { Ok(child) => child, Err(e) => { - debug!("Failed to spawn podman logs: {}", e); + logging::log::debug!("Failed to spawn podman logs: {}", e); return; } }; @@ -183,23 +179,23 @@ impl Driver for PodmanDriver { tokio::select! { // Check for exit signal _ = &mut exit_rx => { - debug!("Received exit signal for log streaming. Killing process."); + logging::log::debug!("Received exit signal for log streaming. Killing process."); let _ = child.kill().await; break; }, // Read from stdout Ok(Some(line)) = stdout_reader.next_line() => { - println!("[{}] {}", container_name_clone, line); + logging::log::debug!("[{}] {}", container_name_clone, line); }, // Read from stderr Ok(Some(line)) = stderr_reader.next_line() => { - eprintln!("[{}] {}", container_name_clone, line); + logging::log::debug!("[{}] {}", container_name_clone, line); }, // Break if log stream ends else => break, } } - debug!( + logging::log::debug!( "Log streaming task for '{}' finished.", container_name_clone ); @@ -214,8 +210,7 @@ impl Driver for PodmanDriver { } fn exit(&self, handle: Self::Handle) -> anyhow::Result<()> { - debug!("Stopping container {}", handle.name); - eprintln!("Stopping container {}", handle.name); + logging::log::debug!("Stopping container {}", handle.name); // Signal the log streaming task to terminate. let _ = handle.exit_tx.send(()); @@ -227,12 +222,12 @@ impl Driver for PodmanDriver { let _ = Self::run_command(stop_cmd); // Remove the container. - debug!("Removing container {}", handle.name); + logging::log::debug!("Removing container {}", handle.name); let mut rm_cmd = std::process::Command::new("podman"); rm_cmd.args(["rm", "-f", &handle.name]); Self::run_command(rm_cmd)?; - debug!("Container {} removed", handle.name); + logging::log::debug!("Container {} removed", handle.name); Ok(()) } } diff --git a/wallet/src/signer/ledger_signer/tests.rs b/wallet/src/signer/ledger_signer/tests.rs index 0361509656..c5565d2202 100644 --- a/wallet/src/signer/ledger_signer/tests.rs +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -47,7 +47,7 @@ use wallet_types::hw_data::LedgerData; use crate::signer::{ ledger_signer::{ - ledger_messages::get_extended_public_key, + ledger_messages::{get_app_name, get_extended_public_key}, speculus::{ Action, Button, Display, Driver, Handle, Model, Options, PodmanDriver, PodmanHandle, }, @@ -145,7 +145,7 @@ async fn setup( sleep(Duration::from_secs(5)).await; let mut transport = TcpTransport::new().unwrap(); - let device = transport + let mut device = transport .connect(TcpInfo { addr: SocketAddr::new( std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -155,6 +155,20 @@ async fn setup( .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 (finish_tx, finish_rx) = mpsc::channel(1); @@ -206,6 +220,7 @@ async fn test_account_extended_public_key() { #[rstest] #[trace] +#[serial_test::serial] #[case(Seed::from_entropy())] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_sign_message( From 8e8fc2c22f77b43aa9c9f6bbd3f4f50559c7133b Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Tue, 23 Sep 2025 00:27:15 +0200 Subject: [PATCH 04/24] Remove podman usage from Ledger tests, fix comments --- .github/workflows/build.yml | 6 +- Cargo.lock | 379 +++++++++--------- Cargo.toml | 8 +- wallet/Cargo.toml | 1 + .../signer/ledger_signer/ledger_messages.rs | 23 +- wallet/src/signer/ledger_signer/mod.rs | 97 ++--- .../ledger_signer/speculos/drivers/mod.rs | 43 ++ .../{speculus => speculos}/handle.rs | 21 +- .../src/signer/ledger_signer/speculos/mod.rs | 23 ++ .../ledger_signer/speculus/drivers/mod.rs | 240 ----------- .../src/signer/ledger_signer/speculus/mod.rs | 139 ------- wallet/src/signer/ledger_signer/tests.rs | 146 +++---- wallet/src/signer/tests/generic_tests.rs | 12 + wallet/src/signer/trezor_signer/tests.rs | 14 +- 14 files changed, 400 insertions(+), 752 deletions(-) create mode 100644 wallet/src/signer/ledger_signer/speculos/drivers/mod.rs rename wallet/src/signer/ledger_signer/{speculus => speculos}/handle.rs (78%) create mode 100644 wallet/src/signer/ledger_signer/speculos/mod.rs delete mode 100644 wallet/src/signer/ledger_signer/speculus/drivers/mod.rs delete mode 100644 wallet/src/signer/ledger_signer/speculus/mod.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d91e19562..407df709a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -283,9 +283,9 @@ jobs: - 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 + --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 diff --git a/Cargo.lock b/Cargo.lock index 4032c03fce..84422cf3be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,7 +144,7 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys 0.6.0+11769913", - "num_enum 0.7.5", + "num_enum", "thiserror 1.0.69", ] @@ -611,15 +611,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -1038,30 +1029,29 @@ dependencies = [ [[package]] name = "bluez-async" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce7d4413c940e8e3cb6afc122d3f4a07096aca259d286781128683fc9f39d9b" +checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" dependencies = [ - "async-trait", "bitflags 2.10.0", "bluez-generated", "dbus", "dbus-tokio", "futures", - "itertools 0.10.5", + "itertools 0.14.0", "log", "serde", "serde-xml-rs", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "uuid", ] [[package]] name = "bluez-generated" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1c659dbc82f0b8ca75606c91a371e763589b7f6acf36858eeed0c705afe367" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" dependencies = [ "dbus", ] @@ -1102,29 +1092,30 @@ dependencies = [ [[package]] name = "btleplug" -version = "0.10.5" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790e133b9d27f6dbd1289e046e735cef97fb0b4c840f18db52c959521dfb8145" +checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" dependencies = [ "async-trait", - "bitflags 1.3.2", + "bitflags 2.10.0", "bluez-async", - "cocoa", - "dashmap", + "dashmap 6.1.0", "dbus", "futures", "jni 0.19.0", "jni-utils", - "libc", "log", - "objc", + "objc2 0.5.2", + "objc2-core-bluetooth", + "objc2-foundation 0.2.2", "once_cell", "static_assertions", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tokio-stream", "uuid", - "windows 0.48.0", + "windows 0.61.3", + "windows-future", ] [[package]] @@ -1532,7 +1523,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1652,36 +1643,6 @@ dependencies = [ "x11rb", ] -[[package]] -name = "cocoa" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation 0.9.4", - "core-graphics 0.22.3", - "foreign-types 0.3.2", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types", - "libc", - "objc", -] - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1932,19 +1893,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types", - "foreign-types 0.3.2", - "libc", -] - [[package]] name = "core-graphics" version = "0.23.2" @@ -2046,12 +1994,6 @@ dependencies = [ "itertools 0.10.5", ] -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - [[package]] name = "crossbeam" version = "0.8.4" @@ -2375,6 +2317,20 @@ dependencies = [ "parking_lot_core 0.9.12", ] +[[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.12", +] + [[package]] name = "data-encoding" version = "2.10.0" @@ -2705,44 +2661,34 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encdec" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25de94e10baa85551f7c65730423239370ed5bed60bf8d2a9cbf2683327ba421" +checksum = "2ec7aafa197dadfa18575eb62d05adaec89237d0fb0663baf2e141529b3a20b0" dependencies = [ - "encdec-base 0.9.0", + "encdec-base", "encdec-macros", ] [[package]] name = "encdec-base" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8542ff2a35da7fc94ffcf280f35dc759219c4b48fa930e0a0f268220d7fb6a" -dependencies = [ - "byteorder", - "num-traits", -] - -[[package]] -name = "encdec-base" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516ae3c7d00515548bf26a6531883335ceac2e9cde4938e70feea7456569be09" +checksum = "5abc9d559c177b2a75892e92c1812216e5cec7e1a14e0682b25ed6f5c0bd78a2" dependencies = [ "byteorder", "heapless", "num-traits", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] name = "encdec-macros" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15497932aae6b53bf8548cc63c65929b4fab6be54e28709c80fc72f5707eeed" +checksum = "ace893fa216d3c4cd14239cf2807b48c1ad8abcf583d779a29f410cd35cff413" dependencies = [ "darling 0.14.4", - "encdec-base 0.8.3", + "encdec-base", "proc-macro2", "quote", "syn 1.0.109", @@ -3272,8 +3218,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link", - "windows-result", + "windows-link 0.2.1", + "windows-result 0.4.1", ] [[package]] @@ -3293,7 +3239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix 1.1.3", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3503,9 +3449,9 @@ dependencies = [ [[package]] name = "hash32" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ "byteorder", ] @@ -3583,14 +3529,11 @@ dependencies = [ [[package]] name = "heapless" -version = "0.7.17" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "atomic-polyfill", "hash32", - "rustc_version", - "spin", "stable_deref_trait", ] @@ -4385,7 +4328,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" dependencies = [ - "dashmap", + "dashmap 5.5.3", "futures", "jni 0.19.0", "log", @@ -4614,7 +4557,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "ledger-lib" version = "0.1.0" -source = "git+https://github.com/ledger-community/rust-ledger.git?rev=4be10b810bb6500b7d8bc0b67e64fef24257e0e8#4be10b810bb6500b7d8bc0b67e64fef24257e0e8" +source = "git+https://github.com/ledger-community/rust-ledger.git?rev=510bb3ca30639af4bdb12a918b6bbbdb75fa5f52#510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" dependencies = [ "async-trait", "btleplug", @@ -4624,8 +4567,8 @@ dependencies = [ "hidapi", "ledger-proto", "once_cell", - "strum 0.24.1", - "thiserror 1.0.69", + "strum 0.26.3", + "thiserror 2.0.18", "tokio", "tracing", "tracing-subscriber", @@ -4635,15 +4578,15 @@ dependencies = [ [[package]] name = "ledger-proto" version = "0.1.0" -source = "git+https://github.com/ledger-community/rust-ledger.git?rev=4be10b810bb6500b7d8bc0b67e64fef24257e0e8#4be10b810bb6500b7d8bc0b67e64fef24257e0e8" +source = "git+https://github.com/ledger-community/rust-ledger.git?rev=510bb3ca30639af4bdb12a918b6bbbdb75fa5f52#510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" dependencies = [ "bitflags 2.10.0", "displaydoc", "encdec", "hex", - "num_enum 0.6.1", + "num_enum", "serde", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -4678,7 +4621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -5251,7 +5194,7 @@ dependencies = [ "jni-sys", "log", "ndk-sys 0.6.0+11769913", - "num_enum 0.7.5", + "num_enum", "raw-window-handle", "thiserror 1.0.69", ] @@ -5605,36 +5548,16 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" -dependencies = [ - "num_enum_derive 0.6.1", -] - [[package]] name = "num_enum" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ - "num_enum_derive 0.7.5", + "num_enum_derive", "rustversion", ] -[[package]] -name = "num_enum_derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "num_enum_derive" version = "0.7.5" @@ -5734,6 +5657,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.10.0", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -6324,7 +6258,7 @@ dependencies = [ "libc", "redox_syscall 0.5.18", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -7409,6 +7343,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.114", +] + [[package]] name = "rusb" version = "0.9.4" @@ -7844,14 +7789,14 @@ dependencies = [ [[package]] name = "serde-xml-rs" -version = "0.6.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" dependencies = [ "log", "serde", - "thiserror 1.0.69", - "xml-rs", + "thiserror 2.0.18", + "xml", ] [[package]] @@ -8387,15 +8332,6 @@ dependencies = [ "sha1", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -8547,15 +8483,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -dependencies = [ - "strum_macros 0.24.3", -] - [[package]] name = "strum" version = "0.26.3" @@ -8574,19 +8501,6 @@ dependencies = [ "strum_macros 0.27.2", ] -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - [[package]] name = "strum_macros" version = "0.26.4" @@ -9769,6 +9683,7 @@ dependencies = [ "reqwest", "rpc-description", "rstest", + "rstest_reuse", "secp256k1", "semver", "serde", @@ -10633,21 +10548,34 @@ dependencies = [ [[package]] name = "windows" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-targets 0.48.5", + "windows-core 0.52.0", + "windows-targets 0.52.6", ] [[package]] name = "windows" -version = "0.52.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -10659,6 +10587,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -10667,9 +10608,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -10694,21 +10646,46 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -10717,7 +10694,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -10726,7 +10712,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -10780,7 +10766,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -10835,7 +10821,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -10846,6 +10832,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -11042,7 +11037,7 @@ dependencies = [ "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation 0.9.4", - "core-graphics 0.23.2", + "core-graphics", "cursor-icon", "dpi", "js-sys", @@ -11205,6 +11200,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "xml" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" + [[package]] name = "xml-rs" version = "0.8.28" diff --git a/Cargo.toml b/Cargo.toml index e2d7bbc16d..6a9d895a83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -234,6 +234,7 @@ 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 } @@ -286,11 +287,11 @@ package = "mintlayer-core-primitives" [workspace.dependencies.ledger-lib] git = "https://github.com/ledger-community/rust-ledger.git" -rev = "4be10b810bb6500b7d8bc0b67e64fef24257e0e8" +rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" [workspace.dependencies.ledger-proto] git = "https://github.com/ledger-community/rust-ledger.git" -rev = "4be10b810bb6500b7d8bc0b67e64fef24257e0e8" +rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" [workspace.dependencies.trezor-client] git = "https://github.com/mintlayer/mintlayer-trezor-firmware" @@ -350,4 +351,5 @@ fontconfig-parser = { git = "https://github.com/Riey/fontconfig-parser", rev = " # subsystem (reproducible on a slow machine during initial sync with 30+ connected peers). # The PR was merged after 1.49, so it should probably be part of 1.50 when it comes out. tokio = { git = "https://github.com/tokio-rs/tokio", rev = "0d6c7af3e43457350bdc03a6dbcafa276fab7352" } -ledger-proto = { git = "https://github.com/ledger-community/rust-ledger.git", rev = "4be10b810bb6500b7d8bc0b67e64fef24257e0e8" } +# The patch is needed because there is now release of the library. We use the same hash for all Ledger libs +ledger-proto = { git = "https://github.com/ledger-community/rust-ledger.git", rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 5408452d78..809dabe579 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -60,6 +60,7 @@ 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 diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs index ee73105254..0a96ef59cb 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -13,8 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, time::Duration}; +use std::{ + collections::BTreeMap, + mem::{size_of, size_of_val}, + time::Duration, +}; +use crate::signer::{ledger_signer::LedgerError, SignerError, SignerResult}; use crypto::key::{ extended::ExtendedPublicKey, hdkd::{ @@ -23,11 +28,10 @@ use crypto::key::{ }, secp256k1::{extended_keys::Secp256k1ExtendedPublicKey, Secp256k1PublicKey}, }; -use ledger_lib::Exchange; use serialization::{Decode, DecodeAll, Encode}; use utils::ensure; -use crate::signer::{ledger_signer::LedgerError, SignerError, SignerResult}; +use ledger_lib::Exchange; const MAX_MSG_SIZE: usize = (u8::MAX - 5) as usize; // 4 bytes for the header + 1 for len const TIMEOUT_DUR: Duration = Duration::from_secs(100); @@ -111,8 +115,8 @@ struct SignatureResult { } /// Check that the response ends with the OK status code and return the rest of the response back -fn ok_response(resp: Vec) -> SignerResult> { - let (resp, status_code) = resp.split_last_chunk().ok_or(LedgerError::InvalidResponse)?; +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!( @@ -120,10 +124,11 @@ fn ok_response(resp: Vec) -> SignerResult> { LedgerError::ErrorResponse(response_status) ); - Ok(resp.to_vec()) + resp.truncate(resp.len() - size_of_val(&response_status)); + Ok(resp) } -/// send a message to the Ledger and check the respons status code is ok +/// Send a message to the Ledger and check the response status code is ok async fn exchange_message( ledger: &mut L, msg_buf: &[u8], @@ -258,7 +263,9 @@ pub async fn sign_tx( let mut msg_buf = Vec::with_capacity(15); msg_buf.extend([CLA, Ins::SIGN_TX, P1::TX_META, P2::MORE]); - msg_buf.push(1 + 1 + 4 + 4); // data len coin + version + 2 u32 lens + let data_len = + (size_of_val(&chain_type) + size_of_val(&TX_VERSION) + size_of::() * 2) as u8; + msg_buf.push(data_len); msg_buf.push(chain_type); msg_buf.push(TX_VERSION); msg_buf.extend((inputs.len() as u32).to_be_bytes()); diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs index a9b516c3fd..15347be685 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -14,11 +14,21 @@ // limitations under the License. mod ledger_messages; -use std::{borrow::Cow, collections::BTreeMap, sync::Arc}; -use async_trait::async_trait; -use itertools::{izip, Itertools}; +use std::{borrow::Cow, collections::BTreeMap, sync::Arc}; +use crate::{ + key_chain::{make_account_path, AccountKeyChains, FoundPubKey}, + signer::{ + ledger_signer::ledger_messages::{ + check_current_app, get_app_name, get_extended_public_key, sign_challenge, sign_tx, + LedgerAddrType, LedgerBip32Path, LedgerInputAddressPath, LedgerSignature, + LedgerTxInput, LedgerTxInputCommitment, LedgerTxOutput, + }, + utils::{is_htlc_utxo, produce_uniparty_signature_for_input}, + Signer, SignerError, SignerResult, + }, +}; use common::{ chain::{ config::ChainType, @@ -54,10 +64,7 @@ use crypto::key::{ signature::SignatureKind, PrivateKey, SigAuxDataProvider, Signature, SignatureError, }; -use ledger_lib::{Exchange, Filters, LedgerHandle, LedgerProvider, Transport}; -use randomness::make_true_rng; use serialization::Encode; -use tokio::sync::Mutex; use utils::ensure; use wallet_storage::{WalletStorageReadLocked, WalletStorageReadUnlocked}; use wallet_types::{ @@ -66,25 +73,18 @@ use wallet_types::{ signature_status::SignatureStatus, }; -use crate::{ - key_chain::{make_account_path, AccountKeyChains, FoundPubKey}, - signer::{ - ledger_signer::ledger_messages::{ - check_current_app, get_app_name, get_extended_public_key, sign_challenge, sign_tx, - LedgerAddrType, LedgerBip32Path, LedgerInputAddressPath, LedgerSignature, - LedgerTxInput, LedgerTxInputCommitment, LedgerTxOutput, - }, - utils::{is_htlc_utxo, produce_uniparty_signature_for_input}, - Signer, SignerError, SignerResult, - }, -}; +use async_trait::async_trait; +use itertools::{izip, Itertools}; +use ledger_lib::{Exchange, Filters, LedgerHandle, LedgerProvider, Transport}; +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("Different active app: \"{0}\", opened on the Ledger. Please open the Mintlayer app.")] + #[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, @@ -94,7 +94,7 @@ pub enum LedgerError { DeviceError(String), #[error("Missing hardware wallet data in database")] MissingHardwareWalletData, - #[error("Derivation path is to long to send to Ledger")] + #[error("Derivation path is too long to send to Ledger")] PathToLong, #[error("Invalid public key returned from Ledger")] InvalidKey, @@ -106,7 +106,7 @@ pub enum LedgerError { MultipleSignaturesReturned, #[error("Missing multisig index for signature returned from Device")] MissingMultisigIndexForSignature, - #[error("Invalid Signature error: {0}")] + #[error("Signature error: {0}")] SignatureError(#[from] SignatureError), } @@ -118,13 +118,13 @@ struct StandaloneInput { type StandaloneInputs = BTreeMap>; #[async_trait] -pub trait LProvider { - type L; +pub trait LedgerFinder { + type Ledger; async fn find_ledger_device_from_db( &self, db_tx: &mut T, - ) -> SignerResult<(Self::L, LedgerData)>; + ) -> SignerResult<(Self::Ledger, LedgerData)>; } pub struct LedgerSigner { @@ -137,7 +137,7 @@ pub struct LedgerSigner { impl LedgerSigner where L: Exchange + Send, - P: LProvider, + P: LedgerFinder, { pub fn new(chain_config: Arc, client: Arc>, provider: P) -> Self { Self::new_with_sig_aux_data_provider( @@ -179,7 +179,7 @@ where Err(ledger_lib::Error::Timeout) => { continue; } - // In case of a USB error try to reconnect, and try again + // In case of a communication error try to reconnect, and try again Err( ledger_lib::Error::Hid(_) | ledger_lib::Error::Tcp(_) @@ -231,7 +231,7 @@ where } #[allow(clippy::too_many_arguments)] - fn make_signature<'a, 'b, F, F2>( + fn make_signature<'a, 'b, MakeWitnessFn, StandaloneSignerFn>( &self, signatures: &[LedgerSignature], standalone_inputs: &'a [StandaloneInput], @@ -239,12 +239,12 @@ where sighash_type: SigHashType, sighash: H256, key_chain: &impl AccountKeyChains, - make_witness: F, - sign_with_standalone_private_key: F2, + make_witness: MakeWitnessFn, + sign_with_standalone_private_key: StandaloneSignerFn, ) -> SignerResult<(Option, SignatureStatus)> where - F: Fn(StandardInputSignature) -> InputWitness, - F2: Fn(&'a StandaloneInput, &'b Destination) -> SignerResult, + MakeWitnessFn: Fn(StandardInputSignature) -> InputWitness, + StandaloneSignerFn: Fn(&'a StandaloneInput, &'b Destination) -> SignerResult, { match destination { Destination::AnyoneCanSpend => Ok(( @@ -345,7 +345,7 @@ where .collect() } - fn check_signature_status( + fn check_multisig_signature_status( &self, sighash: H256, current_signatures: &AuthorizedClassicalMultisigSpend, @@ -387,7 +387,7 @@ where current_signatures.add_signature(idx as u8, sig); } - let status = self.check_signature_status(sighash, ¤t_signatures)?; + let status = self.check_multisig_signature_status(sighash, ¤t_signatures)?; Ok((current_signatures, status)) } @@ -445,7 +445,7 @@ where impl Signer for LedgerSigner where L: Exchange + Send, - P: Send + Sync + LProvider, + P: Send + Sync + LedgerFinder, { async fn sign_tx( &mut self, @@ -470,27 +470,17 @@ where .version_at_height(block_height) .1 .sighash_input_commitment_version(); - let _input_commitment_version = match input_commitment_version { - common::chain::SighashInputCommitmentVersion::V0 => { - trezor_client::client::SighashInputCommitmentsVersion::V0 - } - common::chain::SighashInputCommitmentVersion::V1 => { - trezor_client::client::SighashInputCommitmentsVersion::V1 - } - }; + // 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, - chain_type, - &inputs, - &input_commitments, - &outputs, - // input_commitment_version, - ) - .await + sign_tx(client, chain_type, &inputs, &input_commitments, &outputs).await }, &mut db_tx, key_chain, @@ -1055,7 +1045,8 @@ async fn check_public_keys_against_key_chain Self { + Self { addr } + } +} + +#[async_trait] +impl Handle for PodmanHandle { + fn addr(&self) -> SocketAddr { + self.addr + } +} diff --git a/wallet/src/signer/ledger_signer/speculus/handle.rs b/wallet/src/signer/ledger_signer/speculos/handle.rs similarity index 78% rename from wallet/src/signer/ledger_signer/speculus/handle.rs rename to wallet/src/signer/ledger_signer/speculos/handle.rs index 955c9e5319..ba5dc6827c 100644 --- a/wallet/src/signer/ledger_signer/speculus/handle.rs +++ b/wallet/src/signer/ledger_signer/speculos/handle.rs @@ -14,14 +14,15 @@ // 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. +//! 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 image::{io::Reader as ImageReader, DynamicImage}; use reqwest::Client; use serde::{Deserialize, Serialize}; use strum::Display; @@ -45,7 +46,7 @@ pub enum Action { PressAndRelease, } -/// Button action object for serialisation and use with the HTTP API +/// Button action object for serialization and use with the HTTP API #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] struct ButtonAction { pub action: Action, @@ -72,20 +73,6 @@ pub trait Handle { Ok(()) } - - // /// Fetch a screenshot from the simulator - // async fn screenshot(&self) -> anyhow::Result { - // // Fetch screenshot from HTTP API - // let r = reqwest::get(format!("http://{}/screenshot", self.addr())).await?; - // - // // Read image bytes - // let b = r.bytes().await?; - // - // // Parse image object - // let i = ImageReader::new(Cursor::new(b)).with_guessed_format()?.decode()?; - // - // Ok(i) - // } } #[cfg(test)] 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..ee02147914 --- /dev/null +++ b/wallet/src/signer/ledger_signer/speculos/mod.rs @@ -0,0 +1,23 @@ +// 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 drivers; +pub use drivers::*; + +mod handle; +pub use handle::*; diff --git a/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs b/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs deleted file mode 100644 index c10baaea77..0000000000 --- a/wallet/src/signer/ledger_signer/speculus/drivers/mod.rs +++ /dev/null @@ -1,240 +0,0 @@ -// 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. - -//! Podman driver for speculos execution, runs a speculos instance within -//! a Podman container. - -use core::fmt::Debug; - -use async_trait::async_trait; -use tokio::{ - io::{AsyncBufReadExt, BufReader}, - process::Command as TokioCommand, - sync::oneshot::{channel, Sender}, -}; - -use crate::signer::ledger_signer::speculus::{Handle, Options}; - -use std::{ - net::{IpAddr, Ipv4Addr, SocketAddr}, - path::PathBuf, - process::Stdio, -}; - -/// [`Driver`] trait for speculos providers -#[async_trait] -pub trait Driver { - type Handle: Debug; - - /// Run speculos with the specified app and options - fn run(&self, app: &str, opts: Options) -> anyhow::Result; - - /// Exit task - fn exit(&self, handle: Self::Handle) -> anyhow::Result<()>; -} - -/// Podman-based Speculos driver -pub struct PodmanDriver; - -/// Handle to a Speculos instance running under Podman -#[derive(Debug)] -pub struct PodmanHandle { - name: String, - addr: SocketAddr, - // Sender to signal the log streaming task to shut down. - exit_tx: Sender<()>, -} - -impl PodmanDriver { - /// Create a new podman driver. - pub fn new() -> Result { - Ok(Self) - } - - fn run_command(mut command: std::process::Command) -> anyhow::Result { - let command_str = format!("{:?}", command); - let output = command.output().unwrap(); - - if !output.status.success() { - anyhow::bail!( - "Podman command failed: {}\nSTDOUT: {}\nSTDERR: {}", - command_str, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - logging::log::debug!( - "Successfully ran podman command: {}\nSTDOUT: {}", - command_str, - String::from_utf8_lossy(&output.stdout) - ); - Ok(output) - } -} - -const DEFAULT_IMAGE: &str = "ghcr.io/ledgerhq/speculos"; - -#[async_trait] -impl Driver for PodmanDriver { - type Handle = PodmanHandle; - - fn run(&self, app: &str, opts: Options) -> anyhow::Result { - let name = format!("speculos-{}", opts.http_port); - - // Ensure any previous container with the same name is removed. - logging::log::debug!("Force removing existing container '{}'", &name); - let mut cleanup_cmd = std::process::Command::new("podman"); - cleanup_cmd.args(["rm", "-f", &name]); - // We don't care if this fails (e.g., if the container didn't exist). - let _ = Self::run_command(cleanup_cmd); - - // Path to the application binary and its parent directory. - let app_path = PathBuf::from(app); - let app_file_name = app_path - .file_name() - .and_then(|n| n.to_str()) - .ok_or_else(|| anyhow::anyhow!("Invalid application path: {}", app))?; - let app_parent_dir = app_path - .parent() - .and_then(|p| p.to_str()) - .ok_or_else(|| anyhow::anyhow!("Could not get parent directory of app: {}", app))?; - - // Setup the command to run inside the container. - let mut speculos_cmd_args = opts.args(); - speculos_cmd_args.push(format!("/app/{}", app_file_name)); - - logging::log::debug!("Container command: {}", speculos_cmd_args.join(" ")); - - // Build the `podman run` command. - let mut command = std::process::Command::new("podman"); - command.arg("run"); - command.arg("--detach"); - command.arg("--name"); - command.arg(&name); - - command.arg("--log-driver=k8s-file"); - - // Map ports. - let mut ports = vec![opts.http_port]; - if let Some(p) = opts.apdu_port { - ports.push(p); - } - for port in ports { - command.arg("-p").arg(format!("{}:{}", port, port)); - } - - // Mount the app's directory as a volume - command.arg("-v").arg(format!("{}:/app:ro", app_parent_dir)); - - // Set image and the command to run. - command.arg(DEFAULT_IMAGE); - command.args(&speculos_cmd_args); - - // Create and start the container. - logging::log::debug!("Creating and starting container '{}'", &name); - Self::run_command(command)?; - logging::log::debug!("Container '{}' started", &name); - - let (exit_tx, mut exit_rx) = channel(); - - // Spawn a task to stream container logs. - let container_name_clone = name.clone(); - tokio::spawn(async move { - logging::log::debug!( - "Starting log streaming for container '{}'", - container_name_clone - ); - let mut cmd = TokioCommand::new("podman"); - cmd.args(["logs", "--follow", &container_name_clone]); - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - - let mut child = match cmd.spawn() { - Ok(child) => child, - Err(e) => { - logging::log::debug!("Failed to spawn podman logs: {}", e); - return; - } - }; - - let stdout = child.stdout.take().expect("Failed to open stdout"); - let stderr = child.stderr.take().expect("Failed to open stderr"); - - let mut stdout_reader = BufReader::new(stdout).lines(); - let mut stderr_reader = BufReader::new(stderr).lines(); - - loop { - tokio::select! { - // Check for exit signal - _ = &mut exit_rx => { - logging::log::debug!("Received exit signal for log streaming. Killing process."); - let _ = child.kill().await; - break; - }, - // Read from stdout - Ok(Some(line)) = stdout_reader.next_line() => { - logging::log::debug!("[{}] {}", container_name_clone, line); - }, - // Read from stderr - Ok(Some(line)) = stderr_reader.next_line() => { - logging::log::debug!("[{}] {}", container_name_clone, line); - }, - // Break if log stream ends - else => break, - } - } - logging::log::debug!( - "Log streaming task for '{}' finished.", - container_name_clone - ); - }); - - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), opts.http_port); - Ok(PodmanHandle { - name, - addr, - exit_tx, - }) - } - - fn exit(&self, handle: Self::Handle) -> anyhow::Result<()> { - logging::log::debug!("Stopping container {}", handle.name); - - // Signal the log streaming task to terminate. - let _ = handle.exit_tx.send(()); - - // Stop the container. - let mut stop_cmd = std::process::Command::new("podman"); - stop_cmd.args(["stop", &handle.name]); - // Ignore errors, as we will force remove it anyway. - let _ = Self::run_command(stop_cmd); - - // Remove the container. - logging::log::debug!("Removing container {}", handle.name); - let mut rm_cmd = std::process::Command::new("podman"); - rm_cmd.args(["rm", "-f", &handle.name]); - Self::run_command(rm_cmd)?; - - logging::log::debug!("Container {} removed", handle.name); - Ok(()) - } -} - -#[async_trait] -impl Handle for PodmanHandle { - fn addr(&self) -> SocketAddr { - self.addr - } -} diff --git a/wallet/src/signer/ledger_signer/speculus/mod.rs b/wallet/src/signer/ledger_signer/speculus/mod.rs deleted file mode 100644 index 1e7c0518f6..0000000000 --- a/wallet/src/signer/ledger_signer/speculus/mod.rs +++ /dev/null @@ -1,139 +0,0 @@ -// 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. - -use strum::{Display, EnumString, VariantNames}; - -mod drivers; -pub use drivers::*; - -mod handle; -pub use handle::*; - -/// Device model -#[derive(Copy, Clone, PartialEq, Debug, VariantNames, Display, EnumString)] -#[strum(serialize_all = "lowercase")] -pub enum Model { - /// Nano S - NanoS, - /// Nano S Plus - #[strum(serialize = "nanosplus", to_string = "nanosp")] - NanoSP, - /// Nano X - NanoX, -} - -/// Simulator display mode -#[derive(Copy, Clone, PartialEq, Debug, VariantNames, Display, EnumString)] -#[strum(serialize_all = "lowercase")] -pub enum Display { - /// Headless mode - Headless, - /// QT based rendering - Qt, - /// Text based (command line) rendering - Text, -} - -/// Simulator options -#[derive(Clone, PartialEq, Debug)] -pub struct Options { - /// Model to simulate - pub model: Model, - - /// Display mode - pub display: Display, - - /// SDK version override (defaults based on --model) - pub sdk: Option, - - /// API level override - pub api_level: Option, - - /// BIP39 seed for initialisation - pub seed: Option, - - /// Enable HTTP API port - pub http_port: u16, - - /// Enable APDU TCP port (usually 1237) - pub apdu_port: Option, - - /// Enable debugging and wait for GDB connection (port 1234) - pub debug: bool, - - /// Speculos root (used to configure python paths if set) - pub root: Option, - - /// Trace syscalls - pub trace: bool, -} - -impl Default for Options { - fn default() -> Self { - Self { - model: Model::NanoSP, - display: Display::Headless, - sdk: None, - api_level: None, - seed: None, - http_port: 5000, - apdu_port: None, - debug: false, - root: None, - trace: false, - } - } -} - -impl Options { - /// Build an argument list from [Options] - pub fn args(&self) -> Vec { - // Basic args - let mut args = vec![ - format!("--model={}", self.model), - format!("--display={}", self.display), - format!("--api-port={}", self.http_port), - ]; - - if let Some(seed) = &self.seed { - args.push(format!("--seed={seed}")); - } - - if let Some(apdu_port) = &self.apdu_port { - args.push(format!("--apdu-port={apdu_port}")); - } - - if let Some(sdk) = &self.sdk { - args.push(format!("--sdk={sdk}")); - } - - if let Some(api_level) = &self.api_level { - args.push(format!("--apiLevel={api_level}")); - } - - if self.debug { - args.push("--debug".to_string()); - } - - if self.trace { - args.push("-t".to_string()); - } - - args - } -} diff --git a/wallet/src/signer/ledger_signer/tests.rs b/wallet/src/signer/ledger_signer/tests.rs index c5565d2202..adc48140f0 100644 --- a/wallet/src/signer/ledger_signer/tests.rs +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -14,18 +14,36 @@ // limitations under the License. use std::{ - net::{Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr, sync::Arc, time::Duration, }; -use async_trait::async_trait; +use crate::signer::{ + ledger_signer::{ + ledger_messages::{get_app_name, get_extended_public_key}, + 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 wallet_storage::WalletStorageReadLocked; +use wallet_types::hw_data::LedgerData; + +use async_trait::async_trait; use ledger_lib::{ transport::{TcpDevice, TcpInfo, TcpTransport}, Transport, @@ -42,48 +60,24 @@ use tokio::{ }, time::sleep, }; -use wallet_storage::WalletStorageReadLocked; -use wallet_types::hw_data::LedgerData; - -use crate::signer::{ - ledger_signer::{ - ledger_messages::{get_app_name, get_extended_public_key}, - speculus::{ - Action, Button, Display, Driver, Handle, Model, Options, PodmanDriver, PodmanHandle, - }, - LProvider, LedgerError, LedgerSigner, - }, - tests::{ - generic_tests::{ - test_sign_transaction_generic, test_sign_transaction_intent_generic, MessageToSign, - }, - no_another_signer, - }, - SignerError, SignerResult, -}; #[derive(Debug)] enum ControlMessage { Finish, } -async fn auto_confirmer( - mut finish_rx: mpsc::Receiver, - handle: PodmanHandle, - driver: PodmanDriver, -) { +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(); } - msg = finish_rx.recv() => { + msg = control_msg_rx.recv() => { match msg { Some(ControlMessage::Finish) => { eprintln!("Received finish signal."); - // perform exit action - driver.exit(handle).unwrap(); break; } None => { @@ -101,56 +95,31 @@ async fn auto_confirmer( struct DummyProvider; #[async_trait] -impl LProvider for DummyProvider { - type L = TcpDevice; +impl LedgerFinder for DummyProvider { + type Ledger = TcpDevice; async fn find_ledger_device_from_db( &self, _db_tx: &mut T, - ) -> SignerResult<(Self::L, LedgerData)> { + ) -> SignerResult<(Self::Ledger, LedgerData)> { Err(SignerError::LedgerError(LedgerError::NoDeviceFound)) } } async fn setup( - offset: u16, deterministic_aux: bool, ) -> ( tokio::task::JoinHandle<()>, Sender, impl Fn(Arc, U31) -> LedgerSigner, ) { - let driver = PodmanDriver::new().unwrap(); - - let opts = Options { - model: Model::NanoSP, - api_level: Some("24".to_string()), - seed: Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string()), // Use a deterministic seed for tests - display: Display::Headless, - apdu_port: Some(1237 + offset), - http_port: 5001 + offset, - ..Default::default() - }; - - let app_path = format!( - "{}/../../ledger-mintlayer/target/nanosplus/release/mintlayer-app", - env!("CARGO_MANIFEST_DIR") - ); - let handle = driver.run(&app_path, opts.clone()).unwrap(); - - println!("Emulator is running..."); - - // TODO: instead of sleeping try to connect and wait for a successfull connection to the - // docker - sleep(Duration::from_secs(5)).await; + 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)), - 1237 + offset, - ), + addr: SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9999), }) .await .unwrap(); @@ -171,10 +140,10 @@ async fn setup( let device = Arc::new(Mutex::new(device)); - let (finish_tx, finish_rx) = mpsc::channel(1); - let auto_clicker = tokio::spawn(auto_confirmer(finish_rx, handle, driver)); + let (control_msg_tx, control_msg_rx) = mpsc::channel(1); + let auto_clicker = tokio::spawn(auto_confirmer(control_msg_rx, handle)); - (auto_clicker, finish_tx, move |chain_config, _| { + (auto_clicker, control_msg_tx, move |chain_config, _| { let aux_provider: Box = if deterministic_aux { Box::new(PredefinedSigAuxDataProvider) } else { @@ -192,9 +161,10 @@ async fn setup( #[rstest] #[trace] +#[serial_test::serial] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_account_extended_public_key() { - let (auto_clicker, finish_tx, make_ledger_signer) = setup(1, false).await; + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(false).await; let signer = make_ledger_signer(Arc::new(create_mainnet()), U31::ZERO); @@ -214,32 +184,24 @@ async fn test_account_extended_public_key() { let expected_chain_code = "0b71f99e82c97a4c8f75d8d215e7260bcf9e964d437ec252af26877adf7e8683"; assert_eq!(expected_chain_code, chain_code.hex_encode()); - finish_tx.send(ControlMessage::Finish).await.unwrap(); + 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, - #[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) { 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, finish_tx, make_ledger_signer) = setup(2, false).await; + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(false).await; test_sign_message_generic( &mut rng, @@ -249,29 +211,31 @@ async fn test_sign_message( ) .await; - finish_tx.send(ControlMessage::Finish).await.unwrap(); + 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, finish_tx, make_ledger_signer) = setup(3, false).await; + 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; - finish_tx.send(ControlMessage::Finish).await.unwrap(); + 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( @@ -280,7 +244,7 @@ async fn test_sign_transaction( ) { log::debug!("test_sign_transaction, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); - let (auto_clicker, finish_tx, make_ledger_signer) = setup(4, false).await; + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(false).await; let mut rng = make_seedable_rng(seed); @@ -292,12 +256,13 @@ async fn test_sign_transaction( ) .await; - finish_tx.send(ControlMessage::Finish).await.unwrap(); + 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( @@ -308,18 +273,19 @@ async fn test_fixed_signatures2( log::debug!("test_fixed_signatures2, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); - let (auto_clicker, finish_tx, make_ledger_signer) = setup(6, true).await; + 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; - finish_tx.send(ControlMessage::Finish).await.unwrap(); + 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) { @@ -329,7 +295,7 @@ async fn test_sign_message_sig_consistency(#[case] seed: Seed) { log::debug!("test_sign_message_sig_consistency, seed = {seed:?}"); - let (auto_clicker, finish_tx, make_ledger_signer) = setup(7, true).await; + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; let mut rng = make_seedable_rng(seed); @@ -341,12 +307,13 @@ async fn test_sign_message_sig_consistency(#[case] seed: Seed) { ) .await; - finish_tx.send(ControlMessage::Finish).await.unwrap(); + 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) { @@ -354,7 +321,7 @@ async fn test_sign_transaction_intent_sig_consistency(#[case] seed: Seed) { log::debug!("test_sign_transaction_intent_sig_consistency, seed = {seed:?}"); - let (auto_clicker, finish_tx, make_ledger_signer) = setup(8, true).await; + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; let mut rng = make_seedable_rng(seed); @@ -365,12 +332,13 @@ async fn test_sign_transaction_intent_sig_consistency(#[case] seed: Seed) { ) .await; - finish_tx.send(ControlMessage::Finish).await.unwrap(); + 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( @@ -381,7 +349,7 @@ async fn test_sign_transaction_sig_consistency( log::debug!("test_sign_transaction_sig_consistency, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); - let (auto_clicker, finish_tx, make_ledger_signer) = setup(9, true).await; + let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; let mut rng = make_seedable_rng(seed); @@ -393,6 +361,6 @@ async fn test_sign_transaction_sig_consistency( ) .await; - finish_tx.send(ControlMessage::Finish).await.unwrap(); + control_msg_tx.send(ControlMessage::Finish).await.unwrap(); auto_clicker.await.unwrap(); } diff --git a/wallet/src/signer/tests/generic_tests.rs b/wallet/src/signer/tests/generic_tests.rs index dff8967a3c..c69ef09743 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/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(); From 9546fb748a8623182d03015bd7cac58a0e7aed9f Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Wed, 22 Oct 2025 22:52:50 +0200 Subject: [PATCH 05/24] Use the new ledger messages --- Cargo.lock | 108 ++++++--- Cargo.toml | 12 +- wallet/Cargo.toml | 8 +- .../signer/ledger_signer/ledger_messages.rs | 227 ++++++++---------- wallet/src/signer/ledger_signer/mod.rs | 136 ++++++----- .../ledger_signer/speculos/drivers/mod.rs | 43 ---- .../signer/ledger_signer/speculos/handle.rs | 78 +++--- .../src/signer/ledger_signer/speculos/mod.rs | 3 - wallet/src/signer/ledger_signer/tests.rs | 92 +++++-- wallet/src/signer/trezor_signer/mod.rs | 4 +- 10 files changed, 374 insertions(+), 337 deletions(-) delete mode 100644 wallet/src/signer/ledger_signer/speculos/drivers/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 84422cf3be..5501bbbb15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ name = "accounting" version = "1.2.0" dependencies = [ "common", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "proptest", "rstest", "serialization", @@ -312,7 +312,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", @@ -1009,7 +1009,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", @@ -1341,7 +1341,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", @@ -1417,7 +1417,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", @@ -1504,7 +1504,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", @@ -1724,7 +1724,7 @@ dependencies = [ "num", "num-traits", "once_cell", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "paste", "proptest", "randomness", @@ -1769,7 +1769,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", @@ -2097,7 +2097,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", @@ -2575,7 +2575,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", @@ -3560,9 +3560,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hex-conservative" @@ -4583,9 +4580,7 @@ dependencies = [ "bitflags 2.10.0", "displaydoc", "encdec", - "hex", "num_enum", - "serde", "thiserror 2.0.18", ] @@ -4957,7 +4952,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", ] @@ -4973,6 +4968,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "messages" +version = "0.1.0" +source = "git+https://github.com/mintlayer/mintlayer-ledger-app?rev=2fa1c33e536f94ba3cac94b97054902674580686#2fa1c33e536f94ba3cac94b97054902674580686" +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", +] + [[package]] name = "metal" version = "0.27.0" @@ -5036,7 +5041,7 @@ source = "git+https://github.com/mintlayer/mintlayer-core-primitives?rev=4e21bf8 dependencies = [ "derive_more 2.1.1", "fixed-hash", - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "strum 0.27.2", ] @@ -5232,7 +5237,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", @@ -6006,7 +6011,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", @@ -6081,7 +6086,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", @@ -6149,7 +6154,7 @@ name = "p2p-types" version = "1.2.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", @@ -6190,11 +6195,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" @@ -6207,6 +6225,17 @@ dependencies = [ "syn 2.0.114", ] +[[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.114", +] + [[package]] name = "parking" version = "2.2.1" @@ -6485,7 +6514,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", @@ -7663,7 +7692,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", @@ -7981,7 +8010,7 @@ version = "1.2.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", ] @@ -7989,7 +8018,7 @@ dependencies = [ name = "serialization-tagged" version = "1.2.0" dependencies = [ - "parity-scale-codec", + "parity-scale-codec 3.7.5 (registry+https://github.com/rust-lang/crates.io-index)", "proptest", "serialization", "serialization-core", @@ -8372,7 +8401,7 @@ version = "1.2.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", @@ -8695,7 +8724,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", @@ -8896,7 +8925,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", @@ -9295,6 +9324,17 @@ dependencies = [ "unicode-normalization", ] +[[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 0.26.3", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -9549,7 +9589,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", @@ -9590,7 +9630,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", @@ -9673,11 +9713,11 @@ dependencies = [ "itertools 0.14.0", "lazy_static", "ledger-lib", - "ledger-proto", "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", @@ -10029,7 +10069,7 @@ dependencies = [ "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", diff --git a/Cargo.toml b/Cargo.toml index 6a9d895a83..1ff8ca87dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -289,9 +289,11 @@ package = "mintlayer-core-primitives" git = "https://github.com/ledger-community/rust-ledger.git" rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" -[workspace.dependencies.ledger-proto] -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 = "2fa1c33e536f94ba3cac94b97054902674580686" +package = "messages" [workspace.dependencies.trezor-client] git = "https://github.com/mintlayer/mintlayer-trezor-firmware" @@ -351,5 +353,7 @@ fontconfig-parser = { git = "https://github.com/Riey/fontconfig-parser", rev = " # subsystem (reproducible on a slow machine during initial sync with 30+ connected peers). # The PR was merged after 1.49, so it should probably be part of 1.50 when it comes out. tokio = { git = "https://github.com/tokio-rs/tokio", rev = "0d6c7af3e43457350bdc03a6dbcafa276fab7352" } -# The patch is needed because there is now release of the library. We use the same hash for all Ledger libs + +# 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/wallet/Cargo.toml b/wallet/Cargo.toml index 809dabe579..477a3df4b5 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -47,7 +47,7 @@ tokio = { workspace = true, default-features = false, features = [ ] } trezor-client = { workspace = true, optional = true } ledger-lib = { workspace = true, optional = true } -ledger-proto = { workspace = true, optional = true } +mintlayer-ledger-messages = { workspace = true, optional = true } zeroize.workspace = true [dev-dependencies] @@ -83,5 +83,9 @@ enable-ledger-device-tests = [] # 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 = [] -ledger = ["dep:ledger-lib", "dep:ledger-proto", "wallet-types/ledger"] +ledger = [ + "dep:ledger-lib", + "dep:mintlayer-ledger-messages", + "wallet-types/ledger", +] default = ["trezor", "ledger"] diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs index 0a96ef59cb..00dd968eac 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -13,13 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - collections::BTreeMap, - mem::{size_of, size_of_val}, - time::Duration, -}; +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::{ @@ -32,76 +32,19 @@ use serialization::{Decode, DecodeAll, Encode}; use utils::ensure; 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, InputAdditionalInfoReq, Ins, + OutputValue as LOutputValue, P1SignTx, PubKeyP1, PublicKeyReq, SignMessageReq, SignTxReq, + TxInput as LTxInput, TxInputReq, TxMetadataReq, TxOutput as LTxOutput, TxOutputReq, APDU_CLASS, + H256 as LH256, P1_APP_NAME, P1_SIGN_NEXT, P1_SIGN_START, P2_DONE, P2_SIGN_MORE, +}; -const MAX_MSG_SIZE: usize = (u8::MAX - 5) as usize; // 4 bytes for the header + 1 for len +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 CLA: u8 = 0xE0; const TX_VERSION: u8 = 1; -#[derive(Clone, Copy)] -pub enum LedgerAddrType { - PublicKey, - PublicKeyHash, -} - -impl From for u8 { - fn from(addr_type: LedgerAddrType) -> u8 { - match addr_type { - LedgerAddrType::PublicKey => 0, - LedgerAddrType::PublicKeyHash => 1, - } - } -} - -struct Ins {} - -impl Ins { - const APP_NAME: u8 = 0x04; - const PUB_KEY: u8 = 0x05; - const SIGN_TX: u8 = 0x06; - const SIGN_MSG: u8 = 0x07; -} - -struct P1 {} - -impl P1 { - const TX_META: u8 = 0; - const TX_INPUT: u8 = 1; - const TX_COMMITMENT: u8 = 2; - const TX_OUTPUT: u8 = 3; - const TX_SIG: u8 = 4; -} - -struct P2 {} - -impl P2 { - const DONE: u8 = 0x00; - const MORE: u8 = 0x80; -} - -#[derive(Encode)] -pub struct LedgerBip32Path(pub Vec); - -pub struct LedgerTxInput { - pub inp: Vec, - pub address_paths: Vec, -} - -pub struct LedgerTxInputCommitment { - pub commitment: Vec, -} - -pub struct LedgerTxOutput { - pub out: Vec, -} - -#[derive(Encode, Debug)] -pub struct LedgerInputAddressPath { - pub address_n: Vec, - pub multisig_idx: Option, -} - #[derive(Decode)] pub struct LedgerSignature { pub signature: [u8; 64], @@ -115,7 +58,7 @@ struct SignatureResult { } /// Check that the response ends with the OK status code and return the rest of the response back -fn ok_response(mut resp: Vec) -> SignerResult> { +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); @@ -144,22 +87,22 @@ async fn exchange_message( async fn send_chunked( ledger: &mut L, ins: u8, - step: u8, + p1: u8, message: &[u8], ) -> Result, SignerError> { let mut msg_buf = vec![]; - let mut chunks = message.chunks(MAX_MSG_SIZE).peekable(); + 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::MORE + P2_SIGN_MORE } else { - P2::DONE + P2_DONE }; - msg_buf.extend([CLA, ins, step, p2]); + 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?; @@ -170,23 +113,20 @@ async fn send_chunked( pub async fn sign_challenge( ledger: &mut L, - chain_type: u8, - path: &LedgerBip32Path, - addr_type: LedgerAddrType, + coin: CoinType, + path: LedgerBip32Path, + addr_type: AddrType, message: &[u8], ) -> SignerResult> { - let mut msg_buf = vec![]; + let req = SignMessageReq { + coin, + addr_type, + path, + }; - msg_buf.extend([CLA, Ins::SIGN_MSG, 0, P2::MORE]); - let body = path.encode(); - msg_buf.push(body.len() as u8 + 2); - msg_buf.push(chain_type); - msg_buf.push(addr_type.into()); - msg_buf.extend(body); + send_chunked(ledger, Ins::SIGN_MSG, P1_SIGN_START, &ledger_encode(req)).await?; - exchange_message(ledger, &msg_buf).await?; - - let resp = send_chunked(ledger, Ins::SIGN_MSG, 1, message).await?; + let resp = send_chunked(ledger, Ins::SIGN_MSG, P1_SIGN_NEXT, message).await?; let sig_len = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize; let sig = resp.as_slice().get(1..1 + sig_len).ok_or(LedgerError::InvalidResponse)?; @@ -195,7 +135,7 @@ pub async fn sign_challenge( } pub async fn get_app_name(ledger: &mut L) -> Result, ledger_lib::Error> { - let msg_buf = [CLA, Ins::APP_NAME, 0, P2::DONE]; + let msg_buf = [APDU_CLASS, Ins::APP_NAME, P1_APP_NAME, P2_DONE]; ledger.exchange(&msg_buf, Duration::from_millis(100)).await } @@ -217,22 +157,21 @@ pub async fn check_current_app(ledger: &mut L) -> SignerResult<()> pub async fn get_extended_public_key( ledger: &mut L, - chain_type: u8, + coin_type: CoinType, derivation_path: DerivationPath, ) -> SignerResult { - let address_n = LedgerBip32Path( + let path = LedgerBip32Path( derivation_path.as_slice().iter().map(|c| c.into_encoded_index()).collect(), ); + let req = PublicKeyReq { coin_type, path }; - let mut msg_buf = vec![]; - msg_buf.extend([CLA, Ins::PUB_KEY, 0, P2::DONE]); - let encoded_path = address_n.encode(); - let size: u8 = encoded_path.len().try_into().map_err(|_| LedgerError::PathToLong)?; - msg_buf.push(size + 1); - msg_buf.push(chain_type); - msg_buf.extend(encoded_path); - - let resp = exchange_message(ledger, &msg_buf).await?; + let resp = send_chunked( + ledger, + Ins::PUB_KEY, + PubKeyP1::NoDisplayAddress.into(), + &ledger_encode(req), + ) + .await?; let pk_len = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize; let public_key = resp.as_slice().get(1..1 + pk_len).ok_or(LedgerError::InvalidResponse)?; @@ -255,43 +194,57 @@ pub async fn get_extended_public_key( pub async fn sign_tx( ledger: &mut L, - chain_type: u8, - inputs: &[LedgerTxInput], - input_commitments: &[LedgerTxInputCommitment], - outputs: &[LedgerTxOutput], + chain_type: CoinType, + inputs: Vec, + input_additional_infos: Vec, + outputs: Vec, ) -> SignerResult>> { - let mut msg_buf = Vec::with_capacity(15); - - msg_buf.extend([CLA, Ins::SIGN_TX, P1::TX_META, P2::MORE]); - let data_len = - (size_of_val(&chain_type) + size_of_val(&TX_VERSION) + size_of::() * 2) as u8; - msg_buf.push(data_len); - msg_buf.push(chain_type); - msg_buf.push(TX_VERSION); - msg_buf.extend((inputs.len() as u32).to_be_bytes()); - msg_buf.extend((outputs.len() as u32).to_be_bytes()); - - exchange_message(ledger, &msg_buf).await?; + 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 { - let paths = inp.address_paths.encode(); - send_chunked(ledger, Ins::SIGN_TX, P1::TX_INPUT, &paths).await?; - send_chunked(ledger, Ins::SIGN_TX, P1::TX_INPUT, &inp.inp).await?; + send_chunked( + ledger, + Ins::SIGN_TX, + P1SignTx::Input.into(), + &ledger_encode(SignTxReq::Input(inp)), + ) + .await?; } - for c in input_commitments { - send_chunked(ledger, Ins::SIGN_TX, P1::TX_COMMITMENT, &c.commitment).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, P1::TX_OUTPUT, &o.out).await?; + resp = send_chunked( + ledger, + Ins::SIGN_TX, + P1SignTx::Output.into(), + &ledger_encode(SignTxReq::Output(o)), + ) + .await?; } let mut signatures: BTreeMap<_, Vec<_>> = BTreeMap::new(); - let msg_buf = [CLA, Ins::SIGN_TX, P1::TX_SIG, P2::DONE, 0]; + 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, @@ -313,7 +266,7 @@ pub async fn sign_tx( 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::MORE; + let has_more_signatures = *resp.last().ok_or(LedgerError::InvalidResponse)? == P2_SIGN_MORE; let sig = LedgerSignature::decode_all(&mut &resp[..resp.len() - 1][1..]) .map_err(|_| LedgerError::InvalidResponse)?; @@ -324,3 +277,27 @@ fn decode_signature_response(resp: &[u8]) -> Result 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 index 15347be685..599127099a 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -15,15 +15,15 @@ mod ledger_messages; -use std::{borrow::Cow, collections::BTreeMap, sync::Arc}; +use std::{collections::BTreeMap, sync::Arc}; use crate::{ key_chain::{make_account_path, AccountKeyChains, FoundPubKey}, signer::{ ledger_signer::ledger_messages::{ check_current_app, get_app_name, get_extended_public_key, sign_challenge, sign_tx, - LedgerAddrType, LedgerBip32Path, LedgerInputAddressPath, LedgerSignature, - LedgerTxInput, LedgerTxInputCommitment, LedgerTxOutput, + to_ledger_amount, to_ledger_output_value, to_ledger_tx_input, to_ledger_tx_output, + LedgerSignature, }, utils::{is_htlc_utxo, produce_uniparty_signature_for_input}, Signer, SignerError, SignerResult, @@ -48,9 +48,7 @@ use common::{ standard_signature::StandardInputSignature, InputWitness, }, - sighash::{ - input_commitments::SighashInputCommitment, sighashtype::SigHashType, signature_hash, - }, + sighash::{sighashtype::SigHashType, signature_hash}, DestinationSigError, }, AccountCommand, ChainConfig, Destination, OrderAccountCommand, SignedTransactionIntent, @@ -76,6 +74,10 @@ use wallet_types::{ 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, TxInputReq, TxOutputReq, +}; use randomness::make_true_rng; use tokio::sync::Mutex; @@ -99,7 +101,9 @@ pub enum LedgerError { #[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")] - HardwareWalletDifferentFile, + 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")] @@ -220,7 +224,7 @@ where key_chain: &impl AccountKeyChains, ) -> SignerResult where - F: AsyncFn(&mut L) -> Result, + F: AsyncFnOnce(&mut L) -> Result, { self.check_session(db_tx, key_chain).await?; @@ -337,11 +341,13 @@ where } } - fn to_ledger_output_msgs(&self, ptx: &PartiallySignedTransaction) -> Vec { + fn to_ledger_output_msgs(&self, ptx: &PartiallySignedTransaction) -> Vec { ptx.tx() .outputs() .iter() - .map(|out| LedgerTxOutput { out: out.encode() }) + .map(|out| TxOutputReq { + out: to_ledger_tx_output(out), + }) .collect() } @@ -461,8 +467,8 @@ where )> { let (inputs, standalone_inputs) = to_ledger_input_msgs(&ptx, key_chain, &db_tx)?; let outputs = self.to_ledger_output_msgs(&ptx); - let input_commitments = to_ledger_commitments_msgs(&ptx)?; - let chain_type = to_ledger_chain_type(&self.chain_config); + 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 @@ -480,7 +486,7 @@ where let new_signatures = self .perform_ledger_operation( async move |client| { - sign_tx(client, chain_type, &inputs, &input_commitments, &outputs).await + sign_tx(client, coin_type, inputs, input_additional_infos, outputs).await }, &mut db_tx, key_chain, @@ -702,8 +708,8 @@ where ); let addr_type = match destination { - Destination::PublicKey(_) => LedgerAddrType::PublicKey, - Destination::PublicKeyHash(_) => LedgerAddrType::PublicKeyHash, + Destination::PublicKey(_) => AddrType::PublicKey, + Destination::PublicKeyHash(_) => AddrType::PublicKeyHash, Destination::AnyoneCanSpend => { return Err(SignerError::SigningError( DestinationSigError::AttemptedToProduceSignatureForAnyoneCanSpend, @@ -721,13 +727,12 @@ where } }; - let chain_type = to_ledger_chain_type(&self.chain_config); + 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, chain_type, &address_n, addr_type, &message) - .await + sign_challenge(client, coin_type, address_n, addr_type, &message).await }, &mut db_tx, key_chain, @@ -815,7 +820,7 @@ fn to_ledger_input_msgs( ptx: &PartiallySignedTransaction, key_chain: &impl AccountKeyChains, db_tx: &impl WalletStorageReadUnlocked, -) -> SignerResult<(Vec, StandaloneInputs)> { +) -> SignerResult<(Vec, StandaloneInputs)> { let res: (Vec<_>, BTreeMap<_, _>) = itertools::process_results( ptx.tx().inputs().iter().zip(ptx.destinations()).enumerate().map( |(idx, (inp, dest))| -> SignerResult<_> { @@ -824,9 +829,9 @@ fn to_ledger_input_msgs( destination_to_address_paths(key_chain, dest, db_tx) })?; - let input = LedgerTxInput { - inp: inp.encode(), - address_paths, + let input = TxInputReq { + inp: to_ledger_tx_input(inp), + addresses: address_paths, }; Ok((input, (idx as u32, standalone_inputs))) @@ -863,7 +868,7 @@ fn destination_to_address_paths_impl( .collect(); Ok(( vec![LedgerInputAddressPath { - address_n, + path: LedgerBip32Path(address_n), multisig_idx, }], vec![], @@ -905,14 +910,14 @@ fn destination_to_address_paths_impl( } } -fn to_ledger_commitments_msgs( +fn to_ledger_input_additional_info_reqs( ptx: &PartiallySignedTransaction, -) -> SignerResult> { +) -> SignerResult> { ptx.input_utxos() .iter() .zip(ptx.tx().inputs()) .map(|(utxo, inp)| { - let commitment = match inp { + let additional_info = match inp { TxInput::Utxo(_) => { let utxo = utxo.as_ref().ok_or(SignerError::MissingUtxo)?; match utxo { @@ -921,16 +926,17 @@ fn to_ledger_commitments_msgs( .additional_info() .get_pool_info(pool_id) .ok_or(SignerError::MissingTxExtraInfo)?; - SighashInputCommitment::ProduceBlockFromStakeUtxo { - utxo: Cow::Borrowed(utxo), - staker_balance: pool_info.staker_balance, + InputAdditionalInfoReq::PoolInfo { + utxo: to_ledger_tx_output(utxo), + staker_balance: to_ledger_amount(&pool_info.staker_balance), } - .encode() } - _ => SighashInputCommitment::Utxo(Cow::Borrowed(utxo)).encode(), + _ => InputAdditionalInfoReq::Utxo { + utxo: to_ledger_tx_output(utxo), + }, } } - TxInput::Account(_) => SighashInputCommitment::None.encode(), + TxInput::Account(_) => InputAdditionalInfoReq::None, TxInput::AccountCommand(_, cmd) => match cmd { AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) @@ -938,32 +944,30 @@ fn to_ledger_commitments_msgs( | AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) | AccountCommand::ChangeTokenAuthority(_, _) - | AccountCommand::ChangeTokenMetadataUri(_, _) => { - SighashInputCommitment::None.encode() - } + | AccountCommand::ChangeTokenMetadataUri(_, _) => InputAdditionalInfoReq::None, AccountCommand::FillOrder(order_id, _, _) => { let order_info = ptx .additional_info() .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; - SighashInputCommitment::FillOrderAccountCommand { - initially_asked: order_info.initially_asked.clone(), - initially_given: order_info.initially_given.clone(), + 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), } - .encode() } AccountCommand::ConcludeOrder(order_id) => { let order_info = ptx .additional_info() .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; - SighashInputCommitment::ConcludeOrderAccountCommand { - initially_asked: order_info.initially_asked.clone(), - initially_given: order_info.initially_given.clone(), - ask_balance: order_info.ask_balance, - give_balance: order_info.give_balance, + 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), } - .encode() } }, | TxInput::OrderAccountCommand(cmd) => match cmd { @@ -972,39 +976,39 @@ fn to_ledger_commitments_msgs( .additional_info() .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; - SighashInputCommitment::FillOrderAccountCommand { - initially_asked: order_info.initially_asked.clone(), - initially_given: order_info.initially_given.clone(), + 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), } - .encode() } OrderAccountCommand::ConcludeOrder(order_id) => { let order_info = ptx .additional_info() .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; - SighashInputCommitment::ConcludeOrderAccountCommand { - initially_asked: order_info.initially_asked.clone(), - initially_given: order_info.initially_given.clone(), - ask_balance: order_info.ask_balance, - give_balance: order_info.give_balance, + 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), } - .encode() } - OrderAccountCommand::FreezeOrder(_) => SighashInputCommitment::None.encode(), + OrderAccountCommand::FreezeOrder(_) => InputAdditionalInfoReq::None, }, }; - Ok(LedgerTxInputCommitment { commitment }) + Ok(additional_info) }) .collect() } -fn to_ledger_chain_type(chain_config: &ChainConfig) -> u8 { +fn to_ledger_chain_type(chain_config: &ChainConfig) -> CoinType { match chain_config.chain_type() { - ChainType::Mainnet => 0, - ChainType::Testnet => 1, - ChainType::Signet => 2, - ChainType::Regtest => 3, + ChainType::Mainnet => CoinType::Mainnet, + ChainType::Testnet => CoinType::Testnet, + ChainType::Signet => CoinType::Regtest, + ChainType::Regtest => CoinType::Signet, } } @@ -1046,10 +1050,10 @@ async fn check_public_keys_against_key_chain( @@ -1058,9 +1062,9 @@ async fn fetch_extended_pub_key( account_index: U31, ) -> Result { let derivation_path = make_account_path(chain_config, account_index); - let chain_type = to_ledger_chain_type(chain_config); + let coin_type = to_ledger_chain_type(chain_config); - get_extended_public_key(client, chain_type, derivation_path) + get_extended_public_key(client, coin_type, derivation_path) .await .map_err(|e| LedgerError::DeviceError(e.to_string())) } diff --git a/wallet/src/signer/ledger_signer/speculos/drivers/mod.rs b/wallet/src/signer/ledger_signer/speculos/drivers/mod.rs deleted file mode 100644 index f1caf46458..0000000000 --- a/wallet/src/signer/ledger_signer/speculos/drivers/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -// 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. - -//! Podman driver for speculos execution, runs a speculos instance within -//! a Podman container. - -use core::fmt::Debug; -use std::net::SocketAddr; - -use crate::signer::ledger_signer::speculos::Handle; - -use async_trait::async_trait; - -/// 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 - } -} diff --git a/wallet/src/signer/ledger_signer/speculos/handle.rs b/wallet/src/signer/ledger_signer/speculos/handle.rs index ba5dc6827c..e3b8a74d16 100644 --- a/wallet/src/signer/ledger_signer/speculos/handle.rs +++ b/wallet/src/signer/ledger_signer/speculos/handle.rs @@ -23,13 +23,13 @@ use std::net::SocketAddr; use async_trait::async_trait; +use logging::log; use reqwest::Client; use serde::{Deserialize, Serialize}; -use strum::Display; -use tracing::debug; +use strum::{Display, EnumIter}; /// Button enumeration -#[derive(Clone, Copy, PartialEq, Debug, Display)] +#[derive(Clone, Copy, PartialEq, Debug, Display, EnumIter)] #[strum(serialize_all = "kebab-case")] pub enum Button { Left, @@ -38,7 +38,7 @@ pub enum Button { } /// Button actions -#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Display)] +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Display, EnumIter)] #[serde(rename_all = "kebab-case")] pub enum Action { Press, @@ -49,7 +49,7 @@ pub enum Action { /// Button action object for serialization and use with the HTTP API #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] struct ButtonAction { - pub action: Action, + action: Action, } /// [Handle] trait for interacting with speculos @@ -60,7 +60,7 @@ pub trait Handle { /// Send a button action to the simulator async fn button(&self, button: Button, action: Action) -> anyhow::Result<()> { - debug!("Sending button request: {}:{}", button, action); + log::debug!("Sending button request: {}:{}", button, action); // Post action to HTTP API let r = Client::new() @@ -69,52 +69,62 @@ pub trait Handle { .send() .await?; - debug!("Button request complete: {}", r.status()); + 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() { - let tests = &[(Button::Left, "left"), (Button::Right, "right"), (Button::Both, "both")]; - - for (v, s) in tests { - assert_eq!(&v.to_string(), s); + 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() { - let tests = &[ - ( - ButtonAction { - action: Action::Press, - }, - r#"{"action":"press"}"#, - ), - ( - ButtonAction { - action: Action::Release, - }, - r#"{"action":"release"}"#, - ), - ( - ButtonAction { - action: Action::PressAndRelease, - }, - r#"{"action":"press-and-release"}"#, - ), - ]; - - for (v, s) in tests { - assert_eq!(&serde_json::to_string(v).unwrap(), s); + 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 index ee02147914..0d0085567a 100644 --- a/wallet/src/signer/ledger_signer/speculos/mod.rs +++ b/wallet/src/signer/ledger_signer/speculos/mod.rs @@ -16,8 +16,5 @@ //! Rust wrapper for executing Speculos via podman, //! provided to simplify CI/CD with ledger applications. -mod drivers; -pub use drivers::*; - mod handle; pub use handle::*; diff --git a/wallet/src/signer/ledger_signer/tests.rs b/wallet/src/signer/ledger_signer/tests.rs index adc48140f0..e7bdd165a8 100644 --- a/wallet/src/signer/ledger_signer/tests.rs +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -20,9 +20,27 @@ use std::{ 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::{get_app_name, get_extended_public_key}, + ledger_messages::{get_app_name, get_extended_public_key, ok_response}, speculos::{Action, Button, Handle, PodmanHandle}, LedgerError, LedgerFinder, LedgerSigner, }, @@ -40,27 +58,10 @@ use crypto::key::{ hdkd::{derivation_path::DerivationPath, u31::U31}, PredefinedSigAuxDataProvider, SigAuxDataProvider, }; +use logging::log; use wallet_storage::WalletStorageReadLocked; use wallet_types::hw_data::LedgerData; -use async_trait::async_trait; -use ledger_lib::{ - transport::{TcpDevice, TcpInfo, TcpTransport}, - Transport, -}; -use logging::log; -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, -}; - #[derive(Debug)] enum ControlMessage { Finish, @@ -73,6 +74,7 @@ async fn auto_confirmer(mut control_msg_rx: mpsc::Receiver, hand // 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 { @@ -159,6 +161,45 @@ async fn setup( }) } +#[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:?}"); + + // assert_eq!(info.name, "mintlayer-app"); +} + #[rstest] #[trace] #[serial_test::serial] @@ -169,11 +210,14 @@ async fn test_account_extended_public_key() { 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, 0, derivation_path) - .await - .unwrap() - .into_public_key_and_chain_code(); + 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!( diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index 38e2f73265..c3221cad32 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -152,7 +152,7 @@ pub enum TrezorError { #[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\ @@ -1693,7 +1693,7 @@ fn check_public_keys_against_key_chain( } } - Err(TrezorError::HardwareWalletDifferentFile)? + Err(TrezorError::WalletFileIsSoftwareWallet)? } fn fetch_extended_pub_key( From 863a17cee3cc0e150e641753dd88ff899c96fa83 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Thu, 31 Jul 2025 09:59:39 +0200 Subject: [PATCH 06/24] Add Ledger signer provider --- Cargo.lock | 2 + node-gui/Cargo.toml | 23 +- node-gui/backend/Cargo.toml | 29 +- .../main_widget/tabs/wallet/mod.rs | 1 + .../main_widget/tabs/wallet/status_bar.rs | 11 +- wallet/Cargo.toml | 1 + wallet/src/key_chain/master_key_chain/mod.rs | 4 +- .../signer/ledger_signer/ledger_messages.rs | 28 +- wallet/src/signer/ledger_signer/mod.rs | 160 +++- wallet/src/signer/ledger_signer/tests.rs | 8 +- wallet/src/signer/mod.rs | 9 +- wallet/src/signer/software_signer/mod.rs | 5 +- wallet/src/signer/trezor_signer/mod.rs | 14 +- wallet/src/wallet/mod.rs | 64 +- wallet/src/wallet/test_helpers.rs | 9 +- wallet/src/wallet/tests.rs | 829 ++++++++++-------- wallet/src/wallet_events.rs | 2 +- wallet/types/Cargo.toml | 1 + wallet/types/src/hw_data.rs | 21 + wallet/wallet-cli-commands/Cargo.toml | 11 +- .../src/command_handler/mod.rs | 4 + wallet/wallet-controller/Cargo.toml | 16 +- wallet/wallet-controller/src/helpers/tests.rs | 9 +- wallet/wallet-controller/src/lib.rs | 27 +- .../wallet-controller/src/runtime_wallet.rs | 6 +- wallet/wallet-controller/src/sync/mod.rs | 31 +- .../wallet-controller/src/sync/tests/mod.rs | 26 +- .../src/tests/compose_transaction_tests.rs | 5 +- .../wallet-controller/src/tests/test_utils.rs | 4 +- wallet/wallet-controller/src/types/mod.rs | 4 + wallet/wallet-rpc-client/Cargo.toml | 23 +- wallet/wallet-rpc-lib/Cargo.toml | 5 +- wallet/wallet-rpc-lib/src/rpc/mod.rs | 5 +- wallet/wallet-rpc-lib/src/service/mod.rs | 3 +- wallet/wallet-rpc-lib/src/service/worker.rs | 5 +- wallet/wallet-rpc-lib/tests/utils.rs | 1 + 36 files changed, 918 insertions(+), 488 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5501bbbb15..f94cb5c616 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9709,6 +9709,7 @@ dependencies = [ "consensus", "crypto", "ctor", + "derive_more 1.0.0", "hex", "itertools 0.14.0", "lazy_static", @@ -10066,6 +10067,7 @@ dependencies = [ "bip39", "common", "crypto", + "derive_more 1.0.0", "hex", "itertools 0.14.0", "logging", 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..77dfa3c99e 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,19 @@ 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", +] 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..1938050926 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 @@ -471,6 +471,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", }; 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/wallet/Cargo.toml b/wallet/Cargo.toml index 477a3df4b5..f7e4e4cc48 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -32,6 +32,7 @@ bip39 = { workspace = true, default-features = false, features = [ "std", "zeroize", ] } +derive_more.workspace = true hex.workspace = true itertools.workspace = true parity-scale-codec.workspace = true diff --git a/wallet/src/key_chain/master_key_chain/mod.rs b/wallet/src/key_chain/master_key_chain/mod.rs index a93eb60b92..86c973d191 100644 --- a/wallet/src/key_chain/master_key_chain/mod.rs +++ b/wallet/src/key_chain/master_key_chain/mod.rs @@ -22,7 +22,7 @@ use crypto::vrf::ExtendedVRFPrivateKey; use std::sync::Arc; use wallet_storage::{ StoreTxRwUnlocked, WalletStorageReadLocked, WalletStorageReadUnlocked, - WalletStorageWriteUnlocked, + WalletStorageWriteLocked, WalletStorageWriteUnlocked, }; use wallet_types::seed_phrase::{SerializableSeedPhrase, StoreSeedPhrase}; @@ -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 index 00dd968eac..801a9e262f 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -30,6 +30,7 @@ use crypto::key::{ }; use serialization::{Decode, DecodeAll, Encode}; use utils::ensure; +use wallet_types::hw_data::LedgerFullInfo; use ledger_lib::Exchange; use mintlayer_ledger_messages::{ @@ -37,7 +38,7 @@ use mintlayer_ledger_messages::{ Bip32Path as LedgerBip32Path, CoinType, InputAdditionalInfoReq, Ins, OutputValue as LOutputValue, P1SignTx, PubKeyP1, PublicKeyReq, SignMessageReq, SignTxReq, TxInput as LTxInput, TxInputReq, TxMetadataReq, TxOutput as LTxOutput, TxOutputReq, APDU_CLASS, - H256 as LH256, P1_APP_NAME, P1_SIGN_NEXT, P1_SIGN_START, P2_DONE, P2_SIGN_MORE, + 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 @@ -136,11 +137,15 @@ pub async fn sign_challenge( 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(100)).await + ledger.exchange(&msg_buf, Duration::from_millis(500)).await } -#[allow(dead_code)] -pub async fn check_current_app(ledger: &mut L) -> SignerResult<()> { +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()))?; @@ -152,7 +157,20 @@ pub async fn check_current_app(ledger: &mut L) -> SignerResult<()> LedgerError::DifferentActiveApp(name) ); - Ok(()) + let resp = get_app_version(ledger) + .await + .map_err(|err| LedgerError::DeviceError(err.to_string()))?; + let ver = ok_response(resp)?; + let app_version = match ver.as_slice() { + [major, minor, patch] => common::primitives::semver::SemVer { + major: *major, + minor: *minor, + patch: *patch as u16, + }, + _ => return Err(SignerError::LedgerError(LedgerError::InvalidResponse)), + }; + + Ok(LedgerFullInfo { app_version }) } pub async fn get_extended_public_key( diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs index 599127099a..2920f16ea7 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -18,7 +18,7 @@ mod ledger_messages; use std::{collections::BTreeMap, sync::Arc}; use crate::{ - key_chain::{make_account_path, AccountKeyChains, FoundPubKey}, + 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, @@ -26,8 +26,9 @@ use crate::{ LedgerSignature, }, utils::{is_htlc_utxo, produce_uniparty_signature_for_input}, - Signer, SignerError, SignerResult, + Signer, SignerError, SignerProvider, SignerResult, }, + Account, WalletResult, }; use common::{ chain::{ @@ -64,11 +65,15 @@ use crypto::key::{ }; use serialization::Encode; use utils::ensure; -use wallet_storage::{WalletStorageReadLocked, WalletStorageReadUnlocked}; +use wallet_storage::{ + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteUnlocked, +}; use wallet_types::{ - hw_data::LedgerData, + account_info::DEFAULT_ACCOUNT_INDEX, + hw_data::{HardwareWalletFullInfo, LedgerData, LedgerFullInfo}, partially_signed_transaction::{PartiallySignedTransaction, TokensAdditionalInfo}, signature_status::SignatureStatus, + AccountId, }; use async_trait::async_trait; @@ -86,6 +91,8 @@ use tokio::sync::Mutex; 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")] @@ -128,6 +135,7 @@ pub trait LedgerFinder { async fn find_ledger_device_from_db( &self, db_tx: &mut T, + chain_config: Arc, ) -> SignerResult<(Self::Ledger, LedgerData)>; } @@ -189,13 +197,14 @@ where | ledger_lib::Error::Tcp(_) | ledger_lib::Error::Ble(_), ) => { - let (mut new_client, data) = - self.provider.find_ledger_device_from_db(db_tx).await?; + 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, - &data, key_chain, &self.chain_config, ) @@ -1012,8 +1021,7 @@ fn to_ledger_chain_type(chain_config: &ChainConfig) -> CoinType { } } -#[allow(dead_code)] -async fn find_ledger_device() -> SignerResult<(LedgerHandle, LedgerData)> { +async fn find_ledger_device() -> SignerResult<(LedgerHandle, LedgerFullInfo)> { let mut provider = LedgerProvider::init().await; let mut devices = provider .list(Filters::Any) @@ -1027,9 +1035,9 @@ async fn find_ledger_device() -> SignerResult<(LedgerHandle, LedgerData)> { .await .map_err(|err| LedgerError::DeviceError(err.to_string()))?; - check_current_app(&mut handle).await?; + let full_info = check_current_app(&mut handle).await?; - Ok((handle, LedgerData {})) + Ok((handle, full_info)) } /// Check that the public keys in the provided key chain are the same as the ones from the @@ -1037,7 +1045,6 @@ async fn find_ledger_device() -> SignerResult<(LedgerHandle, LedgerData)> { async fn check_public_keys_against_key_chain( db_tx: &mut T, client: &mut L, - _ledger_data: &LedgerData, key_chain: &impl AccountKeyChains, chain_config: &ChainConfig, ) -> SignerResult<()> { @@ -1056,6 +1063,31 @@ async fn check_public_keys_against_key_chain( + 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, @@ -1085,6 +1117,110 @@ fn single_signature( } } +#[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; diff --git a/wallet/src/signer/ledger_signer/tests.rs b/wallet/src/signer/ledger_signer/tests.rs index e7bdd165a8..b773994b51 100644 --- a/wallet/src/signer/ledger_signer/tests.rs +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -40,7 +40,7 @@ use tokio::{ use crate::signer::{ ledger_signer::{ - ledger_messages::{get_app_name, get_extended_public_key, ok_response}, + ledger_messages::{check_current_app, get_app_name, get_extended_public_key, ok_response}, speculos::{Action, Button, Handle, PodmanHandle}, LedgerError, LedgerFinder, LedgerSigner, }, @@ -103,6 +103,7 @@ impl LedgerFinder for DummyProvider { async fn find_ledger_device_from_db( &self, _db_tx: &mut T, + _chain_config: Arc, ) -> SignerResult<(Self::Ledger, LedgerData)> { Err(SignerError::LedgerError(LedgerError::NoDeviceFound)) } @@ -197,7 +198,10 @@ async fn test_app_name() { let info = device.app_info(Duration::from_millis(100)).await.unwrap(); eprintln!("info: {info:?}"); - // assert_eq!(info.name, "mintlayer-app"); + let info = check_current_app(&mut device).await.unwrap(); + eprintln!("info: {info:?}"); + + assert_eq!(info.app_version.to_string(), "0.1.0"); } #[rstest] diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index bfca2985ec..931ba2dfb4 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -115,6 +115,8 @@ pub enum SignerError { PartiallySignedTransactionError(#[from] PartiallySignedTransactionError), #[error("Duplicate UTXO input: {0:?}")] DuplicateUtxoInput(UtxoOutPoint), + #[error("Wallet not initialized")] + WalletNotInitialized, } type SignerResult = Result; @@ -158,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..35a1397cdc 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -480,6 +480,7 @@ impl SoftwareSignerProvider { } } +#[async_trait] impl SignerProvider for SoftwareSignerProvider { type S = SoftwareSigner; type K = AccountKeyChainImplSoftware; @@ -488,12 +489,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/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index c3221cad32..2d55fc24e8 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -1589,19 +1589,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)?; @@ -1856,6 +1851,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/wallet/mod.rs b/wallet/src/wallet/mod.rs index 50fac3c745..b6bb039e6b 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -351,14 +351,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)?; @@ -367,16 +367,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, @@ -407,7 +407,8 @@ where &mut db_tx, None, &mut signer_provider, - )?; + ) + .await?; let next_unused_account = Wallet::::create_next_unused_account( U31::ONE, @@ -415,7 +416,8 @@ where &mut db_tx, None, &mut signer_provider, - )?; + ) + .await?; db_tx.commit()?; @@ -435,7 +437,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, @@ -449,7 +451,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()?; @@ -588,7 +590,7 @@ 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

, >( @@ -615,7 +617,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)?; @@ -780,11 +782,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() @@ -796,21 +798,25 @@ 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

, >( @@ -832,7 +838,9 @@ where pre_migration, controller_mode, signer_provider, - ) { + ) + .await + { Ok(x) => x, #[cfg(feature = "trezor")] Err(WalletError::SignerError(SignerError::TrezorError( @@ -979,10 +987,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)> { @@ -991,19 +999,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)> { @@ -1033,7 +1038,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); @@ -2505,7 +2511,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, @@ -2523,7 +2529,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..1e9efda4a8 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 { @@ -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, @@ -90,12 +91,13 @@ pub fn create_wallet_with_mnemonic_and_named_db( )?) }, ) + .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 +110,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 0be10cbdcb..8a3f45b424 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -263,8 +263,7 @@ where (coins, token_balances) } -#[track_caller] -fn verify_wallet_balance( +async fn verify_wallet_balance( chain_config: &Arc, wallet: &Wallet, expected_balance: Amount, @@ -288,6 +287,7 @@ fn verify_wallet_balance( false, |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), ) + .await .unwrap() .wallet() .unwrap(); @@ -297,13 +297,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, @@ -324,12 +322,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, @@ -348,12 +345,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, @@ -373,15 +370,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(); @@ -395,13 +394,15 @@ fn wallet_creation_in_memory() { WalletControllerMode::Hot, false, |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 @@ -414,13 +415,15 @@ fn wallet_creation_in_memory() { false, |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(), @@ -455,13 +458,15 @@ fn wallet_migration_to_v2(#[case] seed: Seed) { )?) }, ) + .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(); @@ -513,6 +518,7 @@ fn wallet_migration_to_v2(#[case] seed: Seed) { false, |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), ) + .await .unwrap() .wallet() .unwrap(); @@ -530,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); @@ -544,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()); } @@ -573,6 +581,7 @@ fn wallet_seed_phrase_retrieval(#[case] seed: Seed) { )?) }, ) + .await .unwrap() .wallet() .unwrap(); @@ -647,8 +656,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 @@ -670,6 +679,7 @@ fn wallet_seed_phrase_check_address() { )?) }, ) + .await .unwrap() .wallet() .unwrap(); @@ -713,6 +723,7 @@ fn wallet_seed_phrase_check_address() { )?) }, ) + .await .unwrap() .wallet() .unwrap(); @@ -736,7 +747,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; @@ -759,7 +771,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( @@ -773,7 +786,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 { @@ -791,14 +805,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; } } } @@ -807,7 +823,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)); @@ -834,7 +851,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); @@ -850,13 +867,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); @@ -866,7 +884,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); @@ -874,7 +892,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); @@ -897,7 +916,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); @@ -908,7 +927,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); @@ -931,7 +951,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); @@ -942,19 +962,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( @@ -978,24 +1000,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); @@ -1039,15 +1064,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, @@ -1070,6 +1096,7 @@ fn test_wallet_accounts( false, |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), ) + .await .unwrap() .wallet() .unwrap(); @@ -1087,11 +1114,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, @@ -1099,13 +1127,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; @@ -1128,33 +1157,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( @@ -1167,10 +1197,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); @@ -1180,48 +1211,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() { @@ -1230,9 +1262,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(),); @@ -1252,14 +1284,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(); @@ -1353,7 +1385,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); @@ -1397,7 +1429,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 @@ -1415,7 +1448,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 @@ -1492,10 +1525,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); @@ -1535,7 +1568,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!( @@ -1553,10 +1587,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); @@ -1586,7 +1620,8 @@ async fn wallet_list_mainchain_transactions(#[case] seed: Seed) { vec![tx.clone()], Amount::ZERO, 1, - ); + ) + .await; let tx = wallet .create_transaction_to_addresses( @@ -1609,7 +1644,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 @@ -1638,14 +1674,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); @@ -1768,7 +1804,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 @@ -1815,11 +1851,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), @@ -1837,7 +1873,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)); @@ -1861,7 +1897,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( @@ -1972,14 +2008,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -2039,7 +2075,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); @@ -2101,7 +2138,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -2112,7 +2149,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); let (pool_id, pool_data) = pool_ids.first().unwrap(); @@ -2139,7 +2176,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); let pool_ids = wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); @@ -2166,8 +2204,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); @@ -2176,8 +2214,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids1.is_empty()); @@ -2275,8 +2313,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); @@ -2361,8 +2400,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::Stake).unwrap(); @@ -2415,8 +2454,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids1.is_empty()); @@ -2437,14 +2477,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); @@ -2490,8 +2530,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); @@ -2506,13 +2546,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); @@ -2526,13 +2568,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); @@ -2562,12 +2606,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(); @@ -2597,12 +2643,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 = @@ -2614,7 +2662,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()); @@ -2638,7 +2686,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!( @@ -2659,7 +2708,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); @@ -2667,7 +2716,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -2701,7 +2750,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(),); @@ -2730,7 +2780,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); @@ -2761,7 +2812,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( @@ -2794,7 +2846,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 @@ -2824,7 +2876,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, @@ -2832,7 +2885,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(); @@ -2853,7 +2907,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) @@ -2912,8 +2966,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); @@ -2954,8 +3008,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); @@ -3112,7 +3167,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); @@ -3172,7 +3228,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(); @@ -3239,7 +3296,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); @@ -3249,7 +3306,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); @@ -3300,7 +3357,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); @@ -3310,7 +3367,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); @@ -3344,7 +3401,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( @@ -3377,7 +3435,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()) @@ -3468,7 +3526,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()) @@ -3581,7 +3640,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(); @@ -3614,7 +3674,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); @@ -3624,7 +3684,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); @@ -3657,7 +3717,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( @@ -3776,7 +3837,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 @@ -3837,7 +3899,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 @@ -3878,7 +3941,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); @@ -3888,7 +3951,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); @@ -3921,7 +3984,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( @@ -3980,7 +4044,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 @@ -4040,7 +4105,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 @@ -4081,7 +4147,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); @@ -4091,7 +4157,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); @@ -4124,7 +4190,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( @@ -4182,7 +4249,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 @@ -4243,7 +4311,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 @@ -4278,7 +4347,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 @@ -4344,7 +4414,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); @@ -4377,7 +4447,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); @@ -4432,7 +4502,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 = @@ -4471,7 +4541,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 @@ -4490,7 +4560,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); @@ -4498,7 +4568,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); } @@ -4556,7 +4626,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); @@ -4570,7 +4641,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); @@ -4585,7 +4656,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); @@ -4680,10 +4751,10 @@ async fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { .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; // add mempool transaction in random order transactions.shuffle(&mut rng); @@ -4765,7 +4836,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); @@ -4778,7 +4849,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); @@ -4903,10 +4974,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) @@ -4930,7 +5002,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 @@ -4943,10 +5015,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) @@ -4970,7 +5043,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 @@ -4991,7 +5064,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 @@ -5003,7 +5076,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 @@ -5024,14 +5097,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_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -5041,7 +5114,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; @@ -5069,7 +5142,8 @@ async fn decommission_pool_wrong_account(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; let pool_ids = wallet.get_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -5109,7 +5183,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); @@ -5126,14 +5201,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_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -5143,7 +5218,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; @@ -5171,7 +5246,8 @@ async fn decommission_pool_request_wrong_account(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; let pool_ids = wallet.get_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -5220,14 +5296,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_pools(acc_0_index, WalletPoolsFilter::All).unwrap(); @@ -5238,7 +5314,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; @@ -5287,7 +5363,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); @@ -5332,7 +5409,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); @@ -5347,12 +5424,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); @@ -5360,7 +5437,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -5370,7 +5447,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 @@ -5396,7 +5473,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert_eq!(pool_ids.len(), 1); @@ -5447,7 +5525,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,); @@ -5461,12 +5540,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); @@ -5474,8 +5553,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -5509,7 +5588,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, @@ -5517,7 +5597,8 @@ async fn filter_pools(#[case] seed: Seed) { vec![stake_pool_transaction], Amount::ZERO, 1, - ); + ) + .await; // check wallet1 filter let pool_ids = wallet1.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); @@ -5552,12 +5633,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); @@ -5576,7 +5657,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); @@ -5629,7 +5710,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,); @@ -5671,7 +5753,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); @@ -5689,7 +5771,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); @@ -5722,8 +5804,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); @@ -5768,7 +5850,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); @@ -5856,15 +5938,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); @@ -5920,8 +6002,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); @@ -5982,8 +6065,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); @@ -6000,15 +6084,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); @@ -6084,8 +6168,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); @@ -6147,8 +6232,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); @@ -6163,7 +6249,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); @@ -6173,7 +6259,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); @@ -6205,7 +6291,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(); @@ -6245,7 +6332,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); @@ -6285,7 +6373,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); @@ -6300,7 +6389,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); @@ -6310,7 +6399,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); @@ -6342,7 +6431,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(); @@ -6382,7 +6472,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); @@ -6436,7 +6527,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); @@ -6479,7 +6571,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); @@ -6497,8 +6590,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); @@ -6508,8 +6601,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); @@ -6541,8 +6634,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; @@ -6584,8 +6678,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 = @@ -6649,8 +6744,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 = @@ -6709,8 +6805,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 = @@ -6786,8 +6883,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 = @@ -6855,8 +6953,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 = @@ -6884,8 +6983,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); @@ -6895,8 +6994,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); @@ -6928,8 +7027,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; @@ -6971,8 +7071,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 = @@ -7036,8 +7137,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 = @@ -7096,8 +7198,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 = @@ -7172,8 +7275,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()) @@ -7214,8 +7318,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); @@ -7223,8 +7327,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -7259,8 +7363,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(),); @@ -7290,8 +7395,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); @@ -7323,8 +7429,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!( @@ -7408,9 +7515,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(); @@ -7516,7 +7624,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); @@ -7524,7 +7632,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); @@ -7559,7 +7667,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(),); @@ -7589,7 +7698,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); @@ -7621,7 +7731,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!( @@ -7710,7 +7821,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(); @@ -7768,14 +7880,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; @@ -7804,7 +7916,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(); @@ -7848,7 +7961,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; @@ -7876,7 +7990,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); @@ -7985,7 +8100,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(); @@ -8052,8 +8168,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); @@ -8061,8 +8177,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -8097,8 +8213,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(),); @@ -8128,8 +8245,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); @@ -8162,8 +8280,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!( @@ -8254,9 +8373,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(); @@ -8338,8 +8458,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); @@ -8347,8 +8467,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_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -8383,8 +8503,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(),); @@ -8414,8 +8535,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); @@ -8447,8 +8569,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!( @@ -8457,7 +8580,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 @@ -8490,7 +8613,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); @@ -8514,9 +8637,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(); @@ -8576,7 +8700,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)); @@ -8600,7 +8724,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( @@ -8703,7 +8827,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); @@ -8730,7 +8854,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/types/Cargo.toml b/wallet/types/Cargo.toml index 038a2616d0..ecaac50d19 100644 --- a/wallet/types/Cargo.toml +++ b/wallet/types/Cargo.toml @@ -22,6 +22,7 @@ bip39 = { workspace = true, default-features = false, features = [ "std", "zeroize", ] } +derive_more.workspace = true hex.workspace = true itertools.workspace = true parity-scale-codec.workspace = true diff --git a/wallet/types/src/hw_data.rs b/wallet/types/src/hw_data.rs index b7187e66f2..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,10 +43,25 @@ 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 { @@ -61,6 +78,8 @@ pub enum HardwareWalletData { pub enum HardwareWalletFullInfo { #[cfg(feature = "trezor")] Trezor(TrezorFullInfo), + #[cfg(feature = "ledger")] + Ledger(LedgerFullInfo), } impl From for HardwareWalletData { @@ -68,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/wallet-cli-commands/Cargo.toml b/wallet/wallet-cli-commands/Cargo.toml index c7cbad7f85..893880ff94 100644 --- a/wallet/wallet-cli-commands/Cargo.toml +++ b/wallet/wallet-cli-commands/Cargo.toml @@ -47,7 +47,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", +] } [dev-dependencies] blockprod = { path = "../../blockprod" } @@ -66,4 +72,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 f65b0320a6..dcc97bbf36 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -359,6 +359,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-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 77f8820038..63de01ad68 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)); @@ -854,7 +854,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); @@ -904,7 +904,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 76b6498a13..2a19bb9094 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -122,7 +122,7 @@ use wallet_types::{ with_locked::WithLocked, }; -#[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` @@ -223,7 +223,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( @@ -271,7 +271,7 @@ where }) } - pub fn create_wallet( + pub async fn create_wallet( chain_config: Arc, file_path: impl AsRef, args: WalletTypeArgsComputed, @@ -312,6 +312,7 @@ where )?) }, ) + .await .map_err(ControllerError::WalletError) .map(|w| w.map_wallet(RuntimeWallet::Software)) } @@ -328,6 +329,7 @@ where .map_err(SignerError::TrezorError)?) }, ) + .await .map_err(ControllerError::WalletError) .map(|w| w.map_wallet(RuntimeWallet::Trezor)), }; @@ -336,7 +338,7 @@ where res } - pub fn recover_wallet( + pub async fn recover_wallet( chain_config: Arc, file_path: impl AsRef, args: WalletTypeArgsComputed, @@ -375,6 +377,7 @@ where )?) }, ) + .await .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Software)) } @@ -391,6 +394,7 @@ where .map_err(SignerError::TrezorError)?) }, ) + .await .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Trezor)) } @@ -449,7 +453,7 @@ where Ok(()) } - pub fn open_wallet( + pub async fn open_wallet( chain_config: Arc, file_path: impl AsRef, password: Option, @@ -480,6 +484,7 @@ where force_change_wallet_type, |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), ) + .await .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Software)) } @@ -500,6 +505,7 @@ where ) }, ) + .await .map_err(ControllerError::WalletError)?; Ok(wallet.map_wallet(RuntimeWallet::Trezor)) } @@ -594,6 +600,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, }; @@ -754,11 +764,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( diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs index e605bd02f2..7af94170b1 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -197,14 +197,14 @@ where } } - 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, } } 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 8082f580c4..2283fefe99 100644 --- a/wallet/wallet-controller/src/sync/tests/mod.rs +++ b/wallet/wallet-controller/src/sync/tests/mod.rs @@ -20,6 +20,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; @@ -33,7 +37,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; @@ -43,9 +46,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}; @@ -92,6 +93,7 @@ impl MockWallet { } } +#[async_trait::async_trait] impl SyncingWallet for MockWallet { fn syncing_state(&self) -> WalletSyncingState { WalletSyncingState { @@ -111,7 +113,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()); @@ -133,7 +135,7 @@ impl SyncingWallet for MockWallet { )) .await }) - .unwrap(); + .unwrap() } log::debug!( @@ -145,11 +147,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!( @@ -165,12 +167,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 2a69a270c8..4a5626b6d3 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 bfaf53b889..0f2c853915 100644 --- a/wallet/wallet-controller/src/tests/test_utils.rs +++ b/wallet/wallet-controller/src/tests/test_utils.rs @@ -163,7 +163,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, @@ -187,7 +187,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 f55551550f..daa2aaf73c 100644 --- a/wallet/wallet-controller/src/types/mod.rs +++ b/wallet/wallet-controller/src/types/mod.rs @@ -72,6 +72,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, + }, } // A struct that represents sending a particular amount of unspecified currency. 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-lib/Cargo.toml b/wallet/wallet-rpc-lib/Cargo.toml index 04b97eaf56..af1d0b7743 100644 --- a/wallet/wallet-rpc-lib/Cargo.toml +++ b/wallet/wallet-rpc-lib/Cargo.toml @@ -44,7 +44,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" } @@ -54,4 +54,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/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 6cdaa78ed2..750df8cb99 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -291,7 +291,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/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 aa1f69b1d2..f46081374b 100644 --- a/wallet/wallet-rpc-lib/src/service/worker.rs +++ b/wallet/wallet-rpc-lib/src/service/worker.rs @@ -170,7 +170,8 @@ where force_migrate_wallet_type, open_as_wallet_type, device_id, - )?; + ) + .await?; let wallet = match wallet { wallet::wallet::WalletCreation::Wallet(w) => w, @@ -226,6 +227,7 @@ where wallet_type, options.overwrite_wallet_file, ) + .await } else { WalletController::recover_wallet( self.chain_config.clone(), @@ -233,6 +235,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..ea55917c93 100644 --- a/wallet/wallet-rpc-lib/tests/utils.rs +++ b/wallet/wallet-rpc-lib/tests/utils.rs @@ -80,6 +80,7 @@ impl TestFramework { )?) }, ) + .await .unwrap(); wallet_path From 6cc8338fadae2840431bdf5629a5dc7c29c22d5f Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Tue, 5 Aug 2025 12:32:55 +0200 Subject: [PATCH 07/24] Add ledger support in CLI and GUI wallet --- node-gui/backend/Cargo.toml | 1 + node-gui/backend/src/backend_impl.rs | 78 +++- node-gui/backend/src/error.rs | 4 +- node-gui/src/main_window/main_menu.rs | 22 + .../main_widget/tabs/wallet/left_panel.rs | 86 ++-- .../main_widget/tabs/wallet/mod.rs | 17 +- node-gui/src/main_window/mod.rs | 30 +- wallet/src/key_chain/master_key_chain/mod.rs | 12 +- wallet/src/signer/software_signer/mod.rs | 7 +- wallet/src/wallet/mod.rs | 42 +- wallet/src/wallet/test_helpers.rs | 9 +- wallet/src/wallet/tests.rs | 38 +- wallet/storage/Cargo.toml | 3 +- wallet/types/src/wallet_type.rs | 9 + .../src/command_handler/mod.rs | 9 + wallet/wallet-cli-commands/src/lib.rs | 37 ++ wallet/wallet-cli-lib/Cargo.toml | 25 +- wallet/wallet-cli/Cargo.toml | 17 +- wallet/wallet-controller/src/lib.rs | 88 +++- .../wallet-controller/src/runtime_wallet.rs | 411 ++++++++++++++++++ wallet/wallet-controller/src/types/mod.rs | 11 + wallet/wallet-node-client/Cargo.toml | 11 +- .../src/rpc_client/client_impl.rs | 4 + wallet/wallet-rpc-daemon/Cargo.toml | 9 +- wallet/wallet-rpc-daemon/docs/RPC.md | 9 +- wallet/wallet-rpc-lib/src/lib.rs | 6 +- wallet/wallet-rpc-lib/src/rpc/mod.rs | 7 +- wallet/wallet-rpc-lib/src/rpc/types.rs | 4 + wallet/wallet-rpc-lib/tests/utils.rs | 7 +- 29 files changed, 881 insertions(+), 132 deletions(-) diff --git a/node-gui/backend/Cargo.toml b/node-gui/backend/Cargo.toml index 77dfa3c99e..a94afe1352 100644 --- a/node-gui/backend/Cargo.toml +++ b/node-gui/backend/Cargo.toml @@ -65,3 +65,4 @@ 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 62d3997272..3162363931 100644 --- a/node-gui/backend/src/backend_impl.rs +++ b/node-gui/backend/src/backend_impl.rs @@ -355,9 +355,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) @@ -597,9 +631,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 1938050926..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 { @@ -490,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/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/src/key_chain/master_key_chain/mod.rs b/wallet/src/key_chain/master_key_chain/mod.rs index 86c973d191..5eb67449c0 100644 --- a/wallet/src/key_chain/master_key_chain/mod.rs +++ b/wallet/src/key_chain/master_key_chain/mod.rs @@ -21,8 +21,8 @@ use crypto::key::hdkd::u31::U31; use crypto::vrf::ExtendedVRFPrivateKey; use std::sync::Arc; use wallet_storage::{ - StoreTxRwUnlocked, WalletStorageReadLocked, WalletStorageReadUnlocked, - WalletStorageWriteLocked, WalletStorageWriteUnlocked, + 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, diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index 35a1397cdc..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, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index b6bb039e6b..0753047df5 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -263,8 +263,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}")] @@ -351,7 +353,7 @@ where B: storage::BackendWithSendableTransactions + 'static, P: SignerProvider, { - pub async fn create_new_wallet) -> WalletResult

>( + pub async fn create_new_wallet) -> WalletResult

>( chain_config: Arc, db: Store, best_block: (BlockHeight, Id), @@ -367,7 +369,7 @@ where Ok(wallet) } - pub async fn recover_wallet) -> WalletResult

>( + pub async fn recover_wallet) -> WalletResult

>( chain_config: Arc, db: Store, wallet_type: WalletType, @@ -376,7 +378,7 @@ where Self::new_wallet(chain_config, db, wallet_type, signer_provider).await } - async fn new_wallet) -> WalletResult

>( + async fn new_wallet) -> WalletResult

>( chain_config: Arc, mut db: Store, wallet_type: WalletType, @@ -388,7 +390,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( @@ -592,7 +594,7 @@ where /// Check the wallet DB version and perform any migrations needed 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, @@ -605,7 +607,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()?; @@ -701,12 +703,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(()) } @@ -818,7 +840,7 @@ where pub async fn load_wallet< F: Fn(u32) -> WalletResult<()>, - F2: FnOnce(&StoreTxRo) -> WalletResult

, + F2: AsyncFnOnce(StoreTxRo) -> WalletResult

, >( chain_config: Arc, mut db: Store, diff --git a/wallet/src/wallet/test_helpers.rs b/wallet/src/wallet/test_helpers.rs index 1e9efda4a8..dc3b90cf59 100644 --- a/wallet/src/wallet/test_helpers.rs +++ b/wallet/src/wallet/test_helpers.rs @@ -45,7 +45,7 @@ pub async 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, @@ -81,14 +81,15 @@ pub async 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 diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 8a3f45b424..da677950a2 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -285,7 +285,7 @@ async 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() @@ -393,7 +393,7 @@ async 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 { @@ -413,7 +413,7 @@ async 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(); @@ -448,14 +448,15 @@ async 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 @@ -516,7 +517,7 @@ async 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() @@ -571,14 +572,15 @@ async 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 @@ -669,14 +671,15 @@ async 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 @@ -713,14 +716,15 @@ async 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 @@ -1094,7 +1098,7 @@ async 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() 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/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/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index dcc97bbf36..a2cc98da95 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -277,6 +277,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 diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index 275722a18a..88388beeca 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/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 2a19bb9094..d71ccf9268 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -89,6 +89,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")] @@ -302,14 +304,15 @@ 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 @@ -322,16 +325,28 @@ 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); @@ -367,14 +382,15 @@ 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 @@ -387,17 +403,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); @@ -482,7 +511,9 @@ 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)?; @@ -497,10 +528,10 @@ 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, ) }, @@ -509,6 +540,24 @@ where .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)) + } } } @@ -855,6 +904,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 { @@ -874,6 +927,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 7af94170b1..26cbb00eee 100644 --- a/wallet/wallet-controller/src/runtime_wallet.rs +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -64,6 +64,8 @@ use wallet_types::{ 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,6 +224,8 @@ 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(), } } @@ -205,6 +237,8 @@ where RuntimeWallet::Software(w) => w.create_next_account(name).await, #[cfg(feature = "trezor")] 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_pools(account_index, filter), #[cfg(feature = "trezor")] RuntimeWallet::Trezor(w) => w.get_pools(account_index, filter), + #[cfg(feature = "ledger")] + RuntimeWallet::Ledger(w) => w.get_pools(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 + } } } @@ -1476,6 +1855,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 + } } } @@ -1493,6 +1876,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 + } } } @@ -1536,6 +1923,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 + } } } @@ -1548,6 +1949,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), } } @@ -1565,6 +1968,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) + } } } @@ -1592,6 +1999,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/types/mod.rs b/wallet/wallet-controller/src/types/mod.rs index daa2aaf73c..dfc40777af 100644 --- a/wallet/wallet-controller/src/types/mod.rs +++ b/wallet/wallet-controller/src/types/mod.rs @@ -196,6 +196,8 @@ pub enum WalletTypeArgs { }, #[cfg(feature = "trezor")] Trezor { device_id: Option }, + #[cfg(feature = "ledger")] + Ledger, } #[derive(Debug, Clone, Copy)] @@ -216,6 +218,8 @@ impl WalletTypeArgs { } => controller_mode.into(), #[cfg(feature = "trezor")] Self::Trezor { device_id: _ } => WalletType::Trezor, + #[cfg(feature = "trezor")] + Self::Ledger => WalletType::Ledger, } } @@ -258,6 +262,11 @@ impl WalletTypeArgs { WalletTypeArgsComputed::Trezor { device_id }, CreatedWallet::UserProvidedMnemonic, )), + #[cfg(feature = "ledger")] + Self::Ledger => Ok(( + WalletTypeArgsComputed::Ledger, + CreatedWallet::UserProvidedMnemonic, + )), } } } @@ -270,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 2f6c4d33e7..81744b7bc3 100644 --- a/wallet/wallet-node-client/Cargo.toml +++ b/wallet/wallet-node-client/Cargo.toml @@ -30,7 +30,13 @@ futures.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] @@ -38,4 +44,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/src/rpc_client/client_impl.rs b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs index bcedf93d24..71de1fa14f 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 2993889d08..dd1d6f4006 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -3500,7 +3500,8 @@ Parameters: 1) string 2) null }, } - 2) null, + 2) { "type": "Ledger" } + 3) null, } ``` @@ -3550,7 +3551,8 @@ Parameters: 1) string 2) null }, } - 2) null, + 2) { "type": "Ledger" } + 3) null, } ``` @@ -3597,7 +3599,8 @@ Parameters: 1) string 2) null }, } - 2) null, + 2) { "type": "Ledger" } + 3) null, } ``` 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 750df8cb99..b8a8f27f77 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -85,8 +85,6 @@ use wallet_controller::{ ConnectedPeer, ControllerConfig, ControllerError, NodeInterface, UtxoState, UtxoStates, UtxoType, UtxoTypes, DEFAULT_ACCOUNT_INDEX, }; -#[cfg(feature = "trezor")] -use wallet_types::wallet_type::WalletType; use wallet_types::{ account_info::StandaloneAddressDetails, generic_transaction::GenericTransaction, partially_signed_transaction::PartiallySignedTransaction, scan_blockchain::ScanBlockchain, @@ -99,6 +97,9 @@ use crate::{ WalletHandle, WalletRpcConfig, }; +#[cfg(any(feature = "trezor", feature = "ledger"))] +use wallet_types::wallet_type::WalletType; + use self::types::{ AddressInfo, AddressWithUsageInfo, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, OwnOrderInfo, PoolInfo, PublicKeyInfo, RpcAddress, RpcAmountIn, RpcHexString, @@ -174,6 +175,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 diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 6b6714fca2..68291e309e 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -1103,6 +1103,8 @@ impl NewOrderTransaction { pub enum HardwareWalletType { #[cfg(feature = "trezor")] Trezor { device_id: Option }, + #[cfg(feature = "ledger")] + Ledger, } impl HardwareWalletType { @@ -1146,6 +1148,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/tests/utils.rs b/wallet/wallet-rpc-lib/tests/utils.rs index ea55917c93..ea7722cfa2 100644 --- a/wallet/wallet-rpc-lib/tests/utils.rs +++ b/wallet/wallet-rpc-lib/tests/utils.rs @@ -70,14 +70,15 @@ 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 From 7fb4fe41229e8e7c6d7cef0fd153a9e8d00dff21 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Thu, 6 Nov 2025 01:45:45 +0100 Subject: [PATCH 08/24] Add ledger CI tests --- .github/workflows/build.yml | 109 +++++++++++++++--- Cargo.lock | 2 +- Cargo.toml | 2 +- .../signer/ledger_signer/ledger_messages.rs | 59 ++++------ wallet/src/signer/ledger_signer/mod.rs | 6 +- 5 files changed, 120 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 407df709a5..b61b223513 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,17 +37,17 @@ jobs: --default-toolchain $(python ./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 @@ -89,17 +89,17 @@ jobs: --default-toolchain $(python ./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 @@ -133,17 +133,17 @@ jobs: --default-toolchain $(python ./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 @@ -283,10 +283,89 @@ jobs: - 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 + --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/Cargo.lock b/Cargo.lock index f94cb5c616..9cc0ab5ec2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4971,7 +4971,7 @@ dependencies = [ [[package]] name = "messages" version = "0.1.0" -source = "git+https://github.com/mintlayer/mintlayer-ledger-app?rev=2fa1c33e536f94ba3cac94b97054902674580686#2fa1c33e536f94ba3cac94b97054902674580686" +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)", diff --git a/Cargo.toml b/Cargo.toml index 1ff8ca87dd..3487a44bee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -292,7 +292,7 @@ rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" [workspace.dependencies.mintlayer-ledger-messages] git = "https://github.com/mintlayer/mintlayer-ledger-app" # The commit "Fix comments" -rev = "2fa1c33e536f94ba3cac94b97054902674580686" +rev = "78bd2e2514be3a6db83e7493eaaa03c40acc5409" package = "messages" [workspace.dependencies.trezor-client] diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs index 801a9e262f..3457bbbe76 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -22,23 +22,21 @@ use common::{ }; use crypto::key::{ extended::ExtendedPublicKey, - hdkd::{ - chain_code::{ChainCode, CHAINCODE_LENGTH}, - derivation_path::DerivationPath, - }, + hdkd::{chain_code::ChainCode, derivation_path::DerivationPath}, secp256k1::{extended_keys::Secp256k1ExtendedPublicKey, Secp256k1PublicKey}, }; -use serialization::{Decode, DecodeAll, Encode}; +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, InputAdditionalInfoReq, Ins, - OutputValue as LOutputValue, P1SignTx, PubKeyP1, PublicKeyReq, SignMessageReq, SignTxReq, - 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, + 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 @@ -46,12 +44,6 @@ const TIMEOUT_DUR: Duration = Duration::from_secs(100); const OK_RESPONSE: u16 = 0x9000; const TX_VERSION: u8 = 1; -#[derive(Decode)] -pub struct LedgerSignature { - pub signature: [u8; 64], - pub multisig_idx: Option, -} - struct SignatureResult { sig: LedgerSignature, input_idx: usize, @@ -129,10 +121,9 @@ pub async fn sign_challenge( let resp = send_chunked(ledger, Ins::SIGN_MSG, P1_SIGN_NEXT, message).await?; - let sig_len = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize; - let sig = resp.as_slice().get(1..1 + sig_len).ok_or(LedgerError::InvalidResponse)?; + let sig: MsgSignature = ledger_decode_all(&resp).ok_or(LedgerError::InvalidResponse)?; - Ok(sig.to_vec()) + Ok(sig.signature.to_vec()) } pub async fn get_app_name(ledger: &mut L) -> Result, ledger_lib::Error> { @@ -161,13 +152,12 @@ pub async fn check_current_app(ledger: &mut L) -> SignerResult common::primitives::semver::SemVer { - major: *major, - minor: *minor, - patch: *patch as u16, - }, - _ => return Err(SignerError::LedgerError(LedgerError::InvalidResponse)), + 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 }) @@ -191,20 +181,13 @@ pub async fn get_extended_public_key( ) .await?; - let pk_len = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize; - let public_key = resp.as_slice().get(1..1 + pk_len).ok_or(LedgerError::InvalidResponse)?; - let chain_code_len = *resp.get(1 + pk_len).ok_or(LedgerError::InvalidResponse)? as usize; - let chain_code: [_; CHAINCODE_LENGTH] = resp - .as_slice() - .get(2 + pk_len..2 + pk_len + chain_code_len) - .ok_or(LedgerError::InvalidResponse)? - .try_into() - .map_err(|_| LedgerError::InvalidKey)?; + let resp: GetPublicKeyRespones = + ledger_decode_all(&resp).ok_or(LedgerError::InvalidResponse)?; let extended_public_key = Secp256k1ExtendedPublicKey::new_unchecked( derivation_path, - ChainCode::from(chain_code), - Secp256k1PublicKey::from_bytes(public_key).map_err(|_| LedgerError::InvalidKey)?, + ChainCode::from(resp.chain_code), + Secp256k1PublicKey::from_bytes(&resp.public_key).map_err(|_| LedgerError::InvalidKey)?, ); Ok(ExtendedPublicKey::new(extended_public_key)) @@ -286,8 +269,8 @@ fn decode_signature_response(resp: &[u8]) -> Result { 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()), - None => return Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)) }; let sig = produce_uniparty_signature_for_input( From 620f8c7258be2c20a5b3a572aca115601131426f Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Tue, 11 Nov 2025 19:12:41 +0100 Subject: [PATCH 09/24] fix comments --- Cargo.lock | 8 +- Cargo.toml | 8 +- node-gui/backend/src/error.rs | 2 +- node-gui/src/main_window/main_menu.rs | 4 +- wallet/Cargo.toml | 2 + .../signer/ledger_signer/ledger_messages.rs | 164 +++++++----- wallet/src/signer/ledger_signer/mod.rs | 65 +++-- wallet/src/signer/ledger_signer/speculos.rs | 233 ++++++++++++++++++ .../signer/ledger_signer/speculos/handle.rs | 130 ---------- .../src/signer/ledger_signer/speculos/mod.rs | 20 -- wallet/src/signer/ledger_signer/tests.rs | 223 +++++++++++------ wallet/types/src/hw_data.rs | 3 +- 12 files changed, 543 insertions(+), 319 deletions(-) create mode 100644 wallet/src/signer/ledger_signer/speculos.rs delete mode 100644 wallet/src/signer/ledger_signer/speculos/handle.rs delete mode 100644 wallet/src/signer/ledger_signer/speculos/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 9cc0ab5ec2..b080182cb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3560,6 +3560,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -4580,7 +4583,9 @@ dependencies = [ "bitflags 2.10.0", "displaydoc", "encdec", + "hex", "num_enum", + "serde", "thiserror 2.0.18", ] @@ -4971,7 +4976,7 @@ dependencies = [ [[package]] name = "messages" version = "0.1.0" -source = "git+https://github.com/mintlayer/mintlayer-ledger-app?rev=78bd2e2514be3a6db83e7493eaaa03c40acc5409#78bd2e2514be3a6db83e7493eaaa03c40acc5409" +source = "git+https://github.com/mintlayer/mintlayer-ledger-app?rev=d65b5200a4499c5ebdcf95903cc3a5579f611c6f#d65b5200a4499c5ebdcf95903cc3a5579f611c6f" dependencies = [ "num_enum", "parity-scale-codec 3.7.5 (git+https://github.com/OBorce/parity-scale-codec.git?branch=fix%2Fmissing-target-atomic-ptr)", @@ -9714,6 +9719,7 @@ dependencies = [ "itertools 0.14.0", "lazy_static", "ledger-lib", + "ledger-proto", "logging", "mempool", "messages", diff --git a/Cargo.toml b/Cargo.toml index 3487a44bee..ed9469b810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -289,10 +289,14 @@ package = "mintlayer-core-primitives" git = "https://github.com/ledger-community/rust-ledger.git" rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" +[workspace.dependencies.ledger-proto] +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" +# The commit "Remove App name and version instructions" +rev = "d65b5200a4499c5ebdcf95903cc3a5579f611c6f" package = "messages" [workspace.dependencies.trezor-client] diff --git a/node-gui/backend/src/error.rs b/node-gui/backend/src/error.rs index 42fc4d9525..2e7ebbc87b 100644 --- a/node-gui/backend/src/error.rs +++ b/node-gui/backend/src/error.rs @@ -46,7 +46,7 @@ pub enum BackendError { ColdWallet, #[error("Cannot interact with a hot wallet when in Cold wallet mode")] HotNotSupported, - #[error("Cannot use a Hardware wallet in a Cold wallet mode")] + #[error("Cannot use a hardware wallet in a Cold wallet mode")] HardwareWalletNotSupportedInColdMode, #[error("Invalid console command: {0}")] InvalidConsoleCommand(String), diff --git a/node-gui/src/main_window/main_menu.rs b/node-gui/src/main_window/main_menu.rs index 1600ffc23d..4f931b2b70 100644 --- a/node-gui/src/main_window/main_menu.rs +++ b/node-gui/src/main_window/main_menu.rs @@ -88,7 +88,7 @@ fn base_button<'a>( fn labeled_button<'a>(label: &'a str, msg: MenuMessage) -> button::Button<'a, MenuMessage> { base_button::<'a>( text(label) - .width(Length::Fixed(220.0)) + .width(Length::Fixed(270.0)) .height(Length::Fixed(25.0)) .align_y(alignment::Vertical::Center), msg, @@ -97,7 +97,7 @@ fn labeled_button<'a>(label: &'a str, msg: MenuMessage) -> button::Button<'a, Me fn menu_item(label: &str, msg: MenuMessage) -> Item<'_, MenuMessage, Theme, iced::Renderer> { // Note: if this width is smaller than the text, the menu item will drop the whole last word. - Item::new(labeled_button(label, msg).width(Length::Fixed(270.0))) + Item::new(labeled_button(label, msg).width(Length::Fixed(300.0))) } fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, iced::Renderer> { diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index f7e4e4cc48..ff1f9ea5b7 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -48,6 +48,7 @@ tokio = { workspace = true, default-features = false, features = [ ] } trezor-client = { workspace = true, optional = true } ledger-lib = { workspace = true, optional = true } +ledger-proto = { workspace = true, optional = true } mintlayer-ledger-messages = { workspace = true, optional = true } zeroize.workspace = true @@ -86,6 +87,7 @@ enable-ledger-device-tests = [] use-deterministic-signatures-in-software-signer-for-regtest = [] ledger = [ "dep:ledger-lib", + "dep:ledger-proto", "dep:mintlayer-ledger-messages", "wallet-types/ledger", ] diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs index 3457bbbe76..22ea5c5598 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -29,19 +29,18 @@ use serialization::Encode; use utils::ensure; use wallet_types::hw_data::LedgerFullInfo; -use ledger_lib::Exchange; +use ledger_lib::{Device, Exchange}; +use ledger_proto::StatusCode; 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, + decode_all as ledger_decode_all, encode as ledger_encode, AddrType, Amount as LAmount, Apdu, + Bip32Path as LedgerBip32Path, CoinType, GetPublicKeyRespones, 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_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 { @@ -55,13 +54,16 @@ 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) - ); + let code = StatusCode::try_from(response_status) + .map_err(|_| LedgerError::ErrorResponse(format!("Unknown error: {response_status}")))?; - resp.truncate(resp.len() - size_of_val(&response_status)); - Ok(resp) + match code { + StatusCode::Ok => { + resp.truncate(resp.len() - size_of_val(&response_status)); + Ok(resp) + } + err => Err(LedgerError::ErrorResponse(err.to_string()).into()), + } } /// Send a message to the Ledger and check the response status code is ok @@ -84,26 +86,30 @@ async fn send_chunked( message: &[u8], ) -> Result, SignerError> { let mut msg_buf = vec![]; - let mut chunks = message.chunks(MAX_ADPU_LEN).peekable(); + let chunks = Apdu::new_chunks(ins, p1, message); let mut resp = vec![]; - while let Some(chunk) = chunks.next() { + for chunk in chunks { msg_buf.clear(); + msg_buf.reserve(chunk.bytes_count()); + chunk.write_bytes(&mut msg_buf); - 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) } +async fn send_chunked_expect_empty_ok_response( + ledger: &mut L, + ins: u8, + p1: u8, + message: &[u8], +) -> Result<(), SignerError> { + let resp = send_chunked(ledger, ins, p1, message).await?; + ensure!(resp.is_empty(), LedgerError::InvalidResponse); + Ok(()) +} + pub async fn sign_challenge( ledger: &mut L, coin: CoinType, @@ -117,7 +123,13 @@ pub async fn sign_challenge( path, }; - send_chunked(ledger, Ins::SIGN_MSG, P1_SIGN_START, &ledger_encode(req)).await?; + send_chunked_expect_empty_ok_response( + ledger, + Ins::SIGN_MSG, + P1_SIGN_START, + &ledger_encode(req), + ) + .await?; let resp = send_chunked(ledger, Ins::SIGN_MSG, P1_SIGN_NEXT, message).await?; @@ -126,43 +138,48 @@ pub async fn sign_challenge( 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) +pub async fn check_current_app( + ledger: &mut L, +) -> SignerResult { + let info = ledger + .app_info(TIMEOUT_DUR) .await .map_err(|err| LedgerError::DeviceError(err.to_string()))?; - let resp = ok_response(resp)?; - let name = String::from_utf8(resp).map_err(|_| LedgerError::InvalidResponse)?; + let name = info.name; + let app_version = info.version; 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_raw( + ledger: &mut L, + coin_type: CoinType, + derivation_path: &DerivationPath, +) -> Result, ledger_lib::Error> { + let path = LedgerBip32Path( + derivation_path.as_slice().iter().map(|c| c.into_encoded_index()).collect(), + ); + let req = PublicKeyReq { coin_type, path }; + let encoded_req = ledger_encode(req); + + let apdu = Apdu::new_with_data( + Ins::PUB_KEY, + PubKeyP1::NoDisplayAddress.into(), + &encoded_req, + ) + .expect("ok size"); + + let mut msg_buf = Vec::with_capacity(apdu.bytes_count()); + apdu.write_bytes(&mut msg_buf); + + ledger.exchange(&msg_buf, TIMEOUT_DUR).await +} + pub async fn get_extended_public_key( ledger: &mut L, coin_type: CoinType, @@ -206,10 +223,16 @@ pub async fn sign_tx( num_inputs: inputs.len() as u32, num_outputs: outputs.len() as u32, }); - send_chunked(ledger, Ins::SIGN_TX, P1SignTx::Metadata.into(), &metadata).await?; + send_chunked_expect_empty_ok_response( + ledger, + Ins::SIGN_TX, + P1SignTx::Metadata.into(), + &metadata, + ) + .await?; for inp in inputs { - send_chunked( + send_chunked_expect_empty_ok_response( ledger, Ins::SIGN_TX, P1SignTx::Input.into(), @@ -219,7 +242,7 @@ pub async fn sign_tx( } for info in input_additional_infos { - send_chunked( + send_chunked_expect_empty_ok_response( ledger, Ins::SIGN_TX, P1SignTx::InputAdditionalInfo.into(), @@ -228,16 +251,27 @@ pub async fn sign_tx( .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 num_outputs = outputs.len(); + for (idx, o) in outputs.into_iter().enumerate() { + if idx < num_outputs - 1 { + send_chunked_expect_empty_ok_response( + ledger, + Ins::SIGN_TX, + P1SignTx::Output.into(), + &ledger_encode(SignTxReq::Output(o)), + ) + .await?; + } else { + // the response from the last output will have the first signature returned + resp = send_chunked( + ledger, + Ins::SIGN_TX, + P1SignTx::Output.into(), + &ledger_encode(SignTxReq::Output(o)), + ) + .await?; + } } let mut signatures: BTreeMap<_, Vec<_>> = BTreeMap::new(); diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs index 06e8b59457..ce76e9a0b6 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -21,8 +21,9 @@ 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, + check_current_app, get_extended_public_key, get_extended_public_key_raw, + 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, @@ -93,12 +94,14 @@ pub enum LedgerError { NoDeviceFound, #[error("Connected to an unknown Ledger device model")] UnknownModel, + #[error("Device timeout")] + DeviceTimeout, #[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), + ErrorResponse(String), #[error("Device error: {0}")] DeviceError(String), #[error("Missing hardware wallet data in database")] @@ -121,6 +124,12 @@ pub enum LedgerError { SignatureError(#[from] SignatureError), } +impl From for LedgerError { + fn from(value: ledger_lib::Error) -> Self { + Self::DeviceError(value.to_string()) + } +} + struct StandaloneInput { multisig_idx: Option, private_key: PrivateKey, @@ -174,7 +183,8 @@ where } } - /// Calls initialize on the device with the current session_id. + /// Tries to confirm the running app name is still matching Mintlayer app + /// Also waits after a signing operation for the device to start listening to new instructions /// /// 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. @@ -184,12 +194,31 @@ where key_chain: &impl AccountKeyChains, ) -> SignerResult<()> { let mut client = self.client.lock().await; - + let mut num_tries = 10; + let derivation_path = make_account_path(&self.chain_config, DEFAULT_ACCOUNT_INDEX); + let coin_type = to_ledger_chain_type(&self.chain_config); loop { - match get_app_name(&mut *client).await { - Ok(_) => return Ok(()), + match get_extended_public_key_raw(&mut *client, coin_type, &derivation_path).await { + Ok(_) => { + check_public_keys_against_key_chain( + db_tx, + &mut *client, + key_chain, + &self.chain_config, + ) + .await?; + return Ok(()); + } + // After finishing a signing operation the device shows a status success/failed + // At those times any command sent is not handles so waiting for a response will + // just timeout Err(ledger_lib::Error::Timeout) => { - continue; + num_tries -= 1; + if num_tries > 0 { + continue; + } else { + return Err(SignerError::LedgerError(LedgerError::DeviceTimeout)); + } } // In case of a communication error try to reconnect, and try again Err( @@ -213,11 +242,7 @@ where *client = new_client; return Ok(()); } - Err(err) => { - return Err(SignerError::LedgerError(LedgerError::DeviceError( - err.to_string(), - ))) - } + Err(err) => return Err(SignerError::LedgerError(err.into())), } } } @@ -238,9 +263,7 @@ where 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()) + operation(&mut client).await } #[allow(clippy::too_many_arguments)] @@ -1092,13 +1115,11 @@ async fn fetch_extended_pub_key( client: &mut L, chain_config: &ChainConfig, account_index: U31, -) -> Result { +) -> SignerResult { 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())) + get_extended_public_key(client, coin_type, derivation_path).await } fn single_signature( @@ -1170,9 +1191,7 @@ impl LedgerSignerProvider { 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) + fetch_extended_pub_key(&mut *self.client.lock().await, chain_config, account_index).await } } diff --git a/wallet/src/signer/ledger_signer/speculos.rs b/wallet/src/signer/ledger_signer/speculos.rs new file mode 100644 index 0000000000..fd33c88007 --- /dev/null +++ b/wallet/src/signer/ledger_signer/speculos.rs @@ -0,0 +1,233 @@ +// 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 std::time::Duration; + +use logging::log; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumIter}; +use tokio::time::sleep; + +/// Device types supported by the emulator +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Device { + NanoS, + NanoSPlus, + NanoX, + Stax, + Flex, +} + +impl Device { + /// Returns true if the device has a touch screen + pub fn is_touch(&self) -> bool { + matches!(self, Device::Stax | Device::Flex) + } +} + +/// Button enumeration (Physical buttons) +#[derive(Clone, Copy, PartialEq, Debug, Display, EnumIter)] +#[strum(serialize_all = "kebab-case")] +pub enum Button { + Left, + Right, + Both, +} + +/// Physical Button actions +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Display, EnumIter)] +#[serde(rename_all = "kebab-case")] +pub enum ButtonAction { + Press, + Release, + PressAndRelease, +} + +/// Payload for button endpoint +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +struct ButtonPayload { + action: ButtonAction, +} + +// ----------------------------------------------------------------- +// TOUCH SCREEN LOGIC +// ----------------------------------------------------------------- + +/// Payload for the /finger endpoint +#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +pub struct FingerPayload { + pub x: u32, + pub y: u32, + pub action: ButtonAction, + // delay is optional in speculos, usually 0 + #[serde(skip_serializing_if = "Option::is_none")] + pub delay: Option, +} + +/// Semantic elements on the screen to avoid hardcoding X/Y in tests. +/// Mapped from the "UseCase" python dictionary. +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum ScreenElement { + ReviewTap, // Used to go to next page (lower right) + ReviewConfirm, // Above lower right +} + +impl ScreenElement { + /// Returns the (x, y) coordinates for the specific device + pub fn position(&self, device: Device) -> (u32, u32) { + // Device resolutions + // Stax: 400 x 672 + // Flex: 480 x 600 + match (self, device) { + // --- UseCaseReview --- + (ScreenElement::ReviewTap, Device::Stax) => (335, 606), + (ScreenElement::ReviewTap, Device::Flex) => (430, 530), + + (ScreenElement::ReviewConfirm, Device::Stax) => (335, 515), + (ScreenElement::ReviewConfirm, Device::Flex) => (240, 435), + + // Fallback or unimplemented combinations + _ => panic!("Coordinate not mapped for {:?} on {:?}", self, device), + } + } +} + +// ----------------------------------------------------------------- +// RUNTIME HANDLE +// ----------------------------------------------------------------- + +/// Handle for interacting with a Speculos instance +#[derive(Debug, Clone)] +pub struct Handle { + addr: SocketAddr, + device: Device, +} + +impl Handle { + pub fn new(addr: SocketAddr, device: Device) -> Self { + Self { addr, device } + } + + pub fn addr(&self) -> SocketAddr { + self.addr + } + + pub fn device(&self) -> Device { + self.device + } + + /// Send a physical button action + pub async fn button(&self, button: Button, action: ButtonAction) -> anyhow::Result<()> { + if self.device.is_touch() { + log::warn!("Sending physical button command to a touch device (Stax/Flex). This might be intended (Power button) but usually incorrect for UI navigation."); + } + + log::debug!("Sending button request: {}:{}", button, action); + + let r = Client::new() + .post(format!("http://{}/button/{}", self.addr(), button)) + .json(&ButtonPayload { action }) + .send() + .await?; + + if !r.status().is_success() { + anyhow::bail!("Button request failed: {}", r.status()); + } + + Ok(()) + } + + /// Send a raw finger action to the screen + pub async fn finger(&self, x: u32, y: u32, action: ButtonAction) -> anyhow::Result<()> { + log::debug!("Sending finger request: x={} y={} action={}", x, y, action); + + let payload = FingerPayload { + x, + y, + action, + delay: None, + }; + + let r = Client::new() + .post(format!("http://{}/finger", self.addr())) + .json(&payload) + .send() + .await?; + + if !r.status().is_success() { + anyhow::bail!("Finger request failed: {}", r.status()); + } + + Ok(()) + } + + pub async fn hold(&self, element: ScreenElement) -> anyhow::Result<()> { + let (x, y) = element.position(self.device); + self.finger(x, y, ButtonAction::Press).await?; + sleep(Duration::from_millis(1800)).await; + self.finger(x, y, ButtonAction::Release).await?; + Ok(()) + } + + pub async fn tap(&self, element: ScreenElement) -> anyhow::Result<()> { + let (x, y) = element.position(self.device); + self.finger(x, y, ButtonAction::PressAndRelease).await?; + Ok(()) + } +} + +#[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 ButtonAction::iter() { + let expected = match action { + ButtonAction::Press => r#"{"action":"press"}"#, + ButtonAction::Release => r#"{"action":"release"}"#, + ButtonAction::PressAndRelease => r#"{"action":"press-and-release"}"#, + }; + assert_eq!( + &serde_json::to_string(&ButtonPayload { action }).unwrap(), + expected + ); + } + } +} diff --git a/wallet/src/signer/ledger_signer/speculos/handle.rs b/wallet/src/signer/ledger_signer/speculos/handle.rs deleted file mode 100644 index e3b8a74d16..0000000000 --- a/wallet/src/signer/ledger_signer/speculos/handle.rs +++ /dev/null @@ -1,130 +0,0 @@ -// 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 deleted file mode 100644 index 0d0085567a..0000000000 --- a/wallet/src/signer/ledger_signer/speculos/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 index b773994b51..9da697b0df 100644 --- a/wallet/src/signer/ledger_signer/tests.rs +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -23,7 +23,7 @@ use std::{ use async_trait::async_trait; use ledger_lib::{ transport::{TcpDevice, TcpInfo, TcpTransport}, - Device, Transport, + Device as _, Transport, }; use mintlayer_ledger_messages::CoinType; use randomness::make_true_rng; @@ -40,8 +40,10 @@ use tokio::{ use crate::signer::{ ledger_signer::{ - ledger_messages::{check_current_app, get_app_name, get_extended_public_key, ok_response}, - speculos::{Action, Button, Handle, PodmanHandle}, + ledger_messages::{ + check_current_app, get_extended_public_key, get_extended_public_key_raw, + }, + speculos::{Button, ButtonAction, Device, Handle, ScreenElement}, LedgerError, LedgerFinder, LedgerSigner, }, tests::{ @@ -59,6 +61,7 @@ use crypto::key::{ PredefinedSigAuxDataProvider, SigAuxDataProvider, }; use logging::log; +use utils::env_utils::{bool_from_env, get_from_env}; use wallet_storage::WalletStorageReadLocked; use wallet_types::hw_data::LedgerData; @@ -67,14 +70,52 @@ enum ControlMessage { Finish, } -async fn auto_confirmer(mut control_msg_rx: mpsc::Receiver, handle: PodmanHandle) { +fn emulator_api_port() -> u16 { + get_from_env("LEDGER_TESTS_EMU_API_PORT") + .unwrap() + .map_or(5000, |s| s.parse().unwrap()) +} + +fn emulator_apdu_port() -> u16 { + get_from_env("LEDGER_TESTS_EMU_APDU_PORT") + .unwrap() + .map_or(9999, |s| s.parse().unwrap()) +} + +fn should_auto_confirm() -> bool { + bool_from_env("LEDGER_TESTS_AUTO_CONFIRM").unwrap().unwrap_or(false) +} + +async fn auto_confirmer(mut control_msg_rx: mpsc::Receiver, handle: Handle) { + println!("Starting auto-confirmer for device: {:?}", handle.device()); + 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(); + _ = sleep(Duration::from_millis(500)) => { + // Logic depends on whether we are using a touch screen or buttons + if handle.device().is_touch() { + // TOUCH DEVICE STRATEGY (Stax/Flex) + // On Speculos, blindly tapping coordinates is safe. + // 1. Try to go to the next page (Tap the "Next/Tap" zone) + // 2. Try to confirm (Tap the "Confirm" zone) + + // Attempt to advance review + let _ = handle.tap(ScreenElement::ReviewTap).await; + sleep(Duration::from_millis(100)).await; + + // Attempt to confirm review + let _ = handle.hold(ScreenElement::ReviewConfirm).await; + sleep(Duration::from_millis(100)).await; + } else { + // BUTTON DEVICE STRATEGY (Nano S/S+/X) + // 1. Press Right to scroll + // 2. Press Both to confirm + let _ = handle.button(Button::Right, ButtonAction::PressAndRelease).await; + sleep(Duration::from_millis(100)).await; + let _ = handle.button(Button::Both, ButtonAction::PressAndRelease).await; + sleep(Duration::from_millis(100)).await; + let _ = handle.button(Button::Both, ButtonAction::PressAndRelease).await; + } } msg = control_msg_rx.recv() => { match msg { @@ -110,26 +151,35 @@ impl LedgerFinder for DummyProvider { } async fn setup( - deterministic_aux: bool, + deterministic_signing: bool, ) -> ( - tokio::task::JoinHandle<()>, + Option>, 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 addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), emulator_api_port()); + + let device_model_env: String = get_from_env("LEDGER_TESTS_DEVICE_MODEL") + .expect("Failed to read env var LEDGER_TESTS_DEVICE_MODEL") + .unwrap_or_else(|| "nanosplus".to_string()); + + let device_type = match device_model_env.as_str() { + "nanos" => Device::NanoS, + "nanosplus" | "nanosp" => Device::NanoSPlus, + "nanox" => Device::NanoX, + "stax" => Device::Stax, + "flex" => Device::Flex, + _ => panic!("Unknown ledger device model in env: {}", device_model_env), + }; - 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 handle = Handle::new(addr, device_type); + + let mut device = create_device_connection().await; let mut tries = 0; + let derivation_path = DerivationPath::from_str("m/44h/19788h/0h").unwrap(); loop { - match get_app_name(&mut device).await { + match get_extended_public_key_raw(&mut device, CoinType::Mainnet, &derivation_path).await { Ok(_) => break, Err(_) => { tries += 1; @@ -144,22 +194,30 @@ async fn setup( 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 {}, - ) - }) + let auto_confirmer_handle = if should_auto_confirm() { + None + } else { + Some(tokio::spawn(auto_confirmer(control_msg_rx, handle))) + }; + + ( + auto_confirmer_handle, + control_msg_tx, + move |chain_config, _| { + let aux_provider: Box = if deterministic_signing { + Box::new(PredefinedSigAuxDataProvider) + } else { + Box::new(make_true_rng()) + }; + + LedgerSigner::new_with_sig_aux_data_provider( + chain_config, + device.clone(), + aux_provider, + DummyProvider {}, + ) + }, + ) } #[rstest] @@ -167,17 +225,12 @@ async fn setup( #[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 device = create_device_connection().await; + let derivation_path = DerivationPath::from_str("m/44h/19788h/0h").unwrap(); let mut tries = 0; loop { - match get_app_name(&mut device).await { + match get_extended_public_key_raw(&mut device, CoinType::Mainnet, &derivation_path).await { Ok(_) => break, Err(_) => { tries += 1; @@ -189,19 +242,14 @@ async fn test_app_name() { } } - 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:?}"); + let info = device.app_info(Duration::from_millis(500)).await.unwrap(); - assert_eq!(info.app_version.to_string(), "0.1.0"); + let err = check_current_app(&mut device).await.unwrap_err(); + eprintln!("info: {err:?}"); + assert_eq!( + err, + SignerError::LedgerError(LedgerError::DifferentActiveApp(info.name)) + ) } #[rstest] @@ -209,7 +257,7 @@ async fn test_app_name() { #[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 (auto_confirmer_handle, control_msg_tx, make_ledger_signer) = setup(false).await; let signer = make_ledger_signer(Arc::new(create_mainnet()), U31::ZERO); @@ -233,7 +281,9 @@ async fn test_account_extended_public_key() { assert_eq!(expected_chain_code, chain_code.hex_encode()); control_msg_tx.send(ControlMessage::Finish).await.unwrap(); - auto_clicker.await.unwrap(); + if let Some(auto_confirmer_handle) = auto_confirmer_handle { + auto_confirmer_handle.await.unwrap(); + } } #[rstest_reuse::apply(sign_message_test_params)] @@ -249,7 +299,7 @@ async fn test_sign_message(#[case] seed: Seed, message_to_sign: MessageToSign) { let mut rng = make_seedable_rng(seed); - let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(false).await; + let (auto_confirmer_handle, control_msg_tx, make_ledger_signer) = setup(false).await; test_sign_message_generic( &mut rng, @@ -260,7 +310,9 @@ async fn test_sign_message(#[case] seed: Seed, message_to_sign: MessageToSign) { .await; control_msg_tx.send(ControlMessage::Finish).await.unwrap(); - auto_clicker.await.unwrap(); + if let Some(auto_confirmer_handle) = auto_confirmer_handle { + auto_confirmer_handle.await.unwrap(); + } } #[rstest] @@ -271,14 +323,16 @@ async fn test_sign_message(#[case] seed: Seed, message_to_sign: MessageToSign) { 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 (auto_confirmer_handle, 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(); + if let Some(auto_confirmer_handle) = auto_confirmer_handle { + auto_confirmer_handle.await.unwrap(); + } } #[rstest] @@ -292,7 +346,7 @@ async fn test_sign_transaction( ) { 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 (auto_confirmer_handle, control_msg_tx, make_ledger_signer) = setup(false).await; let mut rng = make_seedable_rng(seed); @@ -305,7 +359,9 @@ async fn test_sign_transaction( .await; control_msg_tx.send(ControlMessage::Finish).await.unwrap(); - auto_clicker.await.unwrap(); + if let Some(auto_confirmer_handle) = auto_confirmer_handle { + auto_confirmer_handle.await.unwrap(); + } } #[rstest] @@ -321,14 +377,16 @@ async fn test_fixed_signatures2( 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 (auto_confirmer_handle, 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(); + if let Some(auto_confirmer_handle) = auto_confirmer_handle { + auto_confirmer_handle.await.unwrap(); + } } #[rstest] @@ -343,7 +401,7 @@ async fn test_sign_message_sig_consistency(#[case] seed: Seed) { log::debug!("test_sign_message_sig_consistency, seed = {seed:?}"); - let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; + let (auto_confirmer_handle, control_msg_tx, make_ledger_signer) = setup(true).await; let mut rng = make_seedable_rng(seed); @@ -356,7 +414,9 @@ async fn test_sign_message_sig_consistency(#[case] seed: Seed) { .await; control_msg_tx.send(ControlMessage::Finish).await.unwrap(); - auto_clicker.await.unwrap(); + if let Some(auto_confirmer_handle) = auto_confirmer_handle { + auto_confirmer_handle.await.unwrap(); + } } #[rstest] @@ -369,7 +429,7 @@ async fn test_sign_transaction_intent_sig_consistency(#[case] seed: Seed) { log::debug!("test_sign_transaction_intent_sig_consistency, seed = {seed:?}"); - let (auto_clicker, control_msg_tx, make_ledger_signer) = setup(true).await; + let (auto_confirmer_handle, control_msg_tx, make_ledger_signer) = setup(true).await; let mut rng = make_seedable_rng(seed); @@ -381,7 +441,9 @@ async fn test_sign_transaction_intent_sig_consistency(#[case] seed: Seed) { .await; control_msg_tx.send(ControlMessage::Finish).await.unwrap(); - auto_clicker.await.unwrap(); + if let Some(auto_confirmer_handle) = auto_confirmer_handle { + auto_confirmer_handle.await.unwrap(); + } } #[rstest] @@ -397,7 +459,7 @@ async fn test_sign_transaction_sig_consistency( 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 (auto_confirmer_handle, control_msg_tx, make_ledger_signer) = setup(true).await; let mut rng = make_seedable_rng(seed); @@ -410,5 +472,20 @@ async fn test_sign_transaction_sig_consistency( .await; control_msg_tx.send(ControlMessage::Finish).await.unwrap(); - auto_clicker.await.unwrap(); + if let Some(auto_confirmer_handle) = auto_confirmer_handle { + auto_confirmer_handle.await.unwrap(); + } +} + +async fn create_device_connection() -> TcpDevice { + let mut transport = TcpTransport::new().unwrap(); + transport + .connect(TcpInfo { + addr: SocketAddr::new( + std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), + emulator_apdu_port(), + ), + }) + .await + .unwrap() } diff --git a/wallet/types/src/hw_data.rs b/wallet/types/src/hw_data.rs index 3ace5d596e..3a736494f9 100644 --- a/wallet/types/src/hw_data.rs +++ b/wallet/types/src/hw_data.rs @@ -13,7 +13,6 @@ // 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. @@ -52,7 +51,7 @@ pub struct LedgerData {} #[cfg(feature = "ledger")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct LedgerFullInfo { - pub app_version: SemVer, + pub app_version: String, } #[cfg(feature = "ledger")] From 36f91d9f05f7cb73eb23ab0e89285e1d21f2db40 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Thu, 13 Nov 2025 18:27:47 +0100 Subject: [PATCH 10/24] Fix comments --- .github/workflows/build.yml | 7 ++-- .../main_widget/tabs/wallet/status_bar.rs | 10 ++++-- node-gui/src/main_window/mod.rs | 6 ++++ node-gui/src/widgets/create_hw_wallet.rs | 32 +++++++++++++++++-- node-gui/src/widgets/mod.rs | 2 +- .../signer/ledger_signer/ledger_messages.rs | 7 ++-- wallet/src/signer/ledger_signer/mod.rs | 19 ++++++++--- wallet/types/src/hw_data.rs | 25 +++++++++++++++ .../src/command_handler/mod.rs | 4 +-- wallet/wallet-controller/src/lib.rs | 1 + wallet/wallet-controller/src/types/mod.rs | 1 + 11 files changed, 96 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b61b223513..3a10c99e39 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -322,6 +322,9 @@ jobs: run_tests_on_ledger: needs: run_tests_on_ledger_preparation runs-on: ubuntu-latest + strategy: + matrix: + model: [nanox, nanosplus] #, flex, stax] steps: - name: Checkout the core repository uses: actions/checkout@v4 @@ -346,7 +349,7 @@ jobs: 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' + sh -c 'cargo ledger build ${{ matrix.model }}' - name: Run Ledger emulator and execute tests run: | set -e @@ -355,7 +358,7 @@ jobs: -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' + sh -c 'speculos --apdu-port 9999 --api-port 5000 --display headless -s "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" target/${{ matrix.model }}/release/mintlayer-app' echo "--- Waiting for emulator to initialize ---" sleep 15 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 fabca6abe1..bbb23aa246 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 @@ -68,9 +68,13 @@ 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),] + WalletExtraInfo::LedgerWallet { app_version, model } => { + row![ + rich_text([span("Model name: ").font(bold_font), span(model.clone())]) + .size(TEXT_SIZE), + rich_text([span("App version: ").font(bold_font), span(app_version.clone())]) + .size(TEXT_SIZE), + ] } }; diff --git a/node-gui/src/main_window/mod.rs b/node-gui/src/main_window/mod.rs index 8109f3476f..ffeac6979c 100644 --- a/node-gui/src/main_window/mod.rs +++ b/node-gui/src/main_window/mod.rs @@ -40,6 +40,8 @@ use wallet_types::{seed_phrase::StoreSeedPhrase, wallet_type::WalletType, Import #[cfg(any(feature = "trezor", feature = "ledger"))] use crate::widgets::create_hw_wallet::hw_wallet_create_dialog; +#[cfg(any(feature = "trezor", feature = "ledger"))] +use crate::widgets::create_hw_wallet::HardwareWalletType; use crate::{ main_window::{main_menu::MenuMessage, main_widget::MainWidgetMessage}, widgets::{ @@ -870,6 +872,7 @@ impl MainWindow { }), Box::new(|| MainWindowMessage::CloseDialog), ImportOrCreate::Create, + HardwareWalletType::Trezor, ) .into(), #[cfg(feature = "ledger")] @@ -880,6 +883,7 @@ impl MainWindow { }), Box::new(|| MainWindowMessage::CloseDialog), ImportOrCreate::Create, + HardwareWalletType::Ledger, ) .into(), }, @@ -904,6 +908,7 @@ impl MainWindow { }), Box::new(|| MainWindowMessage::CloseDialog), ImportOrCreate::Import, + HardwareWalletType::Trezor, ) .into(), #[cfg(feature = "ledger")] @@ -914,6 +919,7 @@ impl MainWindow { }), Box::new(|| MainWindowMessage::CloseDialog), ImportOrCreate::Import, + HardwareWalletType::Ledger, ) .into(), } diff --git a/node-gui/src/widgets/create_hw_wallet.rs b/node-gui/src/widgets/create_hw_wallet.rs index 6ddbf7ee43..ea3f16dbcb 100644 --- a/node-gui/src/widgets/create_hw_wallet.rs +++ b/node-gui/src/widgets/create_hw_wallet.rs @@ -25,21 +25,43 @@ use iced_aw::Card; use wallet_types::ImportOrCreate; +#[derive(Debug, Clone, Copy)] +pub enum HardwareWalletType { + #[cfg(feature = "trezor")] + Trezor, + #[cfg(feature = "ledger")] + Ledger, +} + +impl HardwareWalletType { + fn name(self) -> &'static str { + match self { + #[cfg(feature = "trezor")] + Self::Trezor => "Trezor", + #[cfg(feature = "ledger")] + Self::Ledger => "Ledger", + } + } +} + pub struct CreateHwWalletDialog { on_import: Box Message>, on_close: Box Message>, mode: ImportOrCreate, + hw_wallet_type: HardwareWalletType, } pub fn hw_wallet_create_dialog( on_import: Box Message>, on_close: Box Message>, mode: ImportOrCreate, + hw_wallet_type: HardwareWalletType, ) -> CreateHwWalletDialog { CreateHwWalletDialog { on_import, on_close, mode, + hw_wallet_type, } } @@ -74,14 +96,20 @@ impl Component for CreateHwWalletDialog .width(100.0) .on_press(ImportEvent::Ok); + let hw_wallet_name = self.hw_wallet_type.name(); + let card = match self.mode { ImportOrCreate::Create => Card::new( Text::new("Create new Wallet"), - Text::new("Create a new Trezor wallet using the connected Trezor device"), + Text::new(format!( + "Create a new {hw_wallet_name} wallet using the connected {hw_wallet_name} device" + )), ), ImportOrCreate::Import => Card::new( Text::new("Recover new Wallet"), - Text::new("Recover a new wallet using the connected Trezor device"), + Text::new(format!( + "Recover a new wallet using the connected {hw_wallet_name} device" + )), ), }; if state.importing { diff --git a/node-gui/src/widgets/mod.rs b/node-gui/src/widgets/mod.rs index e433302ca8..c2fcbd3354 100644 --- a/node-gui/src/widgets/mod.rs +++ b/node-gui/src/widgets/mod.rs @@ -14,7 +14,7 @@ // limitations under the License. pub mod confirm_broadcast; -#[cfg(feature = "trezor")] +#[cfg(any(feature = "trezor", feature = "ledger"))] pub mod create_hw_wallet; pub mod esc_handler; pub mod new_wallet_account; diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs index 22ea5c5598..f24c8b1c2a 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -27,7 +27,6 @@ use crypto::key::{ }; use serialization::Encode; use utils::ensure; -use wallet_types::hw_data::LedgerFullInfo; use ledger_lib::{Device, Exchange}; use ledger_proto::StatusCode; @@ -140,7 +139,7 @@ pub async fn sign_challenge( pub async fn check_current_app( ledger: &mut L, -) -> SignerResult { +) -> SignerResult { let info = ledger .app_info(TIMEOUT_DUR) .await @@ -153,7 +152,7 @@ pub async fn check_current_app( LedgerError::DifferentActiveApp(name) ); - Ok(LedgerFullInfo { app_version }) + Ok(app_version) } pub async fn get_extended_public_key_raw( @@ -177,7 +176,7 @@ pub async fn get_extended_public_key_raw( let mut msg_buf = Vec::with_capacity(apdu.bytes_count()); apdu.write_bytes(&mut msg_buf); - ledger.exchange(&msg_buf, TIMEOUT_DUR).await + ledger.exchange(&msg_buf, Duration::from_millis(100)).await } pub async fn get_extended_public_key( diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs index ce76e9a0b6..5582a65d6a 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -70,7 +70,7 @@ use wallet_storage::{ }; use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, - hw_data::{HardwareWalletFullInfo, LedgerData, LedgerFullInfo}, + hw_data::{HardwareWalletFullInfo, LedgerData, LedgerFullInfo, LedgerModel}, partially_signed_transaction::{PartiallySignedTransaction, TokensAdditionalInfo}, signature_status::SignatureStatus, AccountId, @@ -78,7 +78,7 @@ use wallet_types::{ use async_trait::async_trait; use itertools::{izip, Itertools}; -use ledger_lib::{Exchange, Filters, LedgerHandle, LedgerProvider, Transport}; +use ledger_lib::{info::Model, Exchange, Filters, LedgerHandle, LedgerProvider, Transport}; use mintlayer_ledger_messages::{ AddrType, Bip32Path as LedgerBip32Path, CoinType, InputAdditionalInfoReq, InputAddressPath as LedgerInputAddressPath, Signature as LedgerSignature, TxInputReq, @@ -1052,15 +1052,16 @@ async fn find_ledger_device() -> SignerResult<(LedgerHandle, LedgerFullInfo)> { .map_err(|err| LedgerError::DeviceError(err.to_string()))?; let device = devices.pop().ok_or(LedgerError::NoDeviceFound)?; + let model = to_ledger_model(&device.model); let mut handle = provider .connect(device) .await .map_err(|err| LedgerError::DeviceError(err.to_string()))?; - let full_info = check_current_app(&mut handle).await?; + let app_version = check_current_app(&mut handle).await?; - Ok((handle, full_info)) + Ok((handle, LedgerFullInfo { app_version, model })) } /// Check that the public keys in the provided key chain are the same as the ones from the @@ -1240,6 +1241,16 @@ impl SignerProvider for LedgerSignerProvider { } } +fn to_ledger_model(model: &Model) -> LedgerModel { + match model { + Model::NanoS => LedgerModel::NanoS, + Model::NanoSPlus => LedgerModel::NanoSPlus, + Model::NanoX => LedgerModel::NanoX, + Model::Stax => LedgerModel::Stax, + Model::Unknown(m) => LedgerModel::Unknown(*m), + } +} + #[cfg(feature = "enable-ledger-device-tests")] #[cfg(test)] mod tests; diff --git a/wallet/types/src/hw_data.rs b/wallet/types/src/hw_data.rs index 3a736494f9..e50886e564 100644 --- a/wallet/types/src/hw_data.rs +++ b/wallet/types/src/hw_data.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use core::fmt; + use serialization::{Decode, Encode}; /// This is the data that will be stored in the wallet db. @@ -42,6 +44,28 @@ impl From for TrezorData { } } +#[cfg(feature = "ledger")] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum LedgerModel { + NanoS, + NanoSPlus, + NanoX, + Stax, + Unknown(u16), +} + +impl fmt::Display for LedgerModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LedgerModel::NanoS => write!(f, "Nano S"), + LedgerModel::NanoSPlus => write!(f, "Nano S Plus"), + LedgerModel::NanoX => write!(f, "Nano X"), + LedgerModel::Stax => write!(f, "Stax"), + LedgerModel::Unknown(id) => write!(f, "Unknown({})", id), + } + } +} + /// This is the data that will be stored in the wallet db. #[cfg(feature = "ledger")] #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] @@ -52,6 +76,7 @@ pub struct LedgerData {} #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] pub struct LedgerFullInfo { pub app_version: String, + pub model: LedgerModel, } #[cfg(feature = "ledger")] diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index a2cc98da95..eb3919f2c8 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -368,8 +368,8 @@ where device_name, device_id, firmware_version ) } - WalletExtraInfo::LedgerWallet { app_version } => format!( - "This is a ledger wallet, running app version {}", + WalletExtraInfo::LedgerWallet { app_version, model } => format!( + "This is a ledger wallet; model: {model}, running app version {}", app_version ), }; diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index d71ccf9268..0924b94297 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -652,6 +652,7 @@ where #[cfg(feature = "ledger")] HardwareWalletFullInfo::Ledger(ledger_data) => WalletExtraInfo::LedgerWallet { app_version: ledger_data.app_version.to_string(), + model: ledger_data.model.to_string(), }, }, None => WalletExtraInfo::SoftwareWallet, diff --git a/wallet/wallet-controller/src/types/mod.rs b/wallet/wallet-controller/src/types/mod.rs index dfc40777af..8841fcf140 100644 --- a/wallet/wallet-controller/src/types/mod.rs +++ b/wallet/wallet-controller/src/types/mod.rs @@ -75,6 +75,7 @@ pub enum WalletExtraInfo { #[cfg(feature = "ledger")] LedgerWallet { app_version: String, + model: String, }, } From 2b9dfc2f8409109cb6ca16cdbc86b8887dae277a Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Wed, 19 Nov 2025 18:41:22 +0100 Subject: [PATCH 11/24] Add support for Flex and Stax Ledger emulator tests --- .github/workflows/build.yml | 8 ++++++-- wallet/src/signer/ledger_signer/ledger_messages.rs | 2 +- wallet/src/signer/ledger_signer/mod.rs | 5 +++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a10c99e39..9c813a7718 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -324,7 +324,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - model: [nanox, nanosplus] #, flex, stax] + model: [flex, stax, nanox, nanosplus] steps: - name: Checkout the core repository uses: actions/checkout@v4 @@ -356,7 +356,7 @@ jobs: sudo docker run -d --rm --name ledger-emulator \ -v "$(realpath ./mintlayer-ledger-app):/app" \ - --publish 5001:5001 --publish 9999:9999 \ + --publish 5000:5000 --publish 9999:9999 \ ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \ sh -c 'speculos --apdu-port 9999 --api-port 5000 --display headless -s "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" target/${{ matrix.model }}/release/mintlayer-app' @@ -368,6 +368,10 @@ jobs: echo "--- Running Ledger device tests on the host ---" cd ./mintlayer-core + + # Export the device model from the matrix so the Rust test can pick it up + export LEDGER_TESTS_DEVICE_MODEL=${{ matrix.model }} + cargo-nextest nextest run --archive-file ledger-tests.tar.zst -j1 ledger_signer || test_exit_code=$? exit $test_exit_code diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs index f24c8b1c2a..6dffb3b993 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -176,7 +176,7 @@ pub async fn get_extended_public_key_raw( let mut msg_buf = Vec::with_capacity(apdu.bytes_count()); apdu.write_bytes(&mut msg_buf); - ledger.exchange(&msg_buf, Duration::from_millis(100)).await + ledger.exchange(&msg_buf, Duration::from_millis(200)).await } pub async fn get_extended_public_key( diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs index 5582a65d6a..b851d0745a 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -194,8 +194,9 @@ where key_chain: &impl AccountKeyChains, ) -> SignerResult<()> { let mut client = self.client.lock().await; - let mut num_tries = 10; - let derivation_path = make_account_path(&self.chain_config, DEFAULT_ACCOUNT_INDEX); + // Try and wait around 5sec 50 * 100ms for the screen to clear after a signing operation ends + let mut num_tries = 50; + let derivation_path = make_account_path(&self.chain_config, key_chain.account_index()); let coin_type = to_ledger_chain_type(&self.chain_config); loop { match get_extended_public_key_raw(&mut *client, coin_type, &derivation_path).await { From 5e3f47dbd759f1c5a14eb957984afba4eb5d9d71 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 19 Dec 2025 12:11:50 +0100 Subject: [PATCH 12/24] Use new ledger messages --- Cargo.lock | 38 +++---- Cargo.toml | 4 +- .../src/chain/transaction/account_outpoint.rs | 14 ++- .../signer/ledger_signer/ledger_messages.rs | 64 ++++++++--- wallet/src/signer/ledger_signer/mod.rs | 103 +++++++++++++----- wallet/src/signer/ledger_signer/tests.rs | 27 +---- wallet/src/signer/software_signer/tests.rs | 1 + wallet/src/signer/tests/generic_tests.rs | 39 ++++--- wallet/src/signer/trezor_signer/tests.rs | 2 + 9 files changed, 182 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b080182cb4..84ac7a2751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1720,7 +1720,7 @@ dependencies = [ "lazy_static", "logging", "merkletree-mintlayer", - "mintlayer-core-primitives", + "mintlayer-core-primitives 1.0.0 (git+https://github.com/mintlayer/mintlayer-core-primitives?rev=4e21bf85e7fdca1577a49df7599b2b39ec913287)", "num", "num-traits", "once_cell", @@ -4976,11 +4976,11 @@ dependencies = [ [[package]] name = "messages" version = "0.1.0" -source = "git+https://github.com/mintlayer/mintlayer-ledger-app?rev=d65b5200a4499c5ebdcf95903cc3a5579f611c6f#d65b5200a4499c5ebdcf95903cc3a5579f611c6f" +source = "git+https://github.com/mintlayer/mintlayer-ledger-app?rev=b87b78a330a7e2a80d30154e11c5065562ee9d0f#b87b78a330a7e2a80d30154e11c5065562ee9d0f" dependencies = [ + "mintlayer-core-primitives 1.0.0 (git+https://github.com/mintlayer/mintlayer-core-primitives?branch=feature%2Fledger-coin-type)", "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", + "parity-scale-codec 3.7.5 (git+https://github.com/paritytech/parity-scale-codec.git?rev=5021525697edc0661591ebc71392c48d950a10b0)", ] [[package]] @@ -5039,6 +5039,17 @@ dependencies = [ "utxo", ] +[[package]] +name = "mintlayer-core-primitives" +version = "1.0.0" +source = "git+https://github.com/mintlayer/mintlayer-core-primitives?branch=feature%2Fledger-coin-type#8d8926aba4f3352d885d7d3bc125a11eb13ff1fb" +dependencies = [ + "derive_more 2.1.0", + "fixed-hash", + "parity-scale-codec 3.7.5 (git+https://github.com/paritytech/parity-scale-codec.git?rev=5021525697edc0661591ebc71392c48d950a10b0)", + "strum 0.27.2", +] + [[package]] name = "mintlayer-core-primitives" version = "1.0.0" @@ -5055,7 +5066,7 @@ name = "mintlayer-firmware-deps" version = "0.0.0" source = "git+https://github.com/mintlayer/mintlayer-trezor-firmware?rev=35716c865086f017f308c949c041f99d343057c0#35716c865086f017f308c949c041f99d343057c0" dependencies = [ - "mintlayer-core-primitives", + "mintlayer-core-primitives 1.0.0 (git+https://github.com/mintlayer/mintlayer-core-primitives?rev=4e21bf85e7fdca1577a49df7599b2b39ec913287)", ] [[package]] @@ -6208,13 +6219,13 @@ dependencies = [ [[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" +source = "git+https://github.com/paritytech/parity-scale-codec.git?rev=5021525697edc0661591ebc71392c48d950a10b0#5021525697edc0661591ebc71392c48d950a10b0" 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)", + "parity-scale-codec-derive 3.7.5 (git+https://github.com/paritytech/parity-scale-codec.git?rev=5021525697edc0661591ebc71392c48d950a10b0)", "rustversion", ] @@ -6233,7 +6244,7 @@ dependencies = [ [[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" +source = "git+https://github.com/paritytech/parity-scale-codec.git?rev=5021525697edc0661591ebc71392c48d950a10b0#5021525697edc0661591ebc71392c48d950a10b0" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -9329,17 +9340,6 @@ dependencies = [ "unicode-normalization", ] -[[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 0.26.3", -] - [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index ed9469b810..02200775ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -295,8 +295,8 @@ rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52" [workspace.dependencies.mintlayer-ledger-messages] git = "https://github.com/mintlayer/mintlayer-ledger-app" -# The commit "Remove App name and version instructions" -rev = "d65b5200a4499c5ebdcf95903cc3a5579f611c6f" +# The commit "fix tests" +rev = "b87b78a330a7e2a80d30154e11c5065562ee9d0f" package = "messages" [workspace.dependencies.trezor-client] diff --git a/common/src/chain/transaction/account_outpoint.rs b/common/src/chain/transaction/account_outpoint.rs index 9df3e72b39..9934402254 100644 --- a/common/src/chain/transaction/account_outpoint.rs +++ b/common/src/chain/transaction/account_outpoint.rs @@ -61,16 +61,22 @@ impl From<&AccountCommand> for AccountType { } } -impl From for AccountType { - fn from(cmd: OrderAccountCommand) -> Self { - match cmd { +impl OrderAccountCommand { + pub fn order_id(&self) -> OrderId { + match self { OrderAccountCommand::FillOrder(order_id, _) | OrderAccountCommand::FreezeOrder(order_id) - | OrderAccountCommand::ConcludeOrder(order_id) => AccountType::Order(order_id), + | OrderAccountCommand::ConcludeOrder(order_id) => *order_id, } } } +impl From for AccountType { + fn from(cmd: OrderAccountCommand) -> Self { + AccountType::Order(cmd.order_id()) + } +} + /// The type represents the amount to withdraw from a particular account. /// Otherwise it's unclear how much should be deducted from an account balance. /// It also helps solving 2 additional problems: calculating fees and providing ability to sign input balance with the witness. diff --git a/wallet/src/signer/ledger_signer/ledger_messages.rs b/wallet/src/signer/ledger_signer/ledger_messages.rs index 6dffb3b993..0dcb84f2c8 100644 --- a/wallet/src/signer/ledger_signer/ledger_messages.rs +++ b/wallet/src/signer/ledger_signer/ledger_messages.rs @@ -31,13 +31,17 @@ use utils::ensure; use ledger_lib::{Device, Exchange}; use ledger_proto::StatusCode; use mintlayer_ledger_messages::{ - decode_all as ledger_decode_all, encode as ledger_encode, AddrType, Amount as LAmount, Apdu, - Bip32Path as LedgerBip32Path, CoinType, GetPublicKeyRespones, 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_SIGN_NEXT, P1_SIGN_START, - P2_DONE, P2_SIGN_MORE, + decode_all as ledger_decode_all, encode as ledger_encode, AccountCommand as LAccountCommand, + AccountNonce as LAccountNonce, AccountOutPoint as LAccountOutPoint, AdditionalOrderInfo, + AddrType, Amount as LAmount, Apdu, Bip32Path as LedgerBip32Path, CoinType, + GetPublicKeyRespones, Id as LId, Ins, MsgSignature, + OrderAccountCommand as LOrderAccountCommand, OutputValue as LOutputValue, P1SignTx, PubKeyP1, + PublicKeyReq, SighashInputCommitment as LSighashInputCommitment, SignMessageReq, SignTxReq, + Signature as LedgerSignature, TxInputReq, TxMetadataReq, TxOutput as LTxOutput, TxOutputReq, + UtxoOutPoint as LUtxoOutPoint, APDU_CLASS, H256 as LH256, P1_SIGN_NEXT, P1_SIGN_START, P2_DONE, + P2_MORE, }; +use wallet_types::partially_signed_transaction::OrderAdditionalInfo; const TIMEOUT_DUR: Duration = Duration::from_secs(100); const TX_VERSION: u8 = 1; @@ -213,7 +217,7 @@ pub async fn sign_tx( ledger: &mut L, chain_type: CoinType, inputs: Vec, - input_additional_infos: Vec, + input_commitments: Vec, outputs: Vec, ) -> SignerResult>> { let metadata = ledger_encode(TxMetadataReq { @@ -240,12 +244,12 @@ pub async fn sign_tx( .await?; } - for info in input_additional_infos { + for commitment in input_commitments { send_chunked_expect_empty_ok_response( ledger, Ins::SIGN_TX, - P1SignTx::InputAdditionalInfo.into(), - &ledger_encode(SignTxReq::InputAdditionalInfo(info)), + P1SignTx::InputCommitment.into(), + &ledger_encode(SignTxReq::InputCommitment(commitment)), ) .await?; } @@ -300,7 +304,7 @@ pub async fn sign_tx( 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 has_more_signatures = *resp.last().ok_or(LedgerError::InvalidResponse)? == P2_MORE; let sig: LedgerSignature = ledger_decode_all(&resp[..resp.len() - 1][1..]).ok_or(LedgerError::InvalidResponse)?; @@ -316,12 +320,37 @@ 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 { +pub fn to_ledger_amount(value: &primitives::Amount) -> LAmount { + LAmount::from_atoms(value.into_atoms()) +} + +pub fn to_ledger_outpoint(value: &chain::UtxoOutPoint) -> LUtxoOutPoint { 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_account_outpoint(value: &chain::AccountOutPoint) -> LAccountOutPoint { + ledger_decode_all(value.encode().as_slice()).expect("ok") +} + +pub fn to_ledger_account_nonce(value: &chain::AccountNonce) -> LAccountNonce { + ledger_decode_all(value.encode().as_slice()).expect("ok") +} + +pub fn to_ledger_account_command(value: &chain::AccountCommand) -> LAccountCommand { + ledger_decode_all(value.encode().as_slice()).expect("ok") +} + +pub fn to_ledger_order_account_command(value: &chain::OrderAccountCommand) -> LOrderAccountCommand { + ledger_decode_all(value.encode().as_slice()).expect("ok") +} + +pub fn to_ledger_additional_order_info(info: &OrderAdditionalInfo) -> AdditionalOrderInfo { + AdditionalOrderInfo { + initially_asked: to_ledger_output_value(&info.initially_asked), + initially_given: to_ledger_output_value(&info.initially_given), + ask_balance: to_ledger_amount(&info.ask_balance), + give_balance: to_ledger_amount(&info.give_balance), + } } pub fn to_ledger_output_value(value: &chain::output_value::OutputValue) -> LOutputValue { @@ -330,8 +359,9 @@ pub fn to_ledger_output_value(value: &chain::output_value::OutputValue) -> LOutp 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)) - } + chain::output_value::OutputValue::TokenV1(token_id, amount) => LOutputValue::TokenV1( + LId::new(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 index b851d0745a..6abc9bab5a 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -22,7 +22,9 @@ use crate::{ signer::{ ledger_signer::ledger_messages::{ check_current_app, get_extended_public_key, get_extended_public_key_raw, - sign_challenge, sign_tx, to_ledger_amount, to_ledger_output_value, to_ledger_tx_input, + sign_challenge, sign_tx, to_ledger_account_command, to_ledger_account_nonce, + to_ledger_account_outpoint, to_ledger_additional_order_info, to_ledger_amount, + to_ledger_order_account_command, to_ledger_outpoint, to_ledger_output_value, to_ledger_tx_output, }, utils::{is_htlc_utxo, produce_uniparty_signature_for_input}, @@ -71,7 +73,9 @@ use wallet_storage::{ use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, hw_data::{HardwareWalletFullInfo, LedgerData, LedgerFullInfo, LedgerModel}, - partially_signed_transaction::{PartiallySignedTransaction, TokensAdditionalInfo}, + partially_signed_transaction::{ + PartiallySignedTransaction, PtxAdditionalInfo, TokensAdditionalInfo, + }, signature_status::SignatureStatus, AccountId, }; @@ -80,9 +84,9 @@ use async_trait::async_trait; use itertools::{izip, Itertools}; use ledger_lib::{info::Model, Exchange, Filters, LedgerHandle, LedgerProvider, Transport}; use mintlayer_ledger_messages::{ - AddrType, Bip32Path as LedgerBip32Path, CoinType, InputAdditionalInfoReq, - InputAddressPath as LedgerInputAddressPath, Signature as LedgerSignature, TxInputReq, - TxOutputReq, + AdditionalUtxoInfo, AddrType, Bip32Path as LedgerBip32Path, CoinType, + InputAddressPath as LedgerInputAddressPath, SighashInputCommitment as LSighashInputCommitment, + Signature as LedgerSignature, TxInputReq, TxInputWithAdditionalInfo, TxOutputReq, }; use randomness::make_true_rng; use tokio::sync::Mutex; @@ -499,8 +503,8 @@ where Vec, )> { let (inputs, standalone_inputs) = to_ledger_input_msgs(&ptx, key_chain, &db_tx)?; + let input_commitments = to_ledger_input_commitments_reqs(&ptx)?; 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 @@ -519,7 +523,7 @@ where let new_signatures = self .perform_ledger_operation( async move |client| { - sign_tx(client, coin_type, inputs, input_additional_infos, outputs).await + sign_tx(client, coin_type, inputs, input_commitments, outputs).await }, &mut db_tx, key_chain, @@ -855,27 +859,74 @@ fn to_ledger_input_msgs( 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<_> { + ptx.tx() + .inputs() + .iter() + .zip(ptx.destinations()) + .zip(ptx.input_utxos()) + .enumerate() + .map(|(idx, ((inp, dest), utxo))| -> 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), + inp: to_ledger_tx_input_with_additional_info(inp, utxo, ptx.additional_info())?, addresses: address_paths, }; Ok((input, (idx as u32, standalone_inputs))) - }, - ), + }), |iter| iter.unzip(), )?; Ok(res) } +fn to_ledger_tx_input_with_additional_info( + inp: &TxInput, + utxo: &Option, + additional_info: &PtxAdditionalInfo, +) -> SignerResult { + let inp = match inp { + TxInput::Utxo(outpoint) => { + let utxo = utxo.as_ref().ok_or(SignerError::MissingUtxo)?; + let info = match utxo { + TxOutput::ProduceBlockFromStake(_, pool_id) => { + let pool_info = additional_info + .get_pool_info(pool_id) + .ok_or(SignerError::MissingTxExtraInfo)?; + AdditionalUtxoInfo::PoolData { + utxo: to_ledger_tx_output(utxo), + staker_balance: to_ledger_amount(&pool_info.staker_balance), + } + } + _ => AdditionalUtxoInfo::Utxo(to_ledger_tx_output(utxo)), + }; + TxInputWithAdditionalInfo::Utxo(to_ledger_outpoint(outpoint), info) + } + TxInput::Account(acc) => { + TxInputWithAdditionalInfo::Account(to_ledger_account_outpoint(acc)) + } + TxInput::AccountCommand(nonce, cmd) => TxInputWithAdditionalInfo::AccountCommand( + to_ledger_account_nonce(nonce), + to_ledger_account_command(cmd), + ), + TxInput::OrderAccountCommand(cmd) => { + let info = additional_info + .get_order_info(&cmd.order_id()) + .ok_or(SignerError::MissingTxExtraInfo)?; + + TxInputWithAdditionalInfo::OrderAccountCommand( + to_ledger_order_account_command(cmd), + to_ledger_additional_order_info(info), + ) + } + }; + Ok(inp) +} + /// 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, @@ -943,9 +994,9 @@ fn destination_to_address_paths_impl( } } -fn to_ledger_input_additional_info_reqs( +fn to_ledger_input_commitments_reqs( ptx: &PartiallySignedTransaction, -) -> SignerResult> { +) -> SignerResult> { ptx.input_utxos() .iter() .zip(ptx.tx().inputs()) @@ -959,17 +1010,15 @@ fn to_ledger_input_additional_info_reqs( .additional_info() .get_pool_info(pool_id) .ok_or(SignerError::MissingTxExtraInfo)?; - InputAdditionalInfoReq::PoolInfo { + LSighashInputCommitment::ProduceBlockFromStakeUtxo { utxo: to_ledger_tx_output(utxo), staker_balance: to_ledger_amount(&pool_info.staker_balance), } } - _ => InputAdditionalInfoReq::Utxo { - utxo: to_ledger_tx_output(utxo), - }, + _ => LSighashInputCommitment::Utxo(to_ledger_tx_output(utxo)), } } - TxInput::Account(_) => InputAdditionalInfoReq::None, + TxInput::Account(_) => LSighashInputCommitment::None, TxInput::AccountCommand(_, cmd) => match cmd { AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) @@ -977,17 +1026,15 @@ fn to_ledger_input_additional_info_reqs( | AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) | AccountCommand::ChangeTokenAuthority(_, _) - | AccountCommand::ChangeTokenMetadataUri(_, _) => InputAdditionalInfoReq::None, + | AccountCommand::ChangeTokenMetadataUri(_, _) => LSighashInputCommitment::None, AccountCommand::FillOrder(order_id, _, _) => { let order_info = ptx .additional_info() .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; - InputAdditionalInfoReq::OrderInfo { + LSighashInputCommitment::FillOrderAccountCommand { 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) => { @@ -995,7 +1042,7 @@ fn to_ledger_input_additional_info_reqs( .additional_info() .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; - InputAdditionalInfoReq::OrderInfo { + LSighashInputCommitment::ConcludeOrderAccountCommand { 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), @@ -1009,11 +1056,9 @@ fn to_ledger_input_additional_info_reqs( .additional_info() .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; - InputAdditionalInfoReq::OrderInfo { + LSighashInputCommitment::FillOrderAccountCommand { 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) => { @@ -1021,14 +1066,14 @@ fn to_ledger_input_additional_info_reqs( .additional_info() .get_order_info(order_id) .ok_or(SignerError::MissingTxExtraInfo)?; - InputAdditionalInfoReq::OrderInfo { + LSighashInputCommitment::ConcludeOrderAccountCommand { 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, + OrderAccountCommand::FreezeOrder(_) => LSighashInputCommitment::None, }, }; Ok(additional_info) diff --git a/wallet/src/signer/ledger_signer/tests.rs b/wallet/src/signer/ledger_signer/tests.rs index 9da697b0df..b921f63e38 100644 --- a/wallet/src/signer/ledger_signer/tests.rs +++ b/wallet/src/signer/ledger_signer/tests.rs @@ -355,6 +355,7 @@ async fn test_sign_transaction( input_commitments_version, make_ledger_signer, no_another_signer(), + false, ) .await; @@ -364,31 +365,6 @@ async fn test_sign_transaction( } } -#[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_confirmer_handle, 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(); - if let Some(auto_confirmer_handle) = auto_confirmer_handle { - auto_confirmer_handle.await.unwrap(); - } -} - #[rstest] #[trace] #[serial_test::serial] @@ -468,6 +444,7 @@ async fn test_sign_transaction_sig_consistency( input_commitments_version, make_ledger_signer, Some(make_deterministic_software_signer), + false, ) .await; diff --git a/wallet/src/signer/software_signer/tests.rs b/wallet/src/signer/software_signer/tests.rs index c3b25a938c..c3c1137444 100644 --- a/wallet/src/signer/software_signer/tests.rs +++ b/wallet/src/signer/software_signer/tests.rs @@ -73,6 +73,7 @@ async fn test_sign_transaction( input_commitments_version, make_software_signer, no_another_signer(), + true, ) .await; } diff --git a/wallet/src/signer/tests/generic_tests.rs b/wallet/src/signer/tests/generic_tests.rs index c69ef09743..ea6126410c 100644 --- a/wallet/src/signer/tests/generic_tests.rs +++ b/wallet/src/signer/tests/generic_tests.rs @@ -324,6 +324,7 @@ pub async fn test_sign_transaction_generic( input_commitments_version: SighashInputCommitmentVersion, make_signer: MkS1, make_another_signer: Option, + include_orders_v0: bool, ) where MkS1: Fn(Arc, U31) -> S1, MkS2: Fn(Arc, U31) -> S2, @@ -580,7 +581,7 @@ pub async fn test_sign_transaction_generic( rng, ); - let acc_inputs = vec![ + let mut acc_inputs = vec![ TxInput::Account(AccountOutPoint::new( AccountNonce::new(rng.gen_range(0..100)), AccountSpending::DelegationBalance( @@ -622,20 +623,29 @@ pub async fn test_sign_transaction_generic( random_ascii_alphanumeric_string(rng, 10..20).into_bytes(), ), ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::ConcludeOrder(concluded_order1_id), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::FillOrder( - filled_order1_id, - Amount::from_atoms( - rng.gen_range(1..filled_order1_info.initially_asked.amount().into_atoms()), + ]; + + // optional orders V0 + if include_orders_v0 { + acc_inputs.extend([ + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ConcludeOrder(concluded_order1_id), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::FillOrder( + filled_order1_id, + Amount::from_atoms( + rng.gen_range(1..filled_order1_info.initially_asked.amount().into_atoms()), + ), + Destination::AnyoneCanSpend, ), - Destination::AnyoneCanSpend, ), - ), + ]); + } + + acc_inputs.extend([ TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(concluded_order2_id)), TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(frozen_order_id)), TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( @@ -644,7 +654,8 @@ pub async fn test_sign_transaction_generic( rng.gen_range(1..filled_order2_info.initially_asked.amount().into_atoms()), ), )), - ]; + ]); + // Note: the last input is v1 FillOrder, which must not be signed. let acc_dests = (0..acc_inputs.len() - 1) .map(|_| destination_from_account(&mut account, &mut db_tx, rng)) diff --git a/wallet/src/signer/trezor_signer/tests.rs b/wallet/src/signer/trezor_signer/tests.rs index 62c6f602ac..8c379c8237 100644 --- a/wallet/src/signer/trezor_signer/tests.rs +++ b/wallet/src/signer/trezor_signer/tests.rs @@ -132,6 +132,7 @@ async fn test_sign_transaction( input_commitments_version, make_trezor_signer, no_another_signer(), + true, ) .await; } @@ -267,6 +268,7 @@ async fn test_sign_transaction_sig_consistency( input_commitments_version, make_deterministic_trezor_signer, Some(make_deterministic_software_signer), + true, ) .await; } From 2beb8b0a17a58afc915a0609ef9bd89b11ce048a Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Mon, 5 Jan 2026 15:03:50 +0700 Subject: [PATCH 13/24] fix comments --- .github/workflows/build.yml | 19 +- common/Cargo.toml | 4 +- .../src/chain/transaction/account_outpoint.rs | 6 +- common/src/lib.rs | 1 + common/src/primitives_converters.rs | 521 ++++++++++++++++++ .../tests/primitives_repo_consistency/main.rs | 59 +- .../utils/converters.rs | 469 ---------------- .../primitives_repo_consistency/utils/mod.rs | 1 - node-gui/backend/src/backend_impl.rs | 255 +++------ node-gui/backend/src/error.rs | 4 +- .../signer/ledger_signer/ledger_messages.rs | 24 +- wallet/src/signer/ledger_signer/mod.rs | 34 +- wallet/src/signer/ledger_signer/speculos.rs | 233 -------- wallet/src/signer/ledger_signer/tests.rs | 468 ---------------- wallet/src/signer/mod.rs | 10 + wallet/src/signer/trezor_signer/mod.rs | 4 +- wallet/src/wallet/mod.rs | 2 - wallet/src/wallet_events.rs | 2 +- wallet/types/src/hw_data.rs | 6 +- .../src/command_handler/mod.rs | 2 +- wallet/wallet-controller/src/lib.rs | 2 +- wallet/wallet-controller/src/sync/mod.rs | 16 +- .../wallet-controller/src/sync/tests/mod.rs | 4 +- wallet/wallet-rpc-daemon/docs/RPC.md | 7 + 24 files changed, 723 insertions(+), 1430 deletions(-) create mode 100644 common/src/primitives_converters.rs delete mode 100644 common/tests/primitives_repo_consistency/utils/converters.rs delete mode 100644 wallet/src/signer/ledger_signer/speculos.rs delete mode 100644 wallet/src/signer/ledger_signer/tests.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c813a7718..6a1d4d58c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -296,21 +296,27 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the core repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 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: @@ -324,32 +330,37 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - model: [flex, stax, nanox, nanosplus] + model: [apex_p, flex, stax, nanox, nanosplus] steps: - name: Checkout the core repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive path: ./mintlayer-core + - name: Checkout mintlayer-ledger-app repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 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 'cargo ledger build ${{ matrix.model }}' + - name: Run Ledger emulator and execute tests run: | set -e diff --git a/common/Cargo.toml b/common/Cargo.toml index 61c73fbfe1..597a9fefd5 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -28,6 +28,7 @@ hex.workspace = true itertools.workspace = true lazy_static.workspace = true merkletree-mintlayer = { workspace = true, features = ["scale-codec"] } +ml_primitives.workspace = true num.workspace = true once_cell.workspace = true parity-scale-codec.workspace = true @@ -49,7 +50,6 @@ bitcoin-bech32.workspace = true ctor.workspace = true expect-test.workspace = true indoc.workspace = true -ml_primitives.workspace = true num-traits.workspace = true proptest.workspace = true rstest.workspace = true @@ -59,7 +59,7 @@ trezor-client.workspace = true [features] expensive-verification = [] -dev = [] # used by fixed-hash +dev = [] # used by fixed-hash [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] } diff --git a/common/src/chain/transaction/account_outpoint.rs b/common/src/chain/transaction/account_outpoint.rs index 9934402254..42698e0fb4 100644 --- a/common/src/chain/transaction/account_outpoint.rs +++ b/common/src/chain/transaction/account_outpoint.rs @@ -62,18 +62,18 @@ impl From<&AccountCommand> for AccountType { } impl OrderAccountCommand { - pub fn order_id(&self) -> OrderId { + pub fn order_id(&self) -> &OrderId { match self { OrderAccountCommand::FillOrder(order_id, _) | OrderAccountCommand::FreezeOrder(order_id) - | OrderAccountCommand::ConcludeOrder(order_id) => *order_id, + | OrderAccountCommand::ConcludeOrder(order_id) => order_id, } } } impl From for AccountType { fn from(cmd: OrderAccountCommand) -> Self { - AccountType::Order(cmd.order_id()) + AccountType::Order(*cmd.order_id()) } } diff --git a/common/src/lib.rs b/common/src/lib.rs index 1d2753714e..c617543393 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -16,6 +16,7 @@ pub mod address; pub mod chain; pub mod primitives; +pub mod primitives_converters; pub mod size_estimation; pub mod text_summary; pub mod time_getter; diff --git a/common/src/primitives_converters.rs b/common/src/primitives_converters.rs new file mode 100644 index 0000000000..bc7be3a0bf --- /dev/null +++ b/common/src/primitives_converters.rs @@ -0,0 +1,521 @@ +// 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 crate::{ + address::pubkeyhash::PublicKeyHash, + chain::{ + htlc::HashedTimelockContract, + output_value::OutputValue, + signature::sighash::input_commitments::SighashInputCommitment, + stakelock::StakePoolData, + timelock::OutputTimeLock, + tokens::{ + IsTokenFreezable, IsTokenUnfreezable, NftIssuance, TokenId, TokenIssuance, + TokenTotalSupply, + }, + AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, Destination, + GenBlock, OrderAccountCommand, OrderData, OrderId, OutPointSourceId, PoolId, Transaction, + TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{per_thousand::PerThousand, Amount, Id, H256}, +}; +use crypto::{ + key::{secp256k1::Secp256k1PublicKey, KeyKind, PublicKey}, + vrf::{SchnorrkelPublicKey, VRFKeyKind, VRFPublicKey}, +}; +use script::Script; + +#[derive(thiserror::Error, Debug, Eq, PartialEq)] +pub enum PrimitivesConvertersError { + #[error("Tokens V0 are not supported")] + UnsupportedTokenV0, +} + +// Custom "TryFrom" trait to work around the Rust's orphan rule. +pub trait TryConvertFrom: Sized { + fn try_convert_from(value: T) -> Result; +} + +// Custom "TryInto" trait to work around the Rust's orphan rule. +pub trait TryConvertInto { + fn try_convert_into(self) -> Result; +} + +impl TryConvertInto for From +where + To: TryConvertFrom, +{ + fn try_convert_into(self) -> Result { + >::try_convert_from(self) + } +} + +impl TryConvertFrom for ml_primitives::H256 { + fn try_convert_from(value: H256) -> Result { + Ok(Self(value.0)) + } +} + +impl TryConvertFrom for ml_primitives::AccountNonce { + fn try_convert_from(value: AccountNonce) -> Result { + Ok(Self(value.value())) + } +} + +impl TryConvertFrom for ml_primitives::OutPointSourceId { + fn try_convert_from(value: OutPointSourceId) -> Result { + match value { + OutPointSourceId::Transaction(tx_id) => { + Ok(Self::Transaction(tx_id.try_convert_into()?)) + } + OutPointSourceId::BlockReward(block_id) => { + Ok(Self::BlockReward(block_id.try_convert_into()?)) + } + } + } +} + +impl TryConvertFrom for ml_primitives::UtxoOutPoint { + fn try_convert_from(value: UtxoOutPoint) -> Result { + Ok(ml_primitives::UtxoOutPoint::new( + value.source_id().try_convert_into()?, + value.output_index(), + )) + } +} + +impl TryConvertFrom for ml_primitives::Amount { + fn try_convert_from(value: Amount) -> Result { + Ok(Self::from_atoms(value.into_atoms())) + } +} + +impl TryConvertFrom for ml_primitives::AccountSpending { + fn try_convert_from(value: AccountSpending) -> Result { + match value { + AccountSpending::DelegationBalance(delegation_id, amount) => { + Ok(Self::DelegationBalance( + delegation_id.try_convert_into()?, + amount.try_convert_into()?, + )) + } + } + } +} + +impl TryConvertFrom for ml_primitives::AccountOutPoint { + fn try_convert_from(value: AccountOutPoint) -> Result { + Ok(Self { + nonce: ml_primitives::AccountNonce(value.nonce().value()), + spending: value.account().clone().try_convert_into()?, + }) + } +} + +impl TryConvertFrom for ml_primitives::IsTokenUnfreezable { + fn try_convert_from(value: IsTokenUnfreezable) -> Result { + match value { + IsTokenUnfreezable::No => Ok(Self::No), + IsTokenUnfreezable::Yes => Ok(Self::Yes), + } + } +} + +impl TryConvertFrom for ml_primitives::PublicKey { + fn try_convert_from(value: PublicKey) -> Result { + match value.kind() { + KeyKind::Secp256k1Schnorr => { + let key: Secp256k1PublicKey = value.try_into().unwrap(); + Ok(ml_primitives::PublicKey::Secp256k1Schnorr( + ml_primitives::Secp256k1PublicKey(key.as_bytes()), + )) + } + } + } +} + +impl TryConvertFrom for ml_primitives::PublicKeyHash { + fn try_convert_from(value: PublicKeyHash) -> Result { + Ok(Self(value.0)) + } +} + +impl TryConvertFrom for ml_primitives::Destination { + fn try_convert_from(value: Destination) -> Result { + match value { + Destination::AnyoneCanSpend => Ok(Self::AnyoneCanSpend), + Destination::PublicKey(pk) => Ok(Self::PublicKey(pk.try_convert_into()?)), + Destination::ScriptHash(script_id) => { + Ok(Self::ScriptHash(script_id.try_convert_into()?)) + } + Destination::PublicKeyHash(pkh) => Ok(Self::PublicKeyHash(pkh.try_convert_into()?)), + Destination::ClassicMultisig(pkh) => Ok(Self::ClassicMultisig(pkh.try_convert_into()?)), + } + } +} + +impl TryConvertFrom for ml_primitives::AccountCommand { + fn try_convert_from(value: AccountCommand) -> Result { + match value { + AccountCommand::MintTokens(token_id, amount) => Ok(Self::MintTokens( + token_id.try_convert_into()?, + amount.try_convert_into()?, + )), + AccountCommand::UnmintTokens(token_id) => { + Ok(Self::UnmintTokens(token_id.try_convert_into()?)) + } + AccountCommand::LockTokenSupply(token_id) => { + Ok(Self::LockTokenSupply(token_id.try_convert_into()?)) + } + AccountCommand::FreezeToken(token_id, is_unfreezable) => Ok(Self::FreezeToken( + token_id.try_convert_into()?, + is_unfreezable.try_convert_into()?, + )), + AccountCommand::UnfreezeToken(token_id) => { + Ok(Self::UnfreezeToken(token_id.try_convert_into()?)) + } + AccountCommand::ChangeTokenAuthority(token_id, dest) => Ok(Self::ChangeTokenAuthority( + token_id.try_convert_into()?, + dest.try_convert_into()?, + )), + AccountCommand::ConcludeOrder(order_id) => { + Ok(Self::ConcludeOrder(order_id.try_convert_into()?)) + } + AccountCommand::FillOrder(order_id, amount, dest) => Ok(Self::FillOrder( + order_id.try_convert_into()?, + amount.try_convert_into()?, + dest.try_convert_into()?, + )), + AccountCommand::ChangeTokenMetadataUri(token_id, uri) => Ok( + Self::ChangeTokenMetadataUri(token_id.try_convert_into()?, uri), + ), + } + } +} + +impl TryConvertFrom for ml_primitives::OrderAccountCommand { + fn try_convert_from(value: OrderAccountCommand) -> Result { + match value { + OrderAccountCommand::FillOrder(order_id, amount) => Ok(Self::FillOrder( + order_id.try_convert_into()?, + amount.try_convert_into()?, + )), + OrderAccountCommand::FreezeOrder(order_id) => { + Ok(Self::FreezeOrder(order_id.try_convert_into()?)) + } + OrderAccountCommand::ConcludeOrder(order_id) => { + Ok(Self::ConcludeOrder(order_id.try_convert_into()?)) + } + } + } +} + +impl TryConvertFrom for ml_primitives::TxInput { + fn try_convert_from(value: TxInput) -> Result { + match value { + TxInput::Utxo(utxo) => Ok(Self::Utxo(utxo.try_convert_into()?)), + TxInput::Account(acc) => Ok(Self::Account(acc.try_convert_into()?)), + TxInput::AccountCommand(nonce, command) => Ok(Self::AccountCommand( + ml_primitives::AccountNonce(nonce.value()), + command.try_convert_into()?, + )), + TxInput::OrderAccountCommand(command) => { + Ok(Self::OrderAccountCommand(command.try_convert_into()?)) + } + } + } +} + +impl TryConvertFrom for ml_primitives::OutputValue { + fn try_convert_from(value: OutputValue) -> Result { + match value { + OutputValue::Coin(amount) => Ok(Self::Coin(amount.try_convert_into()?)), + // Replaced panic with Error + OutputValue::TokenV0(_) => Err(PrimitivesConvertersError::UnsupportedTokenV0), + OutputValue::TokenV1(token_id, amount) => Ok(Self::TokenV1( + token_id.try_convert_into()?, + amount.try_convert_into()?, + )), + } + } +} + +impl TryConvertFrom for ml_primitives::OutputTimeLock { + fn try_convert_from(value: OutputTimeLock) -> Result { + match value { + OutputTimeLock::UntilHeight(height) => Ok(Self::UntilHeight( + ml_primitives::BlockHeight(height.into_int()), + )), + OutputTimeLock::UntilTime(time) => Ok(Self::UntilTime(ml_primitives::BlockTimestamp( + ml_primitives::SecondsCount(time.as_int_seconds()), + ))), + OutputTimeLock::ForSeconds(secs) => { + Ok(Self::ForSeconds(ml_primitives::SecondsCount(secs))) + } + OutputTimeLock::ForBlockCount(blocks) => { + Ok(Self::ForBlockCount(ml_primitives::BlocksCount(blocks))) + } + } + } +} + +impl TryConvertFrom for ml_primitives::HashedTimelockContract { + fn try_convert_from(value: HashedTimelockContract) -> Result { + Ok(Self { + secret_hash: ml_primitives::HtlcSecretHash(value.secret_hash.0), + spend_key: value.spend_key.try_convert_into()?, + refund_timelock: value.refund_timelock.try_convert_into()?, + refund_key: value.refund_key.try_convert_into()?, + }) + } +} + +impl TryConvertFrom for ml_primitives::OrderData { + fn try_convert_from(value: OrderData) -> Result { + Ok(Self { + conclude_key: value.conclude_key().clone().try_convert_into()?, + ask: value.ask().clone().try_convert_into()?, + give: value.give().clone().try_convert_into()?, + }) + } +} + +impl TryConvertFrom for ml_primitives::VrfPublicKey { + fn try_convert_from(value: VRFPublicKey) -> Result { + match value.kind() { + VRFKeyKind::Schnorrkel => { + let key: SchnorrkelPublicKey = value.try_into().unwrap(); + + Ok(ml_primitives::VrfPublicKey::Schnorrkel( + ml_primitives::SchnorrkelPublicKey(key.as_bytes()), + )) + } + } + } +} + +impl TryConvertFrom for ml_primitives::StakePoolData { + fn try_convert_from(value: StakePoolData) -> Result { + Ok(Self { + pledge: value.pledge().try_convert_into()?, + staker: value.staker().clone().try_convert_into()?, + vrf_public_key: value.vrf_public_key().clone().try_convert_into()?, + decommission_key: value.decommission_key().clone().try_convert_into()?, + margin_ratio_per_thousand: ml_primitives::PerThousand( + value.margin_ratio_per_thousand().value(), + ), + cost_per_block: value.cost_per_block().try_convert_into()?, + }) + } +} + +impl TryConvertFrom for ml_primitives::NftIssuance { + fn try_convert_from(value: NftIssuance) -> Result { + match value { + NftIssuance::V0(data) => Ok(Self::V0(ml_primitives::NftIssuanceV0 { + creator: data + .metadata + .creator + .map(|c| c.public_key.try_convert_into()) + .transpose()?, + name: data.metadata.name, + description: data.metadata.description, + ticker: data.metadata.ticker, + icon_uri: data.metadata.icon_uri.as_ref().clone().map_or(Vec::new(), Into::into), + additional_metadata_uri: data + .metadata + .additional_metadata_uri + .as_ref() + .clone() + .map_or(Vec::new(), Into::into), + media_uri: data.metadata.media_uri.as_ref().clone().map_or(Vec::new(), Into::into), + media_hash: data.metadata.media_hash, + })), + } + } +} + +impl TryConvertFrom for ml_primitives::TokenTotalSupply { + fn try_convert_from(value: TokenTotalSupply) -> Result { + match value { + TokenTotalSupply::Lockable => Ok(Self::Lockable), + TokenTotalSupply::Unlimited => Ok(Self::Unlimited), + TokenTotalSupply::Fixed(amount) => Ok(Self::Fixed(amount.try_convert_into()?)), + } + } +} + +impl TryConvertFrom for ml_primitives::IsTokenFreezable { + fn try_convert_from(value: IsTokenFreezable) -> Result { + match value { + IsTokenFreezable::No => Ok(Self::No), + IsTokenFreezable::Yes => Ok(Self::Yes), + } + } +} + +impl TryConvertFrom for ml_primitives::TokenIssuance { + fn try_convert_from(value: TokenIssuance) -> Result { + match value { + TokenIssuance::V1(data) => Ok(Self::V1(ml_primitives::TokenIssuanceV1 { + token_ticker: data.token_ticker, + number_of_decimals: data.number_of_decimals, + metadata_uri: data.metadata_uri, + total_supply: data.total_supply.try_convert_into()?, + authority: data.authority.try_convert_into()?, + is_freezable: data.is_freezable.try_convert_into()?, + })), + } + } +} + +impl TryConvertFrom for ml_primitives::TxOutput { + fn try_convert_from(value: TxOutput) -> Result { + match value { + TxOutput::Transfer(value, dest) => Ok(Self::Transfer( + value.try_convert_into()?, + dest.try_convert_into()?, + )), + TxOutput::LockThenTransfer(value, dest, lock) => Ok(Self::LockThenTransfer( + value.try_convert_into()?, + dest.try_convert_into()?, + lock.try_convert_into()?, + )), + TxOutput::Burn(amount) => Ok(Self::Burn(amount.try_convert_into()?)), + TxOutput::DataDeposit(data) => Ok(Self::DataDeposit(data)), + TxOutput::CreateDelegationId(dest, pool_id) => Ok(Self::CreateDelegationId( + dest.try_convert_into()?, + pool_id.try_convert_into()?, + )), + TxOutput::DelegateStaking(amount, delegation_id) => Ok(Self::DelegateStaking( + amount.try_convert_into()?, + delegation_id.try_convert_into()?, + )), + TxOutput::ProduceBlockFromStake(dest, pool_id) => Ok(Self::ProduceBlockFromStake( + dest.try_convert_into()?, + pool_id.try_convert_into()?, + )), + TxOutput::Htlc(value, lock) => Ok(Self::Htlc( + value.try_convert_into()?, + (*lock).try_convert_into()?, + )), + TxOutput::CreateOrder(data) => Ok(Self::CreateOrder((*data).try_convert_into()?)), + TxOutput::CreateStakePool(pool_id, data) => Ok(Self::CreateStakePool( + pool_id.try_convert_into()?, + (*data).try_convert_into()?, + )), + TxOutput::IssueNft(token_id, data, dest) => Ok(Self::IssueNft( + token_id.try_convert_into()?, + (*data).try_convert_into()?, + dest.try_convert_into()?, + )), + TxOutput::IssueFungibleToken(data) => { + Ok(Self::IssueFungibleToken((*data).try_convert_into()?)) + } + } + } +} + +impl TryConvertFrom> for ml_primitives::SighashInputCommitment { + fn try_convert_from(value: SighashInputCommitment) -> Result { + type ChainComm<'a> = SighashInputCommitment<'a>; + + match value { + ChainComm::None => Ok(ml_primitives::SighashInputCommitment::None), + ChainComm::Utxo(utxo) => Ok(ml_primitives::SighashInputCommitment::Utxo( + utxo.into_owned().try_convert_into()?, + )), + ChainComm::ProduceBlockFromStakeUtxo { + utxo, + staker_balance, + } => Ok( + ml_primitives::SighashInputCommitment::ProduceBlockFromStakeUtxo { + utxo: utxo.into_owned().try_convert_into()?, + staker_balance: staker_balance.try_convert_into()?, + }, + ), + ChainComm::FillOrderAccountCommand { + initially_asked, + initially_given, + } => Ok( + ml_primitives::SighashInputCommitment::FillOrderAccountCommand { + initially_asked: initially_asked.try_convert_into()?, + initially_given: initially_given.try_convert_into()?, + }, + ), + ChainComm::ConcludeOrderAccountCommand { + initially_asked, + initially_given, + ask_balance, + give_balance, + } => Ok( + ml_primitives::SighashInputCommitment::ConcludeOrderAccountCommand { + initially_asked: initially_asked.try_convert_into()?, + initially_given: initially_given.try_convert_into()?, + ask_balance: ask_balance.try_convert_into()?, + give_balance: give_balance.try_convert_into()?, + }, + ), + } + } +} + +impl TryConvertFrom> for ml_primitives::GenBlockId { + fn try_convert_from(value: Id) -> Result { + Ok(Self::new(value.to_hash().try_convert_into()?)) + } +} + +impl TryConvertFrom> for ml_primitives::TransactionId { + fn try_convert_from(value: Id) -> Result { + Ok(Self::new(value.to_hash().try_convert_into()?)) + } +} + +impl TryConvertFrom for ml_primitives::OrderId { + fn try_convert_from(value: OrderId) -> Result { + Ok(Self::new(value.to_hash().try_convert_into()?)) + } +} + +impl TryConvertFrom for ml_primitives::PoolId { + fn try_convert_from(value: PoolId) -> Result { + Ok(Self::new(value.to_hash().try_convert_into()?)) + } +} + +impl TryConvertFrom for ml_primitives::DelegationId { + fn try_convert_from(value: DelegationId) -> Result { + Ok(Self::new(value.to_hash().try_convert_into()?)) + } +} + +impl TryConvertFrom for ml_primitives::TokenId { + fn try_convert_from(value: TokenId) -> Result { + Ok(Self::new(value.to_hash().try_convert_into()?)) + } +} + +impl TryConvertFrom> for ml_primitives::ScriptId { + fn try_convert_from(value: Id