diff --git a/.github/workflows/bindings-ts.yml b/.github/workflows/bindings-ts.yml index cda8c4a5c..0d2e29048 100644 --- a/.github/workflows/bindings-ts.yml +++ b/.github/workflows/bindings-ts.yml @@ -1,9 +1,9 @@ name: bindings typescript on: - push: - branches: [main, release/**] - pull_request: + push: + branches: [main, release/**] + pull_request: jobs: test: @@ -38,9 +38,10 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - run: rustup update - - run: sudo apt update && sudo apt install -y libdbus-1-dev + - run: sudo apt update && sudo apt install -y libdbus-1-dev libudev-dev - run: cargo build - run: rustup target add wasm32-unknown-unknown - run: make build-test-wasms - run: npm ci && npm run test working-directory: cmd/crates/soroban-spec-typescript/ts-tests + diff --git a/.github/workflows/ledger-emulator.yml b/.github/workflows/ledger-emulator.yml index 251631af7..ec32b861d 100644 --- a/.github/workflows/ledger-emulator.yml +++ b/.github/workflows/ledger-emulator.yml @@ -1,28 +1,31 @@ name: Ledger Emulator Tests on: - push: - branches: [main, release/**] - pull_request: + push: + branches: [main, release/**] + pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || github.ref }} + cancel-in-progress: true defaults: - run: - shell: bash + run: + shell: bash jobs: - emulator-tests: - runs-on: ubuntu-latest - env: - CI_TESTS: true - steps: - - uses: actions/checkout@v4 - - uses: stellar/actions/rust-cache@main - - name: install libudev-dev - run: | - sudo apt update && sudo apt install -y libudev-dev - - run: | - cargo test --manifest-path cmd/crates/stellar-ledger/Cargo.toml --features "emulator-tests" -- --nocapture \ No newline at end of file + emulator-tests: + runs-on: ubuntu-latest + env: + CI_TESTS: true + steps: + - uses: actions/checkout@v4 + - uses: stellar/actions/rust-cache@main + - name: install libudev-dev & libdbus-1-dev + run: | + sudo apt update && sudo apt install -y libudev-dev libdbus-1-dev + - run: | + cargo test --manifest-path cmd/crates/stellar-ledger/Cargo.toml --features "emulator-tests" -- --nocapture + - run: cargo build --features emulator-tests + - run: | + cargo test --features emulator-tests --package soroban-test --test it -- emulator diff --git a/.github/workflows/rpc-tests.yml b/.github/workflows/rpc-tests.yml index 61f8725a5..7f5a38d99 100644 --- a/.github/workflows/rpc-tests.yml +++ b/.github/workflows/rpc-tests.yml @@ -1,12 +1,12 @@ name: RPC Tests on: - push: - branches: [main, release/**] - pull_request: + push: + branches: [main, release/**] + pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref_protected == 'true' && github.sha || github.ref }} + cancel-in-progress: true jobs: test: @@ -39,7 +39,7 @@ jobs: target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - run: rustup update - - run: sudo apt update && sudo apt install -y libdbus-1-dev + - run: sudo apt update && sudo apt install -y libudev-dev libdbus-1-dev - run: cargo build - run: rustup target add wasm32-unknown-unknown - run: make build-test-wasms diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 012ae4c40..537ae82a4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v4 - uses: stellar/actions/rust-cache@main - run: rustup update - - run: sudo apt update && sudo apt install -y libdbus-1-dev + - run: sudo apt update && sudo apt install -y libudev-dev libdbus-1-dev - run: make generate-full-help-doc - run: git add -N . && git diff HEAD --exit-code diff --git a/.gitignore b/.gitignore index b7150ae5d..578b28adf 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ test_snapshots .idea local.sh .stellar +.zed diff --git a/Cargo.lock b/Cargo.lock index 9a596ab82..1aa1ddaa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,8 +345,8 @@ checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.0", - "futures-lite 2.3.0", + "fastrand", + "futures-lite", "slab", ] @@ -358,61 +358,32 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io 2.3.3", - "async-lock 3.4.0", + "async-io", + "async-lock", "blocking", - "futures-lite 2.3.0", + "futures-lite", "once_cell", ] -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - [[package]] name = "async-io" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" dependencies = [ - "async-lock 3.4.0", + "async-lock", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "parking", - "polling 3.7.2", - "rustix 0.38.34", + "polling", + "rustix", "slab", "tracing", "windows-sys 0.52.0", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.4.0" @@ -435,19 +406,21 @@ dependencies = [ [[package]] name = "async-process" -version = "1.8.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", + "async-channel 2.3.1", + "async-io", + "async-lock", "async-signal", + "async-task", "blocking", "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.34", - "windows-sys 0.48.0", + "event-listener 5.3.1", + "futures-lite", + "rustix", + "tracing", ] [[package]] @@ -456,13 +429,13 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32" dependencies = [ - "async-io 2.3.3", - "async-lock 3.4.0", + "async-io", + "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.34", + "rustix", "signal-hook-registry", "slab", "windows-sys 0.52.0", @@ -470,21 +443,21 @@ dependencies = [ [[package]] name = "async-std" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" dependencies = [ "async-attributes", "async-channel 1.9.0", "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", + "async-io", + "async-lock", "async-process", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 1.13.0", + "futures-lite", "gloo-timers", "kv-log-macro", "log", @@ -611,12 +584,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.6.0" @@ -650,7 +617,7 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "piper", ] @@ -1524,17 +1491,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "event-listener" version = "5.3.1" @@ -1556,15 +1512,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.1.0" @@ -1702,28 +1649,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - [[package]] name = "futures-lite" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.1.0", + "fastrand", "futures-core", "futures-io", "parking", @@ -1826,16 +1758,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.6.0", + "bitflags", "ignore", "walkdir", ] [[package]] name = "gloo-timers" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", @@ -2176,7 +2108,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -2302,7 +2234,7 @@ dependencies = [ "http-body 1.0.1", "hyper 1.4.1", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", "tower", "tower-service", @@ -2556,33 +2488,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.7", + "socket2", "widestring", "windows-sys 0.48.0", "winreg", @@ -2869,7 +2781,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags", "libc", ] @@ -2888,12 +2800,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -3182,7 +3088,7 @@ version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ - "bitflags 2.6.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -3431,7 +3337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand", "futures-io", ] @@ -3451,22 +3357,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - [[package]] name = "polling" version = "3.7.2" @@ -3477,7 +3367,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.34", + "rustix", "tracing", "windows-sys 0.52.0", ] @@ -3589,7 +3479,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.0.0", "rustls 0.23.12", - "socket2 0.5.7", + "socket2", "thiserror", "tokio", "tracing", @@ -3620,7 +3510,7 @@ checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" dependencies = [ "libc", "once_cell", - "socket2 0.5.7", + "socket2", "tracing", "windows-sys 0.52.0", ] @@ -3670,7 +3560,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -3898,30 +3788,16 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.37.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.6.0", + "bitflags", "errno", "libc", - "linux-raw-sys 0.4.14", + "linux-raw-sys", "windows-sys 0.52.0", ] @@ -4072,9 +3948,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.3.3" +version = "2.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1" +checksum = "06ff467073ddaff34c3a39e5b454f25dd982484a26fff50254ca793c56a1b714" dependencies = [ "sdd", ] @@ -4136,9 +4012,9 @@ dependencies = [ [[package]] name = "sdd" -version = "3.0.7" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b07779b9b918cc05650cb30f404d4d7835d26df37c235eded8a6832e2fb82cca" +checksum = "177258b64c0faaa9ffd3c65cd3262c2bc7e2588dbbd9c1641d0346145c1bbda8" [[package]] name = "sec1" @@ -4159,7 +4035,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -4241,9 +4117,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.137" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -4482,16 +4358,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.7" @@ -4576,6 +4442,7 @@ dependencies = [ "soroban-spec-rust", "soroban-spec-tools", "soroban-spec-typescript", + "stellar-ledger", "stellar-rpc-client", "stellar-strkey 0.0.11", "stellar-xdr", @@ -4844,8 +4711,11 @@ dependencies = [ "soroban-ledger-snapshot", "soroban-spec", "soroban-spec-tools", + "stellar-ledger", "stellar-rpc-client", "stellar-strkey 0.0.11", + "test-case", + "testcontainers", "thiserror", "tokio", "toml", @@ -4926,7 +4796,6 @@ dependencies = [ "byteorder 1.5.0", "ed25519-dalek", "env_logger", - "futures", "hex", "home", "httpmock", @@ -4937,15 +4806,12 @@ dependencies = [ "phf", "pretty_assertions", "reqwest", - "sep5", "serde", "serde_derive", "serde_json", "serial_test", "sha2 0.10.8", "slipped10", - "soroban-spec", - "stellar-rpc-client", "stellar-strkey 0.0.11", "stellar-xdr", "test-case", @@ -5166,7 +5032,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.6.0", + "bitflags", "core-foundation", "system-configuration-sys", ] @@ -5194,9 +5060,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", - "fastrand 2.1.0", + "fastrand", "once_cell", - "rustix 0.38.34", + "rustix", "windows-sys 0.59.0", ] @@ -5481,7 +5347,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2", "tokio-macros", "windows-sys 0.52.0", ] @@ -5826,12 +5692,6 @@ dependencies = [ "libc", ] -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -5981,9 +5841,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -6068,7 +5928,7 @@ dependencies = [ "home", "once_cell", "regex", - "rustix 0.38.34", + "rustix", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8f1a64a62..353b3d91e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,10 @@ version = "=22.0.4" package = "stellar-rpc-client" version = "=22.0.0" +[workspace.dependencies.stellar-ledger] +version = "=22.2.0" +path = "cmd/crates/stellar-ledger" + # Dependencies from elsewhere shared by crates: [workspace.dependencies] stellar-strkey = "0.0.11" @@ -105,7 +109,9 @@ walkdir = "2.5.0" toml_edit = "0.22.20" toml = "0.8.19" reqwest = "0.12.7" +# testing predicates = "3.1.2" +testcontainers = { version = "0.20.1" } httpmock = "0.7.0" [profile.test-wasms] diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 09f0bb89d..5d583192d 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -2170,6 +2170,7 @@ Sign a transaction envelope appending the signature to the envelope * `--sign-with-key ` — Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path * `--hd-path ` — If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--sign-with-lab` — Sign with https://lab.stellar.org +* `--sign-with-ledger` — Sign with a ledger wallet * `--rpc-url ` — RPC server endpoint * `--rpc-header ` — RPC Header(s) to include in requests to the RPC provider * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index e755c5d36..3a7524524 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -24,6 +24,7 @@ stellar-strkey = { workspace = true } sep5 = { workspace = true } soroban-cli = { workspace = true } soroban-rpc = { workspace = true } +stellar-ledger = { workspace = true } thiserror = "1.0.31" sha2 = "0.10.6" @@ -32,6 +33,7 @@ assert_fs = "1.0.7" predicates = { workspace = true } fs_extra = "1.3.0" toml = { workspace = true } +testcontainers = { workspace = true } home = "0.5.9" [dev-dependencies] @@ -42,9 +44,11 @@ walkdir = "2.4.0" ulid.workspace = true ed25519-dalek = { workspace = true } hex = { workspace = true } +test-case = "3.3.1" tracing = "0.1.40" tracing-subscriber = "0.3.18" httpmock = { workspace = true } [features] it = [] +emulator-tests = ["stellar-ledger/emulator-tests"] diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index b53557d76..afdc6f24a 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -36,6 +36,7 @@ use soroban_cli::{ }; mod wasm; + pub use wasm::Wasm; pub const TEST_ACCOUNT: &str = "test"; @@ -299,9 +300,10 @@ impl TestEnv { } /// Returns the public key corresponding to the test keys's `hd_path` - pub fn test_address(&self, hd_path: usize) -> String { + pub async fn test_address(&self, hd_path: usize) -> String { self.cmd::(&format!("--hd-path={hd_path}")) .public_key() + .await .unwrap() .to_string() } @@ -330,6 +332,21 @@ impl TestEnv { pub fn client(&self) -> soroban_rpc::Client { self.network.rpc_client().unwrap() } + + #[cfg(feature = "emulator-tests")] + pub async fn speculos_container( + ledger_device_model: &str, + ) -> testcontainers::ContainerAsync + { + use stellar_ledger::emulator_test_support::{ + enable_hash_signing, get_container, wait_for_emulator_start_text, + }; + let container = get_container(ledger_device_model).await; + let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); + wait_for_emulator_start_text(ui_host_port).await; + enable_hash_signing(ui_host_port).await; + container + } } pub fn temp_ledger_file() -> OsString { diff --git a/cmd/crates/soroban-test/tests/it/emulator.rs b/cmd/crates/soroban-test/tests/it/emulator.rs new file mode 100644 index 000000000..b959c541c --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/emulator.rs @@ -0,0 +1,90 @@ +use stellar_ledger::Blob; + +use soroban_test::{AssertExt, TestEnv}; +use std::sync::Arc; + +use stellar_ledger::emulator_test_support::*; + +use soroban_cli::{ + tx::builder::TxExt, + xdr::{self, Limits, OperationBody, ReadXdr, TransactionEnvelope, WriteXdr}, +}; + +use test_case::test_case; + +#[test_case("nanos", 0; "when the device is NanoS")] +#[test_case("nanox", 1; "when the device is NanoX")] +#[test_case("nanosp", 2; "when the device is NanoS Plus")] +#[tokio::test] +async fn test_signer(ledger_device_model: &str, hd_path: u32) { + let sandbox = Arc::new(TestEnv::new()); + let container = TestEnv::speculos_container(ledger_device_model).await; + let host_port = container.get_host_port_ipv4(9998).await.unwrap(); + let ui_host_port = container.get_host_port_ipv4(5000).await.unwrap(); + + let ledger = ledger(host_port).await; + + let key = ledger.get_public_key(&hd_path.into()).await.unwrap(); + + let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&key.0).unwrap(); + let body: OperationBody = + (&soroban_cli::commands::tx::new::bump_sequence::Args { bump_to: 100 }).into(); + let operation = xdr::Operation { + body, + source_account: None, + }; + let source_account = xdr::MuxedAccount::Ed25519(key.0.into()); + let tx_env: TransactionEnvelope = + xdr::Transaction::new_tx(source_account, 100, 100, operation).into(); + let tx_env = tx_env.to_xdr_base64(Limits::none()).unwrap(); + + let hash: xdr::Hash = sandbox + .new_assert_cmd("tx") + .arg("hash") + .write_stdin(tx_env.as_bytes()) + .assert() + .success() + .stdout_as_str() + .parse() + .unwrap(); + + let sign = tokio::task::spawn_blocking({ + let sandbox = Arc::clone(&sandbox); + + move || { + sandbox + .new_assert_cmd("tx") + .arg("sign") + .arg("--sign-with-ledger") + .arg("--hd-path") + .arg(hd_path.to_string()) + .write_stdin(tx_env.as_bytes()) + .env("SPECULOS_PORT", host_port.to_string()) + .env("RUST_LOGS", "trace") + .assert() + .success() + .stdout_as_str() + } + }); + + let approve = tokio::task::spawn(approve_tx_hash_signature( + ui_host_port, + ledger_device_model.to_string(), + )); + + let response = sign.await.unwrap(); + approve.await.unwrap(); + let txn_env = + xdr::TransactionEnvelope::from_xdr_base64(&response, xdr::Limits::none()).unwrap(); + let xdr::TransactionEnvelope::Tx(tx_env) = txn_env else { + panic!("expected Tx") + }; + let signatures = tx_env.signatures.to_vec(); + let signature = signatures[0].signature.to_vec(); + verifying_key + .verify_strict( + &hash.0, + &ed25519_dalek::Signature::from_slice(&signature).unwrap(), + ) + .unwrap(); +} diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 3fa85bc09..e4cb17779 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -2,20 +2,37 @@ use soroban_cli::assembled::simulate_and_assemble_transaction; use soroban_cli::xdr::{Limits, ReadXdr, TransactionEnvelope, WriteXdr}; use soroban_test::{AssertExt, TestEnv}; -use crate::integration::util::{deploy_contract, DeployKind, HELLO_WORLD}; +use crate::integration::util::{deploy_contract, DeployKind, DeployOptions, HELLO_WORLD}; pub mod operations; #[tokio::test] async fn simulate() { let sandbox = &TestEnv::new(); - let xdr_base64_build_only = - deploy_contract(sandbox, HELLO_WORLD, DeployKind::BuildOnly, None).await; - let xdr_base64_sim_only = - deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly, None).await; + let salt = Some(String::from("A")); + let xdr_base64_build_only = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + kind: DeployKind::BuildOnly, + salt: salt.clone(), + ..Default::default() + }, + ) + .await; + let xdr_base64_sim_only = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + kind: DeployKind::SimOnly, + salt: salt.clone(), + ..Default::default() + }, + ) + .await; let tx_env = TransactionEnvelope::from_xdr_base64(&xdr_base64_build_only, Limits::none()).unwrap(); - let tx = soroban_cli::commands::tx::xdr::unwrap_envelope_v1(tx_env).unwrap(); + let tx = soroban_cli::commands::tx::xdr::unwrap_envelope_v1(tx_env.clone()).unwrap(); let assembled_str = sandbox .new_assert_cmd("tx") .arg("simulate") @@ -23,6 +40,11 @@ async fn simulate() { .assert() .success() .stdout_as_str(); + let tx_env_from_cli_tx = + TransactionEnvelope::from_xdr_base64(&assembled_str, Limits::none()).unwrap(); + let tx_env_sim_only = + TransactionEnvelope::from_xdr_base64(&xdr_base64_sim_only, Limits::none()).unwrap(); + assert_eq!(tx_env_from_cli_tx, tx_env_sim_only); assert_eq!(xdr_base64_sim_only, assembled_str); let assembled = simulate_and_assemble_transaction(&sandbox.client(), &tx) .await @@ -56,20 +78,37 @@ async fn txn_hash() { #[tokio::test] async fn build_simulate_sign_send() { let sandbox = &TestEnv::new(); + build_sim_sign_send(sandbox, "test", "--sign-with-key=test").await; +} + +pub(crate) async fn build_sim_sign_send(sandbox: &TestEnv, account: &str, sign_with: &str) { sandbox .new_assert_cmd("contract") .arg("install") - .args(["--wasm", HELLO_WORLD.path().as_os_str().to_str().unwrap()]) + .args([ + "--wasm", + HELLO_WORLD.path().as_os_str().to_str().unwrap(), + "--source", + account, + ]) .assert() .success(); - let tx_simulated = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly, None).await; + let tx_simulated = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + kind: DeployKind::SimOnly, + ..Default::default() + }, + ) + .await; dbg!("{tx_simulated}"); let tx_signed = sandbox .new_assert_cmd("tx") .arg("sign") - .arg("--sign-with-key=test") + .arg(sign_with) .write_stdin(tx_simulated.as_bytes()) .assert() .success() diff --git a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs index 82d2eef15..880c8b1cf 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs @@ -9,7 +9,7 @@ use soroban_test::{AssertExt, TestEnv}; use crate::integration::{ hello_world::invoke_hello_world, - util::{deploy_contract, DeployKind, HELLO_WORLD}, + util::{deploy_contract, DeployOptions, HELLO_WORLD}, }; pub fn test_address(sandbox: &TestEnv) -> String { @@ -85,7 +85,15 @@ async fn create_account() { .success(); let test_account_after = client.get_account(&test).await.unwrap(); assert!(test_account_after.balance < test_account.balance); - let id = deploy_contract(sandbox, HELLO_WORLD, DeployKind::Normal, Some("new")).await; + let id = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + deployer: Some("new".to_string()), + ..Default::default() + }, + ) + .await; println!("{id}"); invoke_hello_world(sandbox, &id); } @@ -117,7 +125,15 @@ async fn create_account_with_alias() { .success(); let test_account_after = client.get_account(&test).await.unwrap(); assert!(test_account_after.balance < test_account.balance); - let id = deploy_contract(sandbox, HELLO_WORLD, DeployKind::Normal, Some("new")).await; + let id = deploy_contract( + sandbox, + HELLO_WORLD, + DeployOptions { + deployer: Some("new".to_string()), + ..Default::default() + }, + ) + .await; println!("{id}"); invoke_hello_world(sandbox, &id); } diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index fc7f824b6..554479184 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -27,10 +27,10 @@ where assert_eq!(res, data); } -pub const TEST_SALT: &str = "f55ff16f66f43360266b95db6f8fec01d76031054306ae4a4b380598f6cfd114"; - +#[derive(Default)] pub enum DeployKind { BuildOnly, + #[default] Normal, SimOnly, } @@ -46,45 +46,55 @@ impl Display for DeployKind { } pub async fn deploy_hello(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, HELLO_WORLD, DeployKind::Normal, None).await + deploy_contract(sandbox, HELLO_WORLD, DeployOptions::default()).await } pub async fn deploy_custom(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, CUSTOM_TYPES, DeployKind::Normal, None).await + deploy_contract(sandbox, CUSTOM_TYPES, DeployOptions::default()).await } pub async fn deploy_swap(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, SWAP, DeployKind::Normal, None).await + deploy_contract(sandbox, SWAP, DeployOptions::default()).await } pub async fn deploy_custom_account(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, CUSTOM_ACCOUNT, DeployKind::Normal, None).await + deploy_contract(sandbox, CUSTOM_ACCOUNT, DeployOptions::default()).await +} + +#[derive(Default)] +pub struct DeployOptions { + pub kind: DeployKind, + pub deployer: Option, + pub salt: Option, } pub async fn deploy_contract( sandbox: &TestEnv, wasm: &Wasm<'static>, - deploy: DeployKind, - deployer: Option<&str>, + DeployOptions { + kind, + deployer, + salt, + }: DeployOptions, ) -> String { - let cmd = sandbox.cmd_with_config::<_, commands::contract::deploy::wasm::Cmd>( + let mut cmd = sandbox.cmd_with_config::<_, commands::contract::deploy::wasm::Cmd>( &[ "--fee", "1000000", "--wasm", &wasm.path().to_string_lossy(), - "--salt", - TEST_SALT, "--ignore-checks", - &deploy.to_string(), + &kind.to_string(), ], None, ); + cmd.salt = salt; + let res = sandbox - .run_cmd_with(cmd, deployer.unwrap_or("test")) + .run_cmd_with(cmd, deployer.as_deref().unwrap_or("test")) .await .unwrap(); - match deploy { + match kind { DeployKind::BuildOnly | DeployKind::SimOnly => match res.to_envelope() { commands::txn_result::TxnEnvelopeResult::TxnEnvelope(e) => { return e.to_xdr_base64(Limits::none()).unwrap() diff --git a/cmd/crates/soroban-test/tests/it/main.rs b/cmd/crates/soroban-test/tests/it/main.rs index 5f35d84d6..a16d2311b 100644 --- a/cmd/crates/soroban-test/tests/it/main.rs +++ b/cmd/crates/soroban-test/tests/it/main.rs @@ -1,9 +1,11 @@ mod arg_parsing; mod build; mod config; +#[cfg(feature = "emulator-tests")] +mod emulator; mod help; mod init; -// #[cfg(feature = "it")] +#[cfg(feature = "it")] mod integration; mod log; mod plugin; diff --git a/cmd/crates/stellar-ledger/Cargo.toml b/cmd/crates/stellar-ledger/Cargo.toml index f06d17a2b..190b0373c 100644 --- a/cmd/crates/stellar-ledger/Cargo.toml +++ b/cmd/crates/stellar-ledger/Cargo.toml @@ -16,7 +16,6 @@ rust-version.workspace = true publish = false [dependencies] -soroban-spec = { workspace = true } thiserror = "1.0.32" serde = "1.0.82" serde_derive = "1.0.82" @@ -26,7 +25,6 @@ ed25519-dalek = { workspace = true } stellar-strkey = { workspace = true } ledger-transport-hid = "0.10.0" ledger-transport = "0.10.0" -sep5.workspace = true slip10 = { package = "slipped10", version = "0.4.6" } tracing = { workspace = true } hex.workspace = true @@ -35,10 +33,9 @@ bollard = { workspace = true } home = "0.5.9" tokio = { version = "1", features = ["full"] } reqwest = { workspace = true, features = ["json"] } -soroban-rpc.workspace = true -phf = { version = "0.11.2", features = ["macros"] } -futures = "0.3.30" +phf = { version = "0.11.2", features = ["macros"], optional = true } async-trait = { workspace = true } +testcontainers = { workspace = true, optional = true } [dependencies.stellar-xdr] workspace = true @@ -46,15 +43,16 @@ features = ["curr", "std", "serde"] [dev-dependencies] env_logger = "0.11.3" -futures = "0.3.30" log = "0.4.21" once_cell = "1.19.0" pretty_assertions = "1.2.1" serial_test = "3.0.0" httpmock = { workspace = true } test-case = "3.3.1" -testcontainers = "0.20.1" + [features] -emulator-tests = [] +default = ["http-transport"] +emulator-tests = ["dep:testcontainers", "http-transport", "dep:phf"] +http-transport = [] diff --git a/cmd/crates/stellar-ledger/src/emulator_test_support.rs b/cmd/crates/stellar-ledger/src/emulator_test_support.rs new file mode 100644 index 000000000..e69ec128f --- /dev/null +++ b/cmd/crates/stellar-ledger/src/emulator_test_support.rs @@ -0,0 +1,7 @@ +pub mod http_transport; +#[cfg(feature = "emulator-tests")] +pub mod speculos; +#[cfg(feature = "emulator-tests")] +pub mod util; +#[cfg(feature = "emulator-tests")] +pub use util::*; diff --git a/cmd/crates/stellar-ledger/tests/utils/emulator_http_transport.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs similarity index 95% rename from cmd/crates/stellar-ledger/tests/utils/emulator_http_transport.rs rename to cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs index c90c28d92..48ab4423d 100644 --- a/cmd/crates/stellar-ledger/tests/utils/emulator_http_transport.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/http_transport.rs @@ -23,7 +23,7 @@ pub enum LedgerZemuError { InnerError, } -pub struct EmulatorHttpTransport { +pub struct Emulator { url: String, } @@ -39,8 +39,9 @@ struct ZemuResponse { error: Option, } -impl EmulatorHttpTransport { +impl Emulator { #[allow(dead_code)] //this is being used in tests only + #[must_use] pub fn new(host: &str, port: u16) -> Self { Self { url: format!("http://{host}:{port}"), @@ -49,7 +50,7 @@ impl EmulatorHttpTransport { } #[async_trait] -impl Exchange for EmulatorHttpTransport { +impl Exchange for Emulator { type Error = LedgerZemuError; type AnswerType = Vec; @@ -72,7 +73,7 @@ impl Exchange for EmulatorHttpTransport { let resp: Response = HttpClient::new() .post(&self.url) .headers(headers) - .timeout(Duration::from_secs(25)) + .timeout(Duration::from_secs(60)) .json(&request) .send() .await diff --git a/cmd/crates/stellar-ledger/tests/utils/speculos.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs similarity index 59% rename from cmd/crates/stellar-ledger/tests/utils/speculos.rs rename to cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs index 57439b0af..f8b95c13a 100644 --- a/cmd/crates/stellar-ledger/tests/utils/speculos.rs +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/speculos.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::HashMap, path::PathBuf}; +use std::{borrow::Cow, collections::HashMap, path::PathBuf, str::FromStr}; use testcontainers::{ core::{Mount, WaitFor}, Image, @@ -55,14 +55,54 @@ impl Speculos { } fn get_cmd(ledger_device_model: String) -> String { - let device_model = ledger_device_model.clone(); - let container_elf_path = match device_model.as_str() { - "nanos" => format!("{DEFAULT_APP_PATH}/stellarNanoSApp.elf"), - "nanosp" => format!("{DEFAULT_APP_PATH}/stellarNanoSPApp.elf"), - "nanox" => format!("{DEFAULT_APP_PATH}/stellarNanoXApp.elf"), - _ => panic!("Unsupported device model"), - }; - format!("/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN --display headless -s {TEST_SEED_PHRASE} -m {device_model} {container_elf_path}") + let device_model: DeviceModel = ledger_device_model.parse().unwrap(); + let container_elf_path = format!("{DEFAULT_APP_PATH}/{}", device_model.as_file()); + format!( + "/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN \ + --display headless \ + -s {TEST_SEED_PHRASE} \ + -m {device_model} {container_elf_path}" + ) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum DeviceModel { + NanoS, + NanoSP, + NanoX, +} + +impl FromStr for DeviceModel { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "nanos" => Ok(DeviceModel::NanoS), + "nanosp" => Ok(DeviceModel::NanoSP), + "nanox" => Ok(DeviceModel::NanoX), + _ => Err(format!("Unsupported device model: {}", s)), + } + } +} + +impl std::fmt::Display for DeviceModel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + DeviceModel::NanoS => write!(f, "nanos"), + DeviceModel::NanoSP => write!(f, "nanosp"), + DeviceModel::NanoX => write!(f, "nanox"), + } + } +} + +impl DeviceModel { + pub fn as_file(&self) -> &str { + match self { + DeviceModel::NanoS => "stellarNanoSApp.elf", + DeviceModel::NanoSP => "stellarNanoSPApp.elf", + DeviceModel::NanoX => "stellarNanoXApp.elf", + } } } @@ -92,4 +132,4 @@ impl Image for Speculos { fn cmd(&self) -> impl IntoIterator>> { vec![self.cmd.clone()].into_iter() } -} \ No newline at end of file +} diff --git a/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs b/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs new file mode 100644 index 000000000..c4100e8d1 --- /dev/null +++ b/cmd/crates/stellar-ledger/src/emulator_test_support/util.rs @@ -0,0 +1,211 @@ +use serde::Deserialize; +use std::ops::Range; +use std::sync::LazyLock; +use std::sync::Mutex; + +use crate::{Error, LedgerSigner}; +use std::net::TcpListener; + +use super::{http_transport::Emulator, speculos::Speculos}; + +use std::{collections::HashMap, time::Duration}; + +use stellar_xdr::curr::Hash; + +use testcontainers::{core::ContainerPort, runners::AsyncRunner, ContainerAsync, ImageExt}; +use tokio::time::sleep; + +static PORT_RANGE: LazyLock>> = LazyLock::new(|| Mutex::new(40000..50000)); + +pub const TEST_NETWORK_PASSPHRASE: &[u8] = b"Test SDF Network ; September 2015"; +pub fn test_network_hash() -> Hash { + use sha2::Digest; + Hash(sha2::Sha256::digest(TEST_NETWORK_PASSPHRASE).into()) +} + +pub async fn ledger(host_port: u16) -> LedgerSigner { + LedgerSigner::new(get_http_transport("127.0.0.1", host_port).await.unwrap()) +} + +pub async fn click(ui_host_port: u16, url: &str) { + let previous_events = get_emulator_events(ui_host_port).await; + + let client = reqwest::Client::new(); + let mut payload = HashMap::new(); + payload.insert("action", "press-and-release"); + + let mut screen_has_changed = false; + + client + .post(format!("http://localhost:{ui_host_port}/{url}")) + .json(&payload) + .send() + .await + .unwrap(); + + while !screen_has_changed { + let current_events = get_emulator_events(ui_host_port).await; + + if !(previous_events == current_events) { + screen_has_changed = true + } + } + + sleep(Duration::from_secs(1)).await; +} + +pub async fn enable_hash_signing(ui_host_port: u16) { + click(ui_host_port, "button/right").await; + + click(ui_host_port, "button/both").await; + + click(ui_host_port, "button/both").await; + + click(ui_host_port, "button/right").await; + + click(ui_host_port, "button/right").await; + + click(ui_host_port, "button/both").await; +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct EmulatorEvent { + text: String, + x: u16, + y: u16, + w: u16, + h: u16, +} + +#[derive(Debug, Deserialize)] +struct EventsResponse { + events: Vec, +} + +pub async fn get_container(ledger_device_model: &str) -> ContainerAsync { + let (tcp_port_1, tcp_port_2) = get_available_ports(2); + Speculos::new(ledger_device_model.to_string()) + .with_mapped_port(tcp_port_1, ContainerPort::Tcp(9998)) + .with_mapped_port(tcp_port_2, ContainerPort::Tcp(5000)) + .start() + .await + .unwrap() +} + +pub fn get_available_ports(n: usize) -> (u16, u16) { + let mut range = PORT_RANGE.lock().unwrap(); + let mut ports = Vec::with_capacity(n); + while ports.len() < n { + if let Some(port) = range.next() { + if let Ok(listener) = TcpListener::bind(("0.0.0.0", port)) { + ports.push(port); + drop(listener); + } + } else { + panic!("No more available ports"); + } + } + + (ports[0], ports[1]) +} + +pub async fn get_http_transport(host: &str, port: u16) -> Result { + let max_retries = 5; + let mut retries = 0; + let mut wait_time = Duration::from_secs(1); + // ping the emulator port to make sure it's up and running + // retry with exponential backoff + loop { + match reqwest::get(format!("http://{host}:{port}")).await { + Ok(_) => return Ok(Emulator::new(host, port)), + Err(e) => { + retries += 1; + if retries >= max_retries { + println!("get_http_transport: Exceeded max retries for connecting to emulated device"); + + return Err(Error::APDUExchangeError(format!( + "Failed to connect to emulator: {e}" + ))); + } + sleep(wait_time).await; + wait_time *= 2; + } + } + } +} + +pub async fn wait_for_emulator_start_text(ui_host_port: u16) { + let mut ready = false; + while !ready { + let events = get_emulator_events_with_retries(ui_host_port, 5).await; + + if events.iter().any(|event| event.text == "is ready") { + ready = true; + } + } +} + +pub async fn wait_for_review_transaction_text(ui_host_port: u16) { + let mut review_ready = false; + while !review_ready { + let events = get_emulator_events_with_retries(ui_host_port, 5).await; + + if events.iter().any(|event| event.text == "Review") { + review_ready = true; + } + } +} + +pub async fn get_emulator_events(ui_host_port: u16) -> Vec { + // Allowing for less retries here because presumably the emulator should be up and running since we waited + // for the "is ready" text via wait_for_emulator_start_text + get_emulator_events_with_retries(ui_host_port, 1).await +} + +pub async fn get_emulator_events_with_retries( + ui_host_port: u16, + max_retries: u16, +) -> Vec { + let client = reqwest::Client::new(); + let mut retries = 0; + let mut wait_time = Duration::from_secs(1); + loop { + match client + .get(format!("http://localhost:{ui_host_port}/events")) + .send() + .await + { + Ok(req) => { + let resp = req.json::().await.unwrap(); + return resp.events; + } + Err(e) => { + retries += 1; + if retries >= max_retries { + println!("get_emulator_events_with_retries: Exceeded max retries"); + panic!("get_emulator_events_with_retries: Failed to get emulator events: {e}"); + } + sleep(wait_time).await; + wait_time *= 2; + } + } + } +} + +pub async fn approve_tx_hash_signature(ui_host_port: u16, device_model: String) { + wait_for_review_transaction_text(ui_host_port).await; + let number_of_right_clicks = if device_model == "nanos" { 10 } else { 6 }; + for _ in 0..number_of_right_clicks { + click(ui_host_port, "button/right").await; + } + + click(ui_host_port, "button/both").await; +} + +pub async fn approve_tx_signature(ui_host_port: u16, device_model: String) { + let number_of_right_clicks = if device_model == "nanos" { 17 } else { 11 }; + for _ in 0..number_of_right_clicks { + click(ui_host_port, "button/right").await; + } + click(ui_host_port, "button/both").await; +} diff --git a/cmd/crates/stellar-ledger/src/hd_path.rs b/cmd/crates/stellar-ledger/src/hd_path.rs index 07ed133f1..79fca40a2 100644 --- a/cmd/crates/stellar-ledger/src/hd_path.rs +++ b/cmd/crates/stellar-ledger/src/hd_path.rs @@ -4,6 +4,7 @@ use crate::Error; pub struct HdPath(pub u32); impl HdPath { + #[must_use] pub fn depth(&self) -> u8 { let path: slip10::BIP32Path = self.into(); path.depth() @@ -23,6 +24,9 @@ impl From<&u32> for HdPath { } impl HdPath { + /// # Errors + /// + /// Could fail to convert the path to bytes pub fn to_vec(&self) -> Result, Error> { hd_path_to_bytes(&self.into()) } diff --git a/cmd/crates/stellar-ledger/src/lib.rs b/cmd/crates/stellar-ledger/src/lib.rs index e90c31bb9..1cfc5d133 100644 --- a/cmd/crates/stellar-ledger/src/lib.rs +++ b/cmd/crates/stellar-ledger/src/lib.rs @@ -1,10 +1,14 @@ use hd_path::HdPath; -use ledger_transport::{APDUCommand, Exchange}; +use ledger_transport::APDUCommand; +pub use ledger_transport::Exchange; + use ledger_transport_hid::{ hidapi::{HidApi, HidError}, - LedgerHIDError, TransportNativeHID, + LedgerHIDError, }; +pub use ledger_transport_hid::TransportNativeHID; + use std::vec; use stellar_strkey::DecodeError; use stellar_xdr::curr::{ @@ -16,6 +20,8 @@ pub use crate::signer::Blob; pub mod hd_path; mod signer; +pub mod emulator_test_support; + // this is from https://github.com/LedgerHQ/ledger-live/blob/36cfbf3fa3300fd99bcee2ab72e1fd8f280e6280/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L181 const APDU_MAX_SIZE: u8 = 150; const HD_PATH_ELEMENTS_COUNT: u8 = 3; @@ -79,6 +85,8 @@ pub struct LedgerSigner { unsafe impl Send for LedgerSigner where T: Exchange {} unsafe impl Sync for LedgerSigner where T: Exchange {} +/// # Errors +/// Could fail to make the connection to the Ledger device pub fn native() -> Result, Error> { Ok(LedgerSigner { transport: get_transport()?, @@ -92,6 +100,9 @@ where pub fn new(transport: T) -> Self { Self { transport } } + + /// # Errors + /// Returns an error if there is an issue with connecting with the device pub fn native() -> Result, Error> { Ok(LedgerSigner { transport: get_transport()?, @@ -298,18 +309,13 @@ pub fn test_network_hash() -> Hash { Hash(sha2::Sha256::digest(TEST_NETWORK_PASSPHRASE).into()) } -#[cfg(test)] +#[cfg(all(test, feature = "http-transport"))] mod test { - mod test_helpers { - pub mod test { - include!("../tests/utils/mod.rs"); - } - } use httpmock::prelude::*; use serde_json::json; + use super::emulator_test_support::http_transport::Emulator; use crate::Blob; - use test_helpers::test::emulator_http_transport::EmulatorHttpTransport; use std::vec; @@ -321,8 +327,8 @@ mod test { Memo, MuxedAccount, PaymentOp, Preconditions, SequenceNumber, TransactionExt, }; - fn ledger(server: &MockServer) -> LedgerSigner { - let transport = EmulatorHttpTransport::new(&server.host(), server.port()); + fn ledger(server: &MockServer) -> LedgerSigner { + let transport = Emulator::new(&server.host(), server.port()); LedgerSigner::new(transport) } diff --git a/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs b/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs index 1b0c09bab..880ac95ff 100644 --- a/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs +++ b/cmd/crates/stellar-ledger/tests/test/emulator_tests.rs @@ -1,52 +1,23 @@ -use ledger_transport::Exchange; -use once_cell::sync::Lazy; -use serde::Deserialize; -use std::ops::Range; -use std::sync::Mutex; -use std::vec; - -use std::net::TcpListener; use stellar_ledger::hd_path::HdPath; -use stellar_ledger::{Blob, Error, LedgerSigner}; +use stellar_ledger::{Blob, Error}; use std::sync::Arc; -use std::{collections::HashMap, time::Duration}; use stellar_xdr::curr::{ - self as xdr, Hash, Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, + self as xdr, Memo, MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, SequenceNumber, Transaction, TransactionExt, Uint256, }; -use testcontainers::{core::ContainerPort, runners::AsyncRunner, ContainerAsync, ImageExt}; -use tokio::time::sleep; - -static PORT_RANGE: Lazy>> = Lazy::new(|| Mutex::new(40000..50000)); - -pub const TEST_NETWORK_PASSPHRASE: &[u8] = b"Test SDF Network ; September 2015"; -pub fn test_network_hash() -> Hash { - use sha2::Digest; - Hash(sha2::Sha256::digest(TEST_NETWORK_PASSPHRASE).into()) -} - -async fn ledger(host_port: u16) -> LedgerSigner { - LedgerSigner::new(get_http_transport("127.0.0.1", host_port).await.unwrap()) -} - -mod test_helpers { - pub mod test { - include!("../utils/mod.rs"); - } -} +use stellar_ledger::emulator_test_support::*; use test_case::test_case; -use test_helpers::test::{emulator_http_transport::EmulatorHttpTransport, speculos::Speculos}; -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_get_public_key(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_get_public_key(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -68,12 +39,12 @@ async fn test_get_public_key(ledger_device_model: String) { } } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_get_app_configuration(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_get_app_configuration(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -91,12 +62,12 @@ async fn test_get_app_configuration(ledger_device_model: String) { }; } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -141,7 +112,7 @@ async fn test_sign_tx(ledger_device_model: String) { fee: 100, seq_num: SequenceNumber(1), cond: Preconditions::None, - memo: Memo::Text("Stellar".as_bytes().try_into().unwrap()), + memo: Memo::Text("Stellar".try_into().unwrap()), ext: TransactionExt::V0, operations: [Operation { source_account: Some(MuxedAccount::Ed25519(Uint256(source_account_bytes))), @@ -159,7 +130,10 @@ async fn test_sign_tx(ledger_device_model: String) { let ledger = Arc::clone(&ledger); async move { ledger.sign_transaction(path, tx, test_network_hash()).await } }); - let approve = tokio::task::spawn(approve_tx_signature(ui_host_port, ledger_device_model)); + let approve = tokio::task::spawn(approve_tx_signature( + ui_host_port, + ledger_device_model.to_string(), + )); let result = sign.await.unwrap(); let _ = approve.await.unwrap(); @@ -175,12 +149,12 @@ async fn test_sign_tx(ledger_device_model: String) { }; } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); wait_for_emulator_start_text(ui_host_port).await; @@ -199,12 +173,12 @@ async fn test_sign_tx_hash_when_hash_signing_is_not_enabled(ledger_device_model: } } -#[test_case("nanos".to_string() ; "when the device is NanoS")] -#[test_case("nanox".to_string() ; "when the device is NanoX")] -#[test_case("nanosp".to_string() ; "when the device is NanoS Plus")] +#[test_case("nanos"; "when the device is NanoS")] +#[test_case("nanox"; "when the device is NanoX")] +#[test_case("nanosp"; "when the device is NanoS Plus")] #[tokio::test] -async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: String) { - let container = get_container(ledger_device_model.clone()).await; +async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: &str) { + let container = get_container(ledger_device_model).await; let host_port = container.get_host_port_ipv4(9998).await.unwrap(); let ui_host_port: u16 = container.get_host_port_ipv4(5000).await.unwrap(); @@ -230,7 +204,10 @@ async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: Str let ledger = Arc::clone(&ledger); async move { ledger.sign_transaction_hash(path, &test_hash).await } }); - let approve = tokio::task::spawn(approve_tx_hash_signature(ui_host_port, ledger_device_model)); + let approve = tokio::task::spawn(approve_tx_hash_signature( + ui_host_port, + ledger_device_model.to_string(), + )); let response = sign.await.unwrap(); let _ = approve.await.unwrap(); @@ -244,174 +221,3 @@ async fn test_sign_tx_hash_when_hash_signing_is_enabled(ledger_device_model: Str } } } - -async fn click(ui_host_port: u16, url: &str) { - let previous_events = get_emulator_events(ui_host_port).await; - - let client = reqwest::Client::new(); - let mut payload = HashMap::new(); - payload.insert("action", "press-and-release"); - - let mut screen_has_changed = false; - - client - .post(format!("http://localhost:{ui_host_port}/{url}")) - .json(&payload) - .send() - .await - .unwrap(); - - while !screen_has_changed { - let current_events = get_emulator_events(ui_host_port).await; - - if !(previous_events == current_events) { - screen_has_changed = true - } - } - - sleep(Duration::from_secs(1)).await; -} - -async fn enable_hash_signing(ui_host_port: u16) { - click(ui_host_port, "button/right").await; - - click(ui_host_port, "button/both").await; - - click(ui_host_port, "button/both").await; - - click(ui_host_port, "button/right").await; - - click(ui_host_port, "button/right").await; - - click(ui_host_port, "button/both").await; -} - -#[derive(Debug, Deserialize, PartialEq)] -struct EmulatorEvent { - text: String, - x: u16, - y: u16, - w: u16, - h: u16, -} - -#[derive(Debug, Deserialize)] -struct EventsResponse { - events: Vec, -} - -async fn get_container(ledger_device_model: String) -> ContainerAsync { - let (tcp_port_1, tcp_port_2) = get_available_ports(2); - Speculos::new(ledger_device_model) - .with_mapped_port(tcp_port_1, ContainerPort::Tcp(9998)) - .with_mapped_port(tcp_port_2, ContainerPort::Tcp(5000)) - .start() - .await - .unwrap() -} - -fn get_available_ports(n: usize) -> (u16, u16) { - let mut range = PORT_RANGE.lock().unwrap(); - let mut ports = Vec::with_capacity(n); - while ports.len() < n { - if let Some(port) = range.next() { - if let Ok(listener) = TcpListener::bind(("0.0.0.0", port)) { - ports.push(port); - drop(listener); - } - } else { - panic!("No more available ports"); - } - } - - (ports[0], ports[1]) -} - -async fn get_http_transport(host: &str, port: u16) -> Result { - let max_retries = 5; - let mut retries = 0; - let mut wait_time = Duration::from_secs(1); - // ping the emulator port to make sure it's up and running - // retry with exponential backoff - loop { - match reqwest::get(format!("http://{host}:{port}")).await { - Ok(_) => return Ok(EmulatorHttpTransport::new(host, port)), - Err(e) => { - retries += 1; - if retries >= max_retries { - println!("get_http_transport: Exceeded max retries for connecting to emulated device"); - - return Err(Error::APDUExchangeError(format!( - "Failed to connect to emulator: {e}" - ))); - } - sleep(wait_time).await; - wait_time *= 2; - } - } - } -} - -async fn wait_for_emulator_start_text(ui_host_port: u16) { - let mut ready = false; - while !ready { - let events = get_emulator_events_with_retries(ui_host_port, 5).await; - - if events.iter().any(|event| event.text == "is ready") { - ready = true; - } - } -} - -async fn get_emulator_events(ui_host_port: u16) -> Vec { - // Allowing for less retries here because presumably the emulator should be up and running since we waited - // for the "is ready" text via wait_for_emulator_start_text - get_emulator_events_with_retries(ui_host_port, 1).await -} - -async fn get_emulator_events_with_retries( - ui_host_port: u16, - max_retries: u16, -) -> Vec { - let client = reqwest::Client::new(); - let mut retries = 0; - let mut wait_time = Duration::from_secs(1); - loop { - match client - .get(format!("http://localhost:{ui_host_port}/events")) - .send() - .await - { - Ok(req) => { - let resp = req.json::().await.unwrap(); - return resp.events; - } - Err(e) => { - retries += 1; - if retries >= max_retries { - println!("get_emulator_events_with_retries: Exceeded max retries"); - panic!("get_emulator_events_with_retries: Failed to get emulator events: {e}"); - } - sleep(wait_time).await; - wait_time *= 2; - } - } - } -} - -async fn approve_tx_hash_signature(ui_host_port: u16, device_model: String) { - let number_of_right_clicks = if device_model == "nanos" { 10 } else { 6 }; - for _ in 0..number_of_right_clicks { - click(ui_host_port, "button/right").await; - } - - click(ui_host_port, "button/both").await; -} - -async fn approve_tx_signature(ui_host_port: u16, device_model: String) { - let number_of_right_clicks = if device_model == "nanos" { 17 } else { 11 }; - for _ in 0..number_of_right_clicks { - click(ui_host_port, "button/right").await; - } - click(ui_host_port, "button/both").await; -} diff --git a/cmd/crates/stellar-ledger/tests/utils/mod.rs b/cmd/crates/stellar-ledger/tests/utils/mod.rs deleted file mode 100644 index 5b63732bc..000000000 --- a/cmd/crates/stellar-ledger/tests/utils/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod emulator_http_transport; -pub(crate) mod speculos; diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 3d6a027c1..3d886fb5b 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -39,6 +39,7 @@ default = [] version_lt_23 = [] version_gte_23 = [] opt = ["dep:wasm-opt"] +emulator-tests = ["stellar-ledger/emulator-tests"] [dependencies] stellar-xdr = { workspace = true, features = ["cli"] } @@ -51,6 +52,8 @@ soroban-ledger-snapshot = { workspace = true } stellar-strkey = { workspace = true } soroban-sdk = { workspace = true } soroban-rpc = { workspace = true } +stellar-ledger = { workspace = true } + clap = { workspace = true, features = [ "derive", "env", diff --git a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs index f9733925d..a105630db 100644 --- a/cmd/soroban-cli/src/commands/contract/arg_parsing.rs +++ b/cmd/soroban-cli/src/commands/contract/arg_parsing.rs @@ -14,10 +14,12 @@ use crate::xdr::{ }; use crate::commands::txn_result::TxnResult; + use crate::config::{ self, sc_address::{self, UnresolvedScAddress}, }; + use soroban_spec_tools::Spec; #[derive(thiserror::Error, Debug)] diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 61f2db9ef..b204c14ae 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -128,7 +128,9 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let source_account = config.source_account()?; + + let source_account = config.source_account().await?; + // Get the account sequence number // TODO: use symbols for the method names (both here and in serve) let account_details = client diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index f793d61bd..1299d6dfc 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -223,8 +223,7 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - - let MuxedAccount::Ed25519(bytes) = config.source_account()? else { + let MuxedAccount::Ed25519(bytes) = config.source_account().await? else { return Err(Error::OnlyEd25519AccountsAllowed); }; let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(bytes)); diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index a6ec42c11..6f2c14ba4 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -135,7 +135,7 @@ impl NetworkRunnable for Cmd { tracing::trace!(?network); let keys = self.key.parse_keys(&config.locator, &network)?; let client = network.rpc_client()?; - let source_account = config.source_account()?; + let source_account = config.source_account().await?; let extend_to = self.ledgers_to_extend(); // Get the account sequence number diff --git a/cmd/soroban-cli/src/commands/contract/id.rs b/cmd/soroban-cli/src/commands/contract/id.rs index bb8744d51..f07fa8df6 100644 --- a/cmd/soroban-cli/src/commands/contract/id.rs +++ b/cmd/soroban-cli/src/commands/contract/id.rs @@ -18,10 +18,10 @@ pub enum Error { } impl Cmd { - pub fn run(&self) -> Result<(), Error> { + pub async fn run(&self) -> Result<(), Error> { match &self { Cmd::Asset(asset) => asset.run()?, - Cmd::Wasm(wasm) => wasm.run()?, + Cmd::Wasm(wasm) => wasm.run().await?, } Ok(()) } diff --git a/cmd/soroban-cli/src/commands/contract/id/wasm.rs b/cmd/soroban-cli/src/commands/contract/id/wasm.rs index 349dd167b..a469f10c9 100644 --- a/cmd/soroban-cli/src/commands/contract/id/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/id/wasm.rs @@ -29,12 +29,12 @@ pub enum Error { OnlyEd25519AccountsAllowed, } impl Cmd { - pub fn run(&self) -> Result<(), Error> { + pub async fn run(&self) -> Result<(), Error> { let salt: [u8; 32] = soroban_spec_tools::utils::padded_hex_from_str(&self.salt, 32) .map_err(|_| Error::CannotParseSalt(self.salt.clone()))? .try_into() .map_err(|_| Error::CannotParseSalt(self.salt.clone()))?; - let source_account = match self.config.source_account()? { + let source_account = match self.config.source_account().await? { xdr::MuxedAccount::Ed25519(uint256) => stellar_strkey::ed25519::PublicKey(uint256.0), xdr::MuxedAccount::MuxedEd25519(_) => return Err(Error::OnlyEd25519AccountsAllowed), }; diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 5c84ac920..456dab406 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -249,7 +249,7 @@ impl NetworkRunnable for Cmd { .await?; client - .get_account(&config.source_account()?.to_string()) + .get_account(&config.source_account().await?.to_string()) .await? } else { if should_send == ShouldSend::DefaultNo { diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index 2cdfd3de5..dd642755a 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -155,7 +155,7 @@ impl Cmd { Cmd::Extend(extend) => extend.run().await?, Cmd::Alias(alias) => alias.run(global_args)?, Cmd::Deploy(deploy) => deploy.run(global_args).await?, - Cmd::Id(id) => id.run()?, + Cmd::Id(id) => id.run().await?, Cmd::Info(info) => info.run(global_args).await?, Cmd::Init(init) => init.run(global_args)?, Cmd::Inspect(inspect) => inspect.run(global_args)?, diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index bc46d0770..fd28c95e0 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -134,7 +134,7 @@ impl NetworkRunnable for Cmd { tracing::trace!(?network); let entry_keys = self.key.parse_keys(&config.locator, &network)?; let client = network.rpc_client()?; - let source_account = config.source_account()?; + let source_account = config.source_account().await?; // Get the account sequence number let account_details = client diff --git a/cmd/soroban-cli/src/commands/contract/upload.rs b/cmd/soroban-cli/src/commands/contract/upload.rs index a03d3c6b3..91ae65bf6 100644 --- a/cmd/soroban-cli/src/commands/contract/upload.rs +++ b/cmd/soroban-cli/src/commands/contract/upload.rs @@ -135,7 +135,7 @@ impl NetworkRunnable for Cmd { } // Get the account sequence number - let source_account = config.source_account()?; + let source_account = config.source_account().await?; let account_details = client .get_account(&source_account.clone().to_string()) diff --git a/cmd/soroban-cli/src/commands/keys/fund.rs b/cmd/soroban-cli/src/commands/keys/fund.rs index 28d018d90..2d8bc244d 100644 --- a/cmd/soroban-cli/src/commands/keys/fund.rs +++ b/cmd/soroban-cli/src/commands/keys/fund.rs @@ -25,7 +25,7 @@ pub struct Cmd { impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let print = Print::new(global_args.quiet); - let addr = self.address.public_key()?; + let addr = self.address.public_key().await?; let network = self.network.get(&self.address.locator)?; network.fund_address(&addr).await?; print.checkln(format!( diff --git a/cmd/soroban-cli/src/commands/keys/mod.rs b/cmd/soroban-cli/src/commands/keys/mod.rs index f83be0719..fadcfc94b 100644 --- a/cmd/soroban-cli/src/commands/keys/mod.rs +++ b/cmd/soroban-cli/src/commands/keys/mod.rs @@ -73,7 +73,7 @@ impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match self { Cmd::Add(cmd) => cmd.run(global_args)?, - Cmd::PublicKey(cmd) => cmd.run()?, + Cmd::PublicKey(cmd) => cmd.run().await?, Cmd::Fund(cmd) => cmd.run(global_args).await?, Cmd::Generate(cmd) => cmd.run(global_args).await?, Cmd::Ls(cmd) => cmd.run()?, diff --git a/cmd/soroban-cli/src/commands/keys/public_key.rs b/cmd/soroban-cli/src/commands/keys/public_key.rs index 51ce90ed2..e67934eb7 100644 --- a/cmd/soroban-cli/src/commands/keys/public_key.rs +++ b/cmd/soroban-cli/src/commands/keys/public_key.rs @@ -26,15 +26,16 @@ pub struct Cmd { } impl Cmd { - pub fn run(&self) -> Result<(), Error> { - println!("{}", self.public_key()?); + pub async fn run(&self) -> Result<(), Error> { + println!("{}", self.public_key().await?); Ok(()) } - pub fn public_key(&self) -> Result { + pub async fn public_key(&self) -> Result { let muxed = self .name - .resolve_muxed_account(&self.locator, self.hd_path)?; + .resolve_muxed_account(&self.locator, self.hd_path) + .await?; let bytes = match muxed { soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0, soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0, diff --git a/cmd/soroban-cli/src/commands/snapshot/create.rs b/cmd/soroban-cli/src/commands/snapshot/create.rs index 9e2d3a292..4a269814d 100644 --- a/cmd/soroban-cli/src/commands/snapshot/create.rs +++ b/cmd/soroban-cli/src/commands/snapshot/create.rs @@ -220,7 +220,7 @@ impl Cmd { .address .iter() .cloned() - .filter_map(|a| self.resolve_address(&a, network_passphrase)) + .filter_map(|a| self.resolve_address_sync(&a, network_passphrase)) .partition_map(|a| a); let mut current = SearchInputs { @@ -402,23 +402,42 @@ impl Cmd { .ok_or(Error::ArchiveUrlNotConfigured) } - fn resolve_address( + #[allow(dead_code)] + async fn resolve_address( &self, address: &str, network_passphrase: &str, ) -> Option> { - self.resolve_contract(address, network_passphrase) - .map(Either::Right) - .or_else(|| self.resolve_account(address).map(Either::Left)) + if let Some(contract) = self.resolve_contract(address, network_passphrase) { + Some(Either::Right(contract)) + } else { + self.resolve_account(address).await.map(Either::Left) + } + } + + fn resolve_address_sync( + &self, + address: &str, + network_passphrase: &str, + ) -> Option> { + if let Some(contract) = self.resolve_contract(address, network_passphrase) { + Some(Either::Right(contract)) + } else { + self.resolve_account_sync(address).map(Either::Left) + } } // Resolve an account address to an account id. The address can be a // G-address or a key name (as in `stellar keys address NAME`). - fn resolve_account(&self, address: &str) -> Option { - let address: UnresolvedMuxedAccount = address.parse().ok()?; + async fn resolve_account(&self, address: &str) -> Option { + let address: UnresolvedMuxedAccount = address.parse().ok()?; Some(AccountId(xdr::PublicKey::PublicKeyTypeEd25519( - match address.resolve_muxed_account(&self.locator, None).ok()? { + match address + .resolve_muxed_account(&self.locator, None) + .await + .ok()? + { xdr::MuxedAccount::Ed25519(uint256) => uint256, xdr::MuxedAccount::MuxedEd25519(xdr::MuxedAccountMed25519 { ed25519, .. }) => { ed25519 @@ -426,6 +445,16 @@ impl Cmd { }, ))) } + + // Resolve an account address to an account id. The address can be a + // G-address or a key name (as in `stellar keys address NAME`). + fn resolve_account_sync(&self, address: &str) -> Option { + let address: UnresolvedMuxedAccount = address.parse().ok()?; + let muxed_account = address + .resolve_muxed_account_sync(&self.locator, None) + .ok()?; + Some(muxed_account.account_id()) + } // Resolve a contract address to a contract id. The contract can be a // C-address or a contract alias. fn resolve_contract(&self, address: &str, network_passphrase: &str) -> Option { diff --git a/cmd/soroban-cli/src/commands/tx/args.rs b/cmd/soroban-cli/src/commands/tx/args.rs index 38391e248..d57a26504 100644 --- a/cmd/soroban-cli/src/commands/tx/args.rs +++ b/cmd/soroban-cli/src/commands/tx/args.rs @@ -46,7 +46,7 @@ pub enum Error { impl Args { pub async fn tx(&self, body: impl Into) -> Result { - let source_account = self.source_account()?; + let source_account = self.source_account().await?; let seq_num = self .config .next_sequence_number(source_account.clone().account_id()) @@ -77,6 +77,7 @@ impl Args { let tx = self.tx(op).await?; self.handle_tx(tx, global_args).await } + pub async fn handle_and_print( &self, op: impl Into, @@ -111,15 +112,15 @@ impl Args { Ok(TxnEnvelopeResult::Res(txn_resp)) } - pub fn source_account(&self) -> Result { - Ok(self.config.source_account()?) + pub async fn source_account(&self) -> Result { + Ok(self.config.source_account().await?) } pub fn resolve_muxed_address( &self, address: &UnresolvedMuxedAccount, ) -> Result { - Ok(address.resolve_muxed_account(&self.config.locator, self.config.hd_path)?) + Ok(address.resolve_muxed_account_sync(&self.config.locator, self.config.hd_path)?) } pub fn resolve_account_id( @@ -127,19 +128,24 @@ impl Args { address: &UnresolvedMuxedAccount, ) -> Result { Ok(address - .resolve_muxed_account(&self.config.locator, self.config.hd_path)? + .resolve_muxed_account_sync(&self.config.locator, self.config.hd_path)? .account_id()) } - pub fn add_op( + pub async fn add_op( &self, op_body: impl Into, tx_env: xdr::TransactionEnvelope, op_source: Option<&address::UnresolvedMuxedAccount>, ) -> Result { - let source_account = op_source - .map(|a| self.resolve_muxed_address(a)) - .transpose()?; + let mut source_account = None; + if let Some(account) = op_source { + source_account = Some( + account + .resolve_muxed_account(&self.config.locator, self.config.hd_path) + .await?, + ); + } let op = xdr::Operation { source_account, body: op_body.into(), diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index 02be68bf0..7ece69857 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -53,7 +53,7 @@ impl Cmd { match self { Cmd::Hash(cmd) => cmd.run(global_args)?, Cmd::New(cmd) => cmd.run(global_args).await?, - Cmd::Operation(cmd) => cmd.run(global_args)?, + Cmd::Operation(cmd) => cmd.run(global_args).await?, Cmd::Send(cmd) => cmd.run(global_args).await?, Cmd::Sign(cmd) => cmd.run(global_args).await?, Cmd::Simulate(cmd) => cmd.run(global_args).await?, diff --git a/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs b/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs index 96062bba2..87375ffc1 100644 --- a/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs +++ b/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs @@ -18,6 +18,14 @@ pub struct Args { pub bump_to: i64, } +impl From<&Args> for xdr::OperationBody { + fn from(args: &Args) -> Self { + xdr::OperationBody::BumpSequence(xdr::BumpSequenceOp { + bump_to: args.bump_to.into(), + }) + } +} + impl From<&Cmd> for xdr::OperationBody { fn from(cmd: &Cmd) -> Self { xdr::OperationBody::BumpSequence(xdr::BumpSequenceOp { diff --git a/cmd/soroban-cli/src/commands/tx/op/add/args.rs b/cmd/soroban-cli/src/commands/tx/op/add/args.rs index 0dd195f7f..8ee211302 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/args.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/args.rs @@ -1,4 +1,4 @@ -use crate::{commands::tx, config::address, xdr}; +use crate::config::address; #[derive(Debug, clap::Args, Clone)] #[group(skip)] @@ -13,12 +13,7 @@ pub struct Args { } impl Args { - pub fn add_op( - &self, - op_body: impl Into, - tx_env: xdr::TransactionEnvelope, - tx: &tx::args::Args, - ) -> Result { - tx.add_op(op_body, tx_env, self.operation_source_account.as_ref()) + pub fn source(&self) -> Option<&address::UnresolvedMuxedAccount> { + self.operation_source_account.as_ref() } } diff --git a/cmd/soroban-cli/src/commands/tx/op/add/mod.rs b/cmd/soroban-cli/src/commands/tx/op/add/mod.rs index 1a02d01a2..a61c86150 100644 --- a/cmd/soroban-cli/src/commands/tx/op/add/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/op/add/mod.rs @@ -63,19 +63,20 @@ impl TryFrom<&Cmd> for OperationBody { } impl Cmd { - pub fn run(&self, _: &global::Args) -> Result<(), Error> { + pub async fn run(&self, _: &global::Args) -> Result<(), Error> { let tx_env = tx_envelope_from_stdin()?; let op = OperationBody::try_from(self)?; let res = match self { - Cmd::AccountMerge(cmd) => cmd.args.add_op(op, tx_env, &cmd.op.tx), - Cmd::BumpSequence(cmd) => cmd.args.add_op(op, tx_env, &cmd.op.tx), - Cmd::ChangeTrust(cmd) => cmd.args.add_op(op, tx_env, &cmd.op.tx), - Cmd::CreateAccount(cmd) => cmd.args.add_op(op, tx_env, &cmd.op.tx), - Cmd::ManageData(cmd) => cmd.args.add_op(op, tx_env, &cmd.op.tx), - Cmd::Payment(cmd) => cmd.args.add_op(op, tx_env, &cmd.op.tx), - Cmd::SetOptions(cmd) => cmd.args.add_op(op, tx_env, &cmd.op.tx), - Cmd::SetTrustlineFlags(cmd) => cmd.args.add_op(op, tx_env, &cmd.op.tx), - }?; + Cmd::AccountMerge(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::BumpSequence(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::ChangeTrust(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::CreateAccount(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::ManageData(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::Payment(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::SetOptions(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + Cmd::SetTrustlineFlags(cmd) => cmd.op.tx.add_op(op, tx_env, cmd.args.source()), + } + .await?; println!("{}", res.to_xdr_base64(crate::xdr::Limits::none())?); Ok(()) } diff --git a/cmd/soroban-cli/src/commands/tx/op/mod.rs b/cmd/soroban-cli/src/commands/tx/op/mod.rs index 9c38ecfd3..e5b064a79 100644 --- a/cmd/soroban-cli/src/commands/tx/op/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/op/mod.rs @@ -16,9 +16,9 @@ pub enum Error { } impl Cmd { - pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match self { - Cmd::Add(cmd) => cmd.run(global_args)?, + Cmd::Add(cmd) => cmd.run(global_args).await?, }; Ok(()) } diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index c696f8586..750fd7d29 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -33,12 +33,15 @@ impl Cmd { #[allow(clippy::unused_async)] pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let tx_env = super::xdr::tx_envelope_from_stdin()?; - let tx_env_signed = self.sign_with.sign_tx_env( - &tx_env, - &self.locator, - &self.network.get(&self.locator)?, - global_args.quiet, - )?; + let tx_env_signed = self + .sign_with + .sign_tx_env( + &tx_env, + &self.locator, + &self.network.get(&self.locator)?, + global_args.quiet, + ) + .await?; println!("{}", tx_env_signed.to_xdr_base64(Limits::none())?); Ok(()) } diff --git a/cmd/soroban-cli/src/config/address.rs b/cmd/soroban-cli/src/config/address.rs index 6987b523c..73c5cf190 100644 --- a/cmd/soroban-cli/src/config/address.rs +++ b/cmd/soroban-cli/src/config/address.rs @@ -3,7 +3,10 @@ use std::{ str::FromStr, }; -use crate::xdr; +use crate::{ + signer::{self, ledger}, + xdr, +}; use super::{key, locator, secret}; @@ -12,6 +15,7 @@ use super::{key, locator, secret}; pub enum UnresolvedMuxedAccount { Resolved(xdr::MuxedAccount), AliasOrSecret(String), + Ledger(u32), } impl Default for UnresolvedMuxedAccount { @@ -25,21 +29,36 @@ pub enum Error { #[error(transparent)] Locator(#[from] locator::Error), #[error(transparent)] + Secret(#[from] secret::Error), + #[error(transparent)] + Signer(#[from] signer::Error), + #[error(transparent)] Key(#[from] key::Error), #[error("Address cannot be used to sign {0}")] CannotSign(xdr::MuxedAccount), + #[error("Ledger cannot reveal private keys")] + LedgerPrivateKeyRevealNotSupported, + #[error("Invalid key name: {0}\n `ledger` is not allowed")] + LedgerIsInvalidKeyName(String), #[error("Invalid key name: {0}\n only alphanumeric characters, underscores (_), and hyphens (-) are allowed.")] InvalidKeyNameCharacters(String), #[error("Invalid key name: {0}\n keys cannot exceed 250 characters")] InvalidKeyNameLength(String), #[error("Invalid key name: {0}\n keys cannot be the word \"ledger\"")] InvalidKeyName(String), + #[error("Ledger not supported in this context")] + LedgerNotSupported, } impl FromStr for UnresolvedMuxedAccount { type Err = Error; fn from_str(value: &str) -> Result { + if value.starts_with("ledger") { + if let Some(ledger) = parse_ledger(value) { + return Ok(UnresolvedMuxedAccount::Ledger(ledger)); + } + } Ok(xdr::MuxedAccount::from_str(value).map_or_else( |_| UnresolvedMuxedAccount::AliasOrSecret(value.to_string()), UnresolvedMuxedAccount::Resolved, @@ -47,8 +66,34 @@ impl FromStr for UnresolvedMuxedAccount { } } +fn parse_ledger(value: &str) -> Option { + let vals: Vec<_> = value.split(':').collect(); + if vals.len() > 2 { + return None; + } + if vals.len() == 1 { + return Some(0); + } + vals[1].parse().ok() +} + impl UnresolvedMuxedAccount { - pub fn resolve_muxed_account( + pub async fn resolve_muxed_account( + &self, + locator: &locator::Args, + hd_path: Option, + ) -> Result { + match self { + UnresolvedMuxedAccount::Ledger(hd_path) => Ok(xdr::MuxedAccount::Ed25519( + ledger(*hd_path).await?.public_key().await?.0.into(), + )), + UnresolvedMuxedAccount::Resolved(_) | UnresolvedMuxedAccount::AliasOrSecret(_) => { + self.resolve_muxed_account_sync(locator, hd_path) + } + } + } + + pub fn resolve_muxed_account_sync( &self, locator: &locator::Args, hd_path: Option, @@ -58,6 +103,7 @@ impl UnresolvedMuxedAccount { UnresolvedMuxedAccount::AliasOrSecret(alias_or_secret) => { Ok(locator.read_key(alias_or_secret)?.muxed_account(hd_path)?) } + UnresolvedMuxedAccount::Ledger(_) => Err(Error::LedgerNotSupported), } } @@ -69,6 +115,7 @@ impl UnresolvedMuxedAccount { UnresolvedMuxedAccount::AliasOrSecret(alias_or_secret) => { Ok(locator.read_key(alias_or_secret)?.try_into()?) } + UnresolvedMuxedAccount::Ledger(_) => Err(Error::LedgerPrivateKeyRevealNotSupported), } } } diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index 1327230e7..3d1463908 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -71,10 +71,11 @@ pub struct Args { impl Args { // TODO: Replace PublicKey with MuxedAccount once https://github.com/stellar/rs-stellar-xdr/pull/396 is merged. - pub fn source_account(&self) -> Result { + pub async fn source_account(&self) -> Result { Ok(self .source_account - .resolve_muxed_account(&self.locator, self.hd_path)?) + .resolve_muxed_account(&self.locator, self.hd_path) + .await?) } pub fn key_pair(&self) -> Result { @@ -94,7 +95,7 @@ impl Args { kind: SignerKind::Local(LocalKey { key }), print: Print::new(false), }; - Ok(signer.sign_tx(tx, network)?) + Ok(signer.sign_tx(tx, network).await?) } pub async fn sign_soroban_authorizations( diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index 1e8389e12..3bd1d22f2 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -7,7 +7,7 @@ use stellar_strkey::ed25519::{PrivateKey, PublicKey}; use crate::{ print::Print, - signer::{self, keyring, LocalKey, SecureStoreEntry, Signer, SignerKind}, + signer::{self, keyring, ledger, LocalKey, SecureStoreEntry, Signer, SignerKind}, utils, }; @@ -25,6 +25,10 @@ pub enum Error { InvalidSecretOrSeedPhrase, #[error(transparent)] Signer(#[from] signer::Error), + + #[error("Ledger does not reveal secret key")] + LedgerDoesNotRevealSecretKey, + #[error(transparent)] Keyring(#[from] keyring::Error), #[error("Secure Store does not reveal secret key")] @@ -50,6 +54,7 @@ pub struct Args { pub enum Secret { SecretKey { secret_key: String }, SeedPhrase { seed_phrase: String }, + Ledger, SecureStore { entry_name: String }, } @@ -65,6 +70,8 @@ impl FromStr for Secret { Ok(Secret::SeedPhrase { seed_phrase: s.to_string(), }) + } else if s == "ledger" { + Ok(Secret::Ledger) } else if s.starts_with(keyring::SECURE_STORE_ENTRY_PREFIX) { Ok(Secret::SecureStore { entry_name: s.to_string(), @@ -107,6 +114,7 @@ impl Secret { .private() .0, )?, + Secret::Ledger => panic!("Ledger does not reveal secret key"), Secret::SecureStore { .. } => { return Err(Error::SecureStoreDoesNotRevealSecretKey); } @@ -125,12 +133,19 @@ impl Secret { } } - pub fn signer(&self, hd_path: Option, print: Print) -> Result { + pub async fn signer(&self, hd_path: Option, print: Print) -> Result { let kind = match self { Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => { let key = self.key_pair(hd_path)?; SignerKind::Local(LocalKey { key }) } + Secret::Ledger => { + let hd_path: u32 = hd_path + .unwrap_or_default() + .try_into() + .expect("uszie bigger than u32"); + SignerKind::Ledger(ledger(hd_path).await?) + } Secret::SecureStore { entry_name } => SignerKind::SecureStore(SecureStoreEntry { name: entry_name.to_string(), hd_path, diff --git a/cmd/soroban-cli/src/config/sign_with.rs b/cmd/soroban-cli/src/config/sign_with.rs index 0c4e16ceb..07a4d1327 100644 --- a/cmd/soroban-cli/src/config/sign_with.rs +++ b/cmd/soroban-cli/src/config/sign_with.rs @@ -1,6 +1,6 @@ use crate::{ print::Print, - signer::{self, Signer, SignerKind}, + signer::{self, ledger, Signer, SignerKind}, xdr::{self, TransactionEnvelope}, }; use clap::arg; @@ -38,7 +38,7 @@ pub struct Args { #[arg(long, env = "STELLAR_SIGN_WITH_KEY")] pub sign_with_key: Option, - #[arg(long, requires = "sign_with_key")] + #[arg(long, conflicts_with = "sign_with_lab")] /// If using a seed phrase to sign, sets which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` pub hd_path: Option, @@ -46,10 +46,19 @@ pub struct Args { /// Sign with https://lab.stellar.org #[arg(long, conflicts_with = "sign_with_key", env = "STELLAR_SIGN_WITH_LAB")] pub sign_with_lab: bool, + + /// Sign with a ledger wallet + #[arg( + long, + conflicts_with = "sign_with_key", + conflicts_with = "sign_with_lab", + env = "STELLAR_SIGN_WITH_LEDGER" + )] + pub sign_with_ledger: bool, } impl Args { - pub fn sign_tx_env( + pub async fn sign_tx_env( &self, tx: &TransactionEnvelope, locator: &locator::Args, @@ -62,11 +71,23 @@ impl Args { kind: SignerKind::Lab, print, } + } else if self.sign_with_ledger { + let ledger = ledger( + self.hd_path + .unwrap_or_default() + .try_into() + .unwrap_or_default(), + ) + .await?; + Signer { + kind: SignerKind::Ledger(ledger), + print, + } } else { let key_or_name = self.sign_with_key.as_deref().ok_or(Error::NoSignWithKey)?; let secret = locator.get_secret_key(key_or_name)?; - secret.signer(self.hd_path, print)? + secret.signer(self.hd_path, print).await? }; - Ok(signer.sign_tx_env(tx, network)?) + Ok(signer.sign_tx_env(tx, network).await?) } } diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index 332551bbd..88607ff25 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -9,6 +9,7 @@ use crate::xdr::{ SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, TransactionV1Envelope, Uint256, VecM, WriteXdr, }; +use stellar_ledger::{Blob as _, Exchange, LedgerSigner}; use crate::{config::network::Network, print::Print, utils::transaction_hash}; @@ -28,6 +29,8 @@ pub enum Error { #[error("User cancelled signing, perhaps need to add -y")] UserCancelledSigning, #[error(transparent)] + Ledger(#[from] stellar_ledger::Error), + #[error(transparent)] Xdr(#[from] xdr::Error), #[error("Only Transaction envelope V1 type is supported")] UnsupportedTransactionEnvelopeType, @@ -212,12 +215,16 @@ pub struct Signer { #[allow(clippy::module_name_repetitions, clippy::large_enum_variant)] pub enum SignerKind { Local(LocalKey), + #[cfg(not(feature = "emulator-tests"))] + Ledger(Ledger), + #[cfg(feature = "emulator-tests")] + Ledger(Ledger), Lab, SecureStore(SecureStoreEntry), } impl Signer { - pub fn sign_tx( + pub async fn sign_tx( &self, tx: Transaction, network: &Network, @@ -226,10 +233,10 @@ impl Signer { tx, signatures: VecM::default(), }); - self.sign_tx_env(&tx_env, network) + self.sign_tx_env(&tx_env, network).await } - pub fn sign_tx_env( + pub async fn sign_tx_env( &self, tx_env: &TransactionEnvelope, network: &Network, @@ -242,6 +249,7 @@ impl Signer { let decorated_signature = match &self.kind { SignerKind::Local(key) => key.sign_tx_hash(tx_hash)?, SignerKind::Lab => Lab::sign_tx_env(tx_env, network, &self.print)?, + SignerKind::Ledger(ledger) => ledger.sign_transaction_hash(&tx_hash).await?, SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash)?, }; let mut sigs = signatures.clone().into_vec(); @@ -260,6 +268,76 @@ pub struct LocalKey { pub key: ed25519_dalek::SigningKey, } +#[allow(dead_code)] +pub struct Ledger { + pub(crate) index: u32, + pub(crate) signer: LedgerSigner, +} + +impl Ledger { + pub async fn sign_transaction_hash( + &self, + tx_hash: &[u8; 32], + ) -> Result { + let key = self.public_key().await?; + let hint = SignatureHint(key.0[28..].try_into()?); + let signature = Signature( + self.signer + .sign_transaction_hash(self.index, tx_hash) + .await? + .try_into()?, + ); + Ok(DecoratedSignature { hint, signature }) + } + + pub async fn sign_transaction( + &self, + tx: Transaction, + network_passphrase: &str, + ) -> Result { + let network_id = Hash(Sha256::digest(network_passphrase).into()); + let signature = self + .signer + .sign_transaction(self.index, tx, network_id) + .await?; + let key = self.public_key().await?; + let hint = SignatureHint(key.0[28..].try_into()?); + let signature = Signature(signature.try_into()?); + Ok(DecoratedSignature { hint, signature }) + } + + pub async fn public_key(&self) -> Result { + Ok(self.signer.get_public_key(&self.index.into()).await?) + } +} + +#[cfg(not(feature = "emulator-tests"))] +pub async fn ledger(hd_path: u32) -> Result, Error> { + let signer = stellar_ledger::native()?; + Ok(Ledger { + index: hd_path, + signer, + }) +} + +#[cfg(feature = "emulator-tests")] +pub async fn ledger( + hd_path: u32, +) -> Result, Error> { + use stellar_ledger::emulator_test_support::ledger as emulator_ledger; + // port from SPECULOS_PORT ENV var + let host_port: u16 = std::env::var("SPECULOS_PORT") + .expect("SPECULOS_PORT env var not set") + .parse() + .expect("port must be a number"); + let signer = emulator_ledger(host_port).await; + + Ok(Ledger { + index: hd_path, + signer, + }) +} + impl LocalKey { pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?); diff --git a/cmd/soroban-cli/src/tx/builder/asset.rs b/cmd/soroban-cli/src/tx/builder/asset.rs index e2bf90d2d..f03e2c353 100644 --- a/cmd/soroban-cli/src/tx/builder/asset.rs +++ b/cmd/soroban-cli/src/tx/builder/asset.rs @@ -40,7 +40,9 @@ impl Asset { pub fn resolve(&self, locator: &locator::Args) -> Result { Ok(match self { Asset::Asset(code, issuer) => { - let issuer = issuer.resolve_muxed_account(locator, None)?.account_id(); + let issuer = issuer + .resolve_muxed_account_sync(locator, None)? + .account_id(); match code.clone() { AssetCode::CreditAlphanum4(asset_code) => { xdr::Asset::CreditAlphanum4(AlphaNum4 { asset_code, issuer }) diff --git a/cmd/stellar-cli/Cargo.toml b/cmd/stellar-cli/Cargo.toml index e1f1dce6b..4a2808f6f 100644 --- a/cmd/stellar-cli/Cargo.toml +++ b/cmd/stellar-cli/Cargo.toml @@ -27,6 +27,7 @@ bin-dir = "{ bin }{ binary-ext }" [features] default = [] opt = ["soroban-cli/opt"] +emulator-tests = ["soroban-cli/emulator-tests"] [dependencies] soroban-cli = { workspace = true }