diff --git a/.github/workflows/ledger-emulator.yml b/.github/workflows/ledger-emulator.yml new file mode 100644 index 000000000..4a092c1ba --- /dev/null +++ b/.github/workflows/ledger-emulator.yml @@ -0,0 +1,24 @@ +name: Ledger Emulator Tests + +on: + push: + branches: [main, release/**] + pull_request: + +defaults: + run: + shell: bash + +jobs: + emulator-tests: + runs-on: ubuntu-latest + env: + CI_TESTS: true + steps: + - uses: actions/checkout@v3 + - uses: stellar/actions/rust-cache@main + - name: install libudev-dev + run: | + sudo apt install -y libudev-dev + - run: | + cargo test --manifest-path cmd/crates/stellar-ledger/Cargo.toml --features "emulator-tests" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f212fff1a..50a60714a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,31 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "assert_cmd" version = "2.0.14" @@ -141,6 +166,204 @@ dependencies = [ "tempfile", ] +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy 0.5.2", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.0", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io 2.3.2", + "async-lock 3.3.0", + "blocking", + "futures-lite 2.3.0", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.0", + "rustix 0.38.34", + "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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + +[[package]] +name = "async-object-pool" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb901c30ebc2fc4ab46395bbfbdba9542c16559d853645d75190c3056caf3bc" +dependencies = [ + "async-std", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.34", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-signal" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +dependencies = [ + "async-io 2.3.2", + "async-lock 3.3.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.34", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.76" @@ -152,6 +375,12 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -209,6 +438,17 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-cookies" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + [[package]] name = "beef" version = "0.5.2" @@ -218,6 +458,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -248,21 +503,37 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + [[package]] name = "bollard" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03db470b3c0213c47e978da93200259a1eb4dae2e5512cba9955e2b540a6fc6" +checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bollard-stubs", "bytes", "futures-core", "futures-util", "hex", - "http 0.2.12", - "hyper", - "hyperlocal", + "http 1.1.0", + "http-body-util", + "hyper 1.3.1", + "hyper-named-pipe", + "hyper-util", + "hyperlocal-next", "log", "pin-project-lite", "serde", @@ -273,15 +544,16 @@ dependencies = [ "thiserror", "tokio", "tokio-util", + "tower-service", "url", "winapi", ] [[package]] name = "bollard-stubs" -version = "1.43.0-rc.2" +version = "1.44.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58071e8fd9ec1e930efd28e3a90c1251015872a2ce49f81f36421b86466932e" +checksum = "709d9aa1c37abb89d40f19f5d0ad6f0d88cb1581264e571c9350fc5bb89cf1c5" dependencies = [ "serde", "serde_repr", @@ -482,6 +754,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -578,6 +859,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -843,6 +1130,16 @@ dependencies = [ "dirs-sys 0.3.7", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -866,6 +1163,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -972,6 +1280,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -981,6 +1298,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1009,12 +1349,80 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" +[[package]] +name = "event-listener" +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 = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", +] + [[package]] name = "faster-hex" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +[[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" @@ -1049,6 +1457,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.30" @@ -1104,6 +1518,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -1111,6 +1540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1119,12 +1549,51 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[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", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -1154,6 +1623,7 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1533,7 +2003,7 @@ dependencies = [ "itoa", "libc", "memmap2", - "rustix", + "rustix 0.38.34", "smallvec", "thiserror", ] @@ -1696,7 +2166,7 @@ dependencies = [ "gix-command", "gix-config-value", "parking_lot", - "rustix", + "rustix 0.38.34", "thiserror", ] @@ -1897,7 +2367,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc" dependencies = [ - "fastrand", + "fastrand 2.1.0", "unicode-normalization", ] @@ -1979,6 +2449,18 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "group" version = "0.13.0" @@ -2063,6 +2545,18 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hidapi" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "winapi", +] + [[package]] name = "hmac" version = "0.9.0" @@ -2124,6 +2618,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.8.0" @@ -2136,6 +2653,40 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "httpmock" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-std", + "async-trait", + "base64 0.21.7", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper 0.14.28", + "lazy_static", + "levenshtein", + "log", + "regex", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio", + "url", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.28" @@ -2148,18 +2699,52 @@ dependencies = [ "futures-util", "h2", "http 0.2.12", - "http-body", + "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.7", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.3.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -2168,7 +2753,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper", + "hyper 0.14.28", "log", "rustls 0.21.12", "rustls-native-certs", @@ -2183,23 +2768,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", ] [[package]] -name = "hyperlocal" -version = "0.8.0" +name = "hyper-util" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" +checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e" dependencies = [ + "bytes", + "futures-channel", "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperlocal-next" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf569d43fa9848e510358c07b80f4adf34084ddc28c6a4a651ee8474c070dcc" +dependencies = [ "hex", - "hyper", - "pin-project", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "pin-project-lite", "tokio", + "tower-service", ] [[package]] @@ -2305,6 +2912,15 @@ 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-close" version = "0.3.7" @@ -2315,6 +2931,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -2379,7 +3006,7 @@ dependencies = [ "async-trait", "beef", "futures-util", - "hyper", + "hyper 0.14.28", "jsonrpsee-types", "serde", "serde_json", @@ -2395,7 +3022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f80c17f62c7653ce767e3d7288b793dfec920f97067ceb189ebdd3570f2bc20" dependencies = [ "async-trait", - "hyper", + "hyper 0.14.28", "hyper-rustls", "jsonrpsee-core", "jsonrpsee-types", @@ -2453,10 +3080,93 @@ dependencies = [ ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.11.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax 0.8.3", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata 0.4.6", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "ledger-apdu" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" +dependencies = [ + "arrayref", + "no-std-compat", + "snafu", +] + +[[package]] +name = "ledger-transport" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1117f2143d92c157197785bf57711d7b02f2cfa101e162f8ca7900fb7f976321" +dependencies = [ + "async-trait", + "ledger-apdu", +] + +[[package]] +name = "ledger-transport-hid" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" +dependencies = [ + "byteorder", + "cfg-if", + "hex", + "hidapi", + "ledger-transport", + "libc", + "log", + "thiserror", +] + +[[package]] +name = "levenshtein" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" @@ -2489,6 +3199,12 @@ dependencies = [ "cc", ] +[[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" @@ -2510,6 +3226,9 @@ name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +dependencies = [ + "value-bag", +] [[package]] name = "matchers" @@ -2590,6 +3309,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -2771,11 +3502,17 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2821,6 +3558,73 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.2.6", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.5" @@ -2853,6 +3657,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2875,6 +3690,37 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2887,6 +3733,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "predicates" version = "2.1.5" @@ -3103,13 +3955,15 @@ dependencies = [ "futures-util", "h2", "http 0.2.12", - "http-body", - "hyper", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -3121,6 +3975,7 @@ dependencies = [ "sync_wrapper", "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls", "tower-service", "url", @@ -3232,6 +4087,20 @@ 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" @@ -3241,7 +4110,7 @@ dependencies = [ "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] @@ -3340,6 +4209,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -3371,6 +4249,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "sec1" version = "0.7.2" @@ -3470,6 +4354,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.17" @@ -3532,6 +4426,31 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "serial_test" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -3618,6 +4537,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "similar" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -3644,6 +4575,38 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "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" @@ -3693,7 +4656,7 @@ dependencies = [ "hex", "home", "http 0.2.12", - "hyper", + "hyper 0.14.28", "hyper-tls", "itertools 0.10.5", "jsonrpsee-core", @@ -4038,6 +5001,44 @@ dependencies = [ "soroban-cli", ] +[[package]] +name = "stellar-ledger" +version = "21.0.0-rc.1" +dependencies = [ + "async-trait", + "bollard", + "byteorder", + "ed25519-dalek 2.0.0", + "env_logger", + "futures", + "hex", + "home", + "httpmock", + "ledger-transport", + "ledger-transport-hid", + "log", + "once_cell", + "phf", + "pretty_assertions", + "reqwest", + "sep5", + "serde", + "serde_derive", + "serde_json", + "serial_test", + "sha2 0.9.9", + "slip10", + "soroban-env-host", + "soroban-spec", + "stellar-rpc-client", + "stellar-strkey 0.0.8", + "stellar-xdr", + "testcontainers", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "stellar-rpc-client" version = "21.0.1" @@ -4107,6 +5108,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.10.0" @@ -4224,11 +5238,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand", - "rustix", + "fastrand 2.1.0", + "rustix 0.38.34", "windows-sys 0.52.0", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -4296,6 +5321,22 @@ dependencies = [ "soroban-sdk", ] +[[package]] +name = "testcontainers" +version = "0.15.0" +source = "git+https://github.com/testcontainers/testcontainers-rs.git?rev=4b3e4f08a2c0bdf521636b03f959f004e6d216aa#4b3e4f08a2c0bdf521636b03f959f004e6d216aa" +dependencies = [ + "bollard-stubs", + "futures", + "hex", + "hmac 0.12.1", + "log", + "rand", + "serde", + "serde_json", + "sha2 0.10.8", +] + [[package]] name = "thiserror" version = "1.0.55" @@ -4378,6 +5419,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -4407,7 +5457,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.7", "tokio-macros", "windows-sys 0.48.0", ] @@ -4520,6 +5570,7 @@ dependencies = [ "futures-util", "pin-project", "pin-project-lite", + "tokio", "tower-layer", "tower-service", "tracing", @@ -4674,6 +5725,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "untrusted" version = "0.9.0" @@ -4722,6 +5779,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "value-bag" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4743,6 +5806,12 @@ 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" @@ -4965,7 +6034,7 @@ dependencies = [ "home", "once_cell", "regex", - "rustix", + "rustix 0.38.34", ] [[package]] @@ -5183,9 +6252,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 4c54be5cc..09b7daad2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ ethnum = "1.3.2" hex = "0.4.3" itertools = "0.10.0" async-trait = "0.1.76" +bollard = "0.16.0" serde-aux = "4.1.2" serde_json = "1.0.82" diff --git a/cmd/crates/stellar-ledger/Cargo.toml b/cmd/crates/stellar-ledger/Cargo.toml new file mode 100644 index 000000000..89676db08 --- /dev/null +++ b/cmd/crates/stellar-ledger/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "stellar-ledger" +description = "" +homepage = "https://github.com/stellar/soroban-tools" +repository = "https://github.com/stellar/soroban-tools" +authors = ["Stellar Development Foundation "] +readme = "README.md" +license = "Apache-2.0" +version.workspace = true +edition = "2021" +rust-version.workspace = true + +[dependencies] +soroban-spec = { workspace = true } +thiserror = "1.0.32" +serde = "1.0.82" +serde_derive = "1.0.82" +serde_json = "1.0.82" +sha2 = "0.9.9" +soroban-env-host = { workspace = true } +ed25519-dalek = { workspace = true } +stellar-strkey = { workspace = true } +ledger-transport-hid = "0.10.0" +ledger-transport = "0.10.0" +sep5.workspace = true +slip10 = "0.4.3" +tracing = { workspace = true } +hex.workspace = true +byteorder = "1.5.0" +bollard = { workspace = true } +home = "0.5.9" +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.11", features = ["json"] } +soroban-rpc.workspace = true +testcontainers = { git = "https://github.com/testcontainers/testcontainers-rs.git", rev = "4b3e4f08a2c0bdf521636b03f959f004e6d216aa" } +phf = { version = "0.11.2", features = ["macros"] } +futures = "0.3.30" +async-trait = { workspace = true } + +[dependencies.stellar-xdr] +workspace = true +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 = "0.7.0-rc.1" + + +[features] +emulator-tests = [] diff --git a/cmd/crates/stellar-ledger/README.md b/cmd/crates/stellar-ledger/README.md new file mode 100644 index 000000000..9eefe7d5a --- /dev/null +++ b/cmd/crates/stellar-ledger/README.md @@ -0,0 +1,24 @@ +# Stellar Ledger + +This crate allows for interaction with Ledger devices, and exposes the following functions: + +- `get_app_configuration` +- `get_public_key` +- `sign_transaction_hash` +- `sign_transaction` +- `sign_blob` + +## Tests + +There are several unit tests in lib.rs, as well as integration-like tests in the emulator_tests.rs file. emulator_tests.rs uses [testcontainers-rs](https://github.com/testcontainers/testcontainers-rs) to spin up a docker container running a Ledger emulator called [Speculos](https://github.com/LedgerHQ/speculos). + +## Resources + +- [LedgerHQ/ledger-live/../hw-app-str](https://github.com/LedgerHQ/ledger-live/tree/develop/libs/ledgerjs/packages/hw-app-str) is javascript implementation of the API for interacting with the Stellar app on Ledger devices. We used this as a reference when building the `stellar-ledger` crate. +- The communication protocol used by Ledger devices expects commands to be sent as Application Protocol Data Units (APDU). + - More information about how APDUs are structured can be found here [https://github.com/LedgerHQ/app-stellar/blob/develop/docs/APDU.md](https://github.com/LedgerHQ/app-stellar/blob/develop/docs/APDU.md). + - The list of commands that the Stellar App on Ledger devices currently supports can be found here [https://github.com/LedgerHQ/app-stellar/blob/develop/docs/COMMANDS.md](https://github.com/LedgerHQ/app-stellar/blob/develop/docs/COMMANDS.md). +- The Ledger emulator we're using for integration-style tests is LedgerHQ's [Speculos](https://github.com/LedgerHQ/speculos). +- The testing setup was also partially based on Zondax's [Zemu](https://github.com/Zondax/zemu) testing framework, which makes use of Speculos. +- To connect with a real ledger device, we use Zondax's [ledger-rs](https://github.com/Zondax/ledger-rs) crate. +- To connect with the emulated ledger (Speculos), we created a custom `EmulatorHttpTransport` that can connect to the emulated ledger via HTTP. This is based on [Zondax's `ledger-transport-zemu` crate](https://github.com/Zondax/ledger-rs/blob/20e2a2076d799d449ff6f07eb0128548b358d9bc/ledger-transport-zemu) (which has since been deprecated). diff --git a/cmd/crates/stellar-ledger/apps/stellarNanosApp.elf b/cmd/crates/stellar-ledger/apps/stellarNanosApp.elf new file mode 100755 index 000000000..9163d930d Binary files /dev/null and b/cmd/crates/stellar-ledger/apps/stellarNanosApp.elf differ diff --git a/cmd/crates/stellar-ledger/src/emulator_http_transport.rs b/cmd/crates/stellar-ledger/src/emulator_http_transport.rs new file mode 100644 index 000000000..66454254c --- /dev/null +++ b/cmd/crates/stellar-ledger/src/emulator_http_transport.rs @@ -0,0 +1,101 @@ +// This is based on the `ledger-transport-zemu` crate's TransportZemuHttp: https://github.com/Zondax/ledger-rs/tree/master/ledger-transport-zemu +// Instead of using TransportZemuHttp mod from the crate, we are including a custom copy here for a couple of reasons: +// - we get more control over the mod for our testing purposes +// - the ledger-transport-zemu TransportZemuHttp includes a Grpc implementation that we don't need right now, and was causing some errors with dependency mismatches when trying to use the whole TransportZemuHttp mod. + +use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE}; +use reqwest::{Client as HttpClient, Response}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::time::Duration; + +use ledger_transport::{async_trait, APDUAnswer, APDUCommand, Exchange}; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum LedgerZemuError { + /// zemu reponse error + #[error("Zemu response error")] + ResponseError, + /// Inner error + #[error("Ledger inner error")] + InnerError, +} + +pub struct EmulatorHttpTransport { + url: String, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +struct ZemuRequest { + apdu_hex: String, +} + +#[derive(Deserialize, Debug, Clone)] +struct ZemuResponse { + data: String, + error: Option, +} + +impl EmulatorHttpTransport { + #[allow(dead_code)] //this is being used in tests only + pub fn new(host: &str, port: u16) -> Self { + Self { + url: format!("http://{host}:{port}"), + } + } +} + +#[async_trait] +impl Exchange for EmulatorHttpTransport { + type Error = LedgerZemuError; + type AnswerType = Vec; + + async fn exchange( + &self, + command: &APDUCommand, + ) -> Result, Self::Error> + where + I: Deref + Send + Sync, + { + let raw_command = hex::encode(command.serialize()); + let request = ZemuRequest { + apdu_hex: raw_command, + }; + + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + let resp: Response = HttpClient::new() + .post(&self.url) + .headers(headers) + .timeout(Duration::from_secs(20)) + .json(&request) + .send() + .await + .map_err(|e| { + tracing::error!("create http client error: {:?}", e); + LedgerZemuError::InnerError + })?; + tracing::debug!("http response: {:?}", resp); + + if resp.status().is_success() { + let result: ZemuResponse = resp.json().await.map_err(|e| { + tracing::error!("error response: {:?}", e); + LedgerZemuError::ResponseError + })?; + if result.error.is_none() { + APDUAnswer::from_answer(hex::decode(result.data).expect("decode error")) + .map_err(|_| LedgerZemuError::ResponseError) + } else { + Err(LedgerZemuError::ResponseError) + } + } else { + tracing::error!("error response: {:?}", resp.status()); + Err(LedgerZemuError::ResponseError) + } + } +} diff --git a/cmd/crates/stellar-ledger/src/emulator_tests.rs b/cmd/crates/stellar-ledger/src/emulator_tests.rs new file mode 100644 index 000000000..c6c90dfb1 --- /dev/null +++ b/cmd/crates/stellar-ledger/src/emulator_tests.rs @@ -0,0 +1,353 @@ +use ledger_transport::Exchange; +use serde::Deserialize; +use soroban_env_host::xdr::Transaction; +use soroban_env_host::xdr::{self, Operation, OperationBody, Uint256}; +use std::vec; + +use crate::emulator_http_transport::EmulatorHttpTransport; +use crate::hd_path::HdPath; +use crate::speculos::Speculos; +use crate::{test_network_hash, Blob, Error, LedgerSigner}; + +use std::sync::Arc; +use std::{collections::HashMap, time::Duration}; + +use stellar_xdr::curr::{ + Memo, MuxedAccount, PaymentOp, Preconditions, SequenceNumber, TransactionExt, +}; + +use testcontainers::clients; +use tokio::time::sleep; + +fn ledger(host_port: u16) -> LedgerSigner { + LedgerSigner::new(get_http_transport("127.0.0.1", host_port).unwrap()) +} + +#[tokio::test] +async fn test_get_public_key() { + let docker = clients::Cli::default(); + let node = docker.run(Speculos::new()); + let host_port = node.get_host_port_ipv4(9998); + let ui_host_port: u16 = node.get_host_port_ipv4(5000); + + wait_for_emulator_start_text(ui_host_port).await; + + let ledger = ledger(host_port); + + match ledger.get_public_key(&0.into()).await { + Ok(public_key) => { + let public_key_string = public_key.to_string(); + // This is determined by the seed phrase used to start up the emulator + // TODO: make the seed phrase configurable + let expected_public_key = "GDUTHCF37UX32EMANXIL2WOOVEDZ47GHBTT3DYKU6EKM37SOIZXM2FN7"; + assert_eq!(public_key_string, expected_public_key); + } + Err(e) => { + node.stop(); + println!("{e}"); + assert!(false); + } + } + + node.stop(); +} + +#[tokio::test] +async fn test_get_app_configuration() { + let docker = clients::Cli::default(); + let node = docker.run(Speculos::new()); + let host_port = node.get_host_port_ipv4(9998); + let ui_host_port: u16 = node.get_host_port_ipv4(5000); + + wait_for_emulator_start_text(ui_host_port).await; + + let ledger = ledger(host_port); + + match ledger.get_app_configuration().await { + Ok(config) => { + assert_eq!(config, vec![0, 5, 0, 3]); + } + Err(e) => { + node.stop(); + println!("{e}"); + assert!(false); + } + }; + + node.stop(); +} + +#[tokio::test] +async fn test_sign_tx() { + let docker = clients::Cli::default(); + let node = docker.run(Speculos::new()); + let host_port = node.get_host_port_ipv4(9998); + let ui_host_port: u16 = node.get_host_port_ipv4(5000); + + wait_for_emulator_start_text(ui_host_port).await; + + let ledger = Arc::new(ledger(host_port)); + + let path = HdPath(0); + + let source_account_str = "GAQNVGMLOXSCWH37QXIHLQJH6WZENXYSVWLPAEF4673W64VRNZLRHMFM"; + let source_account_bytes = match stellar_strkey::Strkey::from_string(source_account_str) { + Ok(key) => match key { + stellar_strkey::Strkey::PublicKeyEd25519(p) => p.0, + _ => { + eprintln!("Error decoding public key: {:?}", key); + return; + } + }, + Err(err) => { + eprintln!("Error decoding public key: {}", err); + return; + } + }; + + let destination_account_str = "GCKUD4BHIYSAYHU7HBB5FDSW6CSYH3GSOUBPWD2KE7KNBERP4BSKEJDV"; + let destination_account_bytes = + match stellar_strkey::Strkey::from_string(destination_account_str) { + Ok(key) => match key { + stellar_strkey::Strkey::PublicKeyEd25519(p) => p.0, + _ => { + eprintln!("Error decoding public key: {:?}", key); + return; + } + }, + Err(err) => { + eprintln!("Error decoding public key: {}", err); + return; + } + }; + + let tx = Transaction { + source_account: MuxedAccount::Ed25519(Uint256(source_account_bytes)), + fee: 100, + seq_num: SequenceNumber(1), + cond: Preconditions::None, + memo: Memo::Text("Stellar".as_bytes().try_into().unwrap()), + ext: TransactionExt::V0, + operations: [Operation { + source_account: Some(MuxedAccount::Ed25519(Uint256(source_account_bytes))), + body: OperationBody::Payment(PaymentOp { + destination: MuxedAccount::Ed25519(Uint256(destination_account_bytes)), + asset: xdr::Asset::Native, + amount: 100, + }), + }] + .try_into() + .unwrap(), + }; + + let sign = tokio::task::spawn({ + 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)); + + let result = sign.await.unwrap(); + let _ = approve.await.unwrap(); + + match result { + Ok(response) => { + assert_eq!( hex::encode(response), "5c2f8eb41e11ab922800071990a25cf9713cc6e7c43e50e0780ddc4c0c6da50c784609ef14c528a12f520d8ea9343b49083f59c51e3f28af8c62b3edeaade60e"); + } + Err(e) => { + node.stop(); + println!("{e}"); + assert!(false); + } + }; + + node.stop(); +} + +#[tokio::test] +async fn test_sign_tx_hash_when_hash_signing_is_not_enabled() { + let docker = clients::Cli::default(); + let node = docker.run(Speculos::new()); + let host_port = node.get_host_port_ipv4(9998); + let ui_host_port: u16 = node.get_host_port_ipv4(5000); + + wait_for_emulator_start_text(ui_host_port).await; + + let ledger = ledger(host_port); + + let path = 0; + let test_hash = b"313e8447f569233bb8db39aa607c8889"; + + let result = ledger.sign_transaction_hash(path, test_hash).await; + if let Err(Error::APDUExchangeError(msg)) = result { + assert_eq!(msg, "Ledger APDU retcode: 0x6C66"); + // this error code is SW_TX_HASH_SIGNING_MODE_NOT_ENABLED https://github.com/LedgerHQ/app-stellar/blob/develop/docs/COMMANDS.md + } else { + node.stop(); + panic!("Unexpected result: {:?}", result); + } + + node.stop(); +} + +#[tokio::test] +async fn test_sign_tx_hash_when_hash_signing_is_enabled() { + let docker = clients::Cli::default(); + let node = docker.run(Speculos::new()); + let host_port = node.get_host_port_ipv4(9998); + let ui_host_port: u16 = node.get_host_port_ipv4(5000); + + wait_for_emulator_start_text(ui_host_port).await; + enable_hash_signing(ui_host_port).await; + + let ledger = Arc::new(ledger(host_port)); + + let path = 0; + let mut test_hash = [0u8; 32]; + + match hex::decode_to_slice( + "313e8447f569233bb8db39aa607c8889313e8447f569233bb8db39aa607c8889", + &mut test_hash as &mut [u8], + ) { + Ok(()) => {} + Err(e) => { + node.stop(); + panic!("Unexpected result: {e}"); + } + } + + let sign = tokio::task::spawn({ + 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)); + + let response = sign.await.unwrap(); + let _ = approve.await.unwrap(); + + match response { + Ok(response) => { + assert_eq!( hex::encode(response), "e0fa9d19f34ddd494bbb794645fc82eb5ebab29e74160f1b1d5697e749aada7c6b367236df87326b0fdc921ed39702242fc8b14414f4e0ee3e775f1fd0208101"); + } + Err(e) => { + node.stop(); + panic!("Unexpected result: {e}"); + } + } + + node.stop(); +} + +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, +} + +fn get_http_transport(host: &str, port: u16) -> Result { + Ok(EmulatorHttpTransport::new(host, port)) +} + +async fn wait_for_emulator_start_text(ui_host_port: u16) { + sleep(Duration::from_secs(1)).await; + + let mut ready = false; + while !ready { + let events = get_emulator_events(ui_host_port).await; + + if events.iter().any(|event| event.text == "is ready") { + ready = true; + } + } +} + +async fn get_emulator_events(ui_host_port: u16) -> Vec { + let client = reqwest::Client::new(); + let resp = client + .get(format!("http://localhost:{ui_host_port}/events")) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + resp.events +} + +async fn approve_tx_hash_signature(ui_host_port: u16) { + for _ in 0..10 { + click(ui_host_port, "button/right").await; + } + + click(ui_host_port, "button/both").await; +} + +async fn approve_tx_signature(ui_host_port: u16) { + let mut map = HashMap::new(); + map.insert("action", "press-and-release"); + + let client = reqwest::Client::new(); + for _ in 0..17 { + client + .post(format!("http://localhost:{ui_host_port}/button/right")) + .json(&map) + .send() + .await + .unwrap(); + } + + client + .post(format!("http://localhost:{ui_host_port}/button/both")) + .json(&map) + .send() + .await + .unwrap(); +} diff --git a/cmd/crates/stellar-ledger/src/hd_path.rs b/cmd/crates/stellar-ledger/src/hd_path.rs new file mode 100644 index 000000000..fd2a227f5 --- /dev/null +++ b/cmd/crates/stellar-ledger/src/hd_path.rs @@ -0,0 +1,50 @@ +use crate::Error; + +#[derive(Clone, Copy)] +pub struct HdPath(pub(crate) u32); + +impl HdPath { + pub fn depth(&self) -> u8 { + let path: slip10::BIP32Path = self.into(); + path.depth() + } +} + +impl From for HdPath { + fn from(index: u32) -> Self { + HdPath(index) + } +} + +impl From<&u32> for HdPath { + fn from(index: &u32) -> Self { + HdPath(*index) + } +} + +impl HdPath { + pub fn to_vec(&self) -> Result, Error> { + hd_path_to_bytes(&self.into()) + } +} + +impl From<&HdPath> for slip10::BIP32Path { + fn from(value: &HdPath) -> Self { + let index = value.0; + format!("m/44'/148'/{index}'").parse().unwrap() + } +} + +fn hd_path_to_bytes(hd_path: &slip10::BIP32Path) -> Result, Error> { + let hd_path_indices = 0..hd_path.depth(); + let result = hd_path_indices + .into_iter() + .map(|index| { + Ok(hd_path + .index(index) + .ok_or_else(|| Error::Bip32PathError(format!("{hd_path}")))? + .to_be_bytes()) + }) + .collect::, Error>>()?; + Ok(result.into_iter().flatten().collect()) +} diff --git a/cmd/crates/stellar-ledger/src/lib.rs b/cmd/crates/stellar-ledger/src/lib.rs new file mode 100644 index 000000000..9dd5bdee5 --- /dev/null +++ b/cmd/crates/stellar-ledger/src/lib.rs @@ -0,0 +1,494 @@ +use hd_path::HdPath; +use ledger_transport::{APDUCommand, Exchange}; +use ledger_transport_hid::{ + hidapi::{HidApi, HidError}, + LedgerHIDError, TransportNativeHID, +}; + +use soroban_env_host::xdr::{Hash, Transaction}; +use std::vec; +use stellar_strkey::DecodeError; +use stellar_xdr::curr::{ + self as xdr, Limits, TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + WriteXdr, +}; + +pub use crate::signer::Blob; + +mod emulator_http_transport; +mod signer; +mod speculos; + +#[cfg(all(test, feature = "emulator-tests"))] +mod emulator_tests; +pub mod hd_path; + +// 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; +const BUFFER_SIZE: u8 = 1 + HD_PATH_ELEMENTS_COUNT * 4; +const CHUNK_SIZE: u8 = APDU_MAX_SIZE - BUFFER_SIZE; + +// These constant values are from https://github.com/LedgerHQ/app-stellar/blob/develop/docs/COMMANDS.md +const SIGN_TX_RESPONSE_SIZE: usize = 64; + +const CLA: u8 = 0xE0; + +const GET_PUBLIC_KEY: u8 = 0x02; +const P1_GET_PUBLIC_KEY: u8 = 0x00; +const P2_GET_PUBLIC_KEY_NO_DISPLAY: u8 = 0x00; +const P2_GET_PUBLIC_KEY_DISPLAY: u8 = 0x01; + +const SIGN_TX: u8 = 0x04; +const P1_SIGN_TX_FIRST: u8 = 0x00; +const P1_SIGN_TX_NOT_FIRST: u8 = 0x80; +const P2_SIGN_TX_LAST: u8 = 0x00; +const P2_SIGN_TX_MORE: u8 = 0x80; + +const GET_APP_CONFIGURATION: u8 = 0x06; +const P1_GET_APP_CONFIGURATION: u8 = 0x00; +const P2_GET_APP_CONFIGURATION: u8 = 0x00; + +const SIGN_TX_HASH: u8 = 0x08; +const P1_SIGN_TX_HASH: u8 = 0x00; +const P2_SIGN_TX_HASH: u8 = 0x00; + +const RETURN_CODE_OK: u16 = 36864; // APDUAnswer.retcode which means success from Ledger + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Error occurred while initializing HIDAPI: {0}")] + HidApiError(#[from] HidError), + + #[error("Error occurred while initializing Ledger HID transport: {0}")] + LedgerHidError(#[from] LedgerHIDError), + + #[error("Error with ADPU exchange with Ledger device: {0}")] + APDUExchangeError(String), + + #[error("Error occurred while exchanging with Ledger device: {0}")] + LedgerConnectionError(String), + + #[error("Error occurred while parsing BIP32 path: {0}")] + Bip32PathError(String), + + #[error(transparent)] + XdrError(#[from] xdr::Error), + + #[error(transparent)] + DecodeError(#[from] DecodeError), +} + +pub struct LedgerSigner { + transport: T, +} + +unsafe impl Send for LedgerSigner where T: Exchange {} +unsafe impl Sync for LedgerSigner where T: Exchange {} + +pub fn native() -> Result, Error> { + Ok(LedgerSigner { + transport: get_transport()?, + }) +} + +impl LedgerSigner +where + T: Exchange, +{ + pub fn new(transport: T) -> Self { + Self { transport } + } + pub fn native() -> Result, Error> { + Ok(LedgerSigner { + transport: get_transport()?, + }) + } + /// Get the device app's configuration + /// # Errors + /// Returns an error if there is an issue with connecting with the device or getting the config from the device + pub async fn get_app_configuration(&self) -> Result, Error> { + let command = APDUCommand { + cla: CLA, + ins: GET_APP_CONFIGURATION, + p1: P1_GET_APP_CONFIGURATION, + p2: P2_GET_APP_CONFIGURATION, + data: vec![], + }; + self.send_command_to_ledger(command).await + } + + /// Sign a Stellar transaction hash with the account on the Ledger device + /// based on impl from [https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L166](https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L166) + /// # Errors + /// Returns an error if there is an issue with connecting with the device or signing the given tx on the device. Or, if the device has not enabled hash signing + pub async fn sign_transaction_hash( + &self, + hd_path: impl Into, + transaction_hash: &[u8; 32], + ) -> Result, Error> { + self.sign_blob(&hd_path.into(), transaction_hash).await + } + + /// Sign a Stellar transaction with the account on the Ledger device + /// # Errors + /// Returns an error if there is an issue with connecting with the device or signing the given tx on the device + #[allow(clippy::missing_panics_doc)] + pub async fn sign_transaction( + &self, + hd_path: impl Into, + transaction: Transaction, + network_id: Hash, + ) -> Result, Error> { + let tagged_transaction = TransactionSignaturePayloadTaggedTransaction::Tx(transaction); + let signature_payload = TransactionSignaturePayload { + network_id, + tagged_transaction, + }; + let mut signature_payload_as_bytes = signature_payload.to_xdr(Limits::none())?; + + let mut hd_path_to_bytes = hd_path.into().to_vec()?; + + let capacity = 1 + hd_path_to_bytes.len() + signature_payload_as_bytes.len(); + let mut data: Vec = Vec::with_capacity(capacity); + + data.insert(0, HD_PATH_ELEMENTS_COUNT); + data.append(&mut hd_path_to_bytes); + data.append(&mut signature_payload_as_bytes); + + let chunks = data.chunks(CHUNK_SIZE as usize); + let chunks_count = chunks.len(); + + let mut result = Vec::with_capacity(SIGN_TX_RESPONSE_SIZE); + for (i, chunk) in chunks.enumerate() { + let is_first_chunk = i == 0; + let is_last_chunk = chunks_count == i + 1; + + let command = APDUCommand { + cla: CLA, + ins: SIGN_TX, + p1: if is_first_chunk { + P1_SIGN_TX_FIRST + } else { + P1_SIGN_TX_NOT_FIRST + }, + p2: if is_last_chunk { + P2_SIGN_TX_LAST + } else { + P2_SIGN_TX_MORE + }, + data: chunk.to_vec(), + }; + + let mut r = self.send_command_to_ledger(command).await?; + result.append(&mut r); + } + + Ok(result) + } + + /// The `display_and_confirm` bool determines if the Ledger will display the public key on its screen and requires user approval to share + async fn get_public_key_with_display_flag( + &self, + hd_path: impl Into, + display_and_confirm: bool, + ) -> Result { + // convert the hd_path into bytes to be sent as `data` to the Ledger + // the first element of the data should be the number of elements in the path + let hd_path = hd_path.into(); + let hd_path_elements_count = hd_path.depth(); + let mut hd_path_to_bytes = hd_path.to_vec()?; + hd_path_to_bytes.insert(0, hd_path_elements_count); + + let p2 = if display_and_confirm { + P2_GET_PUBLIC_KEY_DISPLAY + } else { + P2_GET_PUBLIC_KEY_NO_DISPLAY + }; + + // more information about how to build this command can be found at https://github.com/LedgerHQ/app-stellar/blob/develop/docs/COMMANDS.md + let command = APDUCommand { + cla: CLA, + ins: GET_PUBLIC_KEY, + p1: P1_GET_PUBLIC_KEY, + p2, + data: hd_path_to_bytes, + }; + + tracing::info!("APDU in: {}", hex::encode(command.serialize())); + + self.send_command_to_ledger(command) + .await + .and_then(|p| Ok(stellar_strkey::ed25519::PublicKey::from_payload(&p)?)) + } + + async fn send_command_to_ledger( + &self, + command: APDUCommand>, + ) -> Result, Error> { + match self.transport.exchange(&command).await { + Ok(response) => { + tracing::info!( + "APDU out: {}\nAPDU ret code: {:x}", + hex::encode(response.apdu_data()), + response.retcode(), + ); + // Ok means we successfully connected with the Ledger but it doesn't mean our request succeeded. We still need to check the response.retcode + if response.retcode() == RETURN_CODE_OK { + return Ok(response.data().to_vec()); + } + + let retcode = response.retcode(); + let error_string = format!("Ledger APDU retcode: 0x{retcode:X}"); + Err(Error::APDUExchangeError(error_string)) + } + Err(_err) => Err(Error::LedgerConnectionError( + "Error connecting to ledger device".to_string(), + )), + } + } +} + +#[async_trait::async_trait] +impl Blob for LedgerSigner +where + T: Exchange, +{ + type Key = HdPath; + type Error = Error; + /// Get the public key from the device + /// # Errors + /// Returns an error if there is an issue with connecting with the device or getting the public key from the device + async fn get_public_key( + &self, + index: &Self::Key, + ) -> Result { + self.get_public_key_with_display_flag(*index, false).await + } + + /// Sign a blob of data with the account on the Ledger device + /// based on impl from [https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L166](https://github.com/LedgerHQ/ledger-live/blob/develop/libs/ledgerjs/packages/hw-app-str/src/Str.ts#L166) + /// # Errors + /// Returns an error if there is an issue with connecting with the device or signing the given tx on the device. Or, if the device has not enabled hash signing + async fn sign_blob(&self, index: &Self::Key, blob: &[u8]) -> Result, Error> { + let mut hd_path_to_bytes = index.to_vec()?; + + let capacity = 1 + hd_path_to_bytes.len() + blob.len(); + let mut data: Vec = Vec::with_capacity(capacity); + + data.insert(0, HD_PATH_ELEMENTS_COUNT); + data.append(&mut hd_path_to_bytes); + data.extend_from_slice(blob); + + let command = APDUCommand { + cla: CLA, + ins: SIGN_TX_HASH, + p1: P1_SIGN_TX_HASH, + p2: P2_SIGN_TX_HASH, + data, + }; + + self.send_command_to_ledger(command).await + } +} + +fn get_transport() -> Result { + // instantiate the connection to Ledger, this will return an error if Ledger is not connected + let hidapi = HidApi::new().map_err(Error::HidApiError)?; + TransportNativeHID::new(&hidapi).map_err(Error::LedgerHidError) +} + +pub const TEST_NETWORK_PASSPHRASE: &[u8] = b"Test SDF Network ; September 2015"; +#[cfg(test)] +fn test_network_hash() -> Hash { + use sha2::Digest; + Hash(sha2::Sha256::digest(TEST_NETWORK_PASSPHRASE).into()) +} +#[cfg(test)] +mod test { + use httpmock::prelude::*; + use serde_json::json; + + use crate::{emulator_http_transport::EmulatorHttpTransport, Blob}; + + use soroban_env_host::xdr::Transaction; + use std::vec; + + use soroban_env_host::xdr::{self, Operation, OperationBody, Uint256}; + + use crate::{test_network_hash, Error, LedgerSigner}; + + use stellar_xdr::curr::{ + Memo, MuxedAccount, PaymentOp, Preconditions, SequenceNumber, TransactionExt, + }; + + fn ledger(server: &MockServer) -> LedgerSigner { + let transport = EmulatorHttpTransport::new(&server.host(), server.port()); + LedgerSigner::new(transport) + } + + #[tokio::test] + async fn test_get_public_key() { + let server = MockServer::start(); + let mock_server = server.mock(|when, then| { + when.method(POST) + .path("/") + .header("accept", "application/json") + .header("content-type", "application/json") + .json_body(json!({ "apduHex": "e00200000d038000002c8000009480000000" })); + then.status(200) + .header("content-type", "application/json") + .json_body(json!({"data": "e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd9000"})); + }); + let ledger = ledger(&server); + let public_key = ledger.get_public_key(&0u32.into()).await.unwrap(); + let public_key_string = public_key.to_string(); + let expected_public_key = "GDUTHCF37UX32EMANXIL2WOOVEDZ47GHBTT3DYKU6EKM37SOIZXM2FN7"; + assert_eq!(public_key_string, expected_public_key); + + mock_server.assert(); + } + + #[tokio::test] + async fn test_get_app_configuration() { + let server = MockServer::start(); + let mock_server = server.mock(|when, then| { + when.method(POST) + .path("/") + .header("accept", "application/json") + .header("content-type", "application/json") + .json_body(json!({ "apduHex": "e006000000" })); + then.status(200) + .header("content-type", "application/json") + .json_body(json!({"data": "000500039000"})); + }); + let ledger = ledger(&server); + let config = ledger.get_app_configuration().await.unwrap(); + assert_eq!(config, vec![0, 5, 0, 3]); + + mock_server.assert(); + } + + #[tokio::test] + async fn test_sign_tx() { + let server = MockServer::start(); + let mock_request_1 = server.mock(|when, then| { + when.method(POST) + .path("/") + .header("accept", "application/json") + .header("content-type", "application/json") + .json_body(json!({ "apduHex": "e004008089038000002c8000009480000000cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472000000020000000000000000000000000000000000000000000000000000000000000000000000000000006400000000000000010000000000000001000000075374656c6c6172000000000100000001000000000000000000000000" })); + then.status(200) + .header("content-type", "application/json") + .json_body(json!({"data": "9000"})); + }); + + let mock_request_2 = server.mock(|when, then| { + when.method(POST) + .path("/") + .header("accept", "application/json") + .header("content-type", "application/json") + .json_body(json!({ "apduHex": "e0048000500000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006400000000" })); + then.status(200) + .header("content-type", "application/json") + .json_body(json!({"data": "5c2f8eb41e11ab922800071990a25cf9713cc6e7c43e50e0780ddc4c0c6da50c784609ef14c528a12f520d8ea9343b49083f59c51e3f28af8c62b3edeaade60e9000"})); + }); + + let ledger = ledger(&server); + + let fake_source_acct = [0; 32]; + let fake_dest_acct = [0; 32]; + let tx = Transaction { + source_account: MuxedAccount::Ed25519(Uint256(fake_source_acct)), + fee: 100, + seq_num: SequenceNumber(1), + cond: Preconditions::None, + memo: Memo::Text("Stellar".as_bytes().try_into().unwrap()), + ext: TransactionExt::V0, + operations: [Operation { + source_account: Some(MuxedAccount::Ed25519(Uint256(fake_source_acct))), + body: OperationBody::Payment(PaymentOp { + destination: MuxedAccount::Ed25519(Uint256(fake_dest_acct)), + asset: xdr::Asset::Native, + amount: 100, + }), + }] + .try_into() + .unwrap(), + }; + + let response = ledger + .sign_transaction(0, tx, test_network_hash()) + .await + .unwrap(); + assert_eq!( + hex::encode(response), + "5c2f8eb41e11ab922800071990a25cf9713cc6e7c43e50e0780ddc4c0c6da50c784609ef14c528a12f520d8ea9343b49083f59c51e3f28af8c62b3edeaade60e" + ); + + mock_request_1.assert(); + mock_request_2.assert(); + } + + #[tokio::test] + async fn test_sign_tx_hash_when_hash_signing_is_not_enabled() { + let server = MockServer::start(); + let mock_server = server.mock(|when, then| { + when.method(POST) + .path("/") + .header("accept", "application/json") + .header("content-type", "application/json") + .json_body(json!({ "apduHex": "e00800004d038000002c800000948000000033333839653966306631613635663139373336636163663534346332653832353331336538343437663536393233336262386462333961613630376338383839" })); + then.status(200) + .header("content-type", "application/json") + .json_body(json!({"data": "6c66"})); + }); + + let ledger = ledger(&server); + let path = 0; + let test_hash = b"3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889"; + + let err = ledger.sign_blob(&path.into(), test_hash).await.unwrap_err(); + if let Error::APDUExchangeError(msg) = err { + assert_eq!(msg, "Ledger APDU retcode: 0x6C66"); + } else { + panic!("Unexpected error: {err:?}"); + } + + mock_server.assert(); + } + + #[tokio::test] + async fn test_sign_tx_hash_when_hash_signing_is_enabled() { + let server = MockServer::start(); + let mock_server = server.mock(|when, then| { + when.method(POST) + .path("/") + .header("accept", "application/json") + .header("content-type", "application/json") + .json_body(json!({ "apduHex": "e00800002d038000002c80000094800000003389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889" })); + then.status(200) + .header("content-type", "application/json") + .json_body(json!({"data": "6970b9c9d3a6f4de7fb93e8d3920ec704fc4fece411873c40570015bbb1a60a197622bc3bf5644bb38ae73e1b96e4d487d716d142d46c7e944f008dece92df079000"})); + }); + + let ledger = ledger(&server); + let path = 0; + let mut test_hash = vec![0u8; 32]; + + hex::decode_to_slice( + "3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889", + &mut test_hash as &mut [u8], + ) + .unwrap(); + + let response = ledger.sign_blob(&path.into(), &test_hash).await.unwrap(); + + assert_eq!( + hex::encode(response), + "6970b9c9d3a6f4de7fb93e8d3920ec704fc4fece411873c40570015bbb1a60a197622bc3bf5644bb38ae73e1b96e4d487d716d142d46c7e944f008dece92df07" + ); + + mock_server.assert(); + } +} diff --git a/cmd/crates/stellar-ledger/src/signer.rs b/cmd/crates/stellar-ledger/src/signer.rs new file mode 100644 index 000000000..93ca90db5 --- /dev/null +++ b/cmd/crates/stellar-ledger/src/signer.rs @@ -0,0 +1,10 @@ +#[async_trait::async_trait] +pub trait Blob { + type Key: Send; + type Error; + async fn get_public_key( + &self, + key: &Self::Key, + ) -> Result; + async fn sign_blob(&self, key: &Self::Key, blob: &[u8]) -> Result, Self::Error>; +} diff --git a/cmd/crates/stellar-ledger/src/speculos.rs b/cmd/crates/stellar-ledger/src/speculos.rs new file mode 100644 index 000000000..13d154211 --- /dev/null +++ b/cmd/crates/stellar-ledger/src/speculos.rs @@ -0,0 +1,74 @@ +use std::{collections::HashMap, path::PathBuf}; +use testcontainers::{core::WaitFor, Image, ImageArgs}; + +const NAME: &str = "docker.io/zondax/builder-zemu"; +const TAG: &str = "speculos-3a3439f6b45eca7f56395673caaf434c202e7005"; + +#[allow(dead_code)] +static ENV: &Map = &Map(phf::phf_map! { + "BOLOS_SDK"=> "/project/deps/nanos-secure-sdk", + "BOLOS_ENV" => "/opt/bolos", + "DISPLAY" => "host.docker.internal:0", +}); +struct Map(phf::Map<&'static str, &'static str>); + +#[allow(clippy::implicit_hasher)] +impl From<&Map> for HashMap { + fn from(Map(map): &Map) -> Self { + map.into_iter() + .map(|(a, b)| ((*a).to_string(), (*b).to_string())) + .collect() + } +} + +#[derive(Debug, Default)] +pub struct Speculos(HashMap, HashMap); +const DEFAULT_APP_PATH: &str = "/project/app/bin"; +impl Speculos { + #[allow(dead_code)] + pub fn new() -> Self { + #[allow(unused_mut)] + let apps_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("apps"); + let mut volumes = HashMap::new(); + volumes.insert( + apps_dir.to_str().unwrap().to_string(), + DEFAULT_APP_PATH.to_string(), + ); + Speculos(ENV.into(), volumes) + } +} + +#[derive(Debug, Default, Clone)] +pub struct Args; + +impl ImageArgs for Args { + fn into_iterator(self) -> Box> { + let container_elf_path = format!("{DEFAULT_APP_PATH}/stellarNanosApp.elf"); + let command_string = format!("/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN --display headless -s \"other base behind follow wet put glad muscle unlock sell income october\" -m nanos {container_elf_path}"); + Box::new(vec![command_string].into_iter()) + } +} + +impl Image for Speculos { + type Args = Args; + + fn name(&self) -> String { + NAME.to_owned() + } + + fn tag(&self) -> String { + TAG.to_owned() + } + + fn ready_conditions(&self) -> Vec { + vec![WaitFor::message_on_stdout("HTTP proxy started...")] + } + + fn env_vars(&self) -> Box + '_> { + Box::new(self.0.iter()) + } + + fn volumes(&self) -> Box + '_> { + Box::new(self.1.iter()) + } +} diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 696d56270..986f69517 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -107,7 +107,7 @@ ureq = { version = "2.9.1", features = ["json"] } tempfile = "3.8.1" toml_edit = "0.21.0" rust-embed = { version = "8.2.0", features = ["debug-embed"] } -bollard = "0.15.0" +bollard = { workspace=true } futures-util = "0.3.30" home = "0.5.9" # For hyper-tls