From 252a7d63287fb57d610f858eef845f92c0248198 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 15 Feb 2024 10:13:38 -0500 Subject: [PATCH] feat: new NetworkRunnable trait for all commands that use RPC. And fixed tests for new docker test. --- .github/workflows/rpc-test.yml | 44 + Cargo.lock | 522 ++++++- cmd/crates/soroban-rpc/src/lib.rs | 1317 +++++++++++++++++ cmd/crates/soroban-test/Cargo.toml | 18 +- cmd/crates/soroban-test/src/lib.rs | 176 ++- cmd/crates/soroban-test/src/module.rs | 226 +++ cmd/crates/soroban-test/tests/it/config.rs | 44 +- .../soroban-test/tests/it/hello_world.rs | 22 - .../tests/it/integration/custom_types.rs | 12 +- .../tests/it/integration/dotenv.rs | 96 +- .../tests/it/integration/hello_world.rs | 229 +-- .../soroban-test/tests/it/integration/util.rs | 65 +- .../soroban-test/tests/it/integration/wrap.rs | 178 +-- cmd/crates/soroban-test/tests/it/main.rs | 2 +- cmd/crates/soroban-test/tests/it/util.rs | 14 +- cmd/soroban-cli/src/bin/main.rs | 1 + .../src/commands/contract/deploy/asset.rs | 25 +- .../src/commands/contract/deploy/wasm.rs | 34 +- .../src/commands/contract/extend.rs | 23 +- .../src/commands/contract/fetch.rs | 26 +- .../src/commands/contract/id/wasm.rs | 38 +- .../src/commands/contract/install.rs | 29 +- .../src/commands/contract/invoke.rs | 72 +- cmd/soroban-cli/src/commands/contract/read.rs | 30 +- .../src/commands/contract/restore.rs | 20 +- cmd/soroban-cli/src/commands/events.rs | 45 +- cmd/soroban-cli/src/commands/mod.rs | 11 + cmd/soroban-cli/src/commands/network/mod.rs | 6 +- cmd/soroban-rpc/internal/test/integration.go | 356 +++++ 29 files changed, 3135 insertions(+), 546 deletions(-) create mode 100644 .github/workflows/rpc-test.yml create mode 100644 cmd/crates/soroban-rpc/src/lib.rs create mode 100644 cmd/crates/soroban-test/src/module.rs delete mode 100644 cmd/crates/soroban-test/tests/it/hello_world.rs create mode 100644 cmd/soroban-rpc/internal/test/integration.go diff --git a/.github/workflows/rpc-test.yml b/.github/workflows/rpc-test.yml new file mode 100644 index 0000000000..cb5cc6fd85 --- /dev/null +++ b/.github/workflows/rpc-test.yml @@ -0,0 +1,44 @@ + +name: test-rpc + +on: + push: + branches: [main, release/**] + pull_request: + +jobs: + test: + name: Test against RPC + runs-on: ubuntu-22.04 + services: + rpc: + image: stellar/quickstart:latest@sha256:742a649d5d9be826dd4b1a378c95b0e1833e1bcb08c3f4b9b9a8cdd03da653e3 + ports: + - 9999:8000 + env: + ENABLE_LOGS: true + NETWORK: local + ENABLE_SOROBAN_RPC: true + LIMITS: unlimited + options: >- + --health-cmd "curl --no-progress-meter --fail-with-body -X POST \"http://localhost:9999/soroban/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getNetwork\"}' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'" + --health-interval 10s + --health-timeout 5s + --health-retries 50 + steps: + # - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - run: cargo build + # - run: cargo build + - run: make build-test-wasms + # - run: cargo test + - run: SOROBAN_PORT=9999 cargo test --features it --package soroban-test --test it -- integration diff --git a/Cargo.lock b/Cargo.lock index 28f8aaf134..9fa0c2374a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,44 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82e7850583ead5f8bbef247e2a3c37a19bd576e8420cd262a6711921827e1e5" +dependencies = [ + "base64 0.13.1", + "bollard-stubs", + "bytes 1.5.0", + "futures-core", + "futures-util", + "hex", + "http 0.2.11", + "hyper", + "hyperlocal", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.42.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" +dependencies = [ + "serde", + "serde_with 1.14.0", +] + [[package]] name = "bstr" version = "1.8.0" @@ -273,6 +311,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + [[package]] name = "bytes" version = "1.5.0" @@ -434,12 +478,46 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes 1.5.0", + "memchr", +] + [[package]] name = "const-oid" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "containers-api" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef94b0ff8338282b35bafb408eb0a3e53ba05bdb3b36840589ab9a67a6682593" +dependencies = [ + "chrono", + "flate2", + "futures-util", + "http 0.2.11", + "hyper", + "hyperlocal", + "log", + "mime", + "paste", + "pin-project 1.1.3", + "serde", + "serde_json", + "tar", + "thiserror", + "tokio", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -686,14 +764,38 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + [[package]] name = "darling" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.3", + "darling_macro 0.20.3", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", ] [[package]] @@ -710,13 +812,24 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "darling_core", + "darling_core 0.20.3", "quote", "syn 2.0.39", ] @@ -922,6 +1035,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1048,6 +1174,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.29" @@ -1055,6 +1196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1063,12 +1205,34 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +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-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "futures-sink" version = "0.3.29" @@ -1087,8 +1251,10 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1097,6 +1263,18 @@ dependencies = [ "slab", ] +[[package]] +name = "futures_codec" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce54d63f8b0c75023ed920d46fd71d0cbbb830b0ee012726b5b4f506fb6dea5b" +dependencies = [ + "bytes 0.5.6", + "futures", + "memchr", + "pin-project 0.4.30", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1193,7 +1371,7 @@ dependencies = [ "gix-date", "itoa", "thiserror", - "winnow", + "winnow 0.5.34", ] [[package]] @@ -1275,7 +1453,7 @@ dependencies = [ "smallvec", "thiserror", "unicode-bom", - "winnow", + "winnow 0.5.34", ] [[package]] @@ -1354,7 +1532,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "184f7f7d4e45db0e2a362aeaf12c06c5e84817d0ef91d08e8e90170dad9f0b07" dependencies = [ - "bytes", + "bytes 1.5.0", "crc32fast", "flate2", "gix-hash", @@ -1524,7 +1702,7 @@ dependencies = [ "itoa", "smallvec", "thiserror", - "winnow", + "winnow 0.5.34", ] [[package]] @@ -1647,7 +1825,7 @@ dependencies = [ "gix-transport", "maybe-async", "thiserror", - "winnow", + "winnow 0.5.34", ] [[package]] @@ -1680,7 +1858,7 @@ dependencies = [ "gix-validate", "memmap2", "thiserror", - "winnow", + "winnow 0.5.34", ] [[package]] @@ -1928,7 +2106,7 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "futures-core", "futures-sink", @@ -2023,7 +2201,7 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "itoa", ] @@ -2034,7 +2212,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "itoa", ] @@ -2045,7 +2223,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes", + "bytes 1.5.0", "http 0.2.11", "pin-project-lite", ] @@ -2062,13 +2240,19 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[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.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ - "bytes", + "bytes 1.5.0", "futures-channel", "futures-core", "futures-util", @@ -2108,13 +2292,26 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes", + "bytes 1.5.0", "hyper", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyperlocal" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" +dependencies = [ + "futures-util", + "hex", + "hyper", + "pin-project 1.1.3", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -2234,6 +2431,17 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2716,13 +2924,75 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "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", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" +dependencies = [ + "pin-project-internal 0.4.30", +] + [[package]] name = "pin-project" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ - "pin-project-internal", + "pin-project-internal 1.1.3", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -2770,6 +3040,42 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" +[[package]] +name = "podman-api" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0ade207138f12695cb4be3b590283f1cf764c5c4909f39966c4b4b0dba7c1e" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes 1.5.0", + "chrono", + "containers-api", + "flate2", + "futures-util", + "futures_codec", + "log", + "paste", + "podman-api-stubs", + "serde", + "serde_json", + "tar", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "podman-api-stubs" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d280c623f633a0dded88feab9e387f98451506431d5b7308a858c643305dcee" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2834,6 +3140,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + [[package]] name = "prettyplease" version = "0.2.15" @@ -2904,6 +3220,21 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redis" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +dependencies = [ + "combine", + "itoa", + "percent-encoding", + "ryu", + "sha1_smol", + "socket2 0.4.10", + "url", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2975,7 +3306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.5", - "bytes", + "bytes 1.5.0", "encoding_rs", "futures-core", "futures-util", @@ -3316,6 +3647,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3328,6 +3668,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros 1.5.2", +] + [[package]] name = "serde_with" version = "3.4.0" @@ -3341,17 +3691,29 @@ dependencies = [ "indexmap 2.1.0", "serde", "serde_json", - "serde_with_macros", + "serde_with_macros 3.4.0", "time", ] +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling 0.13.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "serde_with_macros" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ - "darling", + "darling 0.20.3", "proc-macro2", "quote", "syn 2.0.39", @@ -3443,6 +3805,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[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" @@ -3561,8 +3929,8 @@ dependencies = [ "termcolor_output", "thiserror", "tokio", - "toml", - "toml_edit", + "toml 0.5.11", + "toml_edit 0.21.0", "tracing", "tracing-appender", "tracing-subscriber", @@ -3654,7 +4022,7 @@ checksum = "1a9cfeb46db19d0fb2e2c97d4e8aa102d660e0c80dc5412409a22dfd14241ca7" dependencies = [ "serde", "serde_json", - "serde_with", + "serde_with 3.4.0", "soroban-env-common", "soroban-env-host", "thiserror", @@ -3719,7 +4087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0afc8337fadde3047fb774fa2abc3877a4a260b8e531868b65d5a1debc60b3b9" dependencies = [ "crate-git-revision 0.0.6", - "darling", + "darling 0.20.3", "itertools 0.11.0", "proc-macro2", "quote", @@ -3838,20 +4206,30 @@ version = "20.3.0" dependencies = [ "assert_cmd", "assert_fs", + "ctor", "fs_extra", + "phf", + "podman-api", "predicates 2.1.5", + "pretty_env_logger", + "redis", "sep5", "serde_json", "sha2 0.10.8", "soroban-cli", "soroban-env-host", "soroban-ledger-snapshot", + "soroban-rpc", "soroban-sdk", "soroban-spec", "soroban-spec-tools 20.3.0", "stellar-strkey 0.0.7", + "testcontainers", + "testcontainers-modules", "thiserror", "tokio", + "toml 0.8.10", + "walkdir", "which", ] @@ -3934,7 +4312,7 @@ dependencies = [ "hex", "serde", "serde_json", - "serde_with", + "serde_with 3.4.0", "stellar-strkey 0.0.8", "thiserror", ] @@ -4031,6 +4409,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "temp-dir" version = "0.1.11" @@ -4117,6 +4506,35 @@ dependencies = [ "soroban-sdk", ] +[[package]] +name = "testcontainers" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" +dependencies = [ + "async-trait", + "bollard", + "bollard-stubs", + "futures", + "hex", + "hmac 0.12.1", + "log", + "rand", + "serde", + "serde_json", + "sha2 0.10.8", + "tokio", +] + +[[package]] +name = "testcontainers-modules" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c461fd6956a37f00d9d90bc7ba9ec5b1acdf1b9ef8620417316f1125d3c4ff3" +dependencies = [ + "testcontainers", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -4219,7 +4637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ "backtrace", - "bytes", + "bytes 1.5.0", "libc", "mio", "num_cpus", @@ -4268,7 +4686,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ - "bytes", + "bytes 1.5.0", "futures-core", "futures-sink", "pin-project-lite", @@ -4285,11 +4703,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.5", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -4299,7 +4732,20 @@ checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ "indexmap 2.1.0", "toml_datetime", - "winnow", + "winnow 0.5.34", +] + +[[package]] +name = "toml_edit" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e68c159e8f5ba8a28c4eb7b0c0c190d77bb479047ca713270048145a9ad28a" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.0", ] [[package]] @@ -4310,7 +4756,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "pin-project", + "pin-project 1.1.3", "pin-project-lite", "tower-layer", "tower-service", @@ -4908,6 +5354,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1dbce9e90e5404c5a52ed82b1d13fc8cfbdad85033b6f57546ffd1265f8451" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -4918,6 +5373,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "xattr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914566e6413e7fa959cc394fb30e563ba80f3541fbd40816d4c05a0fc3f2a0f1" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/cmd/crates/soroban-rpc/src/lib.rs b/cmd/crates/soroban-rpc/src/lib.rs new file mode 100644 index 0000000000..c20a254fde --- /dev/null +++ b/cmd/crates/soroban-rpc/src/lib.rs @@ -0,0 +1,1317 @@ +use http::{uri::Authority, Uri}; +use itertools::Itertools; +use jsonrpsee_core::params::ObjectParams; +use jsonrpsee_core::{self, client::ClientT, rpc_params}; +use jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder}; +use serde_aux::prelude::{ + deserialize_default_from_null, deserialize_number_from_string, + deserialize_option_number_from_string, +}; +use soroban_env_host::xdr::{ + self, AccountEntry, AccountId, ContractDataEntry, DiagnosticEvent, Error as XdrError, + LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, Limited, PublicKey, ReadXdr, + SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData, Transaction, + TransactionEnvelope, TransactionMeta, TransactionMetaV3, TransactionResult, Uint256, VecM, + WriteXdr, +}; +use soroban_sdk::token; +use soroban_sdk::xdr::Limits; +use std::{ + fmt::Display, + str::FromStr, + time::{Duration, Instant}, +}; +use stellar_xdr::curr::ContractEventType; +use termcolor::{Color, ColorChoice, StandardStream, WriteColor}; +use termcolor_output::colored; +use tokio::time::sleep; + +pub mod log; +mod txn; + +pub use txn::Assembled; + +use soroban_spec_tools::contract; + +const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); +pub(crate) const DEFAULT_TRANSACTION_FEES: u32 = 100; + +pub type LogEvents = fn( + footprint: &LedgerFootprint, + auth: &[VecM], + events: &[DiagnosticEvent], +) -> (); + +pub type LogResources = fn(resources: &SorobanResources) -> (); + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + InvalidAddress(#[from] stellar_strkey::DecodeError), + #[error("invalid response from server")] + InvalidResponse, + #[error("provided network passphrase {expected:?} does not match the server: {server:?}")] + InvalidNetworkPassphrase { expected: String, server: String }, + #[error("xdr processing error: {0}")] + Xdr(#[from] XdrError), + #[error("invalid rpc url: {0}")] + InvalidRpcUrl(http::uri::InvalidUri), + #[error("invalid rpc url: {0}")] + InvalidRpcUrlFromUriParts(http::uri::InvalidUriParts), + #[error("invalid friendbot url: {0}")] + InvalidUrl(String), + #[error("jsonrpc error: {0}")] + JsonRpc(#[from] jsonrpsee_core::Error), + #[error("json decoding error: {0}")] + Serde(#[from] serde_json::Error), + #[error("transaction failed: {0}")] + TransactionFailed(String), + #[error("transaction submission failed: {0}")] + TransactionSubmissionFailed(String), + #[error("expected transaction status: {0}")] + UnexpectedTransactionStatus(String), + #[error("transaction submission timeout")] + TransactionSubmissionTimeout, + #[error("transaction simulation failed: {0}")] + TransactionSimulationFailed(String), + #[error("{0} not found: {1}")] + NotFound(String, String), + #[error("Missing result in successful response")] + MissingResult, + #[error("Failed to read Error response from server")] + MissingError, + #[error("Missing signing key for account {address}")] + MissingSignerForAddress { address: String }, + #[error("cursor is not valid")] + InvalidCursor, + #[error("unexpected ({length}) simulate transaction result length")] + UnexpectedSimulateTransactionResultSize { length: usize }, + #[error("unexpected ({count}) number of operations")] + UnexpectedOperationCount { count: usize }, + #[error("Transaction contains unsupported operation type")] + UnsupportedOperationType, + #[error("unexpected contract code data type: {0:?}")] + UnexpectedContractCodeDataType(LedgerEntryData), + #[error(transparent)] + CouldNotParseContractSpec(#[from] contract::Error), + #[error("unexpected contract code got token")] + UnexpectedToken(ContractDataEntry), + #[error(transparent)] + Spec(#[from] soroban_spec::read::FromWasmError), + #[error(transparent)] + SpecBase64(#[from] soroban_spec::read::ParseSpecBase64Error), + #[error("Fee was too large {0}")] + LargeFee(u64), + #[error("Cannot authorize raw transactions")] + CannotAuthorizeRawTransaction, + + #[error("Missing result for tnx")] + MissingOp, + #[error(transparent)] + Http(#[from] http::Error), +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct SendTransactionResponse { + pub hash: String, + pub status: String, + #[serde( + rename = "errorResultXdr", + skip_serializing_if = "Option::is_none", + default + )] + pub error_result_xdr: Option, + #[serde(rename = "latestLedger")] + pub latest_ledger: u32, + #[serde( + rename = "latestLedgerCloseTime", + deserialize_with = "deserialize_number_from_string" + )] + pub latest_ledger_close_time: u32, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct GetTransactionResponseRaw { + pub status: String, + #[serde( + rename = "envelopeXdr", + skip_serializing_if = "Option::is_none", + default + )] + pub envelope_xdr: Option, + #[serde(rename = "resultXdr", skip_serializing_if = "Option::is_none", default)] + pub result_xdr: Option, + #[serde( + rename = "resultMetaXdr", + skip_serializing_if = "Option::is_none", + default + )] + pub result_meta_xdr: Option, + // TODO: add ledger info and application order +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct GetTransactionResponse { + pub status: String, + pub envelope: Option, + pub result: Option, + pub result_meta: Option, +} + +impl TryInto for GetTransactionResponseRaw { + type Error = xdr::Error; + + fn try_into(self) -> Result { + Ok(GetTransactionResponse { + status: self.status, + envelope: self + .envelope_xdr + .map(|v| ReadXdr::from_xdr_base64(v, Limits::none())) + .transpose()?, + result: self + .result_xdr + .map(|v| ReadXdr::from_xdr_base64(v, Limits::none())) + .transpose()?, + result_meta: self + .result_meta_xdr + .map(|v| ReadXdr::from_xdr_base64(v, Limits::none())) + .transpose()?, + }) + } +} + +impl GetTransactionResponse { + /// + /// # Errors + pub fn return_value(&self) -> Result { + if let Some(xdr::TransactionMeta::V3(xdr::TransactionMetaV3 { + soroban_meta: Some(xdr::SorobanTransactionMeta { return_value, .. }), + .. + })) = &self.result_meta + { + Ok(return_value.clone()) + } else { + Err(Error::MissingOp) + } + } + + /// + /// # Errors + pub fn events(&self) -> Result, Error> { + self.result_meta + .as_ref() + .map(extract_events) + .ok_or(Error::MissingOp) + } + + /// + /// # Errors + pub fn contract_events(&self) -> Result, Error> { + Ok(self + .events()? + .into_iter() + .filter(|e| matches!(e.event.type_, ContractEventType::Contract)) + .collect::>()) + } +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct LedgerEntryResult { + pub key: String, + pub xdr: String, + #[serde(rename = "lastModifiedLedgerSeq")] + pub last_modified_ledger: u32, + #[serde( + rename = "liveUntilLedgerSeq", + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_option_number_from_string", + default + )] + pub live_until_ledger_seq_ledger_seq: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct GetLedgerEntriesResponse { + pub entries: Option>, + #[serde(rename = "latestLedger")] + pub latest_ledger: i64, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct GetNetworkResponse { + #[serde( + rename = "friendbotUrl", + skip_serializing_if = "Option::is_none", + default + )] + pub friendbot_url: Option, + pub passphrase: String, + #[serde(rename = "protocolVersion")] + pub protocol_version: u32, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct GetLatestLedgerResponse { + pub id: String, + #[serde(rename = "protocolVersion")] + pub protocol_version: u32, + pub sequence: u32, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] +pub struct Cost { + #[serde( + rename = "cpuInsns", + deserialize_with = "deserialize_number_from_string" + )] + pub cpu_insns: u64, + #[serde( + rename = "memBytes", + deserialize_with = "deserialize_number_from_string" + )] + pub mem_bytes: u64, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct SimulateHostFunctionResultRaw { + #[serde(deserialize_with = "deserialize_default_from_null")] + pub auth: Vec, + pub xdr: String, +} + +#[derive(Debug)] +pub struct SimulateHostFunctionResult { + pub auth: Vec, + pub xdr: xdr::ScVal, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] +pub struct SimulateTransactionResponse { + #[serde( + rename = "minResourceFee", + deserialize_with = "deserialize_number_from_string", + default + )] + pub min_resource_fee: u64, + #[serde(default)] + pub cost: Cost, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub results: Vec, + #[serde(rename = "transactionData", default)] + pub transaction_data: String, + #[serde( + deserialize_with = "deserialize_default_from_null", + skip_serializing_if = "Vec::is_empty", + default + )] + pub events: Vec, + #[serde( + rename = "restorePreamble", + skip_serializing_if = "Option::is_none", + default + )] + pub restore_preamble: Option, + #[serde(rename = "latestLedger")] + pub latest_ledger: u32, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub error: Option, +} + +impl SimulateTransactionResponse { + /// + /// # Errors + pub fn results(&self) -> Result, Error> { + self.results + .iter() + .map(|r| { + Ok(SimulateHostFunctionResult { + auth: r + .auth + .iter() + .map(|a| { + Ok(SorobanAuthorizationEntry::from_xdr_base64( + a, + Limits::none(), + )?) + }) + .collect::>()?, + xdr: xdr::ScVal::from_xdr_base64(&r.xdr, Limits::none())?, + }) + }) + .collect() + } + + /// + /// # Errors + pub fn events(&self) -> Result, Error> { + self.events + .iter() + .map(|e| Ok(DiagnosticEvent::from_xdr_base64(e, Limits::none())?)) + .collect() + } + + /// + /// # Errors + pub fn transaction_data(&self) -> Result { + Ok(SorobanTransactionData::from_xdr_base64( + &self.transaction_data, + Limits::none(), + )?) + } +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Default)] +pub struct RestorePreamble { + #[serde(rename = "transactionData")] + pub transaction_data: String, + #[serde( + rename = "minResourceFee", + deserialize_with = "deserialize_number_from_string" + )] + pub min_resource_fee: u64, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct GetEventsResponse { + #[serde(deserialize_with = "deserialize_default_from_null")] + pub events: Vec, + #[serde(rename = "latestLedger")] + pub latest_ledger: u32, +} + +// Determines whether or not a particular filter matches a topic based on the +// same semantics as the RPC server: +// +// - for an exact segment match, the filter is a base64-encoded ScVal +// - for a wildcard, single-segment match, the string "*" matches exactly one +// segment +// +// The expectation is that a `filter` is a comma-separated list of segments that +// has previously been validated, and `topic` is the list of segments applicable +// for this event. +// +// [API +// Reference](https://docs.google.com/document/d/1TZUDgo_3zPz7TiPMMHVW_mtogjLyPL0plvzGMsxSz6A/edit#bookmark=id.35t97rnag3tx) +// [Code +// Reference](https://github.com/stellar/soroban-tools/blob/bac1be79e8c2590c9c35ad8a0168aab0ae2b4171/cmd/soroban-rpc/internal/methods/get_events.go#L182-L203) +#[must_use] +pub fn does_topic_match(topic: &[String], filter: &[String]) -> bool { + filter.len() == topic.len() + && filter + .iter() + .enumerate() + .all(|(i, s)| *s == "*" || topic[i] == *s) +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct Event { + #[serde(rename = "type")] + pub event_type: String, + + pub ledger: u32, + #[serde(rename = "ledgerClosedAt")] + pub ledger_closed_at: String, + + pub id: String, + #[serde(rename = "pagingToken")] + pub paging_token: String, + + #[serde(rename = "contractId")] + pub contract_id: String, + pub topic: Vec, + pub value: String, +} + +impl Display for Event { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "Event {} [{}]:", + self.paging_token, + self.event_type.to_ascii_uppercase() + )?; + writeln!( + f, + " Ledger: {} (closed at {})", + self.ledger, self.ledger_closed_at + )?; + writeln!(f, " Contract: {}", self.contract_id)?; + writeln!(f, " Topics:")?; + for topic in &self.topic { + let scval = + xdr::ScVal::from_xdr_base64(topic, Limits::none()).map_err(|_| std::fmt::Error)?; + writeln!(f, " {scval:?}")?; + } + let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none()) + .map_err(|_| std::fmt::Error)?; + writeln!(f, " Value: {scval:?}") + } +} + +impl Event { + /// + /// # Errors + pub fn parse_cursor(&self) -> Result<(u64, i32), Error> { + parse_cursor(&self.id) + } + /// + /// # Errors + pub fn pretty_print(&self) -> Result<(), Box> { + let mut stdout = StandardStream::stdout(ColorChoice::Auto); + if !stdout.supports_color() { + println!("{self}"); + return Ok(()); + } + + let color = match self.event_type.as_str() { + "system" => Color::Yellow, + _ => Color::Blue, + }; + colored!( + stdout, + "{}Event{} {}{}{} [{}{}{}{}]:\n", + bold!(true), + bold!(false), + fg!(Some(Color::Green)), + self.paging_token, + reset!(), + bold!(true), + fg!(Some(color)), + self.event_type.to_ascii_uppercase(), + reset!(), + )?; + + colored!( + stdout, + " Ledger: {}{}{} (closed at {}{}{})\n", + fg!(Some(Color::Green)), + self.ledger, + reset!(), + fg!(Some(Color::Green)), + self.ledger_closed_at, + reset!(), + )?; + + colored!( + stdout, + " Contract: {}{}{}\n", + fg!(Some(Color::Green)), + self.contract_id, + reset!(), + )?; + + colored!(stdout, " Topics:\n")?; + for topic in &self.topic { + let scval = xdr::ScVal::from_xdr_base64(topic, Limits::none())?; + colored!( + stdout, + " {}{:?}{}\n", + fg!(Some(Color::Green)), + scval, + reset!(), + )?; + } + + let scval = xdr::ScVal::from_xdr_base64(&self.value, Limits::none())?; + colored!( + stdout, + " Value: {}{:?}{}\n", + fg!(Some(Color::Green)), + scval, + reset!(), + )?; + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] +pub enum EventType { + All, + Contract, + System, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum EventStart { + Ledger(u32), + Cursor(String), +} + +#[derive(Debug)] +pub struct FullLedgerEntry { + pub key: LedgerKey, + pub val: LedgerEntryData, + pub last_modified_ledger: u32, + pub live_until_ledger_seq: u32, +} + +#[derive(Debug)] +pub struct FullLedgerEntries { + pub entries: Vec, + pub latest_ledger: i64, +} + +pub struct Client { + base_uri: http::Uri, +} + +impl Client { + /// + /// # Errors + pub fn new(raw_base_url: &str) -> Result { + // Add the port to the base URL if there is no port explicitly included + // in the URL and the scheme allows us to infer a default port. + // Jsonrpsee requires a port to always be present even if one can be + // inferred. This may change: https://github.com/paritytech/jsonrpsee/issues/1048. + let uri = raw_base_url.parse::().map_err(Error::InvalidRpcUrl)?; + let mut parts = uri.into_parts(); + if let (Some(scheme), Some(authority)) = (&parts.scheme, &parts.authority) { + if authority.port().is_none() { + let port = match scheme.as_str() { + "http" => Some(80), + "https" => Some(443), + _ => None, + }; + if let Some(port) = port { + let host = authority.host(); + parts.authority = Some( + Authority::from_str(&format!("{host}:{port}")) + .map_err(Error::InvalidRpcUrl)?, + ); + } + } + } + let base_uri = Uri::from_parts(parts).map_err(Error::InvalidRpcUrlFromUriParts)?; + tracing::trace!("rpc_uri: {base_uri}"); + Ok(Self { base_uri }) + } + + /// + /// # Errors + fn client(&self) -> Result { + let url = self.base_uri.to_string(); + let mut headers = HeaderMap::new(); + headers.insert("X-Client-Name", "soroban-cli".parse().unwrap()); + let version = VERSION.unwrap_or("devel"); + headers.insert("X-Client-Version", version.parse().unwrap()); + Ok(HttpClientBuilder::default() + .set_headers(headers) + .build(url)?) + } + + /// + /// # Errors + pub async fn friendbot_url(&self) -> Result { + let network = self.get_network().await?; + tracing::trace!("{network:#?}"); + + let uri = network.friendbot_url.ok_or_else(|| { + Error::NotFound( + "Friendbot".to_string(), + "Friendbot is not available on this network".to_string(), + ) + })?; + let uri = http::Uri::from_str(&uri).map_err(|_| Error::InvalidUrl(uri.to_string()))?; + let (rpc_host, friendbot_host) = (self.base_uri.host(), uri.host()); + if rpc_host == friendbot_host { + let authority = uri + .authority() + .ok_or_else(|| Error::InvalidUrl(self.base_uri.to_string()))? + .clone(); + let host = authority.host(); + let authority = format!( + "{host}:{}", + self.base_uri + .port() + .ok_or_else(|| Error::InvalidUrl(self.base_uri.to_string()))? + ); + let res = http::Uri::builder() + .authority(authority) + .scheme( + uri.scheme() + .ok_or_else(|| Error::InvalidUrl(self.base_uri.to_string()))? + .clone(), + ) + .path_and_query( + uri.path_and_query() + .ok_or_else(|| Error::InvalidUrl(self.base_uri.to_string()))? + .clone(), + ) + .build()?; + Ok(res) + } else { + Ok(uri) + } + } + + /// + /// # Errors + /// If the network passphrase does not match the server + pub async fn verify_network_passphrase(&self, expected: Option<&str>) -> Result { + let server = self.get_network().await?.passphrase; + if let Some(expected) = expected { + if expected != server { + return Err(Error::InvalidNetworkPassphrase { + expected: expected.to_string(), + server, + }); + } + } + Ok(server) + } + + /// + /// # Errors + pub async fn get_network(&self) -> Result { + tracing::trace!("Getting network"); + Ok(self.client()?.request("getNetwork", rpc_params![]).await?) + } + + /// + /// # Errors + pub async fn get_latest_ledger(&self) -> Result { + tracing::trace!("Getting latest ledger"); + Ok(self + .client()? + .request("getLatestLedger", rpc_params![]) + .await?) + } + + /// + /// # Errors + pub async fn get_account(&self, address: &str) -> Result { + tracing::trace!("Getting address {}", address); + let key = LedgerKey::Account(LedgerKeyAccount { + account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( + stellar_strkey::ed25519::PublicKey::from_string(address)?.0, + ))), + }); + let keys = Vec::from([key]); + let response = self.get_ledger_entries(&keys).await?; + let entries = response.entries.unwrap_or_default(); + if entries.is_empty() { + return Err(Error::NotFound( + "Account".to_string(), + format!( + r#"{address} +Might need to fund account like: +soroban config identity fund {address} --network +soroban config identity fund {address} --helper-url "# + ), + )); + } + let ledger_entry = &entries[0]; + let mut read = Limited::new(ledger_entry.xdr.as_bytes(), Limits::none()); + if let LedgerEntryData::Account(entry) = LedgerEntryData::read_xdr_base64(&mut read)? { + tracing::trace!(account=?entry); + Ok(entry) + } else { + Err(Error::InvalidResponse) + } + } + + /// + /// # Errors + pub async fn send_transaction( + &self, + tx: &TransactionEnvelope, + ) -> Result { + let client = self.client()?; + tracing::trace!("Sending:\n{tx:#?}"); + let SendTransactionResponse { + hash, + error_result_xdr, + status, + .. + } = client + .request( + "sendTransaction", + rpc_params![tx.to_xdr_base64(Limits::none())?], + ) + .await + .map_err(|err| { + Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}")) + })?; + + if status == "ERROR" { + let error = error_result_xdr + .ok_or(Error::MissingError) + .and_then(|x| { + TransactionResult::read_xdr_base64(&mut Limited::new( + x.as_bytes(), + Limits::none(), + )) + .map_err(|_| Error::InvalidResponse) + }) + .map(|r| r.result); + tracing::error!("TXN failed:\n {error:#?}"); + return Err(Error::TransactionSubmissionFailed(format!("{:#?}", error?))); + } + // even if status == "success" we need to query the transaction status in order to get the result + + // Poll the transaction status + let start = Instant::now(); + loop { + let response: GetTransactionResponse = self.get_transaction(&hash).await?.try_into()?; + match response.status.as_str() { + "SUCCESS" => { + // TODO: the caller should probably be printing this + tracing::trace!("{response:#?}"); + + return Ok(response); + } + "FAILED" => { + tracing::error!("{response:#?}"); + // TODO: provide a more elaborate error + return Err(Error::TransactionSubmissionFailed(format!( + "{:#?}", + response.result + ))); + } + "NOT_FOUND" => (), + _ => { + return Err(Error::UnexpectedTransactionStatus(response.status)); + } + }; + let duration = start.elapsed(); + // TODO: parameterize the timeout instead of using a magic constant + if duration.as_secs() > 100 { + return Err(Error::TransactionSubmissionTimeout); + } + sleep(Duration::from_secs(1)).await; + } + } + + /// + /// # Errors + pub async fn simulate_transaction( + &self, + tx: &TransactionEnvelope, + ) -> Result { + tracing::trace!("Simulating:\n{tx:#?}"); + let base64_tx = tx.to_xdr_base64(Limits::none())?; + let mut builder = ObjectParams::new(); + builder.insert("transaction", base64_tx)?; + let response: SimulateTransactionResponse = self + .client()? + .request("simulateTransaction", builder) + .await?; + tracing::trace!("Simulation response:\n {response:#?}"); + match response.error { + None => Ok(response), + Some(e) => { + log::diagnostic_events(&response.events, tracing::Level::ERROR); + Err(Error::TransactionSimulationFailed(e)) + } + } + } + + /// + /// # Errors + pub async fn send_assembled_transaction( + &self, + txn: txn::Assembled, + source_key: &ed25519_dalek::SigningKey, + signers: &[ed25519_dalek::SigningKey], + network_passphrase: &str, + log_events: Option, + log_resources: Option, + ) -> Result { + let seq_num = txn.sim_response().latest_ledger + 60; //5 min; + let authorized = txn + .handle_restore(self, source_key, network_passphrase) + .await? + .authorize(self, source_key, signers, seq_num, network_passphrase) + .await?; + authorized.log(log_events, log_resources)?; + + let tx = authorized.sign(source_key, network_passphrase)?; + self.send_transaction(&tx).await + } + + /// + /// # Errors + pub async fn prepare_and_send_transaction( + &self, + tx_without_preflight: &Transaction, + source_key: &ed25519_dalek::SigningKey, + signers: &[ed25519_dalek::SigningKey], + network_passphrase: &str, + log_events: Option, + log_resources: Option, + ) -> Result { + let txn = txn::Assembled::new(tx_without_preflight, self).await?; + self.send_assembled_transaction( + txn, + source_key, + signers, + network_passphrase, + log_events, + log_resources, + ) + .await + } + + /// + /// # Errors + pub async fn create_assembled_transaction( + &self, + txn: &Transaction, + ) -> Result { + txn::Assembled::new(txn, self).await + } + + /// + /// # Errors + pub async fn get_transaction(&self, tx_id: &str) -> Result { + Ok(self + .client()? + .request("getTransaction", rpc_params![tx_id]) + .await?) + } + + /// + /// # Errors + pub async fn get_ledger_entries( + &self, + keys: &[LedgerKey], + ) -> Result { + let mut base64_keys: Vec = vec![]; + for k in keys { + let base64_result = k.to_xdr_base64(Limits::none()); + if base64_result.is_err() { + return Err(Error::Xdr(XdrError::Invalid)); + } + base64_keys.push(k.to_xdr_base64(Limits::none())?); + } + Ok(self + .client()? + .request("getLedgerEntries", rpc_params![base64_keys]) + .await?) + } + + /// + /// # Errors + pub async fn get_full_ledger_entries( + &self, + ledger_keys: &[LedgerKey], + ) -> Result { + let keys = ledger_keys + .iter() + .filter(|key| !matches!(key, LedgerKey::Ttl(_))) + .map(Clone::clone) + .collect::>(); + tracing::trace!("keys: {keys:#?}"); + let GetLedgerEntriesResponse { + entries, + latest_ledger, + } = self.get_ledger_entries(&keys).await?; + tracing::trace!("raw: {entries:#?}"); + let entries = entries + .unwrap_or_default() + .iter() + .map( + |LedgerEntryResult { + key, + xdr, + last_modified_ledger, + live_until_ledger_seq_ledger_seq, + }| { + Ok(FullLedgerEntry { + key: LedgerKey::from_xdr_base64(key, Limits::none())?, + val: LedgerEntryData::from_xdr_base64(xdr, Limits::none())?, + live_until_ledger_seq: live_until_ledger_seq_ledger_seq.unwrap_or_default(), + last_modified_ledger: *last_modified_ledger, + }) + }, + ) + .collect::, Error>>()?; + tracing::trace!("parsed: {entries:#?}"); + Ok(FullLedgerEntries { + entries, + latest_ledger, + }) + } + /// + /// # Errors + pub async fn get_events( + &self, + start: EventStart, + event_type: Option, + contract_ids: &[String], + topics: &[String], + limit: Option, + ) -> Result { + let mut filters = serde_json::Map::new(); + + event_type + .and_then(|t| match t { + EventType::All => None, // all is the default, so avoid incl. the param + EventType::Contract => Some("contract"), + EventType::System => Some("system"), + }) + .map(|t| filters.insert("type".to_string(), t.into())); + + filters.insert("topics".to_string(), topics.into()); + filters.insert("contractIds".to_string(), contract_ids.into()); + + let mut pagination = serde_json::Map::new(); + if let Some(limit) = limit { + pagination.insert("limit".to_string(), limit.into()); + } + + let mut oparams = ObjectParams::new(); + match start { + EventStart::Ledger(l) => oparams.insert("startLedger", l)?, + EventStart::Cursor(c) => { + pagination.insert("cursor".to_string(), c.into()); + } + }; + oparams.insert("filters", vec![filters])?; + oparams.insert("pagination", pagination)?; + + Ok(self.client()?.request("getEvents", oparams).await?) + } + + /// + /// # Errors + pub async fn get_contract_data( + &self, + contract_id: &[u8; 32], + ) -> Result { + // Get the contract from the network + let contract_key = LedgerKey::ContractData(xdr::LedgerKeyContractData { + contract: xdr::ScAddress::Contract(xdr::Hash(*contract_id)), + key: xdr::ScVal::LedgerKeyContractInstance, + durability: xdr::ContractDataDurability::Persistent, + }); + let contract_ref = self.get_ledger_entries(&[contract_key]).await?; + let entries = contract_ref.entries.unwrap_or_default(); + if entries.is_empty() { + let contract_address = stellar_strkey::Contract(*contract_id).to_string(); + return Err(Error::NotFound("Contract".to_string(), contract_address)); + } + let contract_ref_entry = &entries[0]; + match LedgerEntryData::from_xdr_base64(&contract_ref_entry.xdr, Limits::none())? { + LedgerEntryData::ContractData(contract_data) => Ok(contract_data), + scval => Err(Error::UnexpectedContractCodeDataType(scval)), + } + } + + /// + /// # Errors + pub async fn get_remote_wasm(&self, contract_id: &[u8; 32]) -> Result, Error> { + match self.get_contract_data(contract_id).await? { + xdr::ContractDataEntry { + val: + xdr::ScVal::ContractInstance(xdr::ScContractInstance { + executable: xdr::ContractExecutable::Wasm(hash), + .. + }), + .. + } => self.get_remote_wasm_from_hash(hash).await, + scval => Err(Error::UnexpectedToken(scval)), + } + } + + /// + /// # Errors + pub async fn get_remote_wasm_from_hash(&self, hash: xdr::Hash) -> Result, Error> { + let code_key = LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() }); + let contract_data = self.get_ledger_entries(&[code_key]).await?; + let entries = contract_data.entries.unwrap_or_default(); + if entries.is_empty() { + return Err(Error::NotFound( + "Contract Code".to_string(), + hex::encode(hash), + )); + } + let contract_data_entry = &entries[0]; + match LedgerEntryData::from_xdr_base64(&contract_data_entry.xdr, Limits::none())? { + LedgerEntryData::ContractCode(xdr::ContractCodeEntry { code, .. }) => Ok(code.into()), + scval => Err(Error::UnexpectedContractCodeDataType(scval)), + } + } + /// + /// # Errors + pub async fn get_remote_contract_spec( + &self, + contract_id: &[u8; 32], + ) -> Result, Error> { + let contract_data = self.get_contract_data(contract_id).await?; + match contract_data.val { + xdr::ScVal::ContractInstance(xdr::ScContractInstance { + executable: xdr::ContractExecutable::Wasm(hash), + .. + }) => Ok( + contract::Spec::new(&self.get_remote_wasm_from_hash(hash).await?) + .map_err(Error::CouldNotParseContractSpec)? + .spec, + ), + xdr::ScVal::ContractInstance(xdr::ScContractInstance { + executable: xdr::ContractExecutable::StellarAsset, + .. + }) => Ok(soroban_spec::read::parse_raw( + &token::StellarAssetSpec::spec_xdr(), + )?), + _ => Err(Error::Xdr(XdrError::Invalid)), + } + } +} + +fn extract_events(tx_meta: &TransactionMeta) -> Vec { + match tx_meta { + TransactionMeta::V3(TransactionMetaV3 { + soroban_meta: Some(meta), + .. + }) => { + // NOTE: we assume there can only be one operation, since we only send one + if meta.diagnostic_events.len() == 1 { + meta.diagnostic_events.clone().into() + } else if meta.events.len() == 1 { + meta.events + .iter() + .map(|e| DiagnosticEvent { + in_successful_contract_call: true, + event: e.clone(), + }) + .collect() + } else { + Vec::new() + } + } + _ => Vec::new(), + } +} + +pub(crate) fn parse_cursor(c: &str) -> Result<(u64, i32), Error> { + let (toid_part, event_index) = c.split('-').collect_tuple().ok_or(Error::InvalidCursor)?; + let toid_part: u64 = toid_part.parse().map_err(|_| Error::InvalidCursor)?; + let start_index: i32 = event_index.parse().map_err(|_| Error::InvalidCursor)?; + Ok((toid_part, start_index)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simulation_transaction_response_parsing() { + let s = r#"{ + "minResourceFee": "100000000", + "cost": { "cpuInsns": "1000", "memBytes": "1000" }, + "transactionData": "", + "latestLedger": 1234 + }"#; + + let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap(); + assert_eq!(resp.min_resource_fee, 100_000_000); + } + + #[test] + fn simulation_transaction_response_parsing_mostly_empty() { + let s = r#"{ + "latestLedger": 1234 + }"#; + + let resp: SimulateTransactionResponse = serde_json::from_str(s).unwrap(); + assert_eq!(resp.latest_ledger, 1_234); + } + + #[test] + fn test_rpc_url_default_ports() { + // Default ports are added. + let client = Client::new("http://example.com").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "http://example.com:80/" + ); + let client = Client::new("https://example.com").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "https://example.com:443/" + ); + + // Ports are not added when already present. + let client = Client::new("http://example.com:8080").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "http://example.com:8080/" + ); + let client = Client::new("https://example.com:8080").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "https://example.com:8080/" + ); + + // Paths are not modified. + let client = Client::new("http://example.com/a/b/c").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "http://example.com:80/a/b/c" + ); + let client = Client::new("https://example.com/a/b/c").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "https://example.com:443/a/b/c" + ); + let client = Client::new("http://example.com/a/b/c/").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "http://example.com:80/a/b/c/" + ); + let client = Client::new("https://example.com/a/b/c/").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "https://example.com:443/a/b/c/" + ); + let client = Client::new("http://example.com/a/b:80/c/").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "http://example.com:80/a/b:80/c/" + ); + let client = Client::new("https://example.com/a/b:80/c/").unwrap(); + assert_eq!( + client.base_uri.to_string().as_str(), + "https://example.com:443/a/b:80/c/" + ); + } + + #[test] + // Taken from [RPC server + // tests](https://github.com/stellar/soroban-tools/blob/main/cmd/soroban-rpc/internal/methods/get_events_test.go#L21). + fn test_does_topic_match() { + struct TestCase<'a> { + name: &'a str, + filter: Vec<&'a str>, + includes: Vec>, + excludes: Vec>, + } + + let xfer = "AAAABQAAAAh0cmFuc2Zlcg=="; + let number = "AAAAAQB6Mcc="; + let star = "*"; + + for tc in vec![ + // No filter means match nothing. + TestCase { + name: "", + filter: vec![], + includes: vec![], + excludes: vec![vec![xfer]], + }, + // "*" should match "transfer/" but not "transfer/transfer" or + // "transfer/amount", because * is specified as a SINGLE segment + // wildcard. + TestCase { + name: "*", + filter: vec![star], + includes: vec![vec![xfer]], + excludes: vec![vec![xfer, xfer], vec![xfer, number]], + }, + // "*/transfer" should match anything preceding "transfer", but + // nothing that isn't exactly two segments long. + TestCase { + name: "*/transfer", + filter: vec![star, xfer], + includes: vec![vec![number, xfer], vec![xfer, xfer]], + excludes: vec![ + vec![number], + vec![number, number], + vec![number, xfer, number], + vec![xfer], + vec![xfer, number], + vec![xfer, xfer, xfer], + ], + }, + // The inverse case of before: "transfer/*" should match any single + // segment after a segment that is exactly "transfer", but no + // additional segments. + TestCase { + name: "transfer/*", + filter: vec![xfer, star], + includes: vec![vec![xfer, number], vec![xfer, xfer]], + excludes: vec![ + vec![number], + vec![number, number], + vec![number, xfer, number], + vec![xfer], + vec![number, xfer], + vec![xfer, xfer, xfer], + ], + }, + // Here, we extend to exactly two wild segments after transfer. + TestCase { + name: "transfer/*/*", + filter: vec![xfer, star, star], + includes: vec![vec![xfer, number, number], vec![xfer, xfer, xfer]], + excludes: vec![ + vec![number], + vec![number, number], + vec![number, xfer], + vec![number, xfer, number, number], + vec![xfer], + vec![xfer, xfer, xfer, xfer], + ], + }, + // Here, we ensure wildcards can be in the middle of a filter: only + // exact matches happen on the ends, while the middle can be + // anything. + TestCase { + name: "transfer/*/number", + filter: vec![xfer, star, number], + includes: vec![vec![xfer, number, number], vec![xfer, xfer, number]], + excludes: vec![ + vec![number], + vec![number, number], + vec![number, number, number], + vec![number, xfer, number], + vec![xfer], + vec![number, xfer], + vec![xfer, xfer, xfer], + vec![xfer, number, xfer], + ], + }, + ] { + for topic in tc.includes { + assert!( + does_topic_match( + &topic + .iter() + .map(std::string::ToString::to_string) + .collect::>(), + &tc.filter + .iter() + .map(std::string::ToString::to_string) + .collect::>() + ), + "test: {}, topic ({:?}) should be matched by filter ({:?})", + tc.name, + topic, + tc.filter + ); + } + + for topic in tc.excludes { + assert!( + !does_topic_match( + // make deep copies of the vecs + &topic + .iter() + .map(std::string::ToString::to_string) + .collect::>(), + &tc.filter + .iter() + .map(std::string::ToString::to_string) + .collect::>() + ), + "test: {}, topic ({:?}) should NOT be matched by filter ({:?})", + tc.name, + topic, + tc.filter + ); + } + } + } +} diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index 786be1b358..445ad52d24 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -25,6 +25,7 @@ stellar-strkey = { workspace = true } soroban-sdk = { workspace = true } sep5 = { workspace = true } soroban-cli = { workspace = true } +soroban-rpc = { workspace = true } thiserror = "1.0.31" sha2 = "0.10.6" @@ -33,10 +34,25 @@ assert_fs = "1.0.7" predicates = "2.1.5" fs_extra = "1.3.0" +## Test containers +testcontainers.version = "0.15.0" + +## Test containers +testcontainers.features = ["experimental"] +testcontainers-modules.version = "0.3.1" +testcontainers-modules.features = ["redis"] +podman-api = "0.10.0" +phf = { version = "0.11.2", features = ["macros"] } +ctor = "0.2.6" +toml = "0.8.10" + [dev-dependencies] serde_json = "1.0.93" which = { workspace = true } tokio = "1.28.1" +redis = "0.24.0" +pretty_env_logger = "0.5.0" +walkdir = "2.4.0" [features] -integration = [] +it = [] diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index bda6ec420f..c9d48f70de 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -30,13 +30,18 @@ use assert_fs::{fixture::FixtureError, prelude::PathChild, TempDir}; use fs_extra::dir::CopyOptions; use soroban_cli::{ - commands::{config, contract, contract::invoke, global, keys}, - CommandParser, Pwd, + commands::{config, contract::invoke, global, keys, network, NetworkRunnable}, + CommandParser, }; +pub mod module; mod wasm; pub use wasm::Wasm; +pub const TEST_ACCOUNT: &str = "test"; + +pub const LOCAL_NETWORK_PASSPHRASE: &str = "Standalone Network ; February 2017"; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] @@ -53,11 +58,16 @@ pub enum Error { /// its own `TempDir` where it will save test-specific configuration. pub struct TestEnv { pub temp_dir: TempDir, + pub rpc_url: String, } impl Default for TestEnv { fn default() -> Self { - Self::new().unwrap() + let temp_dir = TempDir::new().unwrap(); + Self { + temp_dir, + rpc_url: "http://localhost:8889/soroban/rpc".to_string(), + } } } @@ -79,27 +89,61 @@ impl TestEnv { let test_env = TestEnv::default(); f(&test_env); } - pub fn new() -> Result { - let this = TempDir::new().map(|temp_dir| TestEnv { temp_dir })?; - std::env::set_var("XDG_CONFIG_HOME", this.temp_dir.as_os_str()); - this.new_assert_cmd("keys") - .arg("generate") - .arg("test") - .arg("-d") - .arg("--no-fund") - .assert(); - std::env::set_var("SOROBAN_ACCOUNT", "test"); - Ok(this) + + pub fn with_default_network(f: F) { + let test_env = TestEnv::new(); + f(&test_env); + } + + pub fn with_network(host_port: u16) -> TestEnv { + let env = TestEnv { + rpc_url: format!("http://localhost:{host_port}/soroban/rpc"), + ..Default::default() + }; + env.generate_account("test", None).assert().success(); + env } + pub fn new() -> TestEnv { + let host_port = std::env::var("SOROBAN_PORT") + .as_deref() + .ok() + .and_then(|n| n.parse().ok()) + .unwrap_or(8889); + Self::with_network(host_port) + } /// Create a new `assert_cmd::Command` for a given subcommand and set's the current directory /// to be the internal `temp_dir`. pub fn new_assert_cmd(&self, subcommand: &str) -> Command { - let mut this = Command::cargo_bin("soroban").unwrap_or_else(|_| Command::new("soroban")); - this.arg("-q"); - this.arg(subcommand); - this.current_dir(&self.temp_dir); - this + let mut cmd: Command = self.bin(); + cmd.arg(subcommand) + .env("SOROBAN_ACCOUNT", TEST_ACCOUNT) + .env("SOROBAN_RPC_URL", &self.rpc_url) + .env("SOROBAN_NETWORK_PASSPHRASE", LOCAL_NETWORK_PASSPHRASE) + .env("XDG_CONFIG_HOME", self.temp_dir.as_os_str()) + .env("SOROBAN_FEE", "1000000") + .current_dir(&self.temp_dir); + cmd + } + + pub fn bin(&self) -> Command { + Command::cargo_bin("soroban").unwrap_or_else(|_| Command::new("soroban")) + } + + pub fn generate_account(&self, account: &str, seed: Option) -> Command { + let mut cmd = self.new_assert_cmd("keys"); + cmd.arg("generate").arg(account); + if let Some(seed) = seed { + cmd.arg(format!("--seed={seed}")); + } + cmd + } + + pub fn fund_account(&self, account: &str) -> Assert { + self.new_assert_cmd("keys") + .arg("fund") + .arg(account) + .assert() } /// Parses a `&str` into a command and sets the pwd to be the same as the current `TestEnv`. @@ -129,32 +173,77 @@ impl TestEnv { } /// A convenience method for using the invoke command. - pub async fn invoke>(&self, command_str: &[I]) -> Result { - let cmd = contract::invoke::Cmd::parse_arg_vec( - &command_str - .iter() - .map(AsRef::as_ref) - .filter(|s| !s.is_empty()) - .collect::>(), - ) - .unwrap(); - self.invoke_cmd(cmd).await + pub async fn invoke_with_test>( + &self, + command_str: &[I], + ) -> Result { + self.invoke_with(command_str, "test").await + } + + /// A convenience method for using the invoke command. + pub async fn invoke_with>( + &self, + command_str: &[I], + source: &str, + ) -> Result { + let cmd = self.cmd_with_config::(command_str); + self.run_cmd_with(cmd, source).await + } + + /// A convenience method for using the invoke command. + pub fn cmd_with_config, T: CommandParser + NetworkRunnable>( + &self, + command_str: &[I], + ) -> T { + let mut arg = vec![ + "--network=local", + "--rpc-url=http", + "--network-passphrase=AA", + "--source-account=test", + ]; + let input = command_str + .iter() + .map(AsRef::as_ref) + .filter(|s| !s.is_empty()) + .collect::>(); + arg.extend(input); + T::parse_arg_vec(&arg).unwrap() } /// Invoke an already parsed invoke command - pub async fn invoke_cmd(&self, mut cmd: invoke::Cmd) -> Result { - cmd.set_pwd(self.dir()); - cmd.run_against_rpc_server(&global::Args { + pub async fn run_cmd_with( + &self, + cmd: T, + account: &str, + ) -> Result { + let config_dir = Some(self.dir().to_path_buf()); + let config = config::Args { + network: network::Args { + rpc_url: Some(self.rpc_url.clone()), + network_passphrase: Some(LOCAL_NETWORK_PASSPHRASE.to_string()), + network: None, + }, + source_account: account.to_string(), locator: config::locator::Args { global: false, - config_dir: None, + config_dir: config_dir.clone(), }, - filter_logs: Vec::default(), - quiet: false, - verbose: false, - very_verbose: false, - list: false, - }) + hd_path: None, + }; + cmd.run_against_rpc_server( + Some(&global::Args { + locator: config::locator::Args { + global: false, + config_dir, + }, + filter_logs: Vec::default(), + quiet: false, + verbose: false, + very_verbose: false, + list: false, + }), + Some(&config), + ) .await } @@ -181,7 +270,7 @@ impl TestEnv { /// Copy the contents of the current `TestEnv` to another `TestEnv` pub fn fork(&self) -> Result { - let this = TestEnv::new()?; + let this = TestEnv::new(); self.save(&this.temp_dir)?; Ok(this) } @@ -203,6 +292,7 @@ pub fn temp_ledger_file() -> OsString { pub trait AssertExt { fn stdout_as_str(&self) -> String; + fn stderr_as_str(&self) -> String; } impl AssertExt for Assert { @@ -212,6 +302,12 @@ impl AssertExt for Assert { .trim() .to_owned() } + fn stderr_as_str(&self) -> String { + String::from_utf8(self.get_output().stderr.clone()) + .expect("failed to make str") + .trim() + .to_owned() + } } pub trait CommandExt { fn json_arg(&mut self, j: A) -> &mut Self diff --git a/cmd/crates/soroban-test/src/module.rs b/cmd/crates/soroban-test/src/module.rs new file mode 100644 index 0000000000..db1dcc4ce7 --- /dev/null +++ b/cmd/crates/soroban-test/src/module.rs @@ -0,0 +1,226 @@ +use podman_api::models::{PortMapping, Schema2HealthConfig}; +use podman_api::opts::{ContainerListOpts, VolumeListOpts}; +use podman_api::{ + opts::{self}, + Podman, +}; +use soroban_rpc::Client; +use std::collections::HashMap; +use std::time::Duration; + +use testcontainers::{clients, core::WaitFor, Image}; + +const NAME: &str = "stellar/quickstart"; +// const TAG: &str = "latest"; +const TAG: &str = "latest:sha256:742a649d5d9be826dd4b1a378c95b0e1833e1bcb08c3f4b9b9a8cdd03da653e3"; + +static ENV: &Map = &Map(phf::phf_map! { + "ENABLE_SOROBAN_RPC"=> "true", + "ENABLE_SOROBAN_DIAGNOSTIC_EVENTS" => "true", + "ENABLE_LOGS" => "true", + "NETWORK" => "local", + // "POSTGRES_PASSWORD" => "p", + "LIMITS" => "testnet", +}); +struct Map(phf::Map<&'static str, &'static str>); + +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 Soroban(HashMap, HashMap); + +impl Soroban { + pub fn new() -> Self { + #[allow(unused_mut)] + let mut volumes = HashMap::new(); + // volumes.insert("/home/willem/c/s/soroban-tools/opt/stellar".to_string(), "/opt/stellar".to_string()); + Soroban(ENV.into(), volumes) + } +} + +impl Image for Soroban { + type Args = (); + + fn name(&self) -> String { + NAME.to_owned() + } + + fn tag(&self) -> String { + TAG.to_owned() + } + + fn expose_ports(&self) -> Vec { + vec![8000, 11626] + } + + fn ready_conditions(&self) -> Vec { + // vec![WaitFor::seconds(30)] + + vec![WaitFor::message_on_stdout("friendbot: started")] + } + + fn env_vars(&self) -> Box + '_> { + Box::new(self.0.iter()) + } + + fn volumes(&self) -> Box + '_> { + Box::new(self.1.iter()) + } +} + +pub fn docker() -> clients::Cli { + clients::Cli::default() +} + +pub fn start(client: &clients::Cli) -> testcontainers::Container<'_, Soroban> { + client.run(Soroban::new()) +} + +pub async fn podman(port: Option) -> (podman_api::api::Container, String) { + let podman = Podman::unix("/run/user/1001/podman/podman.sock"); + let _volume = podman.volumes().get("Stellar"); + let host_port = port.unwrap_or(8001); + let portmap = PortMapping { + container_port: Some(8000), + host_ip: None, + host_port: Some(host_port), + protocol: None, + range: None, + }; + // let _volume = NamedVolume { + // dest: Some("/opt/stellar".to_string()), + // is_anonymous: None, + // name: Some("Stellar".to_string()), + // options: None, + // }; + + let test = Some(vec!["curl".to_string(), + "--no-progress-meter".to_string(), + "--fail-with-body".to_string(), + "-X POST".to_string(), + "http://localhost:8001/soroban/rpc".to_string(), + "-H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675,\"method\":\"getNetwork\"}'\"".to_string()]); + + let opts: opts::ContainerCreateOpts = opts::ContainerCreateOpts::builder() + .image(format!("{NAME}/{TAG}")) + .name("stellar-test") + .env(ENV.0.into_iter()) + .portmappings([portmap]) + // .volumes([volume]) + .health_config(Schema2HealthConfig { + interval: Some(30), + retries: Some(3), + start_period: Some(0), + timeout: Some(3_000_000_000), + test, + }) + .build(); + let container = podman.containers().create(&opts).await.unwrap(); + println!("ID: {}", container.id); + let url: String = format!("http://localhost:{host_port}/soroban/rpc"); + let client: Client = Client::new(&url).unwrap(); + + let container = podman.containers().get(container.id); + + println!( + r#" + {:#?} + {:#?} + {:#?} + "#, + container, + podman + .volumes() + .list(&VolumeListOpts::builder().build()) + .await, + podman + .containers() + .list(&ContainerListOpts::builder().build()) + .await + ); + container.start(Some("rm".to_string())).await.unwrap(); + for i in 0..100 { + println!("Trying {i}"); + std::thread::sleep(Duration::from_millis(500)); + if client.get_network().await.is_ok() { + break; + } + } + (container, url) +} + +#[cfg(test)] + +mod tests { + + use podman_api::opts; + use std::time::Duration; + use walkdir::WalkDir; + + use crate::TestEnv; + use soroban_rpc::Client; + + use super::Soroban; + use testcontainers::clients; + + #[tokio::test] + #[ignore] + async fn podman_test() { + let (container, url) = super::podman(None).await; + + println!("{url}"); + + std::env::set_var("SOROBAN_RPC_URL", url); + std::env::set_var( + "SOROBAN_NETWORK_PASSPHRASE", + "Standalone Network ; February 2017", + ); + let env = TestEnv::default(); + let dir = env.dir(); + // list all files recursively from dir including in hidden folders + for entry in WalkDir::new(dir) { + println!("{}", entry.unwrap().path().display()); + } + container + .stop(&opts::ContainerStopOpts::builder().build()) + .await + .unwrap(); + container.remove().await.unwrap(); + } + + #[tokio::test] + #[ignore] + async fn testcontainers_work() { + let _ = pretty_env_logger::try_init(); + let docker = clients::Cli::default(); + let node = docker.run(Soroban::new()); + // return; + // let host_port = 8000; + let host_port = node.get_host_port_ipv4(8000); + let url: String = format!("http://localhost:{host_port}/soroban/rpc"); + println!("{url}"); + let client = Client::new(&url).unwrap(); + + for _ in 0..10 { + std::thread::sleep(Duration::from_secs(1)); + println!("{:#?}", client.get_network().await); + } + std::env::set_var("SOROBAN_RPC_URL", url); + std::env::set_var( + "SOROBAN_NETWORK_PASSPHRASE", + "Standalone Network ; February 2017", + ); + let env = TestEnv::default(); + let dir = env.dir(); + // list all files recursively from dir including in hidden folders + for entry in WalkDir::new(dir) { + println!("{}", entry.unwrap().path().display()); + } + } +} diff --git a/cmd/crates/soroban-test/tests/it/config.rs b/cmd/crates/soroban-test/tests/it/config.rs index 5912b2cf52..b6f1bff084 100644 --- a/cmd/crates/soroban-test/tests/it/config.rs +++ b/cmd/crates/soroban-test/tests/it/config.rs @@ -1,5 +1,5 @@ use assert_fs::TempDir; -use soroban_test::TestEnv; +use soroban_test::{AssertExt, TestEnv}; use std::{fs, path::Path}; use crate::util::{add_key, add_test_id, SecretKind, DEFAULT_SEED_PHRASE}; @@ -7,30 +7,35 @@ use soroban_cli::commands::network; const NETWORK_PASSPHRASE: &str = "Local Sandbox Stellar Network ; September 2022"; +fn ls(sandbox: &TestEnv) -> Vec { + sandbox + .new_assert_cmd("network") + .arg("ls") + .assert() + .stdout_as_str() + .split('\n') + .filter(|s| !s.is_empty()) + .map(ToString::to_string) + .collect::>() +} + #[test] fn set_and_remove_network() { TestEnv::with_default(|sandbox| { add_network(sandbox, "local"); let dir = sandbox.dir().join(".soroban").join("network"); - let read_dir = std::fs::read_dir(dir); - println!("{read_dir:#?}"); - let file = read_dir.unwrap().next().unwrap().unwrap(); + let mut read_dir = std::fs::read_dir(dir).unwrap(); + let file = read_dir.next().unwrap().unwrap(); assert_eq!(file.file_name().to_str().unwrap(), "local.toml"); + let res = ls(sandbox); + assert_eq!(res[0], "local"); + sandbox + .new_assert_cmd("network") + .arg("rm") + .arg("local") + .assert() + .success(); - let res = sandbox.cmd::(""); - let res = res.ls().unwrap(); - assert_eq!(res.len(), 1); - assert_eq!(&res[0], "local"); - - sandbox.cmd::("local").run().unwrap(); - - // sandbox - // .new_assert_cmd("config") - // .arg("network") - // .arg("rm") - // .arg("local") - // .assert() - // .stdout(""); sandbox .new_assert_cmd("network") .arg("ls") @@ -105,7 +110,7 @@ fn set_and_remove_global_network() { #[test] fn multiple_networks() { let sandbox = TestEnv::default(); - let ls = || -> Vec { sandbox.cmd::("").ls().unwrap() }; + let ls = || -> Vec { ls(&sandbox) }; add_network(&sandbox, "local"); println!("{:#?}", ls()); @@ -156,7 +161,6 @@ fn generate_key() { sandbox .new_assert_cmd("keys") .arg("generate") - .arg("--network=futurenet") .arg("--no-fund") .arg("--seed") .arg("0000000000000000") diff --git a/cmd/crates/soroban-test/tests/it/hello_world.rs b/cmd/crates/soroban-test/tests/it/hello_world.rs deleted file mode 100644 index 4c45403a13..0000000000 --- a/cmd/crates/soroban-test/tests/it/hello_world.rs +++ /dev/null @@ -1,22 +0,0 @@ -use soroban_cli::commands::contract::{self, fetch}; -use soroban_test::TestEnv; -use std::path::PathBuf; - -use crate::util::{ - add_test_seed, is_rpc, network_passphrase, network_passphrase_arg, rpc_url, rpc_url_arg, - DEFAULT_PUB_KEY, DEFAULT_PUB_KEY_1, DEFAULT_SECRET_KEY, DEFAULT_SEED_PHRASE, HELLO_WORLD, - TEST_SALT, -}; - -#[tokio::test] -async fn fetch() { - if !is_rpc() { - return; - } - let e = TestEnv::default(); - let f = e.dir().join("contract.wasm"); - let id = deploy_hello(&e); - let cmd = e.cmd_arr::(&["--id", &id, "--out-file", f.to_str().unwrap()]); - cmd.run().await.unwrap(); - assert!(f.exists()); -} diff --git a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs index fda2c1f610..99473db4d3 100644 --- a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs +++ b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs @@ -3,7 +3,7 @@ use serde_json::json; use soroban_cli::commands; use soroban_test::TestEnv; -use crate::integration::util::{deploy_custom, extend_contract, CUSTOM_TYPES}; +use crate::integration::util::{deploy_custom, extend_contract}; use super::util::invoke_with_roundtrip; @@ -15,9 +15,9 @@ fn invoke_custom(e: &TestEnv, id: &str, func: &str) -> assert_cmd::Command { #[tokio::test] async fn parse() { - let sandbox = &TestEnv::default(); + let sandbox = &TestEnv::new(); let id = &deploy_custom(sandbox); - extend_contract(sandbox, id, CUSTOM_TYPES).await; + extend_contract(sandbox, id).await; symbol(sandbox, id); string_with_quotes(sandbox, id).await; symbol_with_quotes(sandbox, id).await; @@ -187,7 +187,7 @@ fn number_arg_return_ok(sandbox: &TestEnv, id: &str) { async fn number_arg_return_err(sandbox: &TestEnv, id: &str) { let res = sandbox - .invoke(&["--id", id, "--", "u32_fail_on_even", "--u32_=2"]) + .invoke_with_test(&["--id", id, "--", "u32_fail_on_even", "--u32_=2"]) .await .unwrap_err(); if let commands::contract::invoke::Error::ContractInvoke(name, doc) = &res { @@ -202,7 +202,7 @@ fn void(sandbox: &TestEnv, id: &str) { .assert() .success() .stdout("\n") - .stderr(""); + .stderr(predicates::str::contains("data: Void")); } fn val(sandbox: &TestEnv, id: &str) { @@ -210,7 +210,7 @@ fn val(sandbox: &TestEnv, id: &str) { .assert() .success() .stdout("null\n") - .stderr(""); + .stderr(predicates::str::contains("data: Void")); } async fn i32(sandbox: &TestEnv, id: &str) { diff --git a/cmd/crates/soroban-test/tests/it/integration/dotenv.rs b/cmd/crates/soroban-test/tests/it/integration/dotenv.rs index 7c0f25b3fc..d3ddd140d1 100644 --- a/cmd/crates/soroban-test/tests/it/integration/dotenv.rs +++ b/cmd/crates/soroban-test/tests/it/integration/dotenv.rs @@ -1,67 +1,69 @@ use soroban_test::TestEnv; -use super::util::{deploy_hello, TEST_CONTRACT_ID}; +use super::util::deploy_hello; + +const SOROBAN_FEE: &str = "100"; fn write_env_file(e: &TestEnv, contents: &str) { let env_file = e.dir().join(".env"); - std::fs::write(&env_file, contents).unwrap(); + let contents = format!("SOROBAN_CONTRACT_ID={contents}"); + std::fs::write(&env_file, &contents).unwrap(); assert_eq!(contents, std::fs::read_to_string(env_file).unwrap()); } -fn contract_id() -> String { - format!("SOROBAN_CONTRACT_ID={TEST_CONTRACT_ID}") -} - #[test] fn can_read_file() { - TestEnv::with_default(|e| { - deploy_hello(e); - write_env_file(e, &contract_id()); - e.new_assert_cmd("contract") - .arg("invoke") - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stdout("[\"Hello\",\"world\"]\n") - .success(); - }); + let e = &TestEnv::new(); + let id = deploy_hello(e); + write_env_file(e, &id); + e.new_assert_cmd("contract") + .env("SOROBAN_FEE", SOROBAN_FEE) + .arg("invoke") + .arg("--") + .arg("hello") + .arg("--world=world") + .assert() + .stdout("[\"Hello\",\"world\"]\n") + .success(); } #[test] fn current_env_not_overwritten() { - TestEnv::with_default(|e| { - deploy_hello(e); - write_env_file(e, &contract_id()); + let e = TestEnv::new(); + write_env_file(&e, &deploy_hello(&e)); - e.new_assert_cmd("contract") - .env("SOROBAN_CONTRACT_ID", "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4") - .arg("invoke") - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stderr("error: Contract not found: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4\n"); - }); + e.new_assert_cmd("contract") + .env( + "SOROBAN_CONTRACT_ID", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + ) + .env("SOROBAN_FEE", SOROBAN_FEE) + .arg("invoke") + .arg("--") + .arg("hello") + .arg("--world=world") + .assert() + .stderr( + "error: Contract not found: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4\n", + ); } #[test] fn cli_args_have_priority() { - TestEnv::with_default(|e| { - deploy_hello(e); - write_env_file(e, &contract_id()); - e.new_assert_cmd("contract") - .env( - "SOROBAN_CONTRACT_ID", - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", - ) - .arg("invoke") - .arg("--id") - .arg(TEST_CONTRACT_ID) - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stdout("[\"Hello\",\"world\"]\n"); - }); + let e = &TestEnv::new(); + let id = deploy_hello(e); + write_env_file(e, &id); + e.new_assert_cmd("contract") + .env( + "SOROBAN_CONTRACT_ID", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + ) + .arg("invoke") + .arg("--id") + .arg(id) + .arg("--") + .arg("hello") + .arg("--world=world") + .assert() + .stdout("[\"Hello\",\"world\"]\n"); } diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index 7bd8d596a7..01e059f464 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -1,23 +1,67 @@ +use std::{thread::sleep, time::Duration}; use predicates::boolean::PredicateBooleanExt; use soroban_cli::commands::{ + config::{locator, secret}, contract::{self, fetch}, - keys, }; -use soroban_test::TestEnv; +use soroban_test::{module::podman, AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; -use crate::{integration::util::extend_contract, util::DEFAULT_SEED_PHRASE}; +use crate::integration::util::extend_contract; -use super::util::{ - add_test_seed, deploy_hello, extend, network_passphrase, network_passphrase_arg, rpc_url, - rpc_url_arg, DEFAULT_PUB_KEY, DEFAULT_PUB_KEY_1, DEFAULT_SECRET_KEY, HELLO_WORLD, -}; +use super::util::{deploy_hello, extend}; #[tokio::test] -#[ignore] async fn invoke() { - let sandbox = &TestEnv::default(); + let sandbox = &TestEnv::new(); + + sandbox + .new_assert_cmd("keys") + .arg("fund") + .arg("test") + .assert() + .stderr(predicates::str::contains("Account already exists")); + sandbox + .new_assert_cmd("keys") + .arg("fund") + .arg("test") + .arg("--hd-path=1") + .assert(); + let addr = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test") + .assert() + .stdout_as_str(); + let addr_1 = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test") + .arg("--hd-path=1") + .assert() + .stdout_as_str(); + println!("Addrs {addr}, {addr_1}"); + + let secret_key = sandbox + .new_assert_cmd("keys") + .arg("show") + .arg("test") + .assert() + .stdout_as_str(); + let secret_key_1 = sandbox + .new_assert_cmd("keys") + .arg("show") + .arg("test") + .arg("--hd-path=1") + .assert() + .stdout_as_str(); + let dir = sandbox.dir(); + let seed_phrase = std::fs::read_to_string(dir.join(".soroban/identity/test.toml")).unwrap(); + let s = toml::from_str::(&seed_phrase).unwrap(); + let secret::Secret::SeedPhrase { seed_phrase } = s else { + panic!("Expected seed phrase") + }; let id = &deploy_hello(sandbox); - extend_contract(sandbox, id, HELLO_WORLD).await; + extend_contract(sandbox, id).await; // Note that all functions tested here have no state invoke_hello_world(sandbox, id); sandbox @@ -29,22 +73,40 @@ async fn invoke() { .stdout(predicates::str::contains(id).not()) .success(); invoke_hello_world_with_lib(sandbox, id).await; - sandbox - .new_assert_cmd("events") - .arg("--start-ledger=20") - .arg("--id") - .arg(id) + let config_locator = locator::Args { + global: false, + config_dir: Some(dir.to_path_buf()), + }; + config_locator + .write_identity( + "testone", + &secret::Secret::SecretKey { + secret_key: secret_key_1.clone(), + }, + ) + .unwrap(); + let sk_from_file = std::fs::read_to_string(dir.join(".soroban/identity/testone.toml")).unwrap(); + + assert_eq!(sk_from_file, format!("secret_key = \"{secret_key_1}\"\n")); + let secret_key_1_readin = sandbox + .new_assert_cmd("keys") + .arg("show") + .arg("testone") .assert() - .stdout(predicates::str::contains(id)) - .success(); - invoke_hello_world_with_lib_two(sandbox, id).await; - invoke_auth(sandbox, id); - invoke_auth_with_identity(sandbox, id).await; - invoke_auth_with_different_test_account_fail(sandbox, id).await; + .stdout_as_str(); + assert_eq!(secret_key_1, secret_key_1_readin); + // list all files recursively from dir including in hidden folders + for entry in walkdir::WalkDir::new(dir) { + println!("{}", entry.unwrap().path().display()); + } + invoke_auth(sandbox, id, &addr); + invoke_auth_with_identity(sandbox, id, "test", &addr).await; + invoke_auth_with_identity(sandbox, id, "testone", &addr_1).await; + invoke_auth_with_different_test_account_fail(sandbox, id, &addr_1).await; // invoke_auth_with_different_test_account(sandbox, id); contract_data_read_failure(sandbox, id); - invoke_with_seed(sandbox, id).await; - invoke_with_sk(sandbox, id).await; + invoke_with_seed(sandbox, id, &seed_phrase).await; + invoke_with_sk(sandbox, id, &secret_key).await; // This does add an identity to local config invoke_with_id(sandbox, id).await; handles_kebab_case(sandbox, id).await; @@ -68,48 +130,27 @@ fn invoke_hello_world(sandbox: &TestEnv, id: &str) { } async fn invoke_hello_world_with_lib(e: &TestEnv, id: &str) { - let mut cmd = contract::invoke::Cmd { + let cmd = contract::invoke::Cmd { contract_id: id.to_string(), slop: vec!["hello".into(), "--world=world".into()], ..Default::default() }; - - cmd.config.network.rpc_url = rpc_url(); - cmd.config.network.network_passphrase = network_passphrase(); - - let res = e.invoke_cmd(cmd).await.unwrap(); + let res = e.run_cmd_with(cmd, "test").await.unwrap(); assert_eq!(res, r#"["Hello","world"]"#); } -async fn invoke_hello_world_with_lib_two(e: &TestEnv, id: &str) { - let hello_world = HELLO_WORLD.to_string(); - let mut invoke_args = vec!["--id", id, "--wasm", hello_world.as_str()]; - let args = vec!["--", "hello", "--world=world"]; - let res = - if let (Some(rpc), Some(network_passphrase)) = (rpc_url_arg(), network_passphrase_arg()) { - invoke_args.push(&rpc); - invoke_args.push(&network_passphrase); - e.invoke(&[invoke_args, args].concat()).await.unwrap() - } else { - e.invoke(&[invoke_args, args].concat()).await.unwrap() - }; - assert_eq!(res, r#"["Hello","world"]"#); -} - -fn invoke_auth(sandbox: &TestEnv, id: &str) { +fn invoke_auth(sandbox: &TestEnv, id: &str, addr: &str) { sandbox .new_assert_cmd("contract") .arg("invoke") .arg("--id") .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) .arg("--") .arg("auth") - .arg(&format!("--addr={DEFAULT_PUB_KEY}")) + .arg("--addr=test") .arg("--world=world") .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) + .stdout(format!("\"{addr}\"\n")) .success(); // Invoke it again without providing the contract, to exercise the deployment @@ -120,65 +161,40 @@ fn invoke_auth(sandbox: &TestEnv, id: &str) { .arg(id) .arg("--") .arg("auth") - .arg(&format!("--addr={DEFAULT_PUB_KEY}")) + .arg("--addr=test") .arg("--world=world") .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) + .stdout(format!("\"{addr}\"\n")) .success(); } -async fn invoke_auth_with_identity(sandbox: &TestEnv, id: &str) { - sandbox - .cmd::("test -d ") - .run() - .await - .unwrap(); +async fn invoke_auth_with_identity(sandbox: &TestEnv, id: &str, key: &str, addr: &str) { sandbox .new_assert_cmd("contract") .arg("invoke") + .arg("--source") + .arg(key) .arg("--id") .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) .arg("--") .arg("auth") .arg("--addr") - .arg(DEFAULT_PUB_KEY) + .arg(key) .arg("--world=world") .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) + .stdout(format!("\"{addr}\"\n")) .success(); } -// fn invoke_auth_with_different_test_account(sandbox: &TestEnv, id: &str) { -// sandbox -// .new_assert_cmd("contract") -// .arg("invoke") -// .arg("--hd-path=1") -// .arg("--id") -// .arg(id) -// .arg("--wasm") -// .arg(HELLO_WORLD.path()) -// .arg("--") -// .arg("auth") -// .arg(&format!("--addr={DEFAULT_PUB_KEY_1}")) -// .arg("--world=world") -// .assert() -// .stdout(format!("\"{DEFAULT_PUB_KEY_1}\"\n")) -// .success(); -// } - -async fn invoke_auth_with_different_test_account_fail(sandbox: &TestEnv, id: &str) { +async fn invoke_auth_with_different_test_account_fail(sandbox: &TestEnv, id: &str, addr: &str) { let res = sandbox - .invoke(&[ + .invoke_with_test(&[ "--hd-path=0", "--id", id, - &rpc_url_arg().unwrap_or_default(), - &network_passphrase_arg().unwrap_or_default(), "--", "auth", - &format!("--addr={DEFAULT_PUB_KEY_1}"), + &format!("--addr={addr}"), "--world=world", ]) .await; @@ -207,9 +223,12 @@ fn contract_data_read_failure(sandbox: &TestEnv, id: &str) { #[tokio::test] async fn contract_data_read() { const KEY: &str = "COUNTER"; - let sandbox = &TestEnv::default(); + let sandbox = &TestEnv::new(); let id = &deploy_hello(sandbox); - let res = sandbox.invoke(&["--id", id, "--", "inc"]).await.unwrap(); + let res = sandbox + .invoke_with_test(&["--id", id, "--", "inc"]) + .await + .unwrap(); assert_eq!(res.trim(), "1"); extend(sandbox, id, Some(KEY)).await; @@ -248,30 +267,21 @@ async fn contract_data_read() { .stdout(predicates::str::starts_with("COUNTER,2")); } -async fn invoke_with_seed(sandbox: &TestEnv, id: &str) { - invoke_with_source(sandbox, DEFAULT_SEED_PHRASE, id).await; +async fn invoke_with_seed(sandbox: &TestEnv, id: &str, seed_phrase: &str) { + invoke_with_source(sandbox, seed_phrase, id).await; } -async fn invoke_with_sk(sandbox: &TestEnv, id: &str) { - invoke_with_source(sandbox, DEFAULT_SECRET_KEY, id).await; +async fn invoke_with_sk(sandbox: &TestEnv, id: &str, sk: &str) { + invoke_with_source(sandbox, sk, id).await; } async fn invoke_with_id(sandbox: &TestEnv, id: &str) { - let identity = add_test_seed(sandbox.dir()); - invoke_with_source(sandbox, &identity, id).await; + invoke_with_source(sandbox, "test", id).await; } async fn invoke_with_source(sandbox: &TestEnv, source: &str, id: &str) { let cmd = sandbox - .invoke(&[ - "--source-account", - source, - "--id", - id, - "--", - "hello", - "--world=world", - ]) + .invoke_with(&["--id", id, "--", "hello", "--world=world"], source) .await .unwrap(); assert_eq!(cmd, "[\"Hello\",\"world\"]"); @@ -279,25 +289,32 @@ async fn invoke_with_source(sandbox: &TestEnv, source: &str, id: &str) { async fn handles_kebab_case(e: &TestEnv, id: &str) { assert!(e - .invoke(&["--id", id, "--", "multi-word-cmd", "--contract-owner=world",]) + .invoke_with_test(&["--id", id, "--", "multi-word-cmd", "--contract-owner=world",]) .await .is_ok()); } async fn fetch(sandbox: &TestEnv, id: &str) { let f = sandbox.dir().join("contract.wasm"); - let cmd = sandbox.cmd_arr::(&["--id", id, "--out-file", f.to_str().unwrap()]); + let cmd = sandbox.cmd_arr::(&[ + "--rpc-url", + &sandbox.rpc_url, + "--network-passphrase", + LOCAL_NETWORK_PASSPHRASE, + "--id", + id, + "--out-file", + f.to_str().unwrap(), + ]); cmd.run().await.unwrap(); assert!(f.exists()); } async fn invoke_prng_u64_in_range_test(sandbox: &TestEnv, id: &str) { assert!(sandbox - .invoke(&[ + .invoke_with_test(&[ "--id", id, - "--wasm", - HELLO_WORLD.path().to_str().unwrap(), "--", "prng_u64_in_range", "--low=0", diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index ea27680b76..996af270cd 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -1,23 +1,9 @@ -use soroban_cli::commands::contract; -use soroban_test::{TestEnv, Wasm}; -use std::{fmt::Display, path::Path}; - -use crate::util::{add_key, SecretKind}; +use soroban_test::{AssertExt, TestEnv, Wasm}; +use std::fmt::Display; pub const HELLO_WORLD: &Wasm = &Wasm::Custom("test-wasms", "test_hello_world"); pub const CUSTOM_TYPES: &Wasm = &Wasm::Custom("test-wasms", "test_custom_types"); -pub fn add_test_seed(dir: &Path) -> String { - let name = "test_seed"; - add_key( - dir, - name, - SecretKind::Seed, - "coral light army gather adapt blossom school alcohol coral light army giggle", - ); - name.to_owned() -} - pub async fn invoke_with_roundtrip(e: &TestEnv, id: &str, func: &str, data: D) where D: Display, @@ -25,34 +11,13 @@ where let data = data.to_string(); println!("{data}"); let res = e - .invoke(&["--id", id, "--", func, &format!("--{func}"), &data]) + .invoke_with_test(&["--id", id, "--", func, &format!("--{func}"), &data]) .await .unwrap(); assert_eq!(res, data); } -pub const DEFAULT_PUB_KEY: &str = "GDIY6AQQ75WMD4W46EYB7O6UYMHOCGQHLAQGQTKHDX4J2DYQCHVCR4W4"; -pub const DEFAULT_SECRET_KEY: &str = "SC36BWNUOCZAO7DMEJNNKFV6BOTPJP7IG5PSHLUOLT6DZFRU3D3XGIXW"; - -pub const DEFAULT_PUB_KEY_1: &str = "GCKZUJVUNEFGD4HLFBUNVYM2QY2P5WQQZMGRA3DDL4HYVT5MW5KG3ODV"; pub const TEST_SALT: &str = "f55ff16f66f43360266b95db6f8fec01d76031054306ae4a4b380598f6cfd114"; -pub const TEST_CONTRACT_ID: &str = "CBVTIVBYWAO2HNPNGKDCZW4OZYYESTKNGD7IPRTDGQSFJS4QBDQQJX3T"; - -pub fn rpc_url() -> Option { - std::env::var("SOROBAN_RPC_URL").ok() -} - -pub fn rpc_url_arg() -> Option { - rpc_url().map(|url| format!("--rpc-url={url}")) -} - -pub fn network_passphrase() -> Option { - std::env::var("SOROBAN_NETWORK_PASSPHRASE").ok() -} - -pub fn network_passphrase_arg() -> Option { - network_passphrase().map(|p| format!("--network-passphrase={p}")) -} pub fn deploy_hello(sandbox: &TestEnv) -> String { deploy_contract(sandbox, HELLO_WORLD) @@ -66,6 +31,7 @@ pub fn deploy_contract(sandbox: &TestEnv, wasm: &Wasm) -> String { let hash = wasm.hash().unwrap(); sandbox .new_assert_cmd("contract") + .env("SOROBAN_FEE", "100000") .arg("install") .arg("--wasm") .arg(wasm.path()) @@ -83,22 +49,11 @@ pub fn deploy_contract(sandbox: &TestEnv, wasm: &Wasm) -> String { .arg(TEST_SALT) .arg("--ignore-checks") .assert() - .success() - .stdout(format!("{TEST_CONTRACT_ID}\n")); - TEST_CONTRACT_ID.to_string() + .stdout_as_str() } -pub async fn extend_contract(sandbox: &TestEnv, id: &str, wasm: &Wasm<'_>) { +pub async fn extend_contract(sandbox: &TestEnv, id: &str) { extend(sandbox, id, None).await; - let cmd: contract::extend::Cmd = sandbox.cmd_arr(&[ - "--wasm-hash", - wasm.hash().unwrap().to_string().as_str(), - "--durability", - "persistent", - "--ledgers-to-extend", - "100000", - ]); - cmd.run().await.unwrap(); } pub async fn extend(sandbox: &TestEnv, id: &str, value: Option<&str>) { @@ -114,6 +69,10 @@ pub async fn extend(sandbox: &TestEnv, id: &str, value: Option<&str>) { args.push("--key"); args.push(value); } - let cmd: contract::extend::Cmd = sandbox.cmd_arr(&args); - cmd.run().await.unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("extend") + .args(args) + .assert() + .success(); } diff --git a/cmd/crates/soroban-test/tests/it/integration/wrap.rs b/cmd/crates/soroban-test/tests/it/integration/wrap.rs index a69e70c7c6..aa356ce99c 100644 --- a/cmd/crates/soroban-test/tests/it/integration/wrap.rs +++ b/cmd/crates/soroban-test/tests/it/integration/wrap.rs @@ -1,97 +1,101 @@ -use soroban_cli::CommandParser; -use soroban_cli::{ - commands::{contract::deploy::asset, keys}, - utils::contract_id_hash_from_asset, -}; -use soroban_test::TestEnv; - -use super::util::network_passphrase; +use soroban_cli::utils::contract_id_hash_from_asset; +use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; #[tokio::test] #[ignore] async fn burn() { - let sandbox = &TestEnv::default(); - let network_passphrase = network_passphrase().unwrap(); - println!("NETWORK_PASSPHRASE: {network_passphrase:?}"); - let address = keys::address::Cmd::parse("test") - .unwrap() - .public_key() - .unwrap(); + let sandbox = &TestEnv::new(); + let network_passphrase = LOCAL_NETWORK_PASSPHRASE.to_string(); + let address = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test") + .assert() + .stdout_as_str(); let asset = format!("native:{address}"); - wrap_cmd(&asset).run().await.unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("asset") + .arg("deploy") + .arg("--source=test") + .arg("--asset") + .arg(&asset) + .assert() + .success(); + // wrap_cmd(&asset).run().await.unwrap(); let asset = soroban_cli::utils::parsing::parse_asset(&asset).unwrap(); let hash = contract_id_hash_from_asset(&asset, &network_passphrase).unwrap(); let id = stellar_strkey::Contract(hash.0).to_string(); - assert_eq!( - "CAMTHSPKXZJIRTUXQP5QWJIFH3XIDMKLFAWVQOFOXPTKAW5GKV37ZC4N", - id - ); - assert_eq!( - "true", - sandbox - .invoke(&[ - "--id", - &id, - "--source=test", - "--", - "authorized", - "--id", - &address.to_string() - ]) - .await - .unwrap() - ); - assert_eq!( - "\"9223372036854775807\"", - sandbox - .invoke(&[ - "--id", - &id, - "--source", - "test", - "--", - "balance", - "--id", - &address.to_string() - ]) - .await - .unwrap(), - ); - - println!( - "{}", - sandbox - .invoke(&[ - "--id", - &id, - "--source=test", - "--", - "burn", - "--id", - &address.to_string(), - "--amount=100" - ]) - .await - .unwrap() - ); - - assert_eq!( - "\"9223372036854775707\"", - sandbox - .invoke(&[ - "--id", - &id, - "--source=test", - "--", - "balance", - "--id", - &address.to_string() - ]) - .await - .unwrap(), - ); -} + println!("{id}, {address}"); + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id, + "--", + "balance", + "--id", + &address.to_string(), + ]) + .assert() + .stdout("\"9223372036854775807\"\n"); + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .args([ + "--id", + &id, + "--", + "authorized", + "--id", + &address.to_string(), + ]) + .assert() + .stdout("true\n"); + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id, + "--", + "balance", + "--id", + &address.to_string(), + ]) + .assert() + .stdout("\"9223372036854775807\"\n"); + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .env("RUST_LOGS", "trace") + .args([ + "--source=test", + "--id", + &id, + "--", + "burn", + "--from", + "test", + "--amount=100", + ]) + .assert() + .stdout("") + .stderr(""); -fn wrap_cmd(asset: &str) -> asset::Cmd { - asset::Cmd::parse_arg_vec(&["--source=test", &format!("--asset={asset}")]).unwrap() + println!("hi"); + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id, + "--", + "balance", + "--id", + &address.to_string(), + ]) + .assert() + .stdout("\"9223372036854775707\"\n"); } diff --git a/cmd/crates/soroban-test/tests/it/main.rs b/cmd/crates/soroban-test/tests/it/main.rs index a6b18cb22f..10aea449ca 100644 --- a/cmd/crates/soroban-test/tests/it/main.rs +++ b/cmd/crates/soroban-test/tests/it/main.rs @@ -1,7 +1,7 @@ mod arg_parsing; mod config; mod help; -#[cfg(feature = "integration")] +#[cfg(feature = "it")] mod integration; mod plugin; mod util; diff --git a/cmd/crates/soroban-test/tests/it/util.rs b/cmd/crates/soroban-test/tests/it/util.rs index 112d5f841a..e471c9d696 100644 --- a/cmd/crates/soroban-test/tests/it/util.rs +++ b/cmd/crates/soroban-test/tests/it/util.rs @@ -4,7 +4,7 @@ use soroban_cli::commands::{ config::{locator::KeyType, secret::Secret}, contract, }; -use soroban_test::{TestEnv, Wasm}; +use soroban_test::{TestEnv, Wasm, TEST_ACCOUNT}; pub const CUSTOM_TYPES: &Wasm = &Wasm::Custom("test-wasms", "test_custom_types"); @@ -44,7 +44,7 @@ pub fn add_test_id(dir: &Path) -> String { pub const DEFAULT_SEED_PHRASE: &str = "coral light army gather adapt blossom school alcohol coral light army giggle"; -#[allow(dead_code)] + pub async fn invoke_custom( sandbox: &TestEnv, id: &str, @@ -52,21 +52,15 @@ pub async fn invoke_custom( arg: &str, wasm: &Path, ) -> Result { - let mut i: contract::invoke::Cmd = sandbox.cmd_arr(&[ + let mut i: contract::invoke::Cmd = sandbox.cmd_with_config(&[ "--id", id, - "--network", - "futurenet", - "--source", - "default", "--", func, arg, ]); i.wasm = Some(wasm.to_path_buf()); - i.config.network.network = Some("futurenet".to_owned()); - i.invoke(&soroban_cli::commands::global::Args::default()) - .await + sandbox.run_cmd_with(i, TEST_ACCOUNT).await } pub const DEFAULT_CONTRACT_ID: &str = "CDR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OO5Z"; diff --git a/cmd/soroban-cli/src/bin/main.rs b/cmd/soroban-cli/src/bin/main.rs index 7a87099c07..70eeb61102 100644 --- a/cmd/soroban-cli/src/bin/main.rs +++ b/cmd/soroban-cli/src/bin/main.rs @@ -37,6 +37,7 @@ async fn main() { let builder = fmt::Subscriber::builder() .with_env_filter(e_filter) + .with_ansi(false) .with_writer(std::io::stderr); let subscriber = builder.finish(); diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index c10bf81646..170f90570f 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -12,7 +12,7 @@ use std::convert::Infallible; use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; use crate::{ - commands::config, + commands::{config, global, NetworkRunnable}, rpc::{Client, Error as SorobanRpcError}, utils::{contract_id_hash_from_asset, parsing::parse_asset}, }; @@ -58,21 +58,30 @@ pub struct Cmd { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - // Parse asset - let asset = parse_asset(&self.asset)?; - - let res_str = self.run_against_rpc_server(asset).await?; + let res_str = self.run_against_rpc_server(None, None).await?; println!("{res_str}"); Ok(()) } +} +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = String; + + async fn run_against_rpc_server( + &self, + _: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + // Parse asset + let asset = parse_asset(&self.asset)?; - async fn run_against_rpc_server(&self, asset: Asset) -> Result { - let network = self.config.get_network()?; + let network = config.get_network()?; let client = Client::new(&network.rpc_url)?; client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 76c1301733..7bc0e9986b 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -14,7 +14,10 @@ use soroban_env_host::{ HostError, }; -use crate::commands::contract::{self, id::wasm::get_contract_id}; +use crate::commands::{ + contract::{self, id::wasm::get_contract_id}, + global, NetworkRunnable, +}; use crate::{ commands::{config, contract::install, HEADING_RPC}, rpc::{self, Client}, @@ -92,20 +95,30 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let res_str = self.run_and_get_contract_id().await?; + let res_str = self.run_against_rpc_server(None, None).await?; println!("{res_str}"); Ok(()) } +} + +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = String; - pub async fn run_and_get_contract_id(&self) -> Result { + async fn run_against_rpc_server( + &self, + global_args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); let wasm_hash = if let Some(wasm) = &self.wasm { let hash = install::Cmd { wasm: wasm::Args { wasm: wasm.clone() }, - config: self.config.clone(), + config: config.clone(), fee: self.fee.clone(), ignore_checks: self.ignore_checks, } - .run_and_get_hash() + .run_against_rpc_server(global_args, Some(config)) .await?; hex::encode(hash) } else { @@ -115,18 +128,13 @@ impl Cmd { .to_string() }; - let hash = Hash(utils::contract_id_from_str(&wasm_hash).map_err(|e| { + let wasm_hash = Hash(utils::contract_id_from_str(&wasm_hash).map_err(|e| { Error::CannotParseWasmHash { wasm_hash: wasm_hash.clone(), error: e, } })?); - - self.run_against_rpc_server(hash).await - } - - async fn run_against_rpc_server(&self, wasm_hash: Hash) -> Result { - let network = self.config.get_network()?; + let network = config.get_network()?; let salt: [u8; 32] = match &self.salt { Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32) .map_err(|_| Error::CannotParseSalt { salt: h.clone() })? @@ -139,7 +147,7 @@ impl Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 86ea23b55b..5a5cef4c65 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -9,7 +9,7 @@ use soroban_env_host::xdr::{ }; use crate::{ - commands::config, + commands::{config, global, NetworkRunnable}, key, rpc::{self, Client}, wasm, Pwd, @@ -80,7 +80,7 @@ pub enum Error { impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let ttl_ledger = self.run_against_rpc_server().await?; + let ttl_ledger = self.run_against_rpc_server(None, None).await?; if self.ttl_ledger_only { println!("{ttl_ledger}"); } else { @@ -99,14 +99,23 @@ impl Cmd { } res } - - async fn run_against_rpc_server(&self) -> Result { - let network = self.config.get_network()?; +} +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = u32; + + async fn run_against_rpc_server( + &self, + _args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; tracing::trace!(?network); let keys = self.key.parse_keys()?; - let network = &self.config.get_network()?; + let network = &config.get_network()?; let client = Client::new(&network.rpc_url)?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; let extend_to = self.ledgers_to_extend(); // Get the account sequence number diff --git a/cmd/soroban-cli/src/commands/contract/fetch.rs b/cmd/soroban-cli/src/commands/contract/fetch.rs index 61a82fc473..9d4a876c1b 100644 --- a/cmd/soroban-cli/src/commands/contract/fetch.rs +++ b/cmd/soroban-cli/src/commands/contract/fetch.rs @@ -21,6 +21,7 @@ use stellar_strkey::DecodeError; use super::super::config::{self, locator}; use crate::commands::network::{self, Network}; +use crate::commands::{global, NetworkRunnable}; use crate::{ rpc::{self, Client}, utils, Pwd, @@ -115,15 +116,28 @@ impl Cmd { } pub async fn get_bytes(&self) -> Result, Error> { - self.run_against_rpc_server().await + self.run_against_rpc_server(None, None).await } pub fn network(&self) -> Result { Ok(self.network.get(&self.locator)?) } - pub async fn run_against_rpc_server(&self) -> Result, Error> { - let network = self.network()?; + fn contract_id(&self) -> Result<[u8; 32], Error> { + utils::contract_id_from_str(&self.contract_id) + .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) + } +} + +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = Vec; + async fn run_against_rpc_server( + &self, + _args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result, Error> { + let network = config.map_or_else(|| self.network(), |c| Ok(c.get_network()?))?; tracing::trace!(?network); let contract_id = self.contract_id()?; let client = Client::new(&network.rpc_url)?; @@ -133,13 +147,7 @@ impl Cmd { // async closures are not yet stable Ok(client.get_remote_wasm(&contract_id).await?) } - - fn contract_id(&self) -> Result<[u8; 32], Error> { - utils::contract_id_from_str(&self.contract_id) - .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) - } } - pub fn get_contract_wasm_from_storage( storage: &mut Storage, contract_id: [u8; 32], diff --git a/cmd/soroban-cli/src/commands/contract/id/wasm.rs b/cmd/soroban-cli/src/commands/contract/id/wasm.rs index 9c02f07d24..c8d4ee2bbc 100644 --- a/cmd/soroban-cli/src/commands/contract/id/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/id/wasm.rs @@ -4,19 +4,25 @@ use soroban_env_host::xdr::{ self, AccountId, ContractIdPreimage, ContractIdPreimageFromAddress, Hash, HashIdPreimage, HashIdPreimageContractId, Limits, PublicKey, ScAddress, Uint256, WriteXdr, }; +use soroban_spec_tools::utils::{contract_id_from_str, padded_hex_from_str}; +use stellar_xdr::curr::ReadXdr; use crate::commands::config; #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { - /// ID of the Soroban contract - #[arg(long)] + /// Salt used when deploying + #[arg(long, default_value = "0")] pub salt: String, + /// Base64 encoded contract id + #[arg(long)] + pub from_xdr: Option, #[command(flatten)] pub config: config::Args, } + #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] @@ -27,20 +33,26 @@ pub enum Error { Xdr(#[from] xdr::Error), #[error("cannot parse salt {0}")] CannotParseSalt(String), + #[error("cannot parse hex {0}")] + Hex(String), } impl Cmd { pub 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 contract_id_preimage = - contract_preimage(&self.config.key_pair()?.verifying_key(), salt); - let contract_id = get_contract_id( - contract_id_preimage.clone(), - &self.config.get_network()?.network_passphrase, - )?; - let strkey_contract_id = stellar_strkey::Contract(contract_id.0).to_string(); + let strkey_contract_id = if let Some(xdr) = &self.from_xdr { + stellar_strkey::Contract(&base64::decode(xdr).map_err(|_| Error::Hex(hex.clone()))?); + } else { + let salt: [u8; 32] = padded_hex_from_str(&self.salt, 32) + .map_err(|_| Error::CannotParseSalt(self.salt.clone()))? + .try_into() + .map_err(|_| Error::CannotParseSalt(self.salt.clone()))?; + let contract_id_preimage = + contract_preimage(&self.config.key_pair()?.verifying_key(), salt); + let contract_id = get_contract_id( + contract_id_preimage.clone(), + &self.config.get_network()?.network_passphrase, + )?; + stellar_strkey::Contract(contract_id.0) + }; println!("{strkey_contract_id}"); Ok(()) } diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index c2fa6d889d..8e15f201e5 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -10,6 +10,7 @@ use soroban_env_host::xdr::{ }; use super::restore; +use crate::commands::{global, NetworkRunnable}; use crate::key; use crate::rpc::{self, Client}; use crate::{commands::config, utils, wasm}; @@ -65,17 +66,23 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let res_str = hex::encode(self.run_and_get_hash().await?); + let res_str = hex::encode(self.run_against_rpc_server(None, None).await?); println!("{res_str}"); Ok(()) } +} - pub async fn run_and_get_hash(&self) -> Result { - self.run_against_rpc_server(&self.wasm.read()?).await - } - - async fn run_against_rpc_server(&self, contract: &[u8]) -> Result { - let network = self.config.get_network()?; +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = Hash; + async fn run_against_rpc_server( + &self, + args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + let contract = self.wasm.read()?; + let network = config.get_network()?; let client = Client::new(&network.rpc_url)?; client .verify_network_passphrase(Some(&network.network_passphrase)) @@ -100,7 +107,7 @@ impl Cmd { tracing::warn!("the deployed smart contract {path} was built with Soroban Rust SDK v{rs_sdk_ver}, a release candidate version not intended for use with the Stellar Public Network", path = self.wasm.wasm.display()); } } - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = @@ -109,7 +116,7 @@ impl Cmd { let sequence: i64 = account_details.seq_num.into(); let (tx_without_preflight, hash) = - build_install_contract_code_tx(contract, sequence + 1, self.fee.fee, &key)?; + build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &key)?; // Currently internal errors are not returned if the contract code is expired if let Some(TransactionResult { @@ -138,12 +145,12 @@ impl Cmd { wasm_hash: None, durability: super::Durability::Persistent, }, - config: self.config.clone(), + config: config.clone(), fee: self.fee.clone(), ledgers_to_extend: None, ttl_ledger_only: true, } - .run_against_rpc_server() + .run_against_rpc_server(args, None) .await?; } diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 720c3c6f80..6e797b75f1 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -28,6 +28,7 @@ use super::super::{ config::{self, locator}, events, }; +use crate::commands::NetworkRunnable; use crate::{commands::global, rpc, Pwd}; use soroban_spec_tools::{contract, Spec}; @@ -261,14 +262,44 @@ impl Cmd { } pub async fn invoke(&self, global_args: &global::Args) -> Result { - self.run_against_rpc_server(global_args).await + self.run_against_rpc_server(Some(global_args), None).await } - pub async fn run_against_rpc_server( + pub fn read_wasm(&self) -> Result>, Error> { + Ok(if let Some(wasm) = self.wasm.as_ref() { + Some(fs::read(wasm).map_err(|e| Error::CannotReadContractFile(wasm.clone(), e))?) + } else { + None + }) + } + + pub fn spec_entries(&self) -> Result>, Error> { + self.read_wasm()? + .map(|wasm| { + soroban_spec::read::from_wasm(&wasm).map_err(Error::CannotParseContractSpec) + }) + .transpose() + } +} + +impl Cmd { + fn contract_id(&self) -> Result<[u8; 32], Error> { + soroban_spec_tools::utils::contract_id_from_str(&self.contract_id) + .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) + } +} + +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = String; + + async fn run_against_rpc_server( &self, - global_args: &global::Args, + global_args: Option<&global::Args>, + config: Option<&config::Args>, ) -> Result { - let network = self.config.get_network()?; + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; tracing::trace!(?network); let contract_id = self.contract_id()?; let spec_entries = self.spec_entries()?; @@ -280,7 +311,7 @@ impl Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = @@ -310,6 +341,11 @@ impl Cmd { txn.sim_response().events()?, ) } else { + let global::Args { + verbose, + very_verbose, + .. + } = global_args.map(Clone::clone).unwrap_or_default(); let res = client .send_assembled_transaction( txn, @@ -317,8 +353,7 @@ impl Cmd { &signers, &network.network_passphrase, Some(log_events), - (global_args.verbose || global_args.very_verbose || self.cost) - .then_some(log_resources), + (verbose || very_verbose || self.cost).then_some(log_resources), ) .await?; (res.return_value()?, res.contract_events()?) @@ -327,29 +362,6 @@ impl Cmd { crate::log::diagnostic_events(&events, tracing::Level::INFO); output_to_string(&spec, &return_value, &function) } - - pub fn read_wasm(&self) -> Result>, Error> { - Ok(if let Some(wasm) = self.wasm.as_ref() { - Some(fs::read(wasm).map_err(|e| Error::CannotReadContractFile(wasm.clone(), e))?) - } else { - None - }) - } - - pub fn spec_entries(&self) -> Result>, Error> { - self.read_wasm()? - .map(|wasm| { - soroban_spec::read::from_wasm(&wasm).map_err(Error::CannotParseContractSpec) - }) - .transpose() - } -} - -impl Cmd { - fn contract_id(&self) -> Result<[u8; 32], Error> { - soroban_spec_tools::utils::contract_id_from_str(&self.contract_id) - .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) - } } fn log_events( diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index f25b6c2c0e..72195fee38 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -14,7 +14,7 @@ use soroban_env_host::{ use soroban_sdk::xdr::Limits; use crate::{ - commands::config, + commands::{config, global, NetworkRunnable}, key, rpc::{self, Client, FullLedgerEntries, FullLedgerEntry}, }; @@ -91,19 +91,10 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let entries = self.run_against_rpc_server().await?; + let entries = self.run_against_rpc_server(None, None).await?; self.output_entries(&entries) } - async fn run_against_rpc_server(&self) -> Result { - let network = self.config.get_network()?; - tracing::trace!(?network); - let network = &self.config.get_network()?; - let client = Client::new(&network.rpc_url)?; - let keys = self.key.parse_keys()?; - Ok(client.get_full_ledger_entries(&keys).await?) - } - fn output_entries(&self, entries: &FullLedgerEntries) -> Result<(), Error> { if entries.entries.is_empty() { return Err(Error::NoContractDataEntryFoundForContractID); @@ -178,3 +169,20 @@ impl Cmd { Ok(()) } } + +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = FullLedgerEntries; + async fn run_against_rpc_server( + &self, + _: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; + tracing::trace!(?network); + let client = Client::new(&network.rpc_url)?; + let keys = self.key.parse_keys()?; + Ok(client.get_full_ledger_entries(&keys).await?) + } +} diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 6ed39f8929..720ee221d7 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -13,6 +13,7 @@ use crate::{ commands::{ config::{self, locator}, contract::extend, + global, NetworkRunnable, }, key, rpc::{self, Client}, @@ -87,7 +88,7 @@ pub enum Error { impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let expiration_ledger_seq = self.run_against_rpc_server().await?; + let expiration_ledger_seq = self.run_against_rpc_server(None, None).await?; if let Some(ledgers_to_extend) = self.ledgers_to_extend { extend::Cmd { @@ -105,14 +106,23 @@ impl Cmd { Ok(()) } +} - pub async fn run_against_rpc_server(&self) -> Result { - let network = self.config.get_network()?; +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = u32; + + async fn run_against_rpc_server( + &self, + _: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; tracing::trace!(?network); let entry_keys = self.key.parse_keys()?; - let network = &self.config.get_network()?; let client = Client::new(&network.rpc_url)?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index aa46bbe230..12ac050101 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -3,7 +3,10 @@ use std::io; use soroban_env_host::xdr::{self, Limits, ReadXdr}; -use super::{config::locator, network}; +use super::{ + config::{self, locator}, + global, network, NetworkRunnable, +}; use crate::{rpc, utils}; #[derive(Parser, Debug, Clone)] @@ -119,6 +122,8 @@ pub enum Error { Network(#[from] network::Error), #[error(transparent)] Locator(#[from] locator::Error), + #[error(transparent)] + Config(#[from] config::Error), } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] @@ -162,7 +167,7 @@ impl Cmd { })?; } - let response = self.run_against_rpc_server().await?; + let response = self.run_against_rpc_server(None, None).await?; for event in &response.events { match self.output { @@ -189,9 +194,31 @@ impl Cmd { Ok(()) } - async fn run_against_rpc_server(&self) -> Result { + fn start(&self) -> Result { + let start = match (self.start_ledger, self.cursor.clone()) { + (Some(start), _) => rpc::EventStart::Ledger(start), + (_, Some(c)) => rpc::EventStart::Cursor(c), + // should never happen because of required_unless_present flags + _ => return Err(Error::MissingStartLedgerAndCursor), + }; + Ok(start) + } +} +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = rpc::GetEventsResponse; + + async fn run_against_rpc_server( + &self, + _args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { let start = self.start()?; - let network = self.network.get(&self.locator)?; + let network = if let Some(config) = config { + Ok(config.get_network()?) + } else { + self.network.get(&self.locator) + }?; let client = rpc::Client::new(&network.rpc_url)?; client @@ -208,14 +235,4 @@ impl Cmd { .await .map_err(Error::Rpc) } - - fn start(&self) -> Result { - let start = match (self.start_ledger, self.cursor.clone()) { - (Some(start), _) => rpc::EventStart::Ledger(start), - (_, Some(c)) => rpc::EventStart::Cursor(c), - // should never happen because of required_unless_present flags - _ => return Err(Error::MissingStartLedgerAndCursor), - }; - Ok(start) - } } diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 952869af3b..bb84dbf3b5 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -158,3 +158,14 @@ pub enum Error { #[error(transparent)] Network(#[from] network::Error), } + +pub trait NetworkRunnable { + type Error; + type Result; + + fn run_against_rpc_server( + &self, + global_args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> impl std::future::Future> + Send; +} diff --git a/cmd/soroban-cli/src/commands/network/mod.rs b/cmd/soroban-cli/src/commands/network/mod.rs index 22cba1904e..adaba02431 100644 --- a/cmd/soroban-cli/src/commands/network/mod.rs +++ b/cmd/soroban-cli/src/commands/network/mod.rs @@ -141,11 +141,9 @@ impl Network { pub async fn helper_url(&self, addr: &str) -> Result { tracing::debug!("address {addr:?}"); let client = Client::new(&self.rpc_url)?; - let helper_url_root = client.friendbot_url().await?; - let uri = http::Uri::from_str(&helper_url_root) - .map_err(|_| Error::InvalidUrl(helper_url_root.to_string()))?; + let uri = client.friendbot_url().await?; http::Uri::from_str(&format!("{uri:?}?addr={addr}")) - .map_err(|_| Error::InvalidUrl(helper_url_root.to_string())) + .map_err(|_| Error::InvalidUrl(uri.to_string())) } #[allow(clippy::similar_names)] diff --git a/cmd/soroban-rpc/internal/test/integration.go b/cmd/soroban-rpc/internal/test/integration.go new file mode 100644 index 0000000000..10b24c854b --- /dev/null +++ b/cmd/soroban-rpc/internal/test/integration.go @@ -0,0 +1,356 @@ +package test + +import ( + "context" + "fmt" + "os" + "os/exec" + "os/signal" + "path" + "path/filepath" + "strconv" + "sync" + "syscall" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/stellar/go/clients/stellarcore" + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + + "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config" + "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon" + "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" + "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/ledgerbucketwindow" +) + +const ( + StandaloneNetworkPassphrase = "Standalone Network ; February 2017" + stellarCoreProtocolVersion = 20 + stellarCorePort = 11626 + goModFile = "go.mod" + goMonorepoGithubPath = "github.com/stellar/go" + friendbotURL = "http://localhost:8000/friendbot" + // Needed when Core is run with ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true + checkpointFrequency = 8 + sorobanRPCPort = 8042 + adminPort = 8080 + helloWorldContractPath = "../../../../target/wasm32-unknown-unknown/test-wasms/test_hello_world.wasm" +) + +type Test struct { + t *testing.T + + composePath string // docker compose yml file + + daemon *daemon.Daemon + + coreClient *stellarcore.Client + + masterAccount txnbuild.Account + shutdownOnce sync.Once + shutdownCalls []func() +} + +func NewTest(t *testing.T) *Test { + if os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_ENABLED") == "" { + t.Skip("skipping integration test: SOROBAN_RPC_INTEGRATION_TESTS_ENABLED not set") + } + coreBinaryPath := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") + if coreBinaryPath == "" { + t.Fatal("missing SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") + } + + i := &Test{ + t: t, + composePath: findDockerComposePath(), + } + i.masterAccount = &txnbuild.SimpleAccount{ + AccountID: i.MasterKey().Address(), + Sequence: 0, + } + i.runComposeCommand("up", "--detach", "--quiet-pull", "--no-color") + i.prepareShutdownHandlers() + i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(stellarCorePort)} + i.waitForCore() + i.waitForCheckpoint() + i.launchDaemon(coreBinaryPath) + + return i +} + +func (i *Test) MasterKey() *keypair.Full { + return keypair.Root(StandaloneNetworkPassphrase) +} + +func (i *Test) MasterAccount() txnbuild.Account { + return i.masterAccount +} + +func (i *Test) sorobanRPCURL() string { + return fmt.Sprintf("http://localhost:%d", sorobanRPCPort) +} + +func (i *Test) adminURL() string { + return fmt.Sprintf("http://localhost:%d", adminPort) +} + +func (i *Test) waitForCheckpoint() { + i.t.Log("Waiting for core to be up...") + for t := 30 * time.Second; t >= 0; t -= time.Second { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + info, err := i.coreClient.Info(ctx) + cancel() + if err != nil { + i.t.Logf("could not obtain info response: %v", err) + time.Sleep(time.Second) + continue + } + if info.Info.Ledger.Num <= checkpointFrequency { + i.t.Logf("checkpoint not reached yet: %v", info) + time.Sleep(time.Second) + continue + } + return + } + i.t.Fatal("Core could not reach checkpoint ledger after 30s") +} + +func (i *Test) launchDaemon(coreBinaryPath string) { + var config config.Config + cmd := &cobra.Command{} + if err := config.AddFlags(cmd); err != nil { + i.t.FailNow() + } + if err := config.SetValues(func(string) (string, bool) { return "", false }); err != nil { + i.t.FailNow() + } + + config.Endpoint = fmt.Sprintf("localhost:%d", sorobanRPCPort) + config.AdminEndpoint = fmt.Sprintf("localhost:%d", adminPort) + config.StellarCoreURL = "http://localhost:" + strconv.Itoa(stellarCorePort) + config.CoreRequestTimeout = time.Second * 2 + config.StellarCoreBinaryPath = coreBinaryPath + config.CaptiveCoreConfigPath = path.Join(i.composePath, "captive-core-integration-tests.cfg") + config.CaptiveCoreStoragePath = i.t.TempDir() + config.CaptiveCoreHTTPPort = 0 + config.FriendbotURL = friendbotURL + config.NetworkPassphrase = StandaloneNetworkPassphrase + config.HistoryArchiveURLs = []string{"http://localhost:1570"} + config.LogLevel = logrus.DebugLevel + config.SQLiteDBPath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") + config.IngestionTimeout = 10 * time.Minute + config.EventLedgerRetentionWindow = ledgerbucketwindow.DefaultEventLedgerRetentionWindow + config.CheckpointFrequency = checkpointFrequency + config.MaxHealthyLedgerLatency = time.Second * 10 + config.PreflightEnableDebug = true + + i.daemon = daemon.MustNew(&config) + go i.daemon.Run() + + // wait for the storage to catch up for 1 minute + info, err := i.coreClient.Info(context.Background()) + if err != nil { + i.t.Fatalf("cannot obtain latest ledger from core: %v", err) + } + targetLedgerSequence := uint32(info.Info.Ledger.Num) + + reader := db.NewLedgerEntryReader(i.daemon.GetDB()) + success := false + for t := 30; t >= 0; t -= 1 { + sequence, err := reader.GetLatestLedgerSequence(context.Background()) + if err != nil { + if err != db.ErrEmptyDB { + i.t.Fatalf("cannot access ledger entry storage: %v", err) + } + } else { + if sequence >= targetLedgerSequence { + success = true + break + } + } + time.Sleep(time.Second) + } + if !success { + i.t.Fatalf("LedgerEntryStorage failed to sync in 1 minute") + } +} + +// Runs a docker-compose command applied to the above configs +func (i *Test) runComposeCommand(args ...string) { + integrationYaml := filepath.Join(i.composePath, "docker-compose.yml") + + cmdline := append([]string{"-f", integrationYaml}, args...) + cmd := exec.Command("docker-compose", cmdline...) + + i.t.Log("Running", cmd.Env, cmd.Args) + out, innerErr := cmd.Output() + if exitErr, ok := innerErr.(*exec.ExitError); ok { + fmt.Printf("stdout:\n%s\n", string(out)) + fmt.Printf("stderr:\n%s\n", string(exitErr.Stderr)) + } + + if innerErr != nil { + i.t.Fatalf("Compose command failed: %v", innerErr) + } +} + +func (i *Test) prepareShutdownHandlers() { + i.shutdownCalls = append(i.shutdownCalls, + func() { + if i.daemon != nil { + i.daemon.Close() + } + i.runComposeCommand("down", "-v") + }, + ) + + // Register cleanup handlers (on panic and ctrl+c) so the containers are + // stopped even if ingestion or testing fails. + i.t.Cleanup(i.Shutdown) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + i.Shutdown() + os.Exit(int(syscall.SIGTERM)) + }() +} + +// Shutdown stops the integration tests and destroys all its associated +// resources. It will be implicitly called when the calling test (i.e. the +// `testing.Test` passed to `New()`) is finished if it hasn't been explicitly +// called before. +func (i *Test) Shutdown() { + i.shutdownOnce.Do(func() { + // run them in the opposite order in which they where added + for callI := len(i.shutdownCalls) - 1; callI >= 0; callI-- { + i.shutdownCalls[callI]() + } + }) +} + +// Wait for core to be up and manually close the first ledger +func (i *Test) waitForCore() { + i.t.Log("Waiting for core to be up...") + for t := 30 * time.Second; t >= 0; t -= time.Second { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + _, err := i.coreClient.Info(ctx) + cancel() + if err != nil { + i.t.Logf("could not obtain info response: %v", err) + time.Sleep(time.Second) + continue + } + break + } + + i.UpgradeProtocol(stellarCoreProtocolVersion) + + for t := 0; t < 5; t++ { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + info, err := i.coreClient.Info(ctx) + cancel() + if err != nil || !info.IsSynced() { + i.t.Logf("Core is still not synced: %v %v", err, info) + time.Sleep(time.Second) + continue + } + i.t.Log("Core is up.") + return + } + i.t.Fatal("Core could not sync after 30s") +} + +// UpgradeProtocol arms Core with upgrade and blocks until protocol is upgraded. +func (i *Test) UpgradeProtocol(version uint32) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + err := i.coreClient.Upgrade(ctx, int(version)) + cancel() + if err != nil { + i.t.Fatalf("could not upgrade protocol: %v", err) + } + + for t := 0; t < 10; t++ { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + info, err := i.coreClient.Info(ctx) + cancel() + if err != nil { + i.t.Logf("could not obtain info response: %v", err) + time.Sleep(time.Second) + continue + } + + if info.Info.Ledger.Version == int(version) { + i.t.Logf("Protocol upgraded to: %d", info.Info.Ledger.Version) + return + } + time.Sleep(time.Second) + } + + i.t.Fatalf("could not upgrade protocol in 10s") +} + +// Cluttering code with if err != nil is absolute nonsense. +func panicIf(err error) { + if err != nil { + panic(err) + } +} + +// findProjectRoot iterates upward on the directory until go.mod file is found. +func findProjectRoot(current string) string { + // Lets you check if a particular directory contains a file. + directoryContainsFilename := func(dir string, filename string) bool { + files, innerErr := os.ReadDir(dir) + panicIf(innerErr) + + for _, file := range files { + if file.Name() == filename { + return true + } + } + return false + } + var err error + + // In either case, we try to walk up the tree until we find "go.mod", + // which we hope is the root directory of the project. + for !directoryContainsFilename(current, goModFile) { + current, err = filepath.Abs(filepath.Join(current, "..")) + + // FIXME: This only works on *nix-like systems. + if err != nil || filepath.Base(current)[0] == filepath.Separator { + fmt.Println("Failed to establish project root directory.") + panic(err) + } + } + return current +} + +// findDockerComposePath performs a best-effort attempt to find the project's +// Docker Compose files. +func findDockerComposePath() string { + current, err := os.Getwd() + panicIf(err) + + // + // We have a primary and backup attempt for finding the necessary docker + // files: via $GOPATH and via local directory traversal. + // + + if gopath := os.Getenv("GOPATH"); gopath != "" { + monorepo := filepath.Join(gopath, "src", "github.com", "stellar", "soroban-tools") + if _, err = os.Stat(monorepo); !os.IsNotExist(err) { + current = monorepo + } + } + + current = findProjectRoot(current) + + // Directly jump down to the folder that should contain the configs + return filepath.Join(current, "cmd", "soroban-rpc", "internal", "test") +}