From 367aa181a485e809e211d57c5e501412365d7183 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Mon, 9 Feb 2026 16:00:00 +0000 Subject: [PATCH 01/23] chore: make `crates/gateway` a proper package in this workspace --- Cargo.lock | 421 ++- Cargo.toml | 2 + crates/gateway/.cargo/audit.toml | 16 - crates/gateway/.envrc | 3 - crates/gateway/.github/workflows/ci.yaml | 43 - crates/gateway/.gitignore | 37 - crates/gateway/.vscode/settings.json | 3 - crates/gateway/.yamlfmt.yml | 3 - crates/gateway/Cargo.lock | 3060 ---------------------- crates/gateway/Cargo.toml | 11 +- crates/gateway/LICENSE | 6 - crates/gateway/deny.toml | 22 - crates/gateway/flake.lock | 225 -- crates/gateway/flake.nix | 114 - crates/gateway/nix/devshells.nix | 87 - crates/gateway/nix/internal/darwin.nix | 9 - crates/gateway/nix/internal/linux.nix | 6 - crates/gateway/nix/internal/unix.nix | 214 -- crates/gateway/rustfmt.toml | 14 - deny.toml | 9 + flake.nix | 6 +- nix/devshells.nix | 15 +- nix/internal/unix.nix | 45 +- 23 files changed, 479 insertions(+), 3892 deletions(-) delete mode 100644 crates/gateway/.cargo/audit.toml delete mode 100644 crates/gateway/.envrc delete mode 100644 crates/gateway/.github/workflows/ci.yaml delete mode 100644 crates/gateway/.gitignore delete mode 100644 crates/gateway/.vscode/settings.json delete mode 100644 crates/gateway/.yamlfmt.yml delete mode 100644 crates/gateway/Cargo.lock delete mode 100644 crates/gateway/LICENSE delete mode 100644 crates/gateway/deny.toml delete mode 100644 crates/gateway/flake.lock delete mode 100644 crates/gateway/flake.nix delete mode 100644 crates/gateway/nix/devshells.nix delete mode 100644 crates/gateway/nix/internal/darwin.nix delete mode 100644 crates/gateway/nix/internal/linux.nix delete mode 100644 crates/gateway/nix/internal/unix.nix delete mode 100644 crates/gateway/rustfmt.toml diff --git a/Cargo.lock b/Cargo.lock index 895f70f4..e96c9fcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,13 +307,50 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "base64 0.22.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite 0.24.0", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "axum-core", + "axum-core 0.5.5", "base64 0.22.1", "bytes", "form_urlencoded", @@ -324,7 +361,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -336,13 +373,34 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tower", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-core" version = "0.5.5" @@ -442,7 +500,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.100", ] @@ -549,6 +607,48 @@ dependencies = [ "url", ] +[[package]] +name = "blockfrost-gateway" +version = "0.0.3-rc.3" +dependencies = [ + "anyhow", + "axum 0.7.9", + "base64 0.21.7", + "blake3", + "blockfrost", + "bytes", + "chrono", + "clap", + "colored", + "deadpool-diesel", + "diesel", + "diesel_migrations", + "dirs", + "dotenvy", + "futures", + "futures-util", + "getrandom 0.3.2", + "hex", + "hyper", + "kameo", + "machine-uid", + "nix", + "rand 0.8.5", + "reqwest 0.12.28", + "rstest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite 0.24.0", + "tokio-util", + "toml 0.9.11+spec-1.1.0", + "tracing", + "tracing-subscriber", + "tungstenite 0.24.0", + "uuid", +] + [[package]] name = "blockfrost-openapi" version = "0.1.83" @@ -568,7 +668,7 @@ name = "blockfrost-platform" version = "0.0.3-rc.3" dependencies = [ "anyhow", - "axum", + "axum 0.8.8", "base64 0.22.1", "bech32 0.11.1", "bip39", @@ -616,7 +716,7 @@ dependencies = [ "serde_with", "thiserror 2.0.18", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tokio-util", "toml 0.9.11+spec-1.1.0", "tower", @@ -624,7 +724,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tracing-test", - "tungstenite", + "tungstenite 0.28.0", "twelf", "uuid", ] @@ -652,7 +752,7 @@ name = "blockfrost-platform-common" version = "0.0.3-rc.3" dependencies = [ "anyhow", - "axum", + "axum 0.8.8", "bech32 0.11.1", "blockfrost-platform-api-provider", "blockfrost-platform-build-utils", @@ -682,7 +782,7 @@ name = "blockfrost-platform-data-node" version = "0.0.3-rc.3" dependencies = [ "async-trait", - "axum", + "axum 0.8.8", "blockfrost-platform-api-provider", "blockfrost-platform-common", "dirs", @@ -928,6 +1028,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "config-derive" version = "0.15.0" @@ -1168,11 +1278,34 @@ dependencies = [ "tokio", ] +[[package]] +name = "deadpool-diesel" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590573e9e29c5190a5ff782136f871e6e652e35d598a349888e028693601adf1" +dependencies = [ + "deadpool", + "deadpool-sync", + "diesel", +] + [[package]] name = "deadpool-runtime" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadpool-sync" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" +dependencies = [ + "deadpool-runtime", +] [[package]] name = "debugid" @@ -1232,6 +1365,54 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diesel" +version = "2.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d" +dependencies = [ + "bitflags 2.10.0", + "byteorder", + "chrono", + "diesel_derives", + "downcast-rs", + "itoa", + "pq-sys", +] + +[[package]] +name = "diesel_derives" +version = "2.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47618bf0fac06bb670c036e48404c26a865e6a71af4114dfd97dfe89936e404e" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "diesel_migrations" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "745fd255645f0f1135f9ec55c7b00e0882192af9683ab4731e4bba3da82b8f9c" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" +dependencies = [ + "syn 2.0.100", +] + [[package]] name = "diff" version = "0.1.13" @@ -1311,6 +1492,26 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dsl_auto_type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" +dependencies = [ + "darling", + "either", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "dyn-clone" version = "1.0.19" @@ -1827,6 +2028,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 0.26.11", ] [[package]] @@ -2176,6 +2378,33 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kameo" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c4af7638c67029fd6821d02813c3913c803784648725d4df4082c9b91d7cbb1" +dependencies = [ + "downcast-rs", + "dyn-clone", + "futures", + "kameo_macros", + "serde", + "tokio", + "tracing", +] + +[[package]] +name = "kameo_macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13c324e2d8c8e126e63e66087448b4267e263e6cb8770c56d10a9d0d279d9e2" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -2300,6 +2529,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lzma-rust2" version = "0.15.3" @@ -2336,6 +2571,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matchit" version = "0.8.4" @@ -2404,6 +2645,27 @@ dependencies = [ "sketches-ddsketch", ] +[[package]] +name = "migrations_internals" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" +dependencies = [ + "serde", + "toml 0.9.11+spec-1.1.0", +] + +[[package]] +name = "migrations_macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + [[package]] name = "mime" version = "0.3.17" @@ -3121,6 +3383,17 @@ dependencies = [ "zerocopy 0.8.24", ] +[[package]] +name = "pq-sys" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "574ddd6a267294433f140b02a726b0640c43cf7c6f717084684aaa3b285aba61" +dependencies = [ + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -3278,6 +3551,61 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.6.1", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "lru-slab", + "rand 0.9.0", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -3527,6 +3855,8 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -3534,6 +3864,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -3541,6 +3872,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 1.0.6", ] [[package]] @@ -3646,6 +3978,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3675,6 +4013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -3695,6 +4034,9 @@ name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -4465,6 +4807,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.1", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -4510,6 +4853,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.24.0", +] + [[package]] name = "tokio-tungstenite" version = "0.28.0" @@ -4521,7 +4876,7 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tungstenite", + "tungstenite 0.28.0", ] [[package]] @@ -4759,6 +5114,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.28.0" @@ -5079,6 +5452,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-root-certs" version = "0.26.11" @@ -5097,6 +5480,24 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index e48129be..2fade5ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/api_provider", "crates/node", "crates/data_node", + "crates/gateway", ] default-members = ["crates/platform"] @@ -64,6 +65,7 @@ toml = "0.9.11+spec-1.1.0" tracing = "0.1.44" twelf = { version = "0.15.0", features = ["clap", "toml"] } url = "2" +uuid = { version = "1.10", features = ["serde", "v4"] } [workspace.lints.clippy] uninlined_format_args = "deny" diff --git a/crates/gateway/.cargo/audit.toml b/crates/gateway/.cargo/audit.toml deleted file mode 100644 index 01aa8279..00000000 --- a/crates/gateway/.cargo/audit.toml +++ /dev/null @@ -1,16 +0,0 @@ -[advisories] -ignore = [ - "RUSTSEC-2024-0421", # FIXME: upgrade to idna >=1.0.0 (transitive dependency of `url`) -] -informational_warnings = ["unmaintained"] -severity_threshold = "low" - -[output] -deny = ["unmaintained"] -format = "terminal" -quiet = false -show_tree = true - -[yanked] -enabled = true -update_index = true diff --git a/crates/gateway/.envrc b/crates/gateway/.envrc deleted file mode 100644 index af3b9a0d..00000000 --- a/crates/gateway/.envrc +++ /dev/null @@ -1,3 +0,0 @@ -use flake - -source_env_if_exists .envrc.local diff --git a/crates/gateway/.github/workflows/ci.yaml b/crates/gateway/.github/workflows/ci.yaml deleted file mode 100644 index c66db553..00000000 --- a/crates/gateway/.github/workflows/ci.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: Continuous Integration - -on: - push: - -env: - CARGO_TERM_COLOR: always - -jobs: - build_and_test: - name: Rust Project CI - runs-on: ubuntu-latest - - strategy: - matrix: - rust: - - stable - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Install Rust toolchain - run: | - rustup update --no-self-update stable - rustup component add --toolchain stable rustfmt rust-src - rustup default stable - - - name: Build - run: cargo build --verbose - - - name: Test - run: cargo test --verbose --lib - - - name: Check formatting - run: cargo fmt --all -- --check - - - name: Lint with Clippy - run: cargo clippy --all-targets --all-features - - - name: Tests - run: cargo test --all-features --verbose -- --nocapture diff --git a/crates/gateway/.gitignore b/crates/gateway/.gitignore deleted file mode 100644 index ed03a878..00000000 --- a/crates/gateway/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -#Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - -# RustRover -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Added by cargo - -/target -.DS_Store -.env - -# Nix -result -result-* - -# direnv -.direnv/ -.envrc.local - -*.log diff --git a/crates/gateway/.vscode/settings.json b/crates/gateway/.vscode/settings.json deleted file mode 100644 index 0a947c93..00000000 --- a/crates/gateway/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cSpell.words": ["Blockfrost", "Insertable", "reqwest"] -} diff --git a/crates/gateway/.yamlfmt.yml b/crates/gateway/.yamlfmt.yml deleted file mode 100644 index 0d7c56df..00000000 --- a/crates/gateway/.yamlfmt.yml +++ /dev/null @@ -1,3 +0,0 @@ -formatter: - type: basic - retain_line_breaks_single: true diff --git a/crates/gateway/Cargo.lock b/crates/gateway/Cargo.lock deleted file mode 100644 index f17944a7..00000000 --- a/crates/gateway/Cargo.lock +++ /dev/null @@ -1,3060 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" - -[[package]] -name = "anstyle-parse" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" -dependencies = [ - "anstyle", - "windows-sys 0.52.0", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-trait" -version = "0.1.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core", - "base64 0.22.1", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper", - "tokio", - "tokio-tungstenite", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "blake3" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blockfrost" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3f18d436df2ac619d7cfa04f883b5f241619d3ef43c893d3c035012a271f81" -dependencies = [ - "blockfrost-openapi", - "futures", - "futures-timer", - "reqwest", - "serde", - "serde_json", - "thiserror 2.0.11", - "url", -] - -[[package]] -name = "blockfrost-gateway" -version = "1.3.3" -dependencies = [ - "anyhow", - "axum", - "base64 0.21.7", - "blake3", - "blockfrost", - "bytes", - "chrono", - "clap", - "colored", - "deadpool-diesel", - "diesel", - "diesel_migrations", - "dirs", - "dotenvy", - "futures", - "futures-util", - "getrandom 0.3.4", - "hex", - "hyper", - "kameo", - "machine-uid", - "nix", - "rand", - "reqwest", - "rstest", - "serde", - "serde_json", - "thiserror 1.0.63", - "tokio", - "tokio-tungstenite", - "tokio-util", - "toml 0.9.5", - "tracing", - "tracing-subscriber", - "tungstenite", - "uuid", -] - -[[package]] -name = "blockfrost-openapi" -version = "0.1.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e88a3c131d5e95b82a761d9b4ae0d100b67b138a9fd1d880742b50b26318a5" -dependencies = [ - "serde", - "serde_json", - "serde_with", - "uuid", -] - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" - -[[package]] -name = "cc" -version = "1.2.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-targets 0.52.6", -] - -[[package]] -name = "clap" -version = "4.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" - -[[package]] -name = "colorchoice" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" - -[[package]] -name = "colored" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" -dependencies = [ - "lazy_static", - "windows-sys 0.48.0", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "data-encoding" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" - -[[package]] -name = "deadpool" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" -dependencies = [ - "deadpool-runtime", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-diesel" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590573e9e29c5190a5ff782136f871e6e652e35d598a349888e028693601adf1" -dependencies = [ - "deadpool", - "deadpool-sync", - "diesel", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -dependencies = [ - "tokio", -] - -[[package]] -name = "deadpool-sync" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" -dependencies = [ - "deadpool-runtime", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "diesel" -version = "2.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04001f23ba8843dc315804fa324000376084dfb1c30794ff68dd279e6e5696d5" -dependencies = [ - "bitflags", - "byteorder", - "chrono", - "diesel_derives", - "itoa", - "pq-sys", -] - -[[package]] -name = "diesel_derives" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ff2be1e7312c858b2ef974f5c7089833ae57b5311b334b30923af58e5718d8" -dependencies = [ - "diesel_table_macro_syntax", - "dsl_auto_type", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "diesel_migrations" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" -dependencies = [ - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - -[[package]] -name = "dsl_auto_type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" -dependencies = [ - "darling", - "either", - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "encoding_rs" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "fastrand" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" - -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - -[[package]] -name = "h2" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap 2.3.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 0.26.11", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "socket2 0.5.7", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" -dependencies = [ - "equivalent", - "hashbrown 0.14.5", - "serde", -] - -[[package]] -name = "ipnet" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "kameo" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c4af7638c67029fd6821d02813c3913c803784648725d4df4082c9b91d7cbb1" -dependencies = [ - "downcast-rs", - "dyn-clone", - "futures", - "kameo_macros", - "serde", - "tokio", - "tracing", -] - -[[package]] -name = "kameo_macros" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13c324e2d8c8e126e63e66087448b4267e263e6cb8770c56d10a9d0d279d9e2" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags", - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "machine-uid" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7217d573cdb141d6da43113b098172e057d39915d79c4bdedbc3aacd46bd96" -dependencies = [ - "libc", - "windows-registry 0.6.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "migrations_internals" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" -dependencies = [ - "serde", - "toml 0.8.23", -] - -[[package]] -name = "migrations_macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "mio" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" -dependencies = [ - "hermit-abi", - "libc", - "wasi", - "windows-sys 0.52.0", -] - -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "openssl" -version = "0.10.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "pq-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24ff9e4cf6945c988f0db7005d87747bf72864965c3529d259ad155ac41d584" -dependencies = [ - "vcpkg", -] - -[[package]] -name = "proc-macro-crate" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quinn" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.5.7", - "thiserror 2.0.11", - "tokio", - "tracing", -] - -[[package]] -name = "quinn-proto" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" -dependencies = [ - "bytes", - "getrandom 0.2.15", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.11", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.1", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 2.0.11", -] - -[[package]] -name = "regex" -version = "1.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.4", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - -[[package]] -name = "reqwest" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "mime_guess", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tokio-rustls", - "tower", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 0.26.11", - "windows-registry 0.2.0", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rstest" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" -dependencies = [ - "futures-timer", - "futures-util", - "rstest_macros", -] - -[[package]] -name = "rstest_macros" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" -dependencies = [ - "cfg-if", - "glob", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn", - "unicode-ident", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.23.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" -dependencies = [ - "base64 0.22.1", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" -dependencies = [ - "web-time", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "schannel" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.3.0", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" -dependencies = [ - "futures-core", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" -dependencies = [ - "cfg-if", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "thiserror" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -dependencies = [ - "thiserror-impl 1.0.63", -] - -[[package]] -name = "thiserror" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" -dependencies = [ - "thiserror-impl 2.0.11", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", - "tokio-macros", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - -[[package]] -name = "toml" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" -dependencies = [ - "indexmap 2.3.0", - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.3.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toml_writer" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand", - "sha1", - "thiserror 1.0.63", - "utf-8", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" -dependencies = [ - "getrandom 0.2.15", - "serde", -] - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "web-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.3", -] - -[[package]] -name = "webpki-roots" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml index 55806edb..507143f1 100644 --- a/crates/gateway/Cargo.toml +++ b/crates/gateway/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "blockfrost-gateway" -version = "1.3.3" -license-file = "LICENSE" +version.workspace = true +license.workspace = true +edition.workspace = true publish = false -edition = "2021" build = "build.rs" [features] @@ -40,7 +40,7 @@ dotenvy = "0.15.7" hex = "0.4.3" rand = "0.8.5" base64 = "0.21" -uuid = "1.10" +uuid.workspace = true hyper = "1.4.1" machine-uid = "0.5" blake3 = "1" @@ -63,3 +63,6 @@ reqwest = { version = "0.12", default-features = false, features = [ "blocking", "rustls-tls", ] } + +[lints] +workspace = true diff --git a/crates/gateway/LICENSE b/crates/gateway/LICENSE deleted file mode 100644 index e4d35c67..00000000 --- a/crates/gateway/LICENSE +++ /dev/null @@ -1,6 +0,0 @@ -Copyright (c) 2025 Blockfrost -All rights reserved. - -This software is proprietary and confidential. Unauthorized copying, -distribution, or modification of this software, via any medium, -is strictly prohibited. diff --git a/crates/gateway/deny.toml b/crates/gateway/deny.toml deleted file mode 100644 index cec73ea6..00000000 --- a/crates/gateway/deny.toml +++ /dev/null @@ -1,22 +0,0 @@ -[licenses] -# See for list of possible licenses -allow = [ - "Apache-2.0", - "BSD-2-Clause", - "BSD-3-Clause", - "MIT", - "MPL-2.0", - "Zlib", - "Unicode-3.0", - "ISC", - "CDLA-Permissive-2.0", - "OpenSSL", -] -private = { ignore = true } -confidence-threshold = 0.8 - -[[licenses.clarify]] -name = "ring" -version = "0.17.8" -expression = "Apache-2.0 AND ISC AND MIT AND OpenSSL" -license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] diff --git a/crates/gateway/flake.lock b/crates/gateway/flake.lock deleted file mode 100644 index 6db9b2ec..00000000 --- a/crates/gateway/flake.lock +++ /dev/null @@ -1,225 +0,0 @@ -{ - "nodes": { - "advisory-db": { - "flake": false, - "locked": { - "lastModified": 1737246984, - "narHash": "sha256-cjWzMwqej9zVoFGV5NkefOspMDJJ+gN3+zkFZcBBAkc=", - "owner": "rustsec", - "repo": "advisory-db", - "rev": "cfd49ce116f12c856a3f3c065d041fd0dd7169dc", - "type": "github" - }, - "original": { - "owner": "rustsec", - "repo": "advisory-db", - "type": "github" - } - }, - "cardano-node": { - "flake": false, - "locked": { - "lastModified": 1763736877, - "narHash": "sha256-c1a6DzDlm+wzwa85TWeOFrPEldsfjiZw7+DcMMW9nc4=", - "owner": "IntersectMBO", - "repo": "cardano-node", - "rev": "6c034ec038d8d276a3595e10e2d38643f09bd1f2", - "type": "github" - }, - "original": { - "owner": "IntersectMBO", - "ref": "10.5.3", - "repo": "cardano-node", - "type": "github" - } - }, - "crane": { - "locked": { - "lastModified": 1765145449, - "narHash": "sha256-aBVHGWWRzSpfL++LubA0CwOOQ64WNLegrYHwsVuVN7A=", - "owner": "ipetkov", - "repo": "crane", - "rev": "69f538cdce5955fcd47abfed4395dc6d5194c1c5", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "devshell": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1722113426, - "narHash": "sha256-Yo/3loq572A8Su6aY5GP56knpuKYRvM2a1meP9oJZCw=", - "owner": "numtide", - "repo": "devshell", - "rev": "67cce7359e4cd3c45296fb4aaf6a19e2a9c757ae", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "fenix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1765435813, - "narHash": "sha256-C6tT7K1Lx6VsYw1BY5S3OavtapUvEnDQtmQB5DSgbCc=", - "owner": "nix-community", - "repo": "fenix", - "rev": "6399553b7a300c77e7f07342904eb696a5b6bf9d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1761588595, - "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, - "locked": { - "lastModified": 1726153070, - "narHash": "sha256-HO4zgY0ekfwO5bX0QH/3kJ/h4KvUDFZg8YpkNwIbg1U=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "bcef6817a8b2aa20a5a6dbb19b43e63c5bf8619a", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "hydra": { - "flake": false, - "locked": { - "lastModified": 1759947091, - "narHash": "sha256-V9VBA5cFLcZ/M8g12Bzye5tVSVW3uoUIaRm+Ws0mFbo=", - "owner": "cardano-scaling", - "repo": "hydra", - "rev": "b5e33b55e9fba442c562f82cec6c36b1716d9847", - "type": "github" - }, - "original": { - "owner": "cardano-scaling", - "ref": "1.0.0", - "repo": "hydra", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1765363881, - "narHash": "sha256-3C3xWn8/2Zzr7sxVBmpc1H1QfxjNfta5IMFe3O9ZEPw=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d2b1213bf5ec5e62d96b003ab4b5cbc42abfc0d0", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-lib": { - "locked": { - "lastModified": 1725233747, - "narHash": "sha256-Ss8QWLXdr2JCBPcYChJhz4xJm+h/xjl4G0c0XlP6a74=", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz" - } - }, - "root": { - "inputs": { - "advisory-db": "advisory-db", - "cardano-node": "cardano-node", - "crane": "crane", - "devshell": "devshell", - "fenix": "fenix", - "flake-compat": "flake-compat", - "flake-parts": "flake-parts", - "hydra": "hydra", - "nixpkgs": "nixpkgs", - "treefmt-nix": "treefmt-nix" - } - }, - "rust-analyzer-src": { - "flake": false, - "locked": { - "lastModified": 1765400135, - "narHash": "sha256-D3+4hfNwUhG0fdCpDhOASLwEQ1jKuHi4mV72up4kLQM=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "fface27171988b3d605ef45cf986c25533116f7e", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "treefmt-nix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762938485, - "narHash": "sha256-AlEObg0syDl+Spi4LsZIBrjw+snSVU4T8MOeuZJUJjM=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/crates/gateway/flake.nix b/crates/gateway/flake.nix deleted file mode 100644 index b95c5983..00000000 --- a/crates/gateway/flake.nix +++ /dev/null @@ -1,114 +0,0 @@ -{ - inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; - flake-parts.url = "github:hercules-ci/flake-parts"; - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; - treefmt-nix = { - url = "github:numtide/treefmt-nix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - crane.url = "github:ipetkov/crane"; - fenix = { - url = "github:nix-community/fenix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - cardano-node = { - # Temporarily for `cardano-cli`, for `hydra-node`: - url = "github:IntersectMBO/cardano-node/10.5.3"; - flake = false; # otherwise, +2k dependencies we don’t really use - }; - hydra = { - url = "github:cardano-scaling/hydra/1.0.0"; - flake = false; - }; - devshell = { - url = "github:numtide/devshell"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - advisory-db = { - url = "github:rustsec/advisory-db"; - flake = false; - }; - }; - - outputs = inputs: let - inherit (inputs.nixpkgs) lib; - in - inputs.flake-parts.lib.mkFlake {inherit inputs;} ({config, ...}: { - imports = [ - inputs.devshell.flakeModule - inputs.treefmt-nix.flakeModule - ]; - - systems = [ - "x86_64-linux" - "aarch64-linux" - "aarch64-darwin" - "x86_64-darwin" - ]; - perSystem = {system, ...}: let - internal = inputs.self.internal.${system}; - in { - packages.default = internal.package; - - devshells.default = import ./nix/devshells.nix {inherit inputs;}; - - checks = internal.cargoChecks // internal.nixChecks; - - treefmt = {pkgs, ...}: { - projectRootFile = "flake.nix"; - programs = { - alejandra.enable = true; # Nix - prettier.enable = true; - rustfmt.enable = true; - rustfmt.package = internal.rustPackages.rustfmt; - yamlfmt.enable = pkgs.system != "x86_64-darwin"; # a treefmt-nix+yamlfmt bug on Intel Macs - taplo.enable = true; # TOML - shfmt.enable = true; - }; - settings.formatter.rustfmt.options = [ - "--config-path" - (builtins.path { - name = "rustfmt.toml"; - path = ./rustfmt.toml; - }) - ]; - }; - }; - - flake = { - internal = lib.genAttrs config.systems ( - targetSystem: import ./nix/internal/unix.nix {inherit inputs targetSystem;} - ); - - hydraJobs = let - allJobs = { - blockfrost-gateway = lib.genAttrs config.systems ( - targetSystem: inputs.self.internal.${targetSystem}.package - ); - devshell = lib.genAttrs config.systems ( - targetSystem: inputs.self.devShells.${targetSystem}.default - ); - inherit (inputs.self) checks; - }; - in - allJobs - // { - required = inputs.nixpkgs.legacyPackages.x86_64-linux.releaseTools.aggregate { - name = "github-required"; - meta.description = "All jobs required to pass CI"; - constituents = lib.collect lib.isDerivation allJobs; - }; - }; - - nixConfig = { - extra-substituters = ["https://cache.iog.io"]; - extra-trusted-public-keys = ["hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ="]; - allow-import-from-derivation = "true"; - }; - }; - }); -} diff --git a/crates/gateway/nix/devshells.nix b/crates/gateway/nix/devshells.nix deleted file mode 100644 index 0c6133b3..00000000 --- a/crates/gateway/nix/devshells.nix +++ /dev/null @@ -1,87 +0,0 @@ -{inputs}: { - config, - pkgs, - ... -}: let - inherit (pkgs) lib; - internal = inputs.self.internal.${pkgs.system}; -in { - name = "blockfrost-gateway-devshell"; - - imports = [ - "${inputs.devshell}/extra/language/c.nix" - "${inputs.devshell}/extra/language/rust.nix" - ]; - - devshell.packages = - [ - pkgs.unixtools.xxd - internal.rustPackages.clippy - ] - ++ lib.optionals pkgs.stdenv.isLinux [ - pkgs.pkg-config - ] - ++ lib.optionals pkgs.stdenv.isDarwin [ - pkgs.libiconv - ]; - - commands = [ - {package = inputs.self.formatter.${pkgs.system};} - { - name = "cargo"; - package = internal.rustPackages.cargo; - } - {package = pkgs.cargo-nextest;} - # TODO: add .envrc.local with node env. exports - { - name = "cardano-cli"; - package = internal.cardano-cli; - } - {package = internal.rustPackages.rust-analyzer;} - {package = pkgs.doctl;} - {package = internal.hydra-node;} - ]; - - language.c = { - compiler = - if pkgs.stdenv.isLinux - then pkgs.gcc - else pkgs.clang; - includes = internal.commonArgs.buildInputs; - libraries = internal.commonArgs.buildInputs; - }; - - language.rust = { - packageSet = internal.rustPackages; - tools = ["cargo" "rustfmt"]; # The rest is provided below. - enableDefaultToolchain = true; - }; - - env = - internal.hydraScriptsEnvVars - ++ lib.optionals pkgs.stdenv.isDarwin [ - { - name = "LIBCLANG_PATH"; - value = internal.commonArgs.LIBCLANG_PATH; - } - ] - ++ lib.optionals pkgs.stdenv.isLinux [ - # Embed runtime libs in `RPATH`: - { - name = "RUSTFLAGS"; - eval = ''"-Clink-arg=-fuse-ld=bfd -Clink-arg=-Wl,-rpath,$(pkg-config --variable=libdir openssl libpq | tr ' ' :)"''; - } - { - name = "LD_LIBRARY_PATH"; - eval = lib.mkForce ""; - } - ]; - - devshell.motd = '' - - {202}🔨 Welcome to ${config.name}{reset} - $(menu) - - You can now run ‘{bold}cargo run{reset}’. - ''; -} diff --git a/crates/gateway/nix/internal/darwin.nix b/crates/gateway/nix/internal/darwin.nix deleted file mode 100644 index a1a1c9a3..00000000 --- a/crates/gateway/nix/internal/darwin.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ - targetSystem, - unix, - ... -}: -assert builtins.elem targetSystem [ - "x86_64-darwin" - "aarch64-darwin" -]; unix diff --git a/crates/gateway/nix/internal/linux.nix b/crates/gateway/nix/internal/linux.nix deleted file mode 100644 index 820142a7..00000000 --- a/crates/gateway/nix/internal/linux.nix +++ /dev/null @@ -1,6 +0,0 @@ -{ - targetSystem, - unix, - ... -}: -assert builtins.elem targetSystem ["x86_64-linux" "aarch64-linux"]; unix diff --git a/crates/gateway/nix/internal/unix.nix b/crates/gateway/nix/internal/unix.nix deleted file mode 100644 index 1660779f..00000000 --- a/crates/gateway/nix/internal/unix.nix +++ /dev/null @@ -1,214 +0,0 @@ -{ - inputs, - targetSystem, -}: -assert builtins.elem targetSystem ["x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin"]; let - buildSystem = targetSystem; - pkgs = inputs.nixpkgs.legacyPackages.${buildSystem}; - inherit (pkgs) lib; - extendForTarget = unix: - ( - if pkgs.stdenv.isLinux - then import ./linux.nix - else if pkgs.stdenv.isDarwin - then import ./darwin.nix - else throw "can’t happen" - ) {inherit inputs targetSystem unix;}; -in - extendForTarget rec { - rustPackages = inputs.fenix.packages.${pkgs.system}.stable; - - craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustPackages.toolchain; - - src = lib.cleanSourceWith { - src = lib.cleanSource ../../.; - filter = path: type: - craneLib.filterCargoSources path type - || lib.hasSuffix ".sql" path - || lib.hasSuffix "/LICENSE" path; - name = "source"; - }; - - commonArgs = - { - inherit src; - strictDeps = true; - nativeBuildInputs = lib.optionals pkgs.stdenv.isLinux [ - pkgs.pkg-config - ]; - buildInputs = - [pkgs.postgresql] - ++ lib.optionals pkgs.stdenv.isLinux [ - pkgs.openssl - ] - ++ lib.optionals pkgs.stdenv.isDarwin [ - pkgs.libiconv - pkgs.darwin.apple_sdk_12_3.frameworks.SystemConfiguration - pkgs.darwin.apple_sdk_12_3.frameworks.Security - pkgs.darwin.apple_sdk_12_3.frameworks.CoreFoundation - ]; - } - // lib.optionalAttrs pkgs.stdenv.isDarwin { - # for bindgen, used by libproc, used by metrics_process - LIBCLANG_PATH = "${lib.getLib pkgs.llvmPackages.libclang}/lib"; - } - // lib.optionalAttrs pkgs.stdenv.isLinux { - # The linker bundled with Fenix has wrong interpreter path, and it fails with ENOENT, so: - RUSTFLAGS = "-Clink-arg=-fuse-ld=bfd"; - }; - - # For better caching: - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - - packageName = (craneLib.crateNameFromCargoToml {cargoToml = src + "/Cargo.toml";}).pname; - - GIT_REVISION = inputs.self.rev or "dirty"; - - package = craneLib.buildPackage (commonArgs - // { - inherit cargoArtifacts GIT_REVISION; - doCheck = false; # we run tests with `cargo-nextest` below - meta.mainProgram = packageName; - postInstall = '' - mv $out/bin $out/libexec - mkdir -p $out/bin - ( cd $out/bin && ln -s ../libexec/${packageName} ./ ; ) - ln -s ${hydra-node}/bin/hydra-node $out/libexec/ - ''; - } - // (builtins.listToAttrs hydraScriptsEnvVars)); - - cargoChecks = { - cargo-clippy = craneLib.cargoClippy (commonArgs - // { - inherit cargoArtifacts GIT_REVISION; - # Maybe also add `--deny clippy::pedantic`? - cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; - } - // (builtins.listToAttrs hydraScriptsEnvVars)); - - cargo-doc = craneLib.cargoDoc (commonArgs - // { - inherit cargoArtifacts GIT_REVISION; - RUSTDOCFLAGS = "-D warnings"; - } - // (builtins.listToAttrs hydraScriptsEnvVars)); - - cargo-audit = craneLib.cargoAudit { - inherit src; - inherit (inputs) advisory-db; - }; - - cargo-deny = craneLib.cargoDeny { - inherit src; - }; - - cargo-test = craneLib.cargoNextest (commonArgs - // { - inherit cargoArtifacts GIT_REVISION; - } - // (builtins.listToAttrs hydraScriptsEnvVars)); - }; - - nixChecks = { - nix-statix = - pkgs.runCommandNoCC "nix-statix" { - buildInputs = [pkgs.statix]; - } '' - touch $out - cd ${inputs.self} - exec statix check . - ''; - - nix-deadnix = - pkgs.runCommandNoCC "nix-deadnix" { - buildInputs = [pkgs.deadnix]; - } '' - touch $out - cd ${inputs.self} - exec deadnix --fail . - ''; - - nix-nil = - pkgs.runCommandNoCC "nix-nil" { - buildInputs = [pkgs.nil]; - } '' - ec=0 - touch $out - cd ${inputs.self} - find . -type f -iname '*.nix' | while IFS= read -r file; do - nil diagnostics "$file" || ec=1 - done - exit $ec - ''; - - # From `nixd`: - nix-nixf = - pkgs.runCommandNoCC "nix-nil" { - buildInputs = [pkgs.nixf pkgs.jq]; - } '' - ec=0 - touch $out - cd ${inputs.self} - find . -type f -iname '*.nix' | while IFS= read -r file; do - errors=$(nixf-tidy --variable-lookup --pretty-print <"$file" | jq -c '.[]' | sed -r "s#^#$file: #") - if [ -n "$errors" ] ; then - cat <<<"$errors" - echo - ec=1 - fi - done - exit $ec - ''; - }; - - hydra-flake = (import inputs.flake-compat {src = inputs.hydra;}).defaultNix; - - hydraVersion = hydra-flake.legacyPackages.${targetSystem}.hydra-node.identifier.version; - - hydraNetworksJson = builtins.path { - path = hydra-flake + "/hydra-node/networks.json"; - }; - - hydraScriptsEnvVars = map (network: { - name = "HYDRA_SCRIPTS_TX_ID_${lib.strings.toUpper network}"; - value = (builtins.fromJSON (builtins.readFile hydraNetworksJson)).${network}.${hydraVersion}; - }) ["mainnet" "preprod" "preview"]; - - hydra-node = lib.recursiveUpdate hydra-flake.packages.${targetSystem}.hydra-node { - meta.description = "Layer 2 scalability solution for Cardano"; - }; - - cardano-node-flake = let - unpatched = inputs.cardano-node; - in - (import inputs.flake-compat { - src = - if targetSystem != "aarch64-darwin" && targetSystem != "aarch64-linux" - then unpatched - else { - outPath = toString (pkgs.runCommand "source" {} '' - cp -r ${unpatched} $out - chmod -R +w $out - cd $out - echo ${lib.escapeShellArg (builtins.toJSON [targetSystem])} >$out/nix/supported-systems.nix - ${lib.optionalString (targetSystem == "aarch64-linux") '' - sed -r 's/"-fexternal-interpreter"//g' -i $out/nix/haskell.nix - ''} - ''); - inherit (unpatched) rev shortRev lastModified lastModifiedDate; - }; - }) - .defaultNix; - - cardano-node-packages = - { - x86_64-linux = cardano-node-flake.hydraJobs.x86_64-linux.musl; - inherit (cardano-node-flake.packages) x86_64-darwin aarch64-darwin aarch64-linux; - } - .${ - targetSystem - }; - - inherit (cardano-node-packages) cardano-cli; - } diff --git a/crates/gateway/rustfmt.toml b/crates/gateway/rustfmt.toml deleted file mode 100644 index 33912ffb..00000000 --- a/crates/gateway/rustfmt.toml +++ /dev/null @@ -1,14 +0,0 @@ -hard_tabs = false -tab_spaces = 4 -max_width = 100 - -fn_call_width = 60 - -match_block_trailing_comma = true - -reorder_imports = true -reorder_modules = true - -use_try_shorthand = true -use_field_init_shorthand = true -style_edition = "2024" diff --git a/deny.toml b/deny.toml index 4db64770..9a6ac44b 100644 --- a/deny.toml +++ b/deny.toml @@ -13,5 +13,14 @@ allow = [ "BSD-2-Clause", "Apache-2.0 WITH LLVM-exception", "bzip2-1.0.6", + "CDLA-Permissive-2.0", + "OpenSSL", ] +private = { ignore = true } confidence-threshold = 0.8 + +[[licenses.clarify]] +name = "ring" +version = "0.17.8" +expression = "Apache-2.0 AND ISC AND MIT AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] diff --git a/flake.nix b/flake.nix index 77f5b38f..85223692 100644 --- a/flake.nix +++ b/flake.nix @@ -90,8 +90,8 @@ in { packages = { - default = internal.package; - blockfrost-platform = internal.package; + default = internal.blockfrost-platform; + inherit (internal) blockfrost-platform blockfrost-gateway; inherit (internal) tx-build cardano-address testgen-hs; } // (lib.optionalAttrs (system == "x86_64-linux") { @@ -160,7 +160,7 @@ targetSystem: import ./nix/internal/windows.nix {inherit inputs targetSystem;} ); - nixosModule = { + nixosModule.default = { pkgs, lib, ... diff --git a/nix/devshells.nix b/nix/devshells.nix index 2044a8ee..b820088a 100644 --- a/nix/devshells.nix +++ b/nix/devshells.nix @@ -45,6 +45,7 @@ in { package = internal.rustPackages.cargo; } {package = internal.rustPackages.rust-analyzer;} + {package = pkgs.doctl;} { category = "handy"; package = internal.runNode "preview"; @@ -94,6 +95,7 @@ in { then pkgs.gcc else pkgs.clang; includes = internal.commonArgs.buildInputs; + libraries = internal.commonArgs.buildInputs; }; language.rust = { @@ -103,16 +105,13 @@ in { }; env = - [ + internal.hydraScriptsEnvVars + ++ [ { name = "TESTGEN_HS_PATH"; value = lib.getExe internal.testgen-hs; } ] - ++ (map (network: { - name = "HYDRA_SCRIPTS_TX_ID_${lib.strings.toUpper network}"; - value = (builtins.fromJSON (builtins.readFile internal.hydraNetworksJson)).${network}.${internal.hydraVersion}; - }) ["mainnet" "preprod" "preview"]) ++ lib.optionals pkgs.stdenv.isDarwin [ { name = "LIBCLANG_PATH"; @@ -123,7 +122,11 @@ in { # Embed `openssl` in `RPATH`: { name = "RUSTFLAGS"; - eval = ''"-C link-arg=-Wl,-rpath,$(pkg-config --variable=libdir openssl)"''; + eval = ''"-Clink-arg=-fuse-ld=bfd -Clink-arg=-Wl,-rpath,$(pkg-config --variable=libdir openssl libpq | tr ' ' :)"''; + } + { + name = "LD_LIBRARY_PATH"; + eval = lib.mkForce ""; } ]; diff --git a/nix/internal/unix.nix b/nix/internal/unix.nix index 6055e6ef..3af8cc9b 100644 --- a/nix/internal/unix.nix +++ b/nix/internal/unix.nix @@ -21,7 +21,14 @@ in craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustPackages.toolchain; - src = craneLib.cleanCargoSource ../../.; + src = lib.cleanSourceWith { + src = lib.cleanSource ../../.; + filter = path: type: + craneLib.filterCargoSources path type + || lib.hasSuffix ".sql" path + || lib.hasSuffix "/LICENSE" path; + name = "source"; + }; packageName = craneLib.crateNameFromCargoToml {cargoToml = builtins.path {path = src + "/crates/platform/Cargo.toml";};}; @@ -35,7 +42,8 @@ in ]; TESTGEN_HS_PATH = lib.getExe testgen-hs; # Don’t try to download it in `build.rs`. buildInputs = - lib.optionals pkgs.stdenv.isLinux [ + [pkgs.postgresql] + ++ lib.optionals pkgs.stdenv.isLinux [ pkgs.openssl ] ++ lib.optionals pkgs.stdenv.isDarwin [ @@ -59,10 +67,11 @@ in workspaceCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/Cargo.toml";})); platformCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/crates/platform/Cargo.toml";})); + gatewayCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/crates/gateway/Cargo.toml";})); GIT_REVISION = inputs.self.rev or "dirty"; - package = craneLib.buildPackage (commonArgs + blockfrost-platform = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts GIT_REVISION; doCheck = false; # we run tests with `cargo-nextest` below @@ -82,6 +91,22 @@ in }; }); + blockfrost-gateway = craneLib.buildPackage (commonArgs + // { + inherit cargoArtifacts GIT_REVISION; + pname = gatewayCargoToml.package.name; + doCheck = false; # we run tests with `cargo-nextest` below + meta.mainProgram = gatewayCargoToml.package.name; + postInstall = '' + mv $out/bin $out/libexec + mkdir -p $out/bin + ( cd $out/bin && ln -s ../libexec/${gatewayCargoToml.package.name} ./ ; ) + ln -s ${hydra-node}/bin/hydra-node $out/libexec/ + ''; + cargoExtraArgs = "--package blockfrost-gateway"; + } + // (builtins.listToAttrs hydraScriptsEnvVars)); + cargoChecks = { cargo-clippy = craneLib.cargoClippy (commonArgs // { @@ -111,7 +136,8 @@ in // { inherit cargoArtifacts GIT_REVISION; cargoNextestExtraArgs = "--workspace --lib"; - }); + } + // (builtins.listToAttrs hydraScriptsEnvVars)); }; nixChecks = { @@ -323,14 +349,14 @@ in meta.description = "Builds a valid CBOR transaction for testing ‘/tx/submit’"; }; - releaseBaseUrl = "https://github.com/blockfrost/blockfrost-platform/releases/download/${package.version}"; + releaseBaseUrl = "https://github.com/blockfrost/blockfrost-platform/releases/download/${blockfrost-platform.version}"; # This works for both Linux and Darwin, but we mostly use it on Linux: curl-bash-install = pkgs.runCommandNoCC "curl-bash-install" { nativeBuildInputs = with pkgs; [shellcheck]; projectName = packageName.pname; - projectVersion = package.version; + projectVersion = blockfrost-platform.version; shortRev = inputs.self.shortRev or "dirty"; baseUrl = releaseBaseUrl; } '' @@ -557,7 +583,7 @@ in platform_port=$(python3 -m portpicker) - ${lib.getExe package} \ + ${lib.getExe blockfrost-platform} \ --server-address 127.0.0.1 \ --server-port "$platform_port" \ --log-level info \ @@ -607,6 +633,11 @@ in path = hydra-flake + "/hydra-node/networks.json"; }; + hydraScriptsEnvVars = map (network: { + name = "HYDRA_SCRIPTS_TX_ID_${lib.strings.toUpper network}"; + value = (builtins.fromJSON (builtins.readFile hydraNetworksJson)).${network}.${hydraVersion}; + }) ["mainnet" "preprod" "preview"]; + hydra-node = lib.recursiveUpdate hydra-flake.packages.${targetSystem}.hydra-node { meta.description = "Layer 2 scalability solution for Cardano"; }; From 42aeff43774fdc00ebf1279c6785a77f19bdf6fb Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Wed, 18 Feb 2026 20:54:38 +0100 Subject: [PATCH 02/23] chore: get rid of `+spec-1.1.0` from `toml` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2fade5ef..448b1fe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ tokio = { version = "1.49.0", features = [ "signal", "process", ] } -toml = "0.9.11+spec-1.1.0" +toml = "0.9.11" tracing = "0.1.44" twelf = { version = "0.15.0", features = ["clap", "toml"] } url = "2" From 33790051081b1407fb2d706c146aba9cd9aa291e Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Wed, 18 Feb 2026 21:05:59 +0100 Subject: [PATCH 03/23] chore: fix Clippy issues --- crates/gateway/build.rs | 7 +++---- crates/gateway/src/api/register.rs | 4 ++-- crates/gateway/src/blockfrost.rs | 2 +- crates/gateway/src/errors.rs | 2 +- crates/gateway/src/hydra.rs | 4 +--- crates/gateway/src/hydra/verifications.rs | 18 ++++++++---------- crates/gateway/src/load_balancer.rs | 23 +++++++++-------------- crates/gateway/src/main.rs | 2 +- crates/gateway/tests/load_balancer.rs | 8 ++++---- 9 files changed, 30 insertions(+), 40 deletions(-) diff --git a/crates/gateway/build.rs b/crates/gateway/build.rs index 0e744c94..1c1d8b6b 100644 --- a/crates/gateway/build.rs +++ b/crates/gateway/build.rs @@ -12,7 +12,7 @@ mod git_revision { use std::process::Command; if env::var(GIT_REVISION).is_ok() { - println!("Environment variable {} is set. Not setting.", GIT_REVISION); + println!("Environment variable {GIT_REVISION} is set. Not setting."); return; } @@ -33,7 +33,7 @@ mod git_revision { .to_string() }; - println!("cargo:rustc-env={}={}", GIT_REVISION, revision); + println!("cargo:rustc-env={GIT_REVISION}={revision}"); } } @@ -76,8 +76,7 @@ mod hydra_scripts_id { let (rev, href) = read_hydra_rev_and_ref(&flake_lock_json); let url = format!( - "https://raw.githubusercontent.com/cardano-scaling/hydra/{}/hydra-node/networks.json", - rev + "https://raw.githubusercontent.com/cardano-scaling/hydra/{rev}/hydra-node/networks.json" ); let networks_json = fetch_cached(&url, &rev); diff --git a/crates/gateway/src/api/register.rs b/crates/gateway/src/api/register.rs index b4969b24..5100f668 100644 --- a/crates/gateway/src/api/register.rs +++ b/crates/gateway/src/api/register.rs @@ -105,7 +105,7 @@ pub async fn route( let ip_address: IpAddr = ip_string .parse() - .map_err(|_| APIError::Validation(format!("Invalid IP address: {}", ip_string)))?; + .map_err(|_| APIError::Validation(format!("Invalid IP address: {ip_string}")))?; let skip_port_check_secret = std::env::var("SKIP_PORT_CHECK_SECRET").ok(); @@ -177,7 +177,7 @@ pub async fn route( route: payload.api_prefix, load_balancers: vec![LoadBalancer { // We can’t know if it’s HTTPS or HTTP here, so we have to count on `blockfrost-platform`: - uri: format!("//{}/ws", our_host), + uri: format!("//{our_host}/ws"), access_token: token, }], }; diff --git a/crates/gateway/src/blockfrost.rs b/crates/gateway/src/blockfrost.rs index b3b1d8b7..c34f6d2c 100644 --- a/crates/gateway/src/blockfrost.rs +++ b/crates/gateway/src/blockfrost.rs @@ -32,7 +32,7 @@ impl BlockfrostAPI { let asset_hex = &unit[self.policy_id_size..]; let decoded = hex::decode(asset_hex) - .map_err(|err| APIError::License(format!("Hex decoding failed: {}", err)))?; + .map_err(|err| APIError::License(format!("Hex decoding failed: {err}")))?; let asset_name = AssetName(String::from_utf8_lossy(&decoded).to_string()); diff --git a/crates/gateway/src/errors.rs b/crates/gateway/src/errors.rs index 34508d50..a347d80b 100644 --- a/crates/gateway/src/errors.rs +++ b/crates/gateway/src/errors.rs @@ -55,7 +55,7 @@ impl IntoResponse for APIError { ApiError { status: "failed".to_string(), reason: "no_license".to_string(), - details: format!("Address: {} does not contain the license.", address), + details: format!("Address: {address} does not contain the license."), }, ), APIError::NotAccessible { ip, port } => ( diff --git a/crates/gateway/src/hydra.rs b/crates/gateway/src/hydra.rs index 9080a162..036caa95 100644 --- a/crates/gateway/src/hydra.rs +++ b/crates/gateway/src/hydra.rs @@ -50,9 +50,7 @@ impl HydrasManager { config.lovelace_per_request * config.requests_per_microtransaction; if microtransaction_lovelace < MIN_LOVELACE_PER_TRANSACTION { Err(anyhow!( - "hydras-manager: Please make sure that each microtransaction will be larger than {} lovelace. Currently it would be {}.", - MIN_LOVELACE_PER_TRANSACTION, - microtransaction_lovelace, + "hydras-manager: Please make sure that each microtransaction will be larger than {MIN_LOVELACE_PER_TRANSACTION} lovelace. Currently it would be {microtransaction_lovelace}.", ))? } diff --git a/crates/gateway/src/hydra/verifications.rs b/crates/gateway/src/hydra/verifications.rs index de926ac2..03d874cb 100644 --- a/crates/gateway/src/hydra/verifications.rs +++ b/crates/gateway/src/hydra/verifications.rs @@ -388,7 +388,7 @@ impl super::HydraConfig { let utxo_json = self.query_utxo_json(from_addr).await?; let utxo_body = serde_json::to_vec(&utxo_json).context("failed to serialize utxo JSON")?; - let url = format!("http://127.0.0.1:{}/commit", hydra_api_port); + let url = format!("http://127.0.0.1:{hydra_api_port}/commit"); let client = reqwest::Client::new(); let resp = client .post(url) @@ -457,7 +457,7 @@ impl super::HydraConfig { ) -> Result<()> { use anyhow::Context; - let snapshot_url = format!("http://127.0.0.1:{}/snapshot/utxo", hydra_api_port); + let snapshot_url = format!("http://127.0.0.1:{hydra_api_port}/snapshot/utxo"); let utxo: Value = reqwest::Client::new() .get(&snapshot_url) .send() @@ -491,9 +491,7 @@ impl super::HydraConfig { if lovelace_total < amount_lovelace { return Err(anyhow!( - "insufficient lovelace in selected UTxO: {} < {}", - lovelace_total, - amount_lovelace + "insufficient lovelace in selected UTxO: {lovelace_total} < {amount_lovelace}" )); } @@ -507,9 +505,9 @@ impl super::HydraConfig { "--tx-in", &tx_in, "--tx-out", - &format!("{}+{}", receiver_addr, amount_lovelace), + &format!("{receiver_addr}+{amount_lovelace}"), "--tx-out", - &format!("{}+{}", sender_addr, change), + &format!("{sender_addr}+{change}"), "--fee", "0", "--out-file", @@ -550,7 +548,7 @@ impl super::HydraConfig { serde_json::to_string(&payload)? ); - let ws_url = format!("ws://127.0.0.1:{}/", hydra_api_port); + let ws_url = format!("ws://127.0.0.1:{hydra_api_port}/"); send_one_websocket_msg(&ws_url, payload, std::time::Duration::from_secs(2)).await?; Ok(()) @@ -559,7 +557,7 @@ impl super::HydraConfig { pub(super) async fn hydra_utxo_count(&self, hydra_api_port: u16) -> Result { use anyhow::Context; - let url = format!("http://127.0.0.1:{}/snapshot/utxo", hydra_api_port); + let url = format!("http://127.0.0.1:{hydra_api_port}/snapshot/utxo"); let v: Value = reqwest::Client::new() .get(&url) @@ -718,7 +716,7 @@ pub async fn send_one_websocket_msg( } pub async fn fetch_head_tag(hydra_api_port: u16) -> Result { - let url = format!("http://127.0.0.1:{}/head", hydra_api_port); + let url = format!("http://127.0.0.1:{hydra_api_port}/head"); let v: serde_json::Value = reqwest::get(url).await?.error_for_status()?.json().await?; diff --git a/crates/gateway/src/load_balancer.rs b/crates/gateway/src/load_balancer.rs index c82b63fb..47684bbc 100644 --- a/crates/gateway/src/load_balancer.rs +++ b/crates/gateway/src/load_balancer.rs @@ -252,7 +252,7 @@ pub mod api { Extension(load_balancer): Extension, req: Request, ) -> Result { - handle_prefix_route(load_balancer, uuid, format!("/{}", rest), req).await + handle_prefix_route(load_balancer, uuid, format!("/{rest}"), req).await } #[derive(Serialize, Deserialize, Debug)] @@ -313,7 +313,7 @@ pub mod api { let api_prefix = Uuid::parse_str(&uuid).map_err(|_| { ( StatusCode::NOT_FOUND, - format!("unparsable UUID prefix: {}", uuid), + format!("unparsable UUID prefix: {uuid}"), ) })?; @@ -326,7 +326,7 @@ pub mod api { .ok_or_else(|| { ( StatusCode::NOT_FOUND, - format!("relay {} not found for request: {}", api_prefix, rest), + format!("relay {api_prefix} not found for request: {rest}"), ) }) .map(|rs| (rs.new_request_channel.clone(), rs.name.clone()))?; @@ -686,8 +686,7 @@ pub mod event_loop { request, StatusCode::BAD_GATEWAY, &format!( - "relay disconnected with pending requests: {}", - disconnection_reason_ + "relay disconnected with pending requests: {disconnection_reason_}" ), asset_name, ) @@ -711,10 +710,7 @@ pub mod event_loop { fail_request( request, StatusCode::BAD_GATEWAY, - &format!( - "relay disconnected with in-progress requests: {}", - disconnection_reason_ - ), + &format!("relay disconnected with in-progress requests: {disconnection_reason_}"), asset_name, ) .await; @@ -929,8 +925,7 @@ pub mod event_loop { // This branch is practically impossible, but for the sake of completeness: // Let’s break 'event_loop, this seems the most elegant. let err = format!( - "error when serializing request to JSON (this will never happen): {:?}", - err + "error when serializing request to JSON (this will never happen): {err:?}" ); error!("load balancer: {}: {}", asset_name.as_str(), err); Err(err) @@ -991,7 +986,7 @@ pub mod event_loop { let send_result = match json { Ok(msg) => send_json_msg(socket_tx, &msg, asset_name).await, - Err(err) => Err(format!("error when serializing request to JSON: {:?}", err)), // impossible + Err(err) => Err(format!("error when serializing request to JSON: {err:?}")), // impossible }; match send_result { @@ -1002,7 +997,7 @@ pub mod event_loop { Ok(()) }, Err(err) => { - let err = format!("error when sending request to relay: {:?}", err); + let err = format!("error when sending request to relay: {err:?}"); if let Some(request) = relay_state .requests_in_progress @@ -1065,7 +1060,7 @@ async fn request_to_json( &Method::POST => Ok(JsonRequestMethod::POST), other => Err(( StatusCode::BAD_REQUEST, - format!("unhandled request method: {}", other), + format!("unhandled request method: {other}"), )), })?; diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs index 721f4c35..f80abd3d 100644 --- a/crates/gateway/src/main.rs +++ b/crates/gateway/src/main.rs @@ -82,7 +82,7 @@ async fn main() -> Result<()> { ) .await .unwrap_or_else(|e| { - eprintln!("Server error: {}", e); + eprintln!("Server error: {e}"); std::process::exit(1); }); diff --git a/crates/gateway/tests/load_balancer.rs b/crates/gateway/tests/load_balancer.rs index 67e26292..fba74a9e 100644 --- a/crates/gateway/tests/load_balancer.rs +++ b/crates/gateway/tests/load_balancer.rs @@ -18,7 +18,7 @@ async fn test_websocket_connection_invalid_token() { let router = build_router(lb.clone()).await; let (addr, server_handle) = start_server(router).await; - let url = format!("ws://{}", addr); + let url = format!("ws://{addr}"); let request = hyper::Request::builder() .uri(&url) .header("Authorization", "Bearer invalid") @@ -43,8 +43,8 @@ async fn test_websocket_request_response_flow() { let router = build_router(lb.clone()).await; let (addr, server_handle) = start_server(router).await; - let ws_url = format!("ws://{}/ws", addr); - let http_url = format!("http://{}", addr); + let ws_url = format!("ws://{addr}/ws"); + let http_url = format!("http://{addr}"); let request = hyper::Request::builder() .uri(&ws_url) @@ -100,7 +100,7 @@ async fn test_websocket_request_response_flow() { let client = reqwest::Client::new(); let res = client - .get(format!("{}/{}/test", http_url, prefix)) + .get(format!("{http_url}/{prefix}/test")) .send() .await .expect("http request failed"); From 64b41f5bf1601afcbec03ddc04fb9e0944a3dd33 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Wed, 18 Feb 2026 21:18:37 +0100 Subject: [PATCH 04/23] chore: rename the `hydra` modules according to function --- .../mod.rs} | 2 +- .../tunnel.rs | 0 .../tunnel2.rs | 0 .../verifications.rs | 2 +- crates/gateway/src/lib.rs | 2 +- crates/gateway/src/load_balancer.rs | 30 ++++++++-------- crates/gateway/src/main.rs | 4 +-- .../src/{hydra.rs => hydra_client/mod.rs} | 0 .../src/{hydra => hydra_client}/tunnel.rs | 0 .../src/{hydra => hydra_client}/tunnel2.rs | 0 .../{hydra => hydra_client}/verifications.rs | 2 +- crates/platform/src/icebreakers/manager.rs | 14 ++++---- crates/platform/src/lib.rs | 2 +- crates/platform/src/load_balancer.rs | 36 +++++++++---------- crates/platform/src/main.rs | 2 +- 15 files changed, 49 insertions(+), 47 deletions(-) rename crates/gateway/src/{hydra.rs => hydra_server_platform/mod.rs} (99%) rename crates/gateway/src/{hydra => hydra_server_platform}/tunnel.rs (100%) rename crates/gateway/src/{hydra => hydra_server_platform}/tunnel2.rs (100%) rename crates/gateway/src/{hydra => hydra_server_platform}/verifications.rs (99%) rename crates/platform/src/{hydra.rs => hydra_client/mod.rs} (100%) rename crates/platform/src/{hydra => hydra_client}/tunnel.rs (100%) rename crates/platform/src/{hydra => hydra_client}/tunnel2.rs (100%) rename crates/platform/src/{hydra => hydra_client}/verifications.rs (99%) diff --git a/crates/gateway/src/hydra.rs b/crates/gateway/src/hydra_server_platform/mod.rs similarity index 99% rename from crates/gateway/src/hydra.rs rename to crates/gateway/src/hydra_server_platform/mod.rs index 036caa95..732b98bb 100644 --- a/crates/gateway/src/hydra.rs +++ b/crates/gateway/src/hydra_server_platform/mod.rs @@ -50,7 +50,7 @@ impl HydrasManager { config.lovelace_per_request * config.requests_per_microtransaction; if microtransaction_lovelace < MIN_LOVELACE_PER_TRANSACTION { Err(anyhow!( - "hydras-manager: Please make sure that each microtransaction will be larger than {MIN_LOVELACE_PER_TRANSACTION} lovelace. Currently it would be {microtransaction_lovelace}.", + "hydras-manager: Please make sure that each microtransaction will be larger than {MIN_LOVELACE_PER_TRANSACTION} lovelace. Currently it would be {microtransaction_lovelace}." ))? } diff --git a/crates/gateway/src/hydra/tunnel.rs b/crates/gateway/src/hydra_server_platform/tunnel.rs similarity index 100% rename from crates/gateway/src/hydra/tunnel.rs rename to crates/gateway/src/hydra_server_platform/tunnel.rs diff --git a/crates/gateway/src/hydra/tunnel2.rs b/crates/gateway/src/hydra_server_platform/tunnel2.rs similarity index 100% rename from crates/gateway/src/hydra/tunnel2.rs rename to crates/gateway/src/hydra_server_platform/tunnel2.rs diff --git a/crates/gateway/src/hydra/verifications.rs b/crates/gateway/src/hydra_server_platform/verifications.rs similarity index 99% rename from crates/gateway/src/hydra/verifications.rs rename to crates/gateway/src/hydra_server_platform/verifications.rs index 03d874cb..90b4df00 100644 --- a/crates/gateway/src/hydra/verifications.rs +++ b/crates/gateway/src/hydra_server_platform/verifications.rs @@ -766,7 +766,7 @@ pub fn sigterm(pid: u32) -> Result<()> { /// We use it for `localhost` tests, to detect if the Gateway and Platform are /// running on the same host. Then we cannot set up a -/// `[crate::hydra::tunnel2::Tunnel]`, because the ports are already taken. +/// `[crate::hydra_server_platform::tunnel2::Tunnel]`, because the ports are already taken. pub fn hashed_machine_id() -> String { const MACHINE_ID_NAMESPACE: &str = "blockfrost.machine-id.v1"; diff --git a/crates/gateway/src/lib.rs b/crates/gateway/src/lib.rs index ae376123..d93455b3 100644 --- a/crates/gateway/src/lib.rs +++ b/crates/gateway/src/lib.rs @@ -4,7 +4,7 @@ pub mod config; pub mod db; pub mod errors; pub mod find_libexec; -pub mod hydra; +pub mod hydra_server_platform; pub mod load_balancer; pub mod models; pub mod payload; diff --git a/crates/gateway/src/load_balancer.rs b/crates/gateway/src/load_balancer.rs index 47684bbc..6892d77c 100644 --- a/crates/gateway/src/load_balancer.rs +++ b/crates/gateway/src/load_balancer.rs @@ -1,5 +1,5 @@ use crate::errors::APIError; -use crate::hydra; +use crate::hydra_server_platform; use crate::types::AssetName; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -55,8 +55,8 @@ pub enum JsonRequestMethod { #[derive(Serialize, Deserialize, Debug)] pub enum LoadBalancerMessage { Request(JsonRequest), - HydraKExResponse(hydra::KeyExchangeResponse), - HydraTunnel(hydra::tunnel2::TunnelMsg), + HydraKExResponse(hydra_server_platform::KeyExchangeResponse), + HydraTunnel(hydra_server_platform::tunnel2::TunnelMsg), Ping(u64), Pong(u64), Error { code: u64, msg: String }, @@ -66,8 +66,8 @@ pub enum LoadBalancerMessage { #[derive(Serialize, Deserialize, Debug)] pub enum RelayMessage { Response(JsonResponse), - HydraKExRequest(hydra::KeyExchangeRequest), - HydraTunnel(hydra::tunnel2::TunnelMsg), + HydraKExRequest(hydra_server_platform::KeyExchangeRequest), + HydraTunnel(hydra_server_platform::tunnel2::TunnelMsg), Ping(u64), Pong(u64), } @@ -77,7 +77,7 @@ pub struct LoadBalancerState { pub access_tokens: Arc>>, pub active_relays: Arc>>, pub background_worker: Arc>, - pub hydras: Option, + pub hydras: Option, } #[derive(Debug)] @@ -111,7 +111,7 @@ pub struct RequestState { } impl LoadBalancerState { - pub async fn new(hydras: Option) -> LoadBalancerState { + pub async fn new(hydras: Option) -> LoadBalancerState { let access_tokens = Arc::new(Mutex::new(HashMap::new())); let active_relays = Arc::new(Mutex::new(HashMap::new())); let background_worker = Arc::new(tokio::spawn(Self::clean_up_expired_tokens_periodically( @@ -463,12 +463,14 @@ pub mod event_loop { let mut last_ping_id: u64 = 0; let mut disconnection_reason = None; - let mut initial_hydra_kex: Option<(hydra::KeyExchangeRequest, hydra::KeyExchangeResponse)> = - None; - let mut hydra_controller: Option = None; + let mut initial_hydra_kex: Option<( + hydra_server_platform::KeyExchangeRequest, + hydra_server_platform::KeyExchangeResponse, + )> = None; + let mut hydra_controller: Option = None; let tunnel_cancellation = CancellationToken::new(); - let mut tunnel_controller: Option = None; + let mut tunnel_controller: Option = None; // The actual connection event loop: 'event_loop: while let Some(msg) = event_rx.recv().await { @@ -531,11 +533,11 @@ pub mod event_loop { // on different machines: if platform_machine_id != resp.machine_id { let (tunnel_ctl, mut tunnel_rx) = - hydra::tunnel2::Tunnel::new( - hydra::tunnel2::TunnelConfig { + hydra_server_platform::tunnel2::Tunnel::new( + hydra_server_platform::tunnel2::TunnelConfig { expose_port: resp.gateway_h2h_port, id_prefix_bit: true, - ..(hydra::tunnel2::TunnelConfig::default()) + ..(hydra_server_platform::tunnel2::TunnelConfig::default()) }, tunnel_cancellation.clone(), ); diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs index f80abd3d..dfd99b1a 100644 --- a/crates/gateway/src/main.rs +++ b/crates/gateway/src/main.rs @@ -4,7 +4,7 @@ use axum::{ Extension, Router, routing::{get, post}, }; -use blockfrost_gateway::{api, blockfrost, config, db, hydra, load_balancer}; +use blockfrost_gateway::{api, blockfrost, config, db, hydra_server_platform, load_balancer}; use clap::Parser; use colored::Colorize; use config::{Args, Config}; @@ -33,7 +33,7 @@ async fn main() -> Result<()> { let pool = DB::new(&config.database.connection_string).await; let blockfrost_api = blockfrost::BlockfrostAPI::new(&config.blockfrost.project_id); let hydras_manager = if let Some(hydra) = &config.hydra { - Some(hydra::HydrasManager::new(hydra, &config.server.network).await?) + Some(hydra_server_platform::HydrasManager::new(hydra, &config.server.network).await?) } else { None }; diff --git a/crates/platform/src/hydra.rs b/crates/platform/src/hydra_client/mod.rs similarity index 100% rename from crates/platform/src/hydra.rs rename to crates/platform/src/hydra_client/mod.rs diff --git a/crates/platform/src/hydra/tunnel.rs b/crates/platform/src/hydra_client/tunnel.rs similarity index 100% rename from crates/platform/src/hydra/tunnel.rs rename to crates/platform/src/hydra_client/tunnel.rs diff --git a/crates/platform/src/hydra/tunnel2.rs b/crates/platform/src/hydra_client/tunnel2.rs similarity index 100% rename from crates/platform/src/hydra/tunnel2.rs rename to crates/platform/src/hydra_client/tunnel2.rs diff --git a/crates/platform/src/hydra/verifications.rs b/crates/platform/src/hydra_client/verifications.rs similarity index 99% rename from crates/platform/src/hydra/verifications.rs rename to crates/platform/src/hydra_client/verifications.rs index ef305c14..e838a06d 100644 --- a/crates/platform/src/hydra/verifications.rs +++ b/crates/platform/src/hydra_client/verifications.rs @@ -400,7 +400,7 @@ pub fn sigterm(_pid: u32) -> Result<()> { /// We use it for `localhost` tests, to detect if the Gateway and Platform are /// running on the same host. Then we cannot set up a -/// `[crate::hydra::tunnel2::Tunnel]`, because the ports are already taken. +/// `[crate::hydra_client::tunnel2::Tunnel]`, because the ports are already taken. pub fn hashed_machine_id() -> String { const MACHINE_ID_NAMESPACE: &str = "blockfrost.machine-id.v1"; diff --git a/crates/platform/src/icebreakers/manager.rs b/crates/platform/src/icebreakers/manager.rs index 271ef1bd..df4e6266 100644 --- a/crates/platform/src/icebreakers/manager.rs +++ b/crates/platform/src/icebreakers/manager.rs @@ -1,6 +1,6 @@ use crate::icebreakers::api::IcebreakersAPI; use crate::server::state::ApiPrefix; -use crate::{hydra, load_balancer}; +use crate::{hydra_client, load_balancer}; use axum::Router; use bf_common::errors::BlockfrostError; use std::sync::Arc; @@ -61,9 +61,9 @@ impl IcebreakersManager { pub async fn run( self, hydra_kex: ( - mpsc::Receiver, - mpsc::Sender, - mpsc::Sender, + mpsc::Receiver, + mpsc::Sender, + mpsc::Sender, ), ) { let (dest_watch_tx, dest_watch_rx) = watch::channel(None); @@ -72,9 +72,9 @@ impl IcebreakersManager { // For now, we’re passing a pair with changeable destination of // requests, as we run multiple load balancers to multiple gateways: let mutable_hydra_kex: ( - watch::Sender>>, - mpsc::Sender, - mpsc::Sender, + watch::Sender>>, + mpsc::Sender, + mpsc::Sender, ) = (dest_watch_tx, hydra_kex.1, hydra_kex.2); tokio::spawn(async move { diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index ac2cf4c2..f28eaf8c 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -1,6 +1,6 @@ pub mod api; pub mod health_monitor; -pub mod hydra; +pub mod hydra_client; pub mod icebreakers; pub mod load_balancer; pub mod middlewares; diff --git a/crates/platform/src/load_balancer.rs b/crates/platform/src/load_balancer.rs index 17ccad60..8cb37192 100644 --- a/crates/platform/src/load_balancer.rs +++ b/crates/platform/src/load_balancer.rs @@ -1,4 +1,4 @@ -use crate::hydra; +use crate::hydra_client; use crate::server::state::ApiPrefix; use bf_common::errors::{AppError, BlockfrostError}; use serde::{Deserialize, Serialize}; @@ -36,9 +36,9 @@ pub async fn run_all( health_errors: Arc>>, api_prefix: ApiPrefix, hydra_kex: Option<( - watch::Sender>>, - mpsc::Sender, - mpsc::Sender, + watch::Sender>>, + mpsc::Sender, + mpsc::Sender, )>, ) { assert!( @@ -139,8 +139,8 @@ impl JsonRequestMethod { #[derive(Serialize, Deserialize, Debug)] enum LoadBalancerMessage { Request(JsonRequest), - HydraKExResponse(hydra::KeyExchangeResponse), - HydraTunnel(hydra::tunnel2::TunnelMsg), + HydraKExResponse(hydra_client::KeyExchangeResponse), + HydraTunnel(hydra_client::tunnel2::TunnelMsg), Ping(u64), Pong(u64), } @@ -149,8 +149,8 @@ enum LoadBalancerMessage { #[derive(Serialize, Deserialize, Debug)] enum RelayMessage { Response(JsonResponse), - HydraKExRequest(hydra::KeyExchangeRequest), - HydraTunnel(hydra::tunnel2::TunnelMsg), + HydraKExRequest(hydra_client::KeyExchangeRequest), + HydraTunnel(hydra_client::tunnel2::TunnelMsg), Ping(u64), Pong(u64), } @@ -166,7 +166,7 @@ mod event_loop { enum LBEvent { NewLoadBalancerMessage(LoadBalancerMessage), NewResponse(JsonResponse), - HydraKExRequest(hydra::KeyExchangeRequest), + HydraKExRequest(hydra_client::KeyExchangeRequest), PingTick, SocketError(String), } @@ -180,9 +180,9 @@ mod event_loop { health_errors: Arc>>, api_prefix: ApiPrefix, hydra_kex: Option<( - watch::Sender>>, - mpsc::Sender, - mpsc::Sender, + watch::Sender>>, + mpsc::Sender, + mpsc::Sender, )>, ) -> Result<(), String> { let socket = connect(config.clone()).await?; @@ -230,7 +230,7 @@ mod event_loop { schedule_ping_tick(); let tunnel_cancellation = CancellationToken::new(); - let mut tunnel_controller: Option = None; + let mut tunnel_controller: Option = None; // The actual connection event loop: 'event_loop: while let Some(msg) = event_rx.recv().await { @@ -255,12 +255,12 @@ mod event_loop { if let Some(hydra_kex) = &hydra_kex { // Only start the TCP-over-WebSocket tunnels if we’re running // on different machines: - if resp.machine_id != hydra::verifications::hashed_machine_id() { - let (tunnel_ctl, mut tunnel_rx) = hydra::tunnel2::Tunnel::new( - hydra::tunnel2::TunnelConfig { + if resp.machine_id != hydra_client::verifications::hashed_machine_id() { + let (tunnel_ctl, mut tunnel_rx) = hydra_client::tunnel2::Tunnel::new( + hydra_client::tunnel2::TunnelConfig { expose_port: resp.proposed_platform_h2h_port, id_prefix_bit: true, - ..(hydra::tunnel2::TunnelConfig::default()) + ..(hydra_client::tunnel2::TunnelConfig::default()) }, tunnel_cancellation.clone(), ); @@ -362,7 +362,7 @@ mod event_loop { } if let Some(hydra_kex) = hydra_kex { - let _ = hydra_kex.2.send(hydra::TerminateRequest).await; + let _ = hydra_kex.2.send(hydra_client::TerminateRequest).await; } tunnel_cancellation.cancel(); diff --git a/crates/platform/src/main.rs b/crates/platform/src/main.rs index cdf54f5c..3f36fb9a 100644 --- a/crates/platform/src/main.rs +++ b/crates/platform/src/main.rs @@ -3,7 +3,7 @@ use bf_common::cli::Args; use blockfrost_platform::{ AppError, - hydra::HydraController, + hydra_client::HydraController, icebreakers::manager::IcebreakersManager, server::{build, logging::setup_tracing}, }; From 42af2190abaec978d2495de9dc21d162f2828f3a Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Wed, 18 Feb 2026 21:19:43 +0100 Subject: [PATCH 05/23] chore: remove unused `tunnel.rs` --- .../gateway/src/hydra_server_platform/mod.rs | 1 - .../src/hydra_server_platform/tunnel.rs | 274 ------------------ crates/platform/src/hydra_client/mod.rs | 1 - crates/platform/src/hydra_client/tunnel.rs | 274 ------------------ 4 files changed, 550 deletions(-) delete mode 100644 crates/gateway/src/hydra_server_platform/tunnel.rs delete mode 100644 crates/platform/src/hydra_client/tunnel.rs diff --git a/crates/gateway/src/hydra_server_platform/mod.rs b/crates/gateway/src/hydra_server_platform/mod.rs index 732b98bb..4f117a1d 100644 --- a/crates/gateway/src/hydra_server_platform/mod.rs +++ b/crates/gateway/src/hydra_server_platform/mod.rs @@ -8,7 +8,6 @@ use std::time::Duration; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; -pub mod tunnel; pub mod tunnel2; pub mod verifications; diff --git a/crates/gateway/src/hydra_server_platform/tunnel.rs b/crates/gateway/src/hydra_server_platform/tunnel.rs deleted file mode 100644 index 1debbb4f..00000000 --- a/crates/gateway/src/hydra_server_platform/tunnel.rs +++ /dev/null @@ -1,274 +0,0 @@ -use anyhow::Result; -use bytes::{Bytes, BytesMut}; -use std::sync::atomic::{AtomicU64, Ordering}; -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; - -pub enum TunnelEventThere { - Connect { - id: u64, - write_back: mpsc::Sender, - }, - Write { - id: u64, - chunk: Bytes, - }, - Disconnect { - id: u64, - reason: Option, - }, -} - -pub enum TunnelEventBack { - Write { chunk: Bytes }, - Disconnect { reason: Option }, -} - -pub mod connect_here { - use super::*; - use std::collections::HashMap; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - use tokio::net::TcpStream; - use tokio::task::JoinSet; - - enum ConnCmd { - Write(Bytes), - Disconnect(Option), - } - - pub async fn run_tunnel( - connect_port: u16, - mut event_rx: mpsc::Receiver, - cancel: CancellationToken, - ) -> Result<()> { - let mut conns: HashMap> = HashMap::new(); - let mut joinset: JoinSet = JoinSet::new(); - - loop { - tokio::select! { - _ = cancel.cancelled() => break, - - // Route incoming tunnel events to per-connection tasks. - ev = event_rx.recv() => { - let Some(ev) = ev else { break; }; - - match ev { - TunnelEventThere::Connect { id, write_back } => { - // If a duplicate id appears, drop the old sender (old task will see recv(None) and exit). - conns.remove(&id); - - let (cmd_tx, cmd_rx) = mpsc::channel::(128); - conns.insert(id, cmd_tx); - - let cancel_conn = cancel.clone(); - joinset.spawn(async move { - run_one_connection(connect_port, id, write_back, cmd_rx, cancel_conn).await; - id - }); - } - - TunnelEventThere::Write { id, chunk } => { - if let Some(tx) = conns.get(&id) { - if tx.send(ConnCmd::Write(chunk)).await.is_err() { - conns.remove(&id); - } - } - } - - TunnelEventThere::Disconnect { id, reason } => { - if let Some(tx) = conns.remove(&id) { - // Best-effort; if it fails, the task is already gone. - let _ = tx.send(ConnCmd::Disconnect(reason)).await; - } - } - } - } - - // Reap finished per-connection tasks and drop their routing entry. - res = joinset.join_next(), if !joinset.is_empty() => { - if let Some(Ok(id)) = res { - conns.remove(&id); - } - } - } - } - - // Stop all remaining connections (dropping the senders makes cmd_rx.recv() return None). - conns.clear(); - while joinset.join_next().await.is_some() {} - - Ok(()) - } - - async fn run_one_connection( - connect_port: u16, - _id: u64, - write_back: mpsc::Sender, - mut cmd_rx: mpsc::Receiver, - cancel: CancellationToken, - ) { - let mut sock = match TcpStream::connect(("127.0.0.1", connect_port)).await { - Ok(s) => s, - Err(e) => { - let _ = write_back - .send(TunnelEventBack::Disconnect { reason: Some(e) }) - .await; - return; - }, - }; - - let mut buf = BytesMut::with_capacity(8 * 1024); - let cancel_err = - || std::io::Error::new(std::io::ErrorKind::Interrupted, "tunnel cancelled"); - - let mut notify_disconnect = true; - let reason: Option = loop { - tokio::select! { - _ = cancel.cancelled() => break Some(cancel_err()), - - rv = async { - buf.clear(); - buf.reserve(8 * 1024); - sock.read_buf(&mut buf).await - } => { - match rv { - Ok(0) => break None, // clean EOF - Ok(_) => { - let chunk = buf.split().freeze(); - if write_back.send(TunnelEventBack::Write { chunk }).await.is_err() { - // Other side is gone; no point continuing. - notify_disconnect = false; - break None; - } - } - Err(e) => break Some(e), - } - } - - cmd = cmd_rx.recv() => { - match cmd { - Some(ConnCmd::Write(chunk)) => { - if let Err(e) = sock.write_all(&chunk).await { - break Some(e); - } - } - Some(ConnCmd::Disconnect(r)) => { - // Peer initiated; don't bother echoing a Disconnect back. - notify_disconnect = false; - break r; - } - None => { - // Router dropped; exit quietly. - notify_disconnect = false; - break None; - } - } - } - } - }; - - if notify_disconnect { - let _ = write_back - .send(TunnelEventBack::Disconnect { reason }) - .await; - } - - let _ = sock.shutdown().await; - } -} - -pub mod listen_here { - use super::*; - - static NEXT_CONNECTION_ID: AtomicU64 = AtomicU64::new(1); - - pub async fn run_tunnel( - listen_port: u16, - event_tx_: mpsc::Sender, - cancel: CancellationToken, - ) -> Result<()> { - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - let listener = tokio::net::TcpListener::bind(("127.0.0.1", listen_port)).await?; - - loop { - let (mut sock, _peer) = tokio::select! { - _ = cancel.cancelled() => break, - res = listener.accept() => res?, - }; - - let event_tx = event_tx_.clone(); - let cancel_conn = cancel.clone(); - - tokio::spawn(async move { - let conn_id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); - let (event_back_tx, mut event_back_rx) = mpsc::channel::(128); - - // If the tunnel receiver is gone, close the socket/task. - if event_tx - .send(TunnelEventThere::Connect { - id: conn_id, - write_back: event_back_tx, - }) - .await - .is_err() - { - let _ = sock.shutdown().await; - return; - } - - let mut buf = BytesMut::with_capacity(8 * 1024); - let cancel_err = - || std::io::Error::new(std::io::ErrorKind::Interrupted, "tunnel cancelled"); - - let reason: Option = loop { - tokio::select! { - _ = cancel_conn.cancelled() => break Some(cancel_err()), - - rv = async { - buf.clear(); - buf.reserve(8 * 1024); - sock.read_buf(&mut buf).await - } => { - match rv { - Ok(0) => break None, // clean EOF - Ok(_) => { - let chunk = buf.split().freeze(); // no copy - if event_tx.send(TunnelEventThere::Write { id: conn_id, chunk }).await.is_err() { - let _ = sock.shutdown().await; - return; - } - } - Err(e) => break Some(e), - } - } - - event_back = event_back_rx.recv() => { - match event_back { - Some(TunnelEventBack::Write { chunk }) => { - if let Err(e) = sock.write_all(&chunk).await { - break Some(e); - } - } - Some(TunnelEventBack::Disconnect { reason }) => break reason, - None => break None, // all back-senders dropped - } - } - } - }; - - // Best-effort; if it fails, the tunnel is already gone. - let _ = event_tx - .send(TunnelEventThere::Disconnect { - id: conn_id, - reason, - }) - .await; - - let _ = sock.shutdown().await; - }); - } - - Ok(()) - } -} diff --git a/crates/platform/src/hydra_client/mod.rs b/crates/platform/src/hydra_client/mod.rs index c2737f2f..02fe943f 100644 --- a/crates/platform/src/hydra_client/mod.rs +++ b/crates/platform/src/hydra_client/mod.rs @@ -5,7 +5,6 @@ use std::{path::PathBuf, sync::Arc}; use tokio::sync::{Mutex, mpsc}; use tracing::{debug, error, info, warn}; -pub mod tunnel; pub mod tunnel2; pub mod verifications; diff --git a/crates/platform/src/hydra_client/tunnel.rs b/crates/platform/src/hydra_client/tunnel.rs deleted file mode 100644 index 1debbb4f..00000000 --- a/crates/platform/src/hydra_client/tunnel.rs +++ /dev/null @@ -1,274 +0,0 @@ -use anyhow::Result; -use bytes::{Bytes, BytesMut}; -use std::sync::atomic::{AtomicU64, Ordering}; -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; - -pub enum TunnelEventThere { - Connect { - id: u64, - write_back: mpsc::Sender, - }, - Write { - id: u64, - chunk: Bytes, - }, - Disconnect { - id: u64, - reason: Option, - }, -} - -pub enum TunnelEventBack { - Write { chunk: Bytes }, - Disconnect { reason: Option }, -} - -pub mod connect_here { - use super::*; - use std::collections::HashMap; - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - use tokio::net::TcpStream; - use tokio::task::JoinSet; - - enum ConnCmd { - Write(Bytes), - Disconnect(Option), - } - - pub async fn run_tunnel( - connect_port: u16, - mut event_rx: mpsc::Receiver, - cancel: CancellationToken, - ) -> Result<()> { - let mut conns: HashMap> = HashMap::new(); - let mut joinset: JoinSet = JoinSet::new(); - - loop { - tokio::select! { - _ = cancel.cancelled() => break, - - // Route incoming tunnel events to per-connection tasks. - ev = event_rx.recv() => { - let Some(ev) = ev else { break; }; - - match ev { - TunnelEventThere::Connect { id, write_back } => { - // If a duplicate id appears, drop the old sender (old task will see recv(None) and exit). - conns.remove(&id); - - let (cmd_tx, cmd_rx) = mpsc::channel::(128); - conns.insert(id, cmd_tx); - - let cancel_conn = cancel.clone(); - joinset.spawn(async move { - run_one_connection(connect_port, id, write_back, cmd_rx, cancel_conn).await; - id - }); - } - - TunnelEventThere::Write { id, chunk } => { - if let Some(tx) = conns.get(&id) { - if tx.send(ConnCmd::Write(chunk)).await.is_err() { - conns.remove(&id); - } - } - } - - TunnelEventThere::Disconnect { id, reason } => { - if let Some(tx) = conns.remove(&id) { - // Best-effort; if it fails, the task is already gone. - let _ = tx.send(ConnCmd::Disconnect(reason)).await; - } - } - } - } - - // Reap finished per-connection tasks and drop their routing entry. - res = joinset.join_next(), if !joinset.is_empty() => { - if let Some(Ok(id)) = res { - conns.remove(&id); - } - } - } - } - - // Stop all remaining connections (dropping the senders makes cmd_rx.recv() return None). - conns.clear(); - while joinset.join_next().await.is_some() {} - - Ok(()) - } - - async fn run_one_connection( - connect_port: u16, - _id: u64, - write_back: mpsc::Sender, - mut cmd_rx: mpsc::Receiver, - cancel: CancellationToken, - ) { - let mut sock = match TcpStream::connect(("127.0.0.1", connect_port)).await { - Ok(s) => s, - Err(e) => { - let _ = write_back - .send(TunnelEventBack::Disconnect { reason: Some(e) }) - .await; - return; - }, - }; - - let mut buf = BytesMut::with_capacity(8 * 1024); - let cancel_err = - || std::io::Error::new(std::io::ErrorKind::Interrupted, "tunnel cancelled"); - - let mut notify_disconnect = true; - let reason: Option = loop { - tokio::select! { - _ = cancel.cancelled() => break Some(cancel_err()), - - rv = async { - buf.clear(); - buf.reserve(8 * 1024); - sock.read_buf(&mut buf).await - } => { - match rv { - Ok(0) => break None, // clean EOF - Ok(_) => { - let chunk = buf.split().freeze(); - if write_back.send(TunnelEventBack::Write { chunk }).await.is_err() { - // Other side is gone; no point continuing. - notify_disconnect = false; - break None; - } - } - Err(e) => break Some(e), - } - } - - cmd = cmd_rx.recv() => { - match cmd { - Some(ConnCmd::Write(chunk)) => { - if let Err(e) = sock.write_all(&chunk).await { - break Some(e); - } - } - Some(ConnCmd::Disconnect(r)) => { - // Peer initiated; don't bother echoing a Disconnect back. - notify_disconnect = false; - break r; - } - None => { - // Router dropped; exit quietly. - notify_disconnect = false; - break None; - } - } - } - } - }; - - if notify_disconnect { - let _ = write_back - .send(TunnelEventBack::Disconnect { reason }) - .await; - } - - let _ = sock.shutdown().await; - } -} - -pub mod listen_here { - use super::*; - - static NEXT_CONNECTION_ID: AtomicU64 = AtomicU64::new(1); - - pub async fn run_tunnel( - listen_port: u16, - event_tx_: mpsc::Sender, - cancel: CancellationToken, - ) -> Result<()> { - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - let listener = tokio::net::TcpListener::bind(("127.0.0.1", listen_port)).await?; - - loop { - let (mut sock, _peer) = tokio::select! { - _ = cancel.cancelled() => break, - res = listener.accept() => res?, - }; - - let event_tx = event_tx_.clone(); - let cancel_conn = cancel.clone(); - - tokio::spawn(async move { - let conn_id = NEXT_CONNECTION_ID.fetch_add(1, Ordering::Relaxed); - let (event_back_tx, mut event_back_rx) = mpsc::channel::(128); - - // If the tunnel receiver is gone, close the socket/task. - if event_tx - .send(TunnelEventThere::Connect { - id: conn_id, - write_back: event_back_tx, - }) - .await - .is_err() - { - let _ = sock.shutdown().await; - return; - } - - let mut buf = BytesMut::with_capacity(8 * 1024); - let cancel_err = - || std::io::Error::new(std::io::ErrorKind::Interrupted, "tunnel cancelled"); - - let reason: Option = loop { - tokio::select! { - _ = cancel_conn.cancelled() => break Some(cancel_err()), - - rv = async { - buf.clear(); - buf.reserve(8 * 1024); - sock.read_buf(&mut buf).await - } => { - match rv { - Ok(0) => break None, // clean EOF - Ok(_) => { - let chunk = buf.split().freeze(); // no copy - if event_tx.send(TunnelEventThere::Write { id: conn_id, chunk }).await.is_err() { - let _ = sock.shutdown().await; - return; - } - } - Err(e) => break Some(e), - } - } - - event_back = event_back_rx.recv() => { - match event_back { - Some(TunnelEventBack::Write { chunk }) => { - if let Err(e) = sock.write_all(&chunk).await { - break Some(e); - } - } - Some(TunnelEventBack::Disconnect { reason }) => break reason, - None => break None, // all back-senders dropped - } - } - } - }; - - // Best-effort; if it fails, the tunnel is already gone. - let _ = event_tx - .send(TunnelEventThere::Disconnect { - id: conn_id, - reason, - }) - .await; - - let _ = sock.shutdown().await; - }); - } - - Ok(()) - } -} From 5c6d8bdfb5aefc2fb225466e62e798894c829ac5 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 19 Feb 2026 01:16:30 +0100 Subject: [PATCH 06/23] =?UTF-8?q?chore:=20rename=20TOML=E2=80=99s=20`hydra?= =?UTF-8?q?`=20=E2=86=92=20`hydra=5Fplatform`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/gateway/config/development.toml | 2 +- crates/gateway/src/config.rs | 8 ++++---- crates/gateway/src/main.rs | 10 ++++++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/gateway/config/development.toml b/crates/gateway/config/development.toml index b4293fb7..126e7ff4 100644 --- a/crates/gateway/config/development.toml +++ b/crates/gateway/config/development.toml @@ -10,7 +10,7 @@ connection_string = 'postgresql://user:pass@host:port/db' project_id = 'preview_BLOCKFROST_PROJECT_ID' nft_asset = '4213fc3eac8c781ac85514dd1de9aaabcd5a3a81cc2df4f413b9b295' -[hydra] +[hydra_platform] max_concurrent_hydra_nodes = 2 cardano_signing_key = "/home/mw/.config/blockfrost-platform/hydra/tmp_their_keys/payment.sk" node_socket_path = "/home/mw/.local/share/blockfrost-platform/preview/node.socket" diff --git a/crates/gateway/src/config.rs b/crates/gateway/src/config.rs index 54269fa2..f4e3fc1d 100644 --- a/crates/gateway/src/config.rs +++ b/crates/gateway/src/config.rs @@ -65,7 +65,7 @@ pub struct ConfigInput { pub server: ServerInput, pub database: DbInput, pub blockfrost: BlockfrostInput, - pub hydra: Option, + pub hydra_platform: Option, } #[derive(Debug, Deserialize, Clone)] @@ -73,7 +73,7 @@ pub struct Config { pub server: Server, pub database: Db, pub blockfrost: Blockfrost, - pub hydra: Option, + pub hydra_platform: Option, } #[derive(Debug, Deserialize, Clone)] @@ -145,7 +145,7 @@ pub fn load_config(path: PathBuf) -> Config { project_id, nft_asset: toml_config.blockfrost.nft_asset, }, - hydra: toml_config.hydra, + hydra_platform: toml_config.hydra_platform, }; override_with_env(config) @@ -198,6 +198,6 @@ fn override_with_env(config: Config) -> Config { project_id, nft_asset, }, - hydra: config.hydra, + hydra_platform: config.hydra_platform, } } diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs index dfd99b1a..c59dd4a3 100644 --- a/crates/gateway/src/main.rs +++ b/crates/gateway/src/main.rs @@ -32,8 +32,14 @@ async fn main() -> Result<()> { let pool = DB::new(&config.database.connection_string).await; let blockfrost_api = blockfrost::BlockfrostAPI::new(&config.blockfrost.project_id); - let hydras_manager = if let Some(hydra) = &config.hydra { - Some(hydra_server_platform::HydrasManager::new(hydra, &config.server.network).await?) + let hydras_manager = if let Some(hydra_platform_config) = &config.hydra_platform { + Some( + hydra_server_platform::HydrasManager::new( + hydra_platform_config, + &config.server.network, + ) + .await?, + ) } else { None }; From 5f2a88d88cb2cd34c9ab1d3a8241cc1adf7234ec Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 19 Feb 2026 15:28:42 +0100 Subject: [PATCH 07/23] feat: re-do the initial version in separate files for clarity (we'll refactor later) --- Cargo.lock | 32 + Cargo.toml | 1 + crates/gateway/Cargo.toml | 1 + crates/gateway/config/development.toml | 11 +- crates/gateway/src/config.rs | 4 + crates/gateway/src/hydra_server_bridge/mod.rs | 873 ++++++++++++++++++ .../src/hydra_server_bridge/tunnel2.rs | 336 +++++++ .../src/hydra_server_bridge/verifications.rs | 581 ++++++++++++ crates/gateway/src/lib.rs | 2 + crates/gateway/src/main.rs | 21 +- crates/gateway/src/sdk_bridge_ws.rs | 600 ++++++++++++ crates/sdk_bridge/Cargo.toml | 38 + crates/sdk_bridge/src/config.rs | 91 ++ crates/sdk_bridge/src/find_libexec.rs | 94 ++ crates/sdk_bridge/src/http_proxy.rs | 178 ++++ crates/sdk_bridge/src/hydra_client/mod.rs | 858 +++++++++++++++++ crates/sdk_bridge/src/hydra_client/tunnel2.rs | 337 +++++++ .../src/hydra_client/verifications.rs | 768 +++++++++++++++ crates/sdk_bridge/src/main.rs | 50 + crates/sdk_bridge/src/protocol.rs | 35 + crates/sdk_bridge/src/types.rs | 28 + crates/sdk_bridge/src/ws_client.rs | 457 +++++++++ 22 files changed, 5393 insertions(+), 3 deletions(-) create mode 100644 crates/gateway/src/hydra_server_bridge/mod.rs create mode 100644 crates/gateway/src/hydra_server_bridge/tunnel2.rs create mode 100644 crates/gateway/src/hydra_server_bridge/verifications.rs create mode 100644 crates/gateway/src/sdk_bridge_ws.rs create mode 100644 crates/sdk_bridge/Cargo.toml create mode 100644 crates/sdk_bridge/src/config.rs create mode 100644 crates/sdk_bridge/src/find_libexec.rs create mode 100644 crates/sdk_bridge/src/http_proxy.rs create mode 100644 crates/sdk_bridge/src/hydra_client/mod.rs create mode 100644 crates/sdk_bridge/src/hydra_client/tunnel2.rs create mode 100644 crates/sdk_bridge/src/hydra_client/verifications.rs create mode 100644 crates/sdk_bridge/src/main.rs create mode 100644 crates/sdk_bridge/src/protocol.rs create mode 100644 crates/sdk_bridge/src/types.rs create mode 100644 crates/sdk_bridge/src/ws_client.rs diff --git a/Cargo.lock b/Cargo.lock index e96c9fcc..afd3d776 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -643,6 +643,7 @@ dependencies = [ "tokio-tungstenite 0.24.0", "tokio-util", "toml 0.9.11+spec-1.1.0", + "tower", "tracing", "tracing-subscriber", "tungstenite 0.24.0", @@ -819,6 +820,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "blockfrost-sdk-bridge" +version = "0.0.3-rc.3" +dependencies = [ + "anyhow", + "axum 0.8.8", + "base64 0.22.1", + "blake3", + "bytes", + "clap", + "dirs", + "futures", + "futures-util", + "getrandom 0.3.2", + "hex", + "hyper", + "machine-uid", + "nix", + "reqwest 0.13.1", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite 0.28.0", + "tokio-util", + "tracing", + "tracing-subscriber", + "tungstenite 0.28.0", + "url", + "uuid", +] + [[package]] name = "bumpalo" version = "3.19.1" diff --git a/Cargo.toml b/Cargo.toml index 448b1fe0..a7bf03f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/node", "crates/data_node", "crates/gateway", + "crates/sdk_bridge", ] default-members = ["crates/platform"] diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml index 507143f1..1ad14f1b 100644 --- a/crates/gateway/Cargo.toml +++ b/crates/gateway/Cargo.toml @@ -17,6 +17,7 @@ tokio = { version = "1.39.2", features = ["full"] } tokio-util = "0.7" futures = "0.3" futures-util = "0.3" +tower = "0.5.3" kameo = "0.19" bytes = "1" tokio-tungstenite = "0.24" diff --git a/crates/gateway/config/development.toml b/crates/gateway/config/development.toml index 126e7ff4..e20b7caa 100644 --- a/crates/gateway/config/development.toml +++ b/crates/gateway/config/development.toml @@ -12,7 +12,16 @@ nft_asset = '4213fc3eac8c781ac85514dd1de9aaabcd5a3a81cc2df4f413b9b295' [hydra_platform] max_concurrent_hydra_nodes = 2 -cardano_signing_key = "/home/mw/.config/blockfrost-platform/hydra/tmp_their_keys/payment.sk" +cardano_signing_key = "/home/mw/.config/blockfrost-gateway/hydra/preview/_our-keys/payment/payment.sk" +node_socket_path = "/home/mw/.local/share/blockfrost-platform/preview/node.socket" +commit_ada = 3.0 +lovelace_per_request = 100_000 +requests_per_microtransaction = 10 +microtransactions_per_fanout = 2 + +[hydra_bridge] +max_concurrent_hydra_nodes = 2 +cardano_signing_key = "/home/mw/.config/blockfrost-gateway/hydra/preview/_our-keys/payment/payment.sk" node_socket_path = "/home/mw/.local/share/blockfrost-platform/preview/node.socket" commit_ada = 3.0 lovelace_per_request = 100_000 diff --git a/crates/gateway/src/config.rs b/crates/gateway/src/config.rs index f4e3fc1d..8674ddfe 100644 --- a/crates/gateway/src/config.rs +++ b/crates/gateway/src/config.rs @@ -66,6 +66,7 @@ pub struct ConfigInput { pub database: DbInput, pub blockfrost: BlockfrostInput, pub hydra_platform: Option, + pub hydra_bridge: Option, } #[derive(Debug, Deserialize, Clone)] @@ -74,6 +75,7 @@ pub struct Config { pub database: Db, pub blockfrost: Blockfrost, pub hydra_platform: Option, + pub hydra_bridge: Option, } #[derive(Debug, Deserialize, Clone)] @@ -146,6 +148,7 @@ pub fn load_config(path: PathBuf) -> Config { nft_asset: toml_config.blockfrost.nft_asset, }, hydra_platform: toml_config.hydra_platform, + hydra_bridge: toml_config.hydra_bridge, }; override_with_env(config) @@ -199,5 +202,6 @@ fn override_with_env(config: Config) -> Config { nft_asset, }, hydra_platform: config.hydra_platform, + hydra_bridge: config.hydra_bridge, } } diff --git a/crates/gateway/src/hydra_server_bridge/mod.rs b/crates/gateway/src/hydra_server_bridge/mod.rs new file mode 100644 index 00000000..761f09f5 --- /dev/null +++ b/crates/gateway/src/hydra_server_bridge/mod.rs @@ -0,0 +1,873 @@ +use crate::config::HydraConfig as HydraTomlConfig; +use crate::types::Network; +use anyhow::{Result, anyhow}; +use serde::Deserialize; +use std::path::PathBuf; +use std::sync::{ + Arc, + atomic::{AtomicBool, AtomicU64, Ordering}, +}; +use std::time::Duration; +use tokio::sync::mpsc; +use tracing::{debug, error, info, warn}; + +pub mod tunnel2; +pub mod verifications; + +// FIXME: this should most probably be back to the default of 600 seconds: +const CONTESTATION_PERIOD_SECONDS: Duration = Duration::from_secs(60); + +// FIXME: shouldn’t this be multiplied by `max_concurrent_hydra_nodes`? +const MIN_FUEL_LOVELACE: u64 = 15_000_000; + +// TODO: At least on Preview that is. Where does this come from exactly? +const MIN_LOVELACE_PER_TRANSACTION: u64 = 840_450; + +const CREDIT_POLL_INTERVAL: Duration = Duration::from_secs(1); + +/// After cloning, it still represents the same set of [`HydraController`]s. +#[derive(Clone, Debug)] +pub struct HydrasManager { + config: HydraConfig, + /// This is `Arc>` because we want all clones of the controller to only hold a single copy. + #[allow(clippy::redundant_allocation)] + controller_counter: Arc>, +} + +impl HydrasManager { + pub async fn new(config: &HydraTomlConfig, network: &Network) -> Result { + // Let’s add some ε of 1% just to be sure about rounding etc. + let minimal_commit: f64 = 1.01 + * (config.lovelace_per_request + * config.requests_per_microtransaction + * config.microtransactions_per_fanout + + MIN_LOVELACE_PER_TRANSACTION) as f64 + / 1_000_000.0; + if config.commit_ada < minimal_commit { + Err(anyhow!( + "hydras-manager: Please make sure that configured commit_ada ≥ lovelace_per_request * requests_per_microtransaction * microtransactions_per_fanout + {}.", + MIN_LOVELACE_PER_TRANSACTION as f64 / 1_000_000.0 + ))? + } + + let microtransaction_lovelace: u64 = + config.lovelace_per_request * config.requests_per_microtransaction; + if microtransaction_lovelace < MIN_LOVELACE_PER_TRANSACTION { + Err(anyhow!( + "hydras-manager: Please make sure that each microtransaction will be larger than {MIN_LOVELACE_PER_TRANSACTION} lovelace. Currently it would be {microtransaction_lovelace}." + ))? + } + + Ok(Self { + config: HydraConfig::load(config.clone(), network).await?, + controller_counter: Arc::new(Arc::new(())), + }) + } + + pub async fn initialize_key_exchange( + &self, + req: KeyExchangeRequest, + ) -> Result { + if req.accepted_platform_h2h_port.is_some() { + Err(anyhow!( + "`accepted_platform_h2h_port` must not be set in `initialize_key_exchange`" + ))? + } + + let cur_count = Arc::strong_count(self.controller_counter.as_ref()).saturating_sub(1); + if cur_count as u64 >= self.config.toml.max_concurrent_hydra_nodes { + let err = anyhow!( + "Too many concurrent `hydra-node`s already running. You can increase the limit in config." + ); + warn!("{err}"); + Err(err)? + } + + let have_funds: f64 = self + .config + .lovelace_on_addr(&self.config.gateway_cardano_addr) + .await? as f64 + / 1_000_000.0; + let required_funds_ada: f64 = MIN_FUEL_LOVELACE as f64 / 1_000_000.0; + if have_funds < required_funds_ada { + let err = anyhow!( + "hydra-controller: {} ADA is too little for the Hydra L1 fees on the enterprise address associated with {:?}. Please provide at least {} ADA", + have_funds, + self.config.toml.cardano_signing_key, + required_funds_ada, + ); + error!("{err}"); + Err(err)? + } + info!( + "hydra-controller: funds on cardano_signing_key: {:?} ADA", + have_funds + ); + + use verifications::{find_free_tcp_port, read_json_file}; + + let config_dir = mk_config_dir(&self.config.network, &req.machine_id)?; + self.config.gen_hydra_keys(&config_dir).await?; + + Ok(KeyExchangeResponse { + machine_id: verifications::hashed_machine_id(), + gateway_cardano_vkey: self.config.gateway_cardano_vkey.clone(), + gateway_hydra_vkey: read_json_file(&config_dir.join("hydra.vk"))?, + hydra_scripts_tx_id: hydra_scripts_tx_id(&self.config.network).to_string(), + protocol_parameters: self.config.protocol_parameters.clone(), + contestation_period: CONTESTATION_PERIOD_SECONDS, + proposed_platform_h2h_port: find_free_tcp_port().await?, + gateway_h2h_port: find_free_tcp_port().await?, + kex_done: false, + commit_ada: self.config.toml.commit_ada, + lovelace_per_request: self.config.toml.lovelace_per_request, + requests_per_microtransaction: self.config.toml.requests_per_microtransaction, + microtransactions_per_fanout: self.config.toml.microtransactions_per_fanout, + }) + } + + /// You should first call [`Self::initialize_key_exchange`], and then this + /// function with the initial request/response pair. + pub async fn spawn_new( + &self, + initial: (KeyExchangeRequest, KeyExchangeResponse), + final_req: KeyExchangeRequest, + ) -> Result<(HydraController, KeyExchangeResponse)> { + if initial.0 + != (KeyExchangeRequest { + accepted_platform_h2h_port: None, + ..final_req.clone() + }) + { + Err(anyhow!( + "The 2nd `KeyExchangeRequest` must be the same as the 1st one." + ))? + } + + if final_req.accepted_platform_h2h_port != Some(initial.1.proposed_platform_h2h_port) { + Err(anyhow!( + "The Bridge must accept the same port that was proposed to it." + ))? + } + + // Clone first, to prevent the nastier race condition: + let maybe_new = Arc::clone(self.controller_counter.as_ref()); + let new_count = Arc::strong_count(self.controller_counter.as_ref()).saturating_sub(1); + if new_count as u64 > self.config.toml.max_concurrent_hydra_nodes { + Err(anyhow!( + "Too many concurrent `hydra-node`s already running. You can increase the limit in config." + ))? + } + + if !(matches!( + verifications::is_tcp_port_free(initial.1.gateway_h2h_port).await, + Ok(true) + ) && matches!( + verifications::is_tcp_port_free(initial.1.proposed_platform_h2h_port).await, + Ok(true) + )) { + Err(anyhow!( + "The exchanged ports are no longer free on the gateway, please perform another KEx." + ))? + } + + let final_resp = KeyExchangeResponse { + kex_done: true, + ..initial.1 + }; + + let ctl = HydraController::spawn( + self.config.clone(), + final_req.machine_id.clone(), + maybe_new, + final_req, + final_resp.clone(), + ) + .await?; + + Ok((ctl, final_resp)) + } +} + +#[derive(Debug, Deserialize, Clone)] +struct HydraConfig { + pub toml: HydraTomlConfig, + pub network: Network, + pub hydra_node_exe: String, + pub cardano_cli_exe: String, + pub gateway_cardano_vkey: serde_json::Value, + pub gateway_cardano_addr: String, + pub protocol_parameters: serde_json::Value, +} + +impl HydraConfig { + pub async fn load(toml: HydraTomlConfig, network: &Network) -> Result { + let hydra_node_exe = + crate::find_libexec::find_libexec("hydra-node", "HYDRA_NODE_PATH", &["--version"]) + .map_err(|e| anyhow!(e))?; + let cardano_cli_exe = + crate::find_libexec::find_libexec("cardano-cli", "CARDANO_CLI_PATH", &["version"]) + .map_err(|e| anyhow!(e))?; + let self_ = Self { + toml, + network: network.clone(), + hydra_node_exe, + cardano_cli_exe, + gateway_cardano_vkey: serde_json::Value::Null, + gateway_cardano_addr: String::new(), + protocol_parameters: serde_json::Value::Null, + }; + let gateway_cardano_addr = self_ + .derive_enterprise_address_from_skey(&self_.toml.cardano_signing_key) + .await?; + let gateway_cardano_vkey = self_ + .derive_vkey_from_skey(&self_.toml.cardano_signing_key) + .await?; + let protocol_parameters = self_.gen_protocol_parameters().await?; + let self_ = Self { + gateway_cardano_vkey, + gateway_cardano_addr, + protocol_parameters, + ..self_ + }; + Ok(self_) + } +} + +/// Runs a `hydra-node` and sets up an L2 network with the Bridge for microtransactions. +/// +/// You can safely clone it, and the clone will represent the same `hydra-node` etc. +#[derive(Clone)] +pub struct HydraController { + event_tx: mpsc::Sender, + credits_available: Arc, + head_open: Arc, + _controller_counter: Arc<()>, +} + +#[derive(Debug)] +pub enum CreditError { + HeadNotOpen, + InsufficientCredits, +} + +impl std::fmt::Display for CreditError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CreditError::HeadNotOpen => write!(f, "hydra head is not open"), + CreditError::InsufficientCredits => write!(f, "insufficient prepaid credits"), + } + } +} + +impl std::error::Error for CreditError {} + +// FIXME: send a Quit event on `drop()` of all controller instances + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +pub struct KeyExchangeRequest { + pub machine_id: String, + pub platform_cardano_vkey: serde_json::Value, + pub platform_hydra_vkey: serde_json::Value, + pub accepted_platform_h2h_port: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Clone)] +pub struct KeyExchangeResponse { + pub machine_id: String, + pub gateway_cardano_vkey: serde_json::Value, + pub gateway_hydra_vkey: serde_json::Value, + pub hydra_scripts_tx_id: String, + pub protocol_parameters: serde_json::Value, + pub contestation_period: Duration, + /// Unfortunately the ports have to be the same on both sides, so + /// since we’re tunneling through the WebSocket, and our hosts are + /// both 127.0.0.1, the Gateway has to propose the port on the + /// Bridge, too (as both sides open both ports). + pub proposed_platform_h2h_port: u16, + pub gateway_h2h_port: u16, + /// This being set to `true` means that the ceremony is successful, and the + /// Gateway is going to start its own `hydra-node`, and the Bridge should too. + pub kex_done: bool, + pub commit_ada: f64, + pub lovelace_per_request: u64, + pub requests_per_microtransaction: u64, + pub microtransactions_per_fanout: u64, +} + +impl HydraController { + async fn spawn( + config: HydraConfig, + customer_id: String, + controller_counter: Arc<()>, + kex_req: KeyExchangeRequest, + kex_resp: KeyExchangeResponse, + ) -> Result { + let credits_available = Arc::new(AtomicU64::new(0)); + let head_open = Arc::new(AtomicBool::new(false)); + let event_tx = State::spawn( + config, + customer_id.clone(), + kex_req, + kex_resp, + credits_available.clone(), + head_open.clone(), + ) + .await?; + Ok(Self { + event_tx, + credits_available, + head_open, + _controller_counter: controller_counter, + }) + } + + // FIXME: this is too primitive + pub fn is_alive(&self) -> bool { + !self.event_tx.is_closed() + } + + pub fn try_consume_credit(&self) -> Result<(), CreditError> { + if !self.head_open.load(Ordering::SeqCst) { + return Err(CreditError::HeadNotOpen); + } + + let mut current = self.credits_available.load(Ordering::SeqCst); + loop { + if current == 0 { + return Err(CreditError::InsufficientCredits); + } + + match self.credits_available.compare_exchange( + current, + current - 1, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => return Ok(()), + Err(next) => current = next, + } + } + } + + pub async fn terminate(&self) { + let _ = self.event_tx.send(Event::Terminate).await; + } +} + +enum Event { + Restart, + Terminate, + TryToInitHead, + TryToCommit, + WaitForOpen, + MonitorCredits, + TryToClose, + WaitForClosed { retries_before_reclose: u64 }, + DoFanout, + WaitForIdleAfterClose, +} + +fn mk_config_dir(network: &Network, customer_machine_id: &str) -> Result { + let config_dir = dirs::config_dir() + .ok_or(anyhow!("`dirs::config_dir()` returned `None`"))? + .join("blockfrost-gateway") + .join("hydra") + .join(network.as_str()) + .join(format!("customer-{customer_machine_id}")); + std::fs::create_dir_all(&config_dir)?; + Ok(config_dir) +} + +// FIXME: don’t construct all key and other paths manually, keep them in a single place +struct State { + config: HydraConfig, + customer_log_id: String, + config_dir: PathBuf, + event_tx: mpsc::Sender, + kex_req: KeyExchangeRequest, + kex_resp: KeyExchangeResponse, + api_port: u16, + metrics_port: u16, + hydra_peers_connected: bool, + hydra_head_open: bool, + credits_available: Arc, + head_open_flag: Arc, + credits_last_balance: u64, + received_microtransactions: u64, + is_closing: bool, + hydra_pid: Option, +} + +impl State { + const RESTART_DELAY: Duration = Duration::from_secs(5); + + async fn spawn( + config: HydraConfig, + customer_id: String, + kex_req: KeyExchangeRequest, + kex_resp: KeyExchangeResponse, + credits_available: Arc, + head_open_flag: Arc, + ) -> Result> { + let config_dir = mk_config_dir(&config.network, &customer_id)?; + let customer_log_id = format!("customer-{customer_id}"); + + let (event_tx, mut event_rx) = mpsc::channel::(32); + + let mut self_ = Self { + config, + customer_log_id, + config_dir, + event_tx: event_tx.clone(), + kex_req, + kex_resp, + api_port: 0, + metrics_port: 0, + hydra_peers_connected: false, + hydra_head_open: false, + credits_available, + head_open_flag, + credits_last_balance: 0, + received_microtransactions: 0, + is_closing: false, + hydra_pid: None, + }; + + self_.send(Event::Restart).await; + + tokio::spawn(async move { + while let Some(event) = event_rx.recv().await { + match self_.process_event(event).await { + Ok(()) => (), + Err(err) => { + error!( + "hydra-controller: {}: error: {}; will restart in {:?}…", + self_.customer_log_id, + err, + Self::RESTART_DELAY + ); + tokio::time::sleep(Self::RESTART_DELAY).await; + self_.send(Event::Restart).await; + }, + } + } + }); + + Ok(event_tx) + } + + async fn send(&self, event: Event) { + self.event_tx + .send(event) + .await + .expect("we never close the event receiver"); + } + + async fn send_delayed(&self, event: Event, delay: Duration) { + let event_tx = self.event_tx.clone(); + tokio::spawn(async move { + tokio::time::sleep(delay).await; + event_tx.send(event).await + }); + } + + async fn process_event(&mut self, event: Event) -> Result<()> { + match event { + Event::Restart => { + info!("hydra-controller: {}: starting…", self.customer_log_id); + self.hydra_head_open = false; + self.head_open_flag.store(false, Ordering::SeqCst); + self.credits_available.store(0, Ordering::SeqCst); + self.credits_last_balance = 0; + self.received_microtransactions = 0; + self.is_closing = false; + self.start_hydra_node().await?; + self.send_delayed(Event::TryToInitHead, Duration::from_secs(1)) + .await + }, + + Event::Terminate => { + if let Some(pid) = self.hydra_pid { + verifications::sigterm(pid)? + } + }, + + Event::TryToInitHead => { + let ready = verifications::prometheus_metric_at_least( + &format!("http://127.0.0.1:{}/metrics", self.metrics_port), + "hydra_head_peers_connected", + 1.0, + ) + .await; + + info!( + "hydra-controller: {}: waiting for hydras to connect: ready={:?}", + self.customer_log_id, ready + ); + + if matches!(ready, Ok(true)) { + self.hydra_peers_connected = true; + + verifications::send_one_websocket_msg( + &format!("ws://127.0.0.1:{}/", self.api_port), + serde_json::json!({"tag":"Init"}), + Duration::from_secs(2), + ) + .await?; + + self.send_delayed(Event::TryToCommit, Duration::from_secs(3)) + .await + } else { + self.send_delayed(Event::TryToInitHead, Duration::from_secs(1)) + .await + } + }, + + Event::TryToCommit => { + let status = verifications::fetch_head_tag(self.api_port).await; + + info!( + "hydra-controller: {}: waiting for the Initial head status: status={:?}", + self.customer_log_id, status + ); + + match status.as_deref() { + Err(_) => { + self.send_delayed(Event::TryToCommit, Duration::from_secs(3)) + .await + }, + Ok(status) => { + if status == "Initial" { + info!( + "hydra-controller: {}: submitting an empty Commit transaction to join the Hydra Head", + self.customer_log_id + ); + self.config + .empty_commit_to_hydra( + self.api_port, + &self.config.toml.cardano_signing_key, + ) + .await?; + self.send_delayed(Event::WaitForOpen, Duration::from_secs(3)) + .await + } else if status == "Open" { + self.send_delayed(Event::WaitForOpen, Duration::from_secs(3)) + .await + } else { + self.send_delayed(Event::TryToCommit, Duration::from_secs(3)) + .await + } + }, + } + }, + + Event::WaitForOpen => { + let status = verifications::fetch_head_tag(self.api_port).await?; + info!( + "hydra-controller: {}: waiting for the Open head status: status={:?}", + self.customer_log_id, status + ); + if status == "Open" { + self.hydra_head_open = true; + self.head_open_flag.store(true, Ordering::SeqCst); + self.credits_available.store(0, Ordering::SeqCst); + self.credits_last_balance = 0; + self.received_microtransactions = 0; + self.send_delayed(Event::MonitorCredits, CREDIT_POLL_INTERVAL) + .await; + } else { + self.send_delayed(Event::WaitForOpen, Duration::from_secs(3)) + .await + } + }, + + Event::MonitorCredits => { + if self.hydra_head_open && !self.is_closing { + match verifications::lovelace_in_snapshot_for_address( + self.api_port, + &self.config.gateway_cardano_addr, + ) + .await + { + Ok(current_balance) => { + if current_balance < self.credits_last_balance { + warn!( + "hydra-controller: {}: snapshot balance decreased ({} -> {}), resetting", + self.customer_log_id, + self.credits_last_balance, + current_balance + ); + self.credits_last_balance = current_balance; + } else { + let delta = current_balance - self.credits_last_balance; + if delta > 0 { + let microtransaction_lovelace = + self.config.toml.lovelace_per_request + * self.config.toml.requests_per_microtransaction; + if microtransaction_lovelace == 0 { + warn!( + "hydra-controller: {}: microtransaction value is zero; ignoring credits", + self.customer_log_id + ); + } else if delta >= microtransaction_lovelace { + let new_microtransactions = + delta / microtransaction_lovelace; + let new_credits = new_microtransactions + * self.config.toml.requests_per_microtransaction; + self.credits_available + .fetch_add(new_credits, Ordering::SeqCst); + self.received_microtransactions += new_microtransactions; + info!( + "hydra-controller: {}: received {} microtransaction(s), req. credits +{}", + self.customer_log_id, + new_microtransactions, + new_credits + ); + } else { + warn!( + "hydra-controller: {}: snapshot delta {} is below expected microtransaction size {}", + self.customer_log_id, delta, microtransaction_lovelace + ); + } + self.credits_last_balance = current_balance; + } + } + + if self.received_microtransactions + >= self.config.toml.microtransactions_per_fanout + && !self.is_closing + { + self.is_closing = true; + self.send_delayed(Event::TryToClose, Duration::from_secs(1)) + .await; + } + }, + Err(err) => warn!( + "hydra-controller: {}: failed to read snapshot/utxo: {err}", + self.customer_log_id + ), + } + self.send_delayed(Event::MonitorCredits, CREDIT_POLL_INTERVAL) + .await; + } + }, + + Event::TryToClose => { + self.hydra_head_open = false; + self.head_open_flag.store(false, Ordering::SeqCst); + verifications::send_one_websocket_msg( + &format!("ws://127.0.0.1:{}", self.api_port), + serde_json::json!({"tag":"Close"}), + Duration::from_secs(2), + ) + .await?; + self.send_delayed( + Event::WaitForClosed { + retries_before_reclose: 10, + }, + Duration::from_secs(3), + ) + .await; + }, + + Event::WaitForClosed { + retries_before_reclose, + } => { + let status = verifications::fetch_head_tag(self.api_port).await?; + info!( + "hydra-controller: {}: waiting for the Closed head status: status={:?}", + self.customer_log_id, status + ); + if status == "Closed" { + let invalidity_period = (2 + 1) * CONTESTATION_PERIOD_SECONDS; + info!( + "hydra-controller: {}: will wait through the invalidity period ({:?}) before requesting `Fanout`", + self.customer_log_id, invalidity_period, + ); + self.send_delayed(Event::DoFanout, invalidity_period).await + } else { + self.send_delayed( + if retries_before_reclose <= 1 { + Event::TryToClose + } else { + Event::WaitForClosed { + retries_before_reclose: retries_before_reclose - 1, + } + }, + Duration::from_secs(3), + ) + .await + } + }, + + Event::DoFanout => { + info!( + "hydra-controller: {}: requesting `Fanout`", + self.customer_log_id, + ); + verifications::send_one_websocket_msg( + &format!("ws://127.0.0.1:{}", self.api_port), + serde_json::json!({"tag":"Fanout"}), + Duration::from_secs(2), + ) + .await?; + self.send_delayed(Event::WaitForIdleAfterClose, Duration::from_secs(3)) + .await; + }, + + Event::WaitForIdleAfterClose => { + let status = verifications::fetch_head_tag(self.api_port).await?; + info!( + "hydra-controller: {}: waiting for the Idle head status (after Fanout): status={:?}", + self.customer_log_id, status + ); + if status == "Idle" { + info!( + "hydra-controller: {}: re-initializing the Hydra Head for another L2 session", + self.customer_log_id, + ); + + self.is_closing = false; + self.received_microtransactions = 0; + self.credits_last_balance = 0; + self.send_delayed(Event::TryToInitHead, Duration::from_secs(3)) + .await; + } else { + self.send_delayed(Event::WaitForIdleAfterClose, Duration::from_secs(3)) + .await; + } + }, + } + Ok(()) + } + + async fn start_hydra_node(&mut self) -> Result<()> { + use std::process::Stdio; + use tokio::io::{AsyncBufReadExt, BufReader}; + + self.api_port = verifications::find_free_tcp_port().await?; + self.metrics_port = verifications::find_free_tcp_port().await?; + + // FIXME: somehow do shutdown once we’re killed + // cf. + // cf. + // TODO: Write a ticket in `hydra-node`. + + let protocol_parameters_path = self.config_dir.join("protocol-parameters.json"); + verifications::write_json_if_changed( + &protocol_parameters_path, + &self.kex_resp.protocol_parameters, + )?; + + let platform_hydra_vkey_path = self.config_dir.join("platform-hydra.vk"); + verifications::write_json_if_changed( + &platform_hydra_vkey_path, + &self.kex_req.platform_hydra_vkey, + )?; + + let platform_cardano_vkey_path = self.config_dir.join("platform-payment.vk"); + verifications::write_json_if_changed( + &platform_cardano_vkey_path, + &self.kex_req.platform_cardano_vkey, + )?; + + let mut child = tokio::process::Command::new(&self.config.hydra_node_exe) + .arg("--node-id") + .arg("gateway-node") + .arg("--persistence-dir") + .arg(self.config_dir.join("persistence")) + .arg("--cardano-signing-key") + .arg(&self.config.toml.cardano_signing_key) + .arg("--hydra-signing-key") + .arg(self.config_dir.join("hydra.sk")) + .arg("--hydra-scripts-tx-id") + .arg(&self.kex_resp.hydra_scripts_tx_id) + .arg("--ledger-protocol-parameters") + .arg(&protocol_parameters_path) + .arg("--contestation-period") + .arg(format!("{}s", self.kex_resp.contestation_period.as_secs())) + .args(if self.config.network == Network::Mainnet { + vec!["-mainnet".to_string()] + } else { + vec![ + "--testnet-magic".to_string(), + format!("{}", self.config.network.network_magic()), + ] + }) + .arg("--node-socket") + .arg(&self.config.toml.node_socket_path) + .arg("--api-port") + .arg(format!("{}", self.api_port)) + .arg("--api-host") + .arg("127.0.0.1") + .arg("--listen") + .arg(format!("127.0.0.1:{}", self.kex_resp.gateway_h2h_port)) + .arg("--peer") + .arg(format!( + "127.0.0.1:{}", + self.kex_resp.proposed_platform_h2h_port + )) + .arg("--monitoring-port") + .arg(format!("{}", self.metrics_port)) + .arg("--hydra-verification-key") + .arg(platform_hydra_vkey_path) + .arg("--cardano-verification-key") + .arg(platform_cardano_vkey_path) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + self.hydra_pid = child.id(); + + let stdout = child.stdout.take().expect("child stdout"); + let stderr = child.stderr.take().expect("child stderr"); + + tokio::spawn(async move { + let mut lines = BufReader::new(stdout).lines(); + while let Ok(Some(line)) = lines.next_line().await { + debug!("hydra-node: {}", line); + } + debug!("hydra-node: stdout closed"); + }); + + tokio::spawn(async move { + let mut lines = BufReader::new(stderr).lines(); + while let Ok(Some(line)) = lines.next_line().await { + info!("hydra-node: {}", line); + } + info!("hydra-node: stderr closed"); + }); + + let event_tx = self.event_tx.clone(); + tokio::spawn(async move { + match child.wait().await { + Ok(status) => { + warn!("hydra-node: exited: {}", status); + tokio::time::sleep(Self::RESTART_DELAY).await; + event_tx + .send(Event::Restart) + .await + .expect("we never close the event receiver"); + }, + Err(e) => { + error!("hydra-node: failed to wait: {e}"); + }, + } + }); + + Ok(()) + } +} + +pub fn hydra_scripts_tx_id(network: &Network) -> &'static str { + // FIXME: also define them in a `build.rs` script without Nix – consult + // `flake.lock` to get the exact Hydra version. + use Network::*; + match network { + Mainnet => env!("HYDRA_SCRIPTS_TX_ID_MAINNET"), + Preprod => env!("HYDRA_SCRIPTS_TX_ID_PREPROD"), + Preview => env!("HYDRA_SCRIPTS_TX_ID_PREVIEW"), + } +} diff --git a/crates/gateway/src/hydra_server_bridge/tunnel2.rs b/crates/gateway/src/hydra_server_bridge/tunnel2.rs new file mode 100644 index 00000000..9b783d15 --- /dev/null +++ b/crates/gateway/src/hydra_server_bridge/tunnel2.rs @@ -0,0 +1,336 @@ +use anyhow::Result; +use base64::{Engine as _, engine::general_purpose::STANDARD as B64}; +use bytes::{Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{TcpListener, TcpStream}, + sync::{Mutex, mpsc}, +}; +use tokio_util::sync::CancellationToken; + +/// JSON-serializable tunnel messages (base64 for buffers). +/// +/// Plug into a WebSocket protocol as e.g. `WsProto::HydraTunnel(TunnelMsg)`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", rename_all = "snake_case")] +pub enum TunnelMsg { + /// Ask peer to open its *configured* local service port for stream `id`. + Open { id: u64 }, + + /// Bytes for connection `id` encoded as base64 string. + Data { id: u64, b64: String }, + + /// Close stream `id`. `code` is small+stable, `msg` is optional. + Close { + id: u64, + code: u8, + msg: Option, + }, +} + +pub mod close_code { + pub const CLEAN: u8 = 0; + pub const IO: u8 = 1; + pub const CANCELLED: u8 = 2; + pub const PROTOCOL: u8 = 3; +} + +/// Tunnel config. +#[derive(Debug, Clone)] +pub struct TunnelConfig { + /// Host used for local TCP connects when peer sends Open. + pub local_connect_host: IpAddr, + + /// The *only* local port that the peer is allowed to connect to (via Open). + pub expose_port: u16, + + /// If true, set bit 63 in all locally-allocated IDs. + /// Set opposite values on the two peers to avoid ID collisions. + pub id_prefix_bit: bool, + + /// Outbound TunnelMsg buffer (what the WebSocket event loop drains). + pub outbound_capacity: usize, + + /// Per-connection command channel capacity. + pub per_conn_cmd_capacity: usize, + + /// Max bytes per TCP read. + pub read_chunk: usize, +} + +impl Default for TunnelConfig { + fn default() -> Self { + Self { + local_connect_host: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + expose_port: 0, + id_prefix_bit: false, + outbound_capacity: 256, + per_conn_cmd_capacity: 128, + read_chunk: 8 * 1024, + } + } +} + +enum ConnCmd { + Write(Bytes), + /// Close local TCP. If notify_peer=false, don’t emit a Close back. + Close { + notify_peer: bool, + }, +} + +struct Inner { + cfg: TunnelConfig, + cancel: CancellationToken, + out_tx: mpsc::Sender, + conns: Mutex>>, + next_id: AtomicU64, +} + +/// Cloneable handle kept in a WebSocket connection state/event-loop. +#[derive(Clone)] +pub struct Tunnel { + inner: Arc, +} + +impl Tunnel { + /// Create tunnel + outbound receiver (to drain in the WebSocket event loop). + pub fn new(cfg: TunnelConfig, cancel: CancellationToken) -> (Self, mpsc::Receiver) { + let (out_tx, out_rx) = mpsc::channel(cfg.outbound_capacity); + + let prefix = if cfg.id_prefix_bit { 1u64 << 63 } else { 0 }; + let next_id = AtomicU64::new(1 | prefix); + + let inner = Arc::new(Inner { + cfg, + cancel, + out_tx, + conns: Mutex::new(HashMap::new()), + next_id, + }); + + (Self { inner }, out_rx) + } + + pub fn cancel(&self) { + self.inner.cancel.cancel(); + } + + /// Call this from the WebSocket event loop when it receives a tunnel message. + pub async fn on_msg(&self, msg: TunnelMsg) -> Result<()> { + match msg { + TunnelMsg::Open { id } => { + // Always connect to the single configured local port. + let addr = SocketAddr::new( + self.inner.cfg.local_connect_host, + self.inner.cfg.expose_port, + ); + match TcpStream::connect(addr).await { + Ok(sock) => self.attach_stream_with_id(id, sock).await?, + Err(e) => { + let _ = self + .inner + .out_tx + .send(TunnelMsg::Close { + id, + code: close_code::IO, + msg: Some(e.to_string()), + }) + .await; + }, + } + }, + + TunnelMsg::Data { id, b64 } => { + let bytes = match B64.decode(b64.as_bytes()) { + Ok(v) => Bytes::from(v), + Err(_) => { + let _ = self + .inner + .out_tx + .send(TunnelMsg::Close { + id, + code: close_code::PROTOCOL, + msg: Some("invalid base64".into()), + }) + .await; + return Ok(()); + }, + }; + + let tx = { self.inner.conns.lock().await.get(&id).cloned() }; + if let Some(tx) = tx { + let _ = tx.send(ConnCmd::Write(bytes)).await; + } + }, + + TunnelMsg::Close { id, .. } => { + let tx = { self.inner.conns.lock().await.remove(&id) }; + if let Some(tx) = tx { + let _ = tx.send(ConnCmd::Close { notify_peer: false }).await; + } + }, + } + + Ok(()) + } + + /// Spawn a TCP listener on *this* side. Each accepted local TCP connection becomes + /// a tunneled stream to the peer’s configured `expose_port`. + pub async fn spawn_listener(&self, listen_port: u16) -> Result<()> { + let listener = TcpListener::bind((self.inner.cfg.local_connect_host, listen_port)).await?; + let this = self.clone(); + + tokio::spawn(async move { + loop { + let (mut sock, _) = tokio::select! { + _ = this.inner.cancel.cancelled() => break, + res = listener.accept() => match res { Ok(v) => v, Err(_) => break } + }; + + let id = this.alloc_local_id(); + + // Ask peer to open its configured port. + if this + .inner + .out_tx + .send(TunnelMsg::Open { id }) + .await + .is_err() + { + let _ = sock.shutdown().await; + break; + } + + // Attach local accepted socket. + if this.attach_stream_with_id(id, sock).await.is_err() { + let _ = this + .inner + .out_tx + .send(TunnelMsg::Close { + id, + code: close_code::PROTOCOL, + msg: Some("attach failed".into()), + }) + .await; + } + } + }); + + Ok(()) + } + + /// If you already accepted a TcpStream elsewhere and want to tunnel it: + /// sends Open (no port) and returns the allocated id. + /// + /// *Warning*: it’s a little controversial, but trivial to add. Probably + /// shouldn’t be used. + pub async fn attach_stream(&self, sock: TcpStream) -> Result { + let id = self.alloc_local_id(); + self.inner.out_tx.send(TunnelMsg::Open { id }).await?; + self.attach_stream_with_id(id, sock).await?; + Ok(id) + } + + fn alloc_local_id(&self) -> u64 { + let prefix = if self.inner.cfg.id_prefix_bit { + 1u64 << 63 + } else { + 0 + }; + let base = self.inner.next_id.fetch_add(1, Ordering::Relaxed) & !(1u64 << 63); + base | prefix + } + + async fn attach_stream_with_id(&self, id: u64, sock: TcpStream) -> Result<()> { + let (cmd_tx, mut cmd_rx) = mpsc::channel::(self.inner.cfg.per_conn_cmd_capacity); + + // Insert route, replacing duplicates (and closing old). + { + let mut m = self.inner.conns.lock().await; + if let Some(old) = m.insert(id, cmd_tx) { + let _ = old.send(ConnCmd::Close { notify_peer: false }).await; + } + } + + let out_tx = self.inner.out_tx.clone(); + let cancel = self.inner.cancel.clone(); + let cfg = self.inner.cfg.clone(); + let inner = Arc::clone(&self.inner); + + tokio::spawn(async move { + let mut sock = sock; + let mut buf = BytesMut::with_capacity(cfg.read_chunk); + let mut notify_peer_close = true; + + let close_reason: Option<(u8, Option)> = loop { + tokio::select! { + _ = cancel.cancelled() => { + break Some((close_code::CANCELLED, Some("cancelled".into()))); + } + + // TCP -> WS (encode bytes as base64) + rv = async { + buf.clear(); + buf.reserve(cfg.read_chunk); + sock.read_buf(&mut buf).await + } => { + match rv { + Ok(0) => break Some((close_code::CLEAN, None)), // EOF + Ok(_) => { + let chunk = buf.split().freeze(); + let b64 = B64.encode(&chunk); + if out_tx.send(TunnelMsg::Data { id, b64 }).await.is_err() { + notify_peer_close = false; + break None; + } + } + Err(e) => break Some((close_code::IO, Some(e.to_string()))), + } + } + + // WS -> TCP (decode already done in on_msg) + cmd = cmd_rx.recv() => { + match cmd { + Some(ConnCmd::Write(chunk)) => { + if let Err(e) = sock.write_all(&chunk).await { + break Some((close_code::IO, Some(e.to_string()))); + } + } + Some(ConnCmd::Close { notify_peer }) => { + notify_peer_close = notify_peer; + break Some((close_code::CLEAN, None)); + } + None => { + notify_peer_close = false; + break None; + } + } + } + } + }; + + // Remove route + let _ = inner.conns.lock().await.remove(&id); + + if notify_peer_close { + let (code, msg) = + close_reason.unwrap_or((close_code::CANCELLED, Some("closed".into()))); + let _ = out_tx.send(TunnelMsg::Close { id, code, msg }).await; + } + + let _ = sock.shutdown().await; + }); + + Ok(()) + } +} diff --git a/crates/gateway/src/hydra_server_bridge/verifications.rs b/crates/gateway/src/hydra_server_bridge/verifications.rs new file mode 100644 index 00000000..2888024b --- /dev/null +++ b/crates/gateway/src/hydra_server_bridge/verifications.rs @@ -0,0 +1,581 @@ +use anyhow::{Result, anyhow}; +use serde_json::Value; +use std::path::Path; +use tracing::info; + +use crate::types::Network; + +/// FIXME: don’t use `cardano-cli`. +/// +/// FIXME: proper errors, not `anyhow!` +impl super::HydraConfig { + /// Generates Hydra keys if they don’t exist. + pub(super) async fn gen_hydra_keys(&self, target_dir: &Path) -> Result<()> { + std::fs::create_dir_all(target_dir)?; + + let key_path = target_dir.join("hydra.sk"); + + if !key_path.exists() { + info!("hydra-controller: generating hydra keys"); + + let status = tokio::process::Command::new(&self.hydra_node_exe) + .arg("gen-hydra-key") + .arg("--output-file") + .arg(target_dir.join("hydra")) + .status() + .await?; + + if !status.success() { + Err(anyhow!("gen-hydra-key failed with status: {status}"))?; + } + } else { + info!("hydra-controller: hydra keys already exist"); + } + + Ok(()) + } + + fn cardano_cli_env(&self) -> Vec<(&str, String)> { + vec![ + ( + "CARDANO_NODE_SOCKET_PATH", + self.toml.node_socket_path.to_string_lossy().to_string(), + ), + ( + "CARDANO_NODE_NETWORK_ID", + match &self.network { + Network::Mainnet => self.network.as_str().to_string(), + other => other.network_magic().to_string(), + }, + ), + ] + } + + /// Generates Hydra `protocol-parameters.json` if they don’t exist. These + /// are L1 parameters with zeroed transaction fees. + pub(super) async fn gen_protocol_parameters(&self) -> Result { + use serde_json::Value; + + let output = tokio::process::Command::new(&self.cardano_cli_exe) + .envs(self.cardano_cli_env()) + .args(["query", "protocol-parameters"]) + .output() + .await?; + + if !output.status.success() { + Err(anyhow!( + "cardano-cli failed with status: {} (stdout: {}) (stderr: {})", + output.status, + String::from_utf8_lossy(&output.stdout).trim(), + String::from_utf8_lossy(&output.stderr).trim() + ))?; + } + + let mut params: Value = serde_json::from_slice(&output.stdout)?; + + // .txFeeFixed := 0 + // .txFeePerByte := 0 + if let Some(obj) = params.as_object_mut() { + obj.insert("txFeeFixed".to_string(), 0.into()); + obj.insert("txFeePerByte".to_string(), 0.into()); + + // .executionUnitPrices.priceMemory := 0 + // .executionUnitPrices.priceSteps := 0 + if let Some(exec_prices) = obj + .get_mut("executionUnitPrices") + .and_then(Value::as_object_mut) + { + exec_prices.insert("priceMemory".to_string(), 0.into()); + exec_prices.insert("priceSteps".to_string(), 0.into()); + } + } + + Ok(params) + } + + /// Check how much lovelace is available on an address. + pub(super) async fn lovelace_on_addr(&self, address: &str) -> Result { + let utxo_json = self.query_utxo_json(address).await?; + Self::sum_lovelace_from_utxo_json(&utxo_json) + } + + pub(super) async fn derive_vkey_from_skey( + &self, + skey_path: &Path, + ) -> Result { + let vkey_output = tokio::process::Command::new(&self.cardano_cli_exe) + .envs(self.cardano_cli_env()) + .args(["key", "verification-key", "--signing-key-file"]) + .arg(skey_path) + .args(["--verification-key-file", "/dev/stdout"]) + .output() + .await?; + Ok(serde_json::from_slice(&vkey_output.stdout)?) + } + + pub(super) async fn derive_enterprise_address_from_skey( + &self, + skey_path: &Path, + ) -> Result { + let vkey_output = tokio::process::Command::new(&self.cardano_cli_exe) + .envs(self.cardano_cli_env()) + .args(["key", "verification-key", "--signing-key-file"]) + .arg(skey_path) + .args(["--verification-key-file", "/dev/stdout"]) + .output() + .await?; + + if !vkey_output.status.success() { + return Err(anyhow!( + "cardano-cli key verification-key failed: {}", + String::from_utf8_lossy(&vkey_output.stderr) + )); + } + + let mut child = tokio::process::Command::new(&self.cardano_cli_exe) + .envs(self.cardano_cli_env()) + .args([ + "address", + "build", + "--payment-verification-key-file", + "/dev/stdin", + ]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn()?; + + { + let stdin = child.stdin.as_mut().ok_or(anyhow!( + "failed to open stdin for cardano-cli address build" + ))?; + use tokio::io::AsyncWriteExt; + stdin.write_all(&vkey_output.stdout).await?; + } + + let addr_output = child.wait_with_output().await?; + if !addr_output.status.success() { + Err(anyhow!( + "cardano-cli address build failed: {}", + String::from_utf8_lossy(&addr_output.stderr) + ))?; + } + + let address = String::from_utf8(addr_output.stdout)?.trim().to_string(); + if address.is_empty() { + return Err(anyhow!("derived address is empty")); + } + + Ok(address) + } + + pub(super) async fn query_utxo_json(&self, address: &str) -> Result { + let utxo_json = self + .cardano_cli_capture( + &[ + "query", + "utxo", + "--address", + address, + "--out-file", + "/dev/stdout", + ], + None, + ) + .await? + .0; + Ok(utxo_json) + } + + fn sum_lovelace_from_utxo_json(json: &serde_json::Value) -> Result { + let obj = json + .as_object() + .ok_or(anyhow!("UTxO JSON root is not an object"))?; + + let mut total: u64 = 0; + + for (_txin, utxo) in obj { + if let Some(value_obj) = utxo.get("value").and_then(|v| v.as_object()) { + if let Some(lovelace_val) = value_obj.get("lovelace") { + total = total + .checked_add(Self::as_u64(lovelace_val)?) + .ok_or(anyhow!("cannot add"))?; + continue; + } + } + + if let Some(amount_arr) = utxo.get("amount").and_then(|v| v.as_array()) { + if let Some(lovelace_val) = amount_arr.first() { + total = total + .checked_add(Self::as_u64(lovelace_val)?) + .ok_or(anyhow!("cannot add"))?; + } + } + } + + Ok(total) + } + + /// Convert a JSON value into u64, allowing either number or string. + fn as_u64(v: &Value) -> Result { + if let Some(n) = v.as_u64() { + return Ok(n); + } + if let Some(s) = v.as_str() { + return Ok(s.parse()?); + } + Err(anyhow!("lovelace value is neither u64 nor string")) + } + + async fn cardano_cli_capture( + &self, + args: &[&str], + stdin_bytes: Option<&[u8]>, + ) -> Result<(serde_json::Value, Vec)> { + use tokio::io::AsyncWriteExt; + + let mut cmd = tokio::process::Command::new(&self.cardano_cli_exe); + cmd.envs(self.cardano_cli_env()); + cmd.args(args) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + + if stdin_bytes.is_some() { + cmd.stdin(std::process::Stdio::piped()); + } else { + cmd.stdin(std::process::Stdio::null()); + } + + let mut child = cmd.spawn()?; + + if let Some(bytes) = stdin_bytes { + let mut stdin = child + .stdin + .take() + .ok_or_else(|| anyhow!("failed to open stdin pipe"))?; + stdin.write_all(bytes).await?; + stdin.shutdown().await?; + } + + let out = child.wait_with_output().await?; + + if !out.status.success() { + return Err(anyhow!( + "cardano-cli failed (exit={}):\nstdout: {}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stdout).trim(), + String::from_utf8_lossy(&out.stderr).trim(), + )); + } + + let (json, rest) = parse_first_json_and_rest(&out.stdout)?; + Ok((json, rest)) + } + + pub(super) async fn empty_commit_to_hydra( + &self, + hydra_api_port: u16, + signing_skey: &Path, + ) -> Result<()> { + use anyhow::Context; + use reqwest::header; + + let url = format!("http://127.0.0.1:{hydra_api_port}/commit"); + let client = reqwest::Client::new(); + let resp = client + .post(url) + .header(header::CONTENT_TYPE, "application/json") + .body("{}") + .send() + .await + .context("failed to POST /commit to hydra-node")?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.bytes().await.unwrap_or_default(); + return Err(anyhow!( + "hydra /commit failed with {}: {}", + status, + String::from_utf8_lossy(&body) + )); + } + + let commit_tx_bytes = resp + .bytes() + .await + .context("failed to read hydra /commit response body")? + .to_vec(); + + let _: serde_json::Value = serde_json::from_slice(&commit_tx_bytes) + .context("hydra /commit response was not valid JSON")?; + + let signed_tx = self + .cardano_cli_capture( + &[ + "latest", + "transaction", + "sign", + "--tx-file", + "/dev/stdin", + "--signing-key-file", + signing_skey + .to_str() + .ok_or_else(|| anyhow!("commit_funds_skey is not valid UTF-8"))?, + "--out-file", + "/dev/stdout", + ], + Some(&commit_tx_bytes), + ) + .await? + .0; + + let _ = self + .cardano_cli_capture( + &["latest", "transaction", "submit", "--tx-file", "/dev/stdin"], + Some(&serde_json::to_vec(&signed_tx)?), + ) + .await?; + Ok(()) + } +} + +pub async fn lovelace_in_snapshot_for_address(hydra_api_port: u16, address: &str) -> Result { + use anyhow::Context; + + let snapshot_url = format!("http://127.0.0.1:{hydra_api_port}/snapshot/utxo"); + let utxo: Value = reqwest::Client::new() + .get(&snapshot_url) + .send() + .await? + .error_for_status()? + .json() + .await + .context("snapshot/utxo: failed to decode JSON")?; + + let utxo_obj = utxo + .as_object() + .context("snapshot/utxo: expected top-level JSON object")?; + + let mut filtered: serde_json::Map = serde_json::Map::new(); + for (k, v) in utxo_obj.iter() { + if v.get("address").and_then(Value::as_str) == Some(address) { + filtered.insert(k.clone(), v.clone()); + } + } + + let filtered_json = Value::Object(filtered); + super::HydraConfig::sum_lovelace_from_utxo_json(&filtered_json) +} + +/// Reads a JSON file from disk. +pub fn read_json_file(path: &Path) -> Result { + let contents = std::fs::read_to_string(path)?; + let json: serde_json::Value = serde_json::from_str(&contents)?; + Ok(json) +} + +/// Writes `json` to `path` (pretty-printed) **only if** the JSON content differs +/// from what is already on disk. Returns `true` if the file was written. +pub fn write_json_if_changed(path: &Path, json: &serde_json::Value) -> Result { + use std::fs::File; + use std::io::Write; + + if path.exists() { + if let Ok(existing_str) = std::fs::read_to_string(path) { + if let Ok(existing_json) = serde_json::from_str::(&existing_str) { + if existing_json == *json { + return Ok(false); + } + } + } + } + + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent)?; + } + } + + let mut file = File::create(path)?; + serde_json::to_writer_pretty(&mut file, json)?; + file.write_all(b"\n")?; + + Ok(true) +} + +/// Finds a free port by bind to port 0, to let the OS pick a free port. +pub async fn find_free_tcp_port() -> std::io::Result { + let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await?; + let port = listener.local_addr()?.port(); + drop(listener); + Ok(port) +} + +/// Returns `Ok(true)` if `port` can be bound on 127.0.0.1 (so it's free), +/// `Ok(false)` if it's already in use, and `Err(_)` for other IO errors. +pub async fn is_tcp_port_free(port: u16) -> std::io::Result { + match tokio::net::TcpListener::bind(("127.0.0.1", port)).await { + Ok(listener) => { + drop(listener); + Ok(true) + }, + Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => Ok(false), + Err(e) => Err(e), + } +} + +/// Checks if a Prometheus `metric` at `url` is greater or equal to `threshold`. +pub async fn prometheus_metric_at_least(url: &str, metric: &str, threshold: f64) -> Result { + let client = reqwest::Client::new(); + let body = client + .get(url) + .send() + .await? + .error_for_status()? + .text() + .await?; + + let mut found_any = false; + let mut max_value: Option = None; + + for line in body.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + // {labels} [timestamp] + let mut parts = line.split_whitespace(); + let name_and_labels = match parts.next() { + Some(x) => x, + None => continue, + }; + + let name = name_and_labels.split('{').next().unwrap_or(name_and_labels); + if name != metric { + continue; + } + + let value_str = match parts.next() { + Some(v) => v, + None => continue, + }; + + let value: f64 = value_str.parse()?; + found_any = true; + max_value = Some(max_value.map_or(value, |m| m.max(value))); + } + + if !found_any { + return Err(anyhow!("metric {metric} not found in /metrics output")); + } + + Ok(max_value.unwrap_or(f64::NEG_INFINITY) >= threshold) +} + +/// Sends a single WebSocket message, and waits a bit before closing the +/// connection cleanly. Particularly useful for Hydra. +pub async fn send_one_websocket_msg( + url: &str, + payload: serde_json::Value, + wait_before_close: std::time::Duration, +) -> Result<()> { + use futures_util::{SinkExt, StreamExt}; + use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; + + let (ws_stream, _resp) = connect_async(url).await?; + let (mut write, mut read) = ws_stream.split(); + + write.send(Message::Text(payload.to_string())).await?; + + tokio::time::sleep(wait_before_close).await; + + write.send(Message::Close(None)).await?; + + // Drain until we observe the close handshake (or the peer drops): + while let Some(msg) = read.next().await { + match msg? { + Message::Close(_) => break, + Message::Text(msg) => { + tracing::info!("hydra-controller: got WebSocket message: {}", msg) + }, + msg => tracing::info!("hydra-controller: got WebSocket message: {:?}", msg), + } + } + + Ok(()) +} + +pub async fn fetch_head_tag(hydra_api_port: u16) -> Result { + let url = format!("http://127.0.0.1:{hydra_api_port}/head"); + + let v: serde_json::Value = reqwest::get(url).await?.error_for_status()?.json().await?; + + v.get("tag") + .ok_or(anyhow!("missing tag")) + .and_then(|a| a.as_str().ok_or(anyhow!("tag is not a string"))) + .map(|a| a.to_string()) +} + +/// Parse the first JSON value from e.g. stdout, and return the remainder. +fn parse_first_json_and_rest(stdout: &[u8]) -> Result<(serde_json::Value, Vec)> { + let mut start = stdout + .iter() + .position(|b| !b.is_ascii_whitespace()) + .unwrap_or(0); + + if !matches!(stdout.get(start), Some(b'{') | Some(b'[')) { + if let Some(i) = stdout.iter().position(|&b| b == b'{' || b == b'[') { + start = i; + } + } + + let mut it = serde_json::Deserializer::from_slice(&stdout[start..]).into_iter::(); + + let first = it + .next() + .ok_or_else(|| anyhow!("no JSON value found in stdout"))? + .map_err(|e| anyhow!("failed to parse first JSON value from stdout: {e}"))?; + + let consumed = it.byte_offset(); + let rest = stdout[start + consumed..].to_vec(); + + Ok((first, rest)) +} + +#[cfg(unix)] +pub fn sigterm(pid: u32) -> Result<()> { + use nix::sys::signal::{Signal, kill}; + use nix::unistd::Pid; + Ok(kill(Pid::from_raw(pid as i32), Signal::SIGTERM)?) +} + +#[cfg(windows)] +pub fn sigterm(_pid: u32) -> Result<()> { + unreachable!() +} + +/// We use it for `localhost` tests, to detect if the Gateway and Bridge are +/// running on the same host. Then we cannot set up a +/// `[crate::hydra_server_bridge::tunnel2::Tunnel]`, because the ports are already taken. +pub fn hashed_machine_id() -> String { + const MACHINE_ID_NAMESPACE: &str = "blockfrost.machine-id.v1"; + + let mut hasher = blake3::Hasher::new(); + hasher.update(MACHINE_ID_NAMESPACE.as_bytes()); + hasher.update(b":"); + + match machine_uid::get() { + Ok(id) => { + hasher.update(id.as_bytes()); + }, + Err(e) => { + tracing::warn!(error = ?e, "machine_uid::get() failed; falling back to random bytes"); + let mut fallback = [0u8; 32]; + getrandom::fill(&mut fallback) + .expect("getrandom::fill shouldn’t fail in normal circumstances"); + hasher.update(&fallback); + }, + } + + hasher.finalize().to_hex().to_string() +} diff --git a/crates/gateway/src/lib.rs b/crates/gateway/src/lib.rs index d93455b3..bc28168f 100644 --- a/crates/gateway/src/lib.rs +++ b/crates/gateway/src/lib.rs @@ -4,9 +4,11 @@ pub mod config; pub mod db; pub mod errors; pub mod find_libexec; +pub mod hydra_server_bridge; pub mod hydra_server_platform; pub mod load_balancer; pub mod models; pub mod payload; pub mod schema; +pub mod sdk_bridge_ws; pub mod types; diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs index c59dd4a3..714fc2ab 100644 --- a/crates/gateway/src/main.rs +++ b/crates/gateway/src/main.rs @@ -4,7 +4,10 @@ use axum::{ Extension, Router, routing::{get, post}, }; -use blockfrost_gateway::{api, blockfrost, config, db, hydra_server_platform, load_balancer}; +use blockfrost_gateway::{ + api, blockfrost, config, db, hydra_server_bridge, hydra_server_platform, load_balancer, + sdk_bridge_ws, +}; use clap::Parser; use colored::Colorize; use config::{Args, Config}; @@ -43,9 +46,17 @@ async fn main() -> Result<()> { } else { None }; + let hydras_bridge_manager = if let Some(hydra_bridge_config) = &config.hydra_bridge { + Some( + hydra_server_bridge::HydrasManager::new(hydra_bridge_config, &config.server.network) + .await?, + ) + } else { + None + }; let load_balancer = load_balancer::LoadBalancerState::new(hydras_manager).await; - let app = Router::new() + let base_router = Router::new() .route("/", get(root::route)) .route("/register", post(register::route)) .route("/ws", get(load_balancer::api::websocket_route)) @@ -67,6 +78,12 @@ async fn main() -> Result<()> { .layer(Extension(pool)) .layer(Extension(blockfrost_api)); + let sdk_state = sdk_bridge_ws::SdkBridgeState::new(base_router.clone(), hydras_bridge_manager); + + let app = base_router + .route("/sdk/ws", get(sdk_bridge_ws::websocket_route)) + .layer(Extension(sdk_state)); + let listener = tokio::net::TcpListener::bind(&config.server.address) .await .expect("Failed to bind to address"); diff --git a/crates/gateway/src/sdk_bridge_ws.rs b/crates/gateway/src/sdk_bridge_ws.rs new file mode 100644 index 00000000..6c511e16 --- /dev/null +++ b/crates/gateway/src/sdk_bridge_ws.rs @@ -0,0 +1,600 @@ +use crate::hydra_server_bridge; +use axum::Extension; +use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; +use axum::response::IntoResponse; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; +use uuid::Uuid; + +const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); +const MAX_BODY_BYTES: usize = 1024 * 1024; +const WS_PING_TIMEOUT: Duration = Duration::from_secs(15); + +#[derive(Clone)] +pub struct SdkBridgeState { + router: axum::Router, + hydras: Option, +} + +impl SdkBridgeState { + pub fn new(router: axum::Router, hydras: Option) -> Self { + Self { router, hydras } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct RequestId(Uuid); + +#[derive(Serialize, Deserialize, Debug)] +pub struct JsonRequest { + pub id: RequestId, + pub method: JsonRequestMethod, + pub path: String, + pub header: Vec, + pub body_base64: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct JsonResponse { + pub id: RequestId, + pub code: u16, + pub header: Vec, + pub body_base64: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct JsonHeader { + pub name: String, + pub value: String, +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Serialize, Deserialize, Debug)] +pub enum JsonRequestMethod { + GET, + POST, +} + +/// The WebSocket messages that we send. +#[derive(Serialize, Deserialize, Debug)] +pub enum GatewayMessage { + Response(JsonResponse), + HydraKExResponse(hydra_server_bridge::KeyExchangeResponse), + HydraTunnel(hydra_server_bridge::tunnel2::TunnelMsg), + Ping(u64), + Pong(u64), + Error { code: u64, msg: String }, +} + +/// The WebSocket messages that we receive. +#[derive(Serialize, Deserialize, Debug)] +pub enum BridgeMessage { + Request(JsonRequest), + HydraKExRequest(hydra_server_bridge::KeyExchangeRequest), + HydraTunnel(hydra_server_bridge::tunnel2::TunnelMsg), + Ping(u64), + Pong(u64), +} + +pub async fn websocket_route( + ws: WebSocketUpgrade, + Extension(state): Extension, +) -> impl IntoResponse { + ws.on_upgrade(|socket| event_loop::run(state, socket)) +} + +/// The WebSocket event loop, passing messages between HTTP<->WebSocket, keeping +/// track of persistent connection liveness, etc. +pub mod event_loop { + use super::*; + use axum::http::StatusCode; + + /// For clarity, let’s have a single connection 'event_loop per WebSocket + /// connection, with the following events: + enum BridgeEvent { + NewBridgeMessage(BridgeMessage), + NewResponse(JsonResponse), + PingTick, + Finish(String), + } + + /// Top-level logic of a single WebSocket connection with a bridge. + pub async fn run(state: SdkBridgeState, socket: WebSocket) { + let (event_tx, mut event_rx) = mpsc::channel::(64); + let (socket_tx, response_task, arbitrary_msg_task) = + wire_socket(event_tx.clone(), socket).await; + + let schedule_ping_tick = { + let event_tx = event_tx.clone(); + move || { + let tx = event_tx.clone(); + tokio::spawn(async move { + tokio::time::sleep(WS_PING_TIMEOUT).await; + let _ignored_failure: Result<_, _> = tx.send(BridgeEvent::PingTick).await; + }) + } + }; + + // Schedule the first `PingTick` immediately, otherwise we won’t start + // checking for ping timeout: + let _ignored_failure: Result<_, _> = event_tx.send(BridgeEvent::PingTick).await; + + // Event loop state (let’s keep it minimal, please): + let mut last_ping_sent_at: Option = None; + let mut last_ping_id: u64 = 0; + let mut disconnection_reason = None; + + let mut initial_hydra_kex: Option<( + hydra_server_bridge::KeyExchangeRequest, + hydra_server_bridge::KeyExchangeResponse, + )> = None; + let mut hydra_controller: Option = None; + + let tunnel_cancellation = CancellationToken::new(); + let mut tunnel_controller: Option = None; + + // The actual connection event loop: + 'event_loop: while let Some(msg) = event_rx.recv().await { + match msg { + BridgeEvent::Finish(reason) => { + disconnection_reason = Some(reason); + break 'event_loop; + }, + + BridgeEvent::NewBridgeMessage(BridgeMessage::HydraTunnel(tun_msg)) => { + if let Some(tunnel_ctl) = &tunnel_controller { + match tunnel_ctl.on_msg(tun_msg).await { + Ok(()) => (), + Err(err) => error!( + "hydra-tunnel: got an error when passing message through WebSocket: {err}; ignoring" + ), + } + } + }, + + BridgeEvent::NewBridgeMessage(BridgeMessage::HydraKExRequest(req)) => { + let already_exists = match &hydra_controller { + None => false, + Some(ctl) => ctl.is_alive(), + }; + + let reply = match ( + already_exists, + &state.hydras, + &req.accepted_platform_h2h_port, + initial_hydra_kex.take(), + ) { + (true, _, _, _) => GatewayMessage::Error { + code: 538, + msg: "Hydra controller already exists on this connection".to_string(), + }, + (false, None, _, _) => GatewayMessage::Error { + code: 536, + msg: "Hydra micropayments not supported".to_string(), + }, + (false, Some(hydras), Some(_accepted_port), Some(initial_kex)) => { + let bridge_machine_id = req.machine_id.clone(); + match hydras.spawn_new(initial_kex, req).await { + Ok((ctl, resp)) => { + hydra_controller = Some(ctl); + + // Only start the TCP-over-WebSocket tunnels if we’re running + // on different machines: + if bridge_machine_id != resp.machine_id { + let (tunnel_ctl, mut tunnel_rx) = + hydra_server_bridge::tunnel2::Tunnel::new( + hydra_server_bridge::tunnel2::TunnelConfig { + expose_port: resp.gateway_h2h_port, + id_prefix_bit: true, + ..(hydra_server_bridge::tunnel2::TunnelConfig::default()) + }, + tunnel_cancellation.clone(), + ); + + tunnel_ctl.spawn_listener(resp.proposed_platform_h2h_port).await.expect("FIXME: this really shouldn’t fail, unless we hit the TOCTOU race condition…"); + + let socket_tx_ = socket_tx.clone(); + tokio::spawn(async move { + while let Some(tun_msg) = tunnel_rx.recv().await { + if send_json_msg( + &socket_tx_, + &GatewayMessage::HydraTunnel(tun_msg), + ) + .await + .is_err() + { + break; + } + } + }); + + tunnel_controller = Some(tunnel_ctl); + } + + GatewayMessage::HydraKExResponse(resp) + }, + Err(err) => GatewayMessage::Error { + code: 537, + msg: format!("Hydra micropayments setup error: {err}"), + }, + } + }, + (false, Some(hydras), _, _) => { + match hydras.initialize_key_exchange(req.clone()).await { + Ok(resp) => { + initial_hydra_kex = Some((req, resp.clone())); + GatewayMessage::HydraKExResponse(resp) + }, + Err(err) => GatewayMessage::Error { + code: 537, + msg: format!("Hydra micropayments setup error: {err}"), + }, + } + }, + }; + + if send_json_msg(&socket_tx, &reply).await.is_err() { + break 'event_loop; + } + }, + + BridgeEvent::NewBridgeMessage(BridgeMessage::Request(request)) => { + let request_id = request.id.clone(); + + let response = match &hydra_controller { + None => Some(error_response( + request_id, + StatusCode::SERVICE_UNAVAILABLE, + "Hydra head is not ready".to_string(), + )), + Some(ctl) => { + if !ctl.is_alive() { + Some(error_response( + request_id, + StatusCode::SERVICE_UNAVAILABLE, + "Hydra controller is not running".to_string(), + )) + } else { + match ctl.try_consume_credit() { + Ok(()) => None, + Err(hydra_server_bridge::CreditError::HeadNotOpen) => { + Some(error_response( + request_id, + StatusCode::SERVICE_UNAVAILABLE, + "Hydra head is not open".to_string(), + )) + }, + Err(hydra_server_bridge::CreditError::InsufficientCredits) => { + Some(error_response( + request_id, + StatusCode::PAYMENT_REQUIRED, + "Prepaid credits exhausted".to_string(), + )) + }, + } + } + }, + }; + + if let Some(response) = response { + if send_json_msg(&socket_tx, &GatewayMessage::Response(response)) + .await + .is_err() + { + break 'event_loop; + } + } else { + let router = state.router.clone(); + let event_tx = event_tx.clone(); + tokio::spawn(async move { + let response = handle_one(router, request).await; + let _ignored_failure: Result<_, _> = + event_tx.send(BridgeEvent::NewResponse(response)).await; + }); + } + }, + + BridgeEvent::NewResponse(response) => { + if send_json_msg(&socket_tx, &GatewayMessage::Response(response)) + .await + .is_err() + { + break 'event_loop; + } + }, + + BridgeEvent::NewBridgeMessage(BridgeMessage::Ping(ping_id)) => { + if send_json_msg(&socket_tx, &GatewayMessage::Pong(ping_id)) + .await + .is_err() + { + break 'event_loop; + } + }, + + BridgeEvent::NewBridgeMessage(BridgeMessage::Pong(pong_id)) => { + if pong_id == last_ping_id { + last_ping_sent_at = None; + } + }, + + BridgeEvent::PingTick => { + if let Some(_sent_at) = last_ping_sent_at { + // Ping timeout: + disconnection_reason = Some("ping timeout".to_string()); + break 'event_loop; + } else { + // The periodic `PingTick` loop: + schedule_ping_tick(); + // Time to send a new ping: + last_ping_id += 1; + last_ping_sent_at = Some(std::time::Instant::now()); + if send_json_msg(&socket_tx, &GatewayMessage::Ping(last_ping_id)) + .await + .is_err() + { + break 'event_loop; + } + } + }, + } + } + + if let Some(ctl) = hydra_controller { + ctl.terminate().await + } + + tunnel_cancellation.cancel(); + + let disconnection_reason_ = disconnection_reason + .clone() + .unwrap_or("reason unknown".to_string()); + + warn!( + "sdk-bridge-ws: connection event loop finished: {}", + disconnection_reason_ + ); + + let _ignored_failure: Result<_, _> = socket_tx + .send(Message::Close(disconnection_reason.map(|why| { + axum::extract::ws::CloseFrame { + code: tungstenite::protocol::frame::coding::CloseCode::Normal.into(), + reason: why.into(), + } + }))) + .await; + + // Wait for all children to finish: + let children = [response_task, arbitrary_msg_task]; + children.iter().for_each(|t| t.abort()); + futures::future::join_all(children).await; + + info!("sdk-bridge-ws: connection closed"); + } + + async fn wire_socket( + event_tx: mpsc::Sender, + socket: WebSocket, + ) -> (mpsc::Sender, JoinHandle<()>, JoinHandle<()>) { + use futures_util::{SinkExt, StreamExt}; + + let (msg_tx, mut msg_rx) = mpsc::channel::(64); + let (mut sock_tx, mut sock_rx) = socket.split(); + let response_task = tokio::spawn(async move { + while let Some(Ok(msg)) = sock_rx.next().await { + match msg { + Message::Text(text) => { + match serde_json::from_str::(&text) { + Ok(msg) => { + if event_tx + .send(BridgeEvent::NewBridgeMessage(msg)) + .await + .is_err() + { + break; + } + }, + Err(err) => warn!( + "sdk-bridge-ws: received unparsable text message: {:?}: {:?}", + text, err, + ), + }; + }, + Message::Binary(bin) => { + warn!( + "sdk-bridge-ws: received unexpected binary message: {:?}", + hex::encode(bin), + ); + }, + Message::Close(frame) => { + warn!( + "sdk-bridge-ws: bridge disconnected (CloseFrame: {:?})", + frame, + ); + let _ignored_failure: Result<_, _> = event_tx + .send(BridgeEvent::Finish("bridge disconnected".to_string())) + .await; + break; + }, + Message::Ping(_) | Message::Pong(_) => {}, + } + } + }); + let arbitrary_msg_task = tokio::spawn(async move { + while let Some(msg) = msg_rx.recv().await { + match sock_tx.send(msg).await { + Ok(()) => (), + Err(err) => { + error!("sdk-bridge-ws: error when sending a message: {:?}", err); + break; + }, + } + } + }); + (msg_tx, response_task, arbitrary_msg_task) + } + + async fn send_json_msg(socket_tx: &mpsc::Sender, msg: &J) -> Result<(), String> + where + J: ?Sized + serde::ser::Serialize, + { + match serde_json::to_string(msg) { + Ok(msg) => match socket_tx.send(Message::Text(msg)).await { + Ok(_) => Ok(()), + Err(err) => { + error!("sdk-bridge-ws: error when sending a message: {:?}", err); + Err("broken connection with the bridge".to_string()) + }, + }, + Err(err) => { + let err = format!( + "error when serializing request to JSON (this will never happen): {err:?}" + ); + error!("sdk-bridge-ws: {}", err); + Err(err) + }, + } + } + + fn error_response(request_id: RequestId, code: StatusCode, why: String) -> JsonResponse { + JsonResponse { + id: request_id, + code: code.as_u16(), + header: vec![], + body_base64: why, + } + } +} + +/// Passes one [`JsonRequest`] through our underlying original HTTP server. +/// Everything happens internally, in memory, without opening new TCP +/// connections etc. – very light. +async fn handle_one(http_router: axum::Router, request: JsonRequest) -> JsonResponse { + use axum::body::Body; + use hyper::StatusCode; + use hyper::{Request, Response}; + use tower::ServiceExt; + + let request_id = request.id.clone(); + let request_id_ = request.id.clone(); + + let rv: Result = async { + let req: Request = json_to_request(request)?; + + let response: Response = + tokio::time::timeout(REQUEST_TIMEOUT, http_router.into_service().oneshot(req)) + .await + .map_err(|_elapsed| { + ( + StatusCode::GATEWAY_TIMEOUT, + format!("Timed out while waiting {REQUEST_TIMEOUT:?} for a response"), + ) + })? + .unwrap(); + + response_to_json(response, request_id).await + } + .await; + + match rv { + Ok(ok) => ok, + Err((code, err)) => { + error!("sdk-bridge-ws: returning {}, because: {}", code, err); + JsonResponse { + id: request_id_, + code: code.into(), + header: vec![], + body_base64: err, + } + }, + } +} + +fn json_to_request( + json: JsonRequest, +) -> Result, (hyper::StatusCode, String)> { + use axum::body::Body; + use axum::http::{Method, StatusCode}; + use hyper::Request; + + let body: Body = { + if json.body_base64.is_empty() { + Body::empty() + } else { + use base64::{Engine as _, engine::general_purpose}; + let body_bytes: Vec = + general_purpose::STANDARD + .decode(json.body_base64) + .map_err(|err| { + ( + StatusCode::BAD_REQUEST, + format!("Invalid base64 encoding of body_base64: {err}"), + ) + })?; + Body::from(body_bytes) + } + }; + + let method = match json.method { + JsonRequestMethod::GET => Method::GET, + JsonRequestMethod::POST => Method::POST, + }; + + let mut rv = Request::builder().method(method).uri(json.path); + + for h in json.header { + rv = rv.header(h.name, h.value); + } + + rv.body(body).map_err(|err| { + ( + StatusCode::BAD_REQUEST, + format!("Error when constructing a request from JSON request: {err}"), + ) + }) +} + +async fn response_to_json( + response: hyper::Response, + request_id: RequestId, +) -> Result { + use hyper::StatusCode; + + let header: Vec = response + .headers() + .iter() + .flat_map(|(name, value)| { + value.to_str().ok().map(|value| JsonHeader { + name: name.to_string(), + value: value.to_string(), + }) + }) + .collect(); + + let code: u16 = response.status().into(); + + let body_base64: String = { + let body = response.into_body(); + let body_bytes = axum::body::to_bytes(body, MAX_BODY_BYTES) + .await + .map_err(|err| { + ( + StatusCode::BAD_GATEWAY, + format!("Cannot read body of the response: {err}"), + ) + })?; + use base64::{Engine as _, engine::general_purpose}; + general_purpose::STANDARD.encode(body_bytes) + }; + + Ok(JsonResponse { + id: request_id, + code, + header, + body_base64, + }) +} diff --git a/crates/sdk_bridge/Cargo.toml b/crates/sdk_bridge/Cargo.toml new file mode 100644 index 00000000..46d77640 --- /dev/null +++ b/crates/sdk_bridge/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "blockfrost-sdk-bridge" +version.workspace = true +license.workspace = true +edition.workspace = true +publish = false + +[dependencies] +anyhow.workspace = true +axum.workspace = true +tokio.workspace = true +tokio-util = "0.7" +futures.workspace = true +futures-util.workspace = true +tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] } +tungstenite = { version = "0.28.0", features = ["native-tls"] } +tracing.workspace = true +tracing-subscriber = { version = "0.3.22", features = ["env-filter", "fmt"] } +serde.workspace = true +serde_json.workspace = true +clap.workspace = true +uuid.workspace = true +base64 = "0.22.1" +hyper = "1.8.1" +bytes = "1" +machine-uid = "0.5" +blake3 = "1" +getrandom = "0.3" +reqwest.workspace = true +dirs.workspace = true +url.workspace = true +hex.workspace = true + +[lints] +workspace = true + +[target.'cfg(unix)'.dependencies] +nix = { version = "0.30", default-features = false, features = ["signal"] } diff --git a/crates/sdk_bridge/src/config.rs b/crates/sdk_bridge/src/config.rs new file mode 100644 index 00000000..31358053 --- /dev/null +++ b/crates/sdk_bridge/src/config.rs @@ -0,0 +1,91 @@ +use crate::types::Network; +use anyhow::{Result, anyhow}; +use clap::Parser; +use std::net::SocketAddr; +use std::path::PathBuf; +use url::Url; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + #[arg( + long, + default_value = "wss://icebreakers1.platform.blockfrost.io/sdk/ws" + )] + pub gateway_ws_url: String, + + #[arg(long, default_value = "127.0.0.1:3001")] + pub listen_address: String, + + #[arg(long, value_enum)] + pub network: Network, + + #[arg(long, value_name = "FILE")] + pub node_socket_path: PathBuf, + + #[arg(long, value_name = "FILE")] + pub cardano_signing_key: PathBuf, +} + +#[derive(Clone, Debug)] +pub struct BridgeConfig { + pub gateway_ws_url: String, + pub listen_address: SocketAddr, + pub network: Network, + pub node_socket_path: PathBuf, + pub cardano_signing_key: PathBuf, +} + +impl BridgeConfig { + pub fn from_args(args: Args) -> Result { + let listen_address = args + .listen_address + .parse::() + .map_err(|err| anyhow!("Invalid listen address: {err}"))?; + let gateway_ws_url = normalize_gateway_ws_url(&args.gateway_ws_url)?; + + Ok(Self { + gateway_ws_url, + listen_address, + network: args.network, + node_socket_path: args.node_socket_path, + cardano_signing_key: args.cardano_signing_key, + }) + } +} + +fn normalize_gateway_ws_url(raw: &str) -> Result { + let mut url = Url::parse(raw).map_err(|err| anyhow!("Invalid gateway URL: {err}"))?; + + match url.scheme() { + "http" => { + url.set_scheme("ws") + .map_err(|_| anyhow!("invalid URL scheme"))?; + }, + "https" => { + url.set_scheme("wss") + .map_err(|_| anyhow!("invalid URL scheme"))?; + }, + "ws" | "wss" => {}, + other => { + return Err(anyhow!("Unsupported URL scheme: {other}")); + }, + } + + let path = url.path().to_string(); + if path.is_empty() || path == "/" { + url.set_path("/sdk/ws"); + } else if path.ends_with("/sdk/ws/") { + let trimmed = path.trim_end_matches('/'); + url.set_path(trimmed); + } else if !path.ends_with("/sdk/ws") { + let new_path = if path.ends_with('/') { + format!("{path}sdk/ws") + } else { + format!("{path}/sdk/ws") + }; + url.set_path(&new_path); + } + + Ok(url.to_string()) +} diff --git a/crates/sdk_bridge/src/find_libexec.rs b/crates/sdk_bridge/src/find_libexec.rs new file mode 100644 index 00000000..5e7eb9a0 --- /dev/null +++ b/crates/sdk_bridge/src/find_libexec.rs @@ -0,0 +1,94 @@ +use std::{ + env, + path::{Path, PathBuf}, + process::Command, +}; + +use tracing::debug; + +/// Searches for a “libexec” executable in multiple expected directories. +/// +/// These are executables we use sort of like libraries, without linking them +/// into our executable. E.g. `hydra-node`, `testgen-hs`. +/// +/// # Arguments +/// +/// * `exe_name` - The name of the executable (without `.exe` on Windows). +/// +/// * `env_name` - Allow overriding the path to the executable with this +/// environment variable name. +/// +/// * `test_args` - Arguments to a test invocation of the found command (to +/// check that it really is executable). Maybe in the future we should have a +/// lambda to actually look at the output of this invocation? +/// +pub fn find_libexec(exe_name: &str, env_name: &str, test_args: &[&str]) -> Result { + let env_var_dir: Option = env::var(env_name) + .ok() + .and_then(|a| PathBuf::from(a).parent().map(|a| a.to_path_buf())); + + // This is the most important one for relocatable directories (that keep the initial + // structure) on Windows, Linux, macOS: + let current_exe_dir: Option = + std::fs::canonicalize(env::current_exe().map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())? + .parent() + .map(|a| a.to_path_buf().join(exe_name)); + + // Similar, but accounts for the `nix-bundle-exe` structure on Linux: + let current_package_dir: Option = current_exe_dir + .clone() + .and_then(|a| a.parent().map(PathBuf::from)) + .and_then(|a| a.parent().map(PathBuf::from)); + + let cargo_target_dir: Option = env::var("CARGO_MANIFEST_DIR") + .ok() + .map(|root| PathBuf::from(root).join("target/testgen-hs/extracted/testgen-hs")); + + let docker_path: Option = Some(PathBuf::from(format!("/app/{exe_name}"))); + + let system_path: Vec = env::var("PATH") + .map(|p| env::split_paths(&p).collect()) + .unwrap_or_default(); + + let search_path: Vec = vec![ + env_var_dir, + current_exe_dir, + current_package_dir, + cargo_target_dir, + docker_path, + ] + .into_iter() + .flatten() + .chain(system_path) + .collect(); + + let extension = if cfg!(target_os = "windows") { + ".exe" + } else { + "" + }; + + let exe_name_ext = format!("{exe_name}{extension}"); + + debug!("{} search directories = {:?}", exe_name_ext, search_path); + + // Checks if the path is runnable. Adjust for platform specifics if needed. + // TODO: check that the --version matches what we expect. + let is_our_executable = + |path: &Path| -> bool { Command::new(path).args(test_args).output().is_ok() }; + + // Look in each candidate directory to find a matching file + for candidate in &search_path { + let path = candidate.join(&exe_name_ext); + + if path.is_file() && is_our_executable(path.as_path()) { + return Ok(path.to_string_lossy().to_string()); + } + } + + Err(format!( + "No valid `{}` binary found in {:?}.", + exe_name_ext, &search_path + )) +} diff --git a/crates/sdk_bridge/src/http_proxy.rs b/crates/sdk_bridge/src/http_proxy.rs new file mode 100644 index 00000000..20b86d0a --- /dev/null +++ b/crates/sdk_bridge/src/http_proxy.rs @@ -0,0 +1,178 @@ +use crate::protocol::{JsonHeader, JsonRequest, JsonRequestMethod, JsonResponse, RequestId}; +use crate::ws_client::{BridgeError, BridgeHandle}; +use axum::body::Body; +use axum::http::{Method, Request, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::{Extension, Router}; +use std::net::SocketAddr; +use tracing::{error, warn}; + +const MAX_BODY_BYTES: usize = 1024 * 1024; + +#[derive(Clone)] +struct ProxyState { + bridge: BridgeHandle, +} + +pub async fn serve(addr: SocketAddr, bridge: BridgeHandle) -> anyhow::Result<()> { + let app = Router::new() + .fallback(proxy_route) + .layer(Extension(ProxyState { bridge })); + + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app).await?; + Ok(()) +} + +async fn proxy_route( + Extension(state): Extension, + req: Request, +) -> impl IntoResponse { + let json_req = match request_to_json(req).await { + Ok(req) => req, + Err((code, reason)) => { + error!("sdk-bridge: request conversion error: {}: {}", code, reason); + return (code, reason).into_response(); + }, + }; + + match state.bridge.hydra().try_reserve_credit() { + Ok(()) => (), + Err(crate::hydra_client::CreditError::HeadNotOpen) => { + return (StatusCode::SERVICE_UNAVAILABLE, "Hydra head is not open").into_response(); + }, + Err(crate::hydra_client::CreditError::InsufficientCredits) => { + return (StatusCode::PAYMENT_REQUIRED, "Prepaid credits exhausted").into_response(); + }, + } + + let response = match state.bridge.forward_request(json_req).await { + Ok(resp) => resp, + Err(err) => return bridge_error_to_response(err), + }; + + if (200..400).contains(&response.code) { + state.bridge.hydra().account_one_request().await; + } + + match json_to_response(response).await { + Ok(resp) => resp.into_response(), + Err((code, reason)) => { + warn!( + "sdk-bridge: response conversion error: {}: {}", + code, reason + ); + (code, reason).into_response() + }, + } +} + +fn bridge_error_to_response(err: BridgeError) -> Response { + match err { + BridgeError::ConnectionClosed => ( + StatusCode::SERVICE_UNAVAILABLE, + "Gateway WebSocket is not available", + ) + .into_response(), + BridgeError::Timeout => ( + StatusCode::GATEWAY_TIMEOUT, + "Gateway WebSocket request timed out", + ) + .into_response(), + BridgeError::ResponseDropped => ( + StatusCode::BAD_GATEWAY, + "Gateway WebSocket dropped the response", + ) + .into_response(), + } +} + +async fn request_to_json(request: Request) -> Result { + let method = match request.method() { + &Method::GET => JsonRequestMethod::GET, + &Method::POST => JsonRequestMethod::POST, + other => { + return Err(( + StatusCode::BAD_REQUEST, + format!("unhandled request method: {other}"), + )); + }, + }; + + let header: Vec = request + .headers() + .iter() + .flat_map(|(name, value)| { + value.to_str().ok().map(|value| JsonHeader { + name: name.to_string(), + value: value.to_string(), + }) + }) + .collect(); + + let path = request + .uri() + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or_else(|| request.uri().path()) + .to_string(); + + let body = request.into_body(); + let body_bytes = axum::body::to_bytes(body, MAX_BODY_BYTES) + .await + .map_err(|err| { + ( + StatusCode::BAD_REQUEST, + format!("failed to read body bytes: {err}"), + ) + })?; + + use base64::{Engine as _, engine::general_purpose}; + let body_base64 = general_purpose::STANDARD.encode(body_bytes); + + Ok(JsonRequest { + id: RequestId(uuid::Uuid::new_v4()), + path, + method, + body_base64, + header, + }) +} + +async fn json_to_response(json: JsonResponse) -> Result { + let body: Body = { + if json.body_base64.is_empty() { + Body::empty() + } else { + use base64::{Engine as _, engine::general_purpose}; + let body_bytes: Vec = + general_purpose::STANDARD + .decode(json.body_base64) + .map_err(|err| { + ( + StatusCode::BAD_GATEWAY, + format!("Invalid base64 encoding of response body_base64: {err}"), + ) + })?; + Body::from(body_bytes) + } + }; + + let mut rv = Response::builder().status(StatusCode::from_u16(json.code).map_err(|err| { + ( + StatusCode::BAD_GATEWAY, + format!("Invalid response status code {}: {}", json.code, err), + ) + })?); + + for h in json.header { + rv = rv.header(h.name, h.value); + } + + rv.body(body).map_err(|err| { + ( + StatusCode::BAD_GATEWAY, + format!("Error when constructing a response: {err}"), + ) + }) +} diff --git a/crates/sdk_bridge/src/hydra_client/mod.rs b/crates/sdk_bridge/src/hydra_client/mod.rs new file mode 100644 index 00000000..44690056 --- /dev/null +++ b/crates/sdk_bridge/src/hydra_client/mod.rs @@ -0,0 +1,858 @@ +use crate::types::Network; +use anyhow::{Result, anyhow}; +use std::path::PathBuf; +use std::sync::{ + Arc, + atomic::{AtomicBool, AtomicU64, Ordering}, +}; +use std::time::Duration; +use tokio::sync::mpsc; +use tracing::{debug, error, info, warn}; + +pub mod tunnel2; +pub mod verifications; + +const MIN_FUEL_LOVELACE: u64 = 15_000_000; +const CREDIT_POLL_INTERVAL: Duration = Duration::from_secs(1); + +#[derive(Clone, Debug)] +pub struct HydraConfig { + pub cardano_signing_key: PathBuf, + pub node_socket_path: PathBuf, + pub network: Network, +} + +/// Runs a `hydra-node` and sets up an L2 network with the Gateway for microtransactions. +/// +/// You can safely clone it, and the clone will represent the same `hydra-node` etc. +#[derive(Clone)] +pub struct HydraController { + event_tx: mpsc::Sender, + credits_available: Arc, + head_open: Arc, +} + +#[derive(Debug)] +pub enum CreditError { + HeadNotOpen, + InsufficientCredits, +} + +impl std::fmt::Display for CreditError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CreditError::HeadNotOpen => write!(f, "hydra head is not open"), + CreditError::InsufficientCredits => write!(f, "insufficient prepaid credits"), + } + } +} + +impl std::error::Error for CreditError {} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +pub struct KeyExchangeRequest { + pub machine_id: String, + pub platform_cardano_vkey: serde_json::Value, + pub platform_hydra_vkey: serde_json::Value, + pub accepted_platform_h2h_port: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Clone)] +pub struct KeyExchangeResponse { + pub machine_id: String, + pub gateway_cardano_vkey: serde_json::Value, + pub gateway_hydra_vkey: serde_json::Value, + pub hydra_scripts_tx_id: String, + pub protocol_parameters: serde_json::Value, + pub contestation_period: std::time::Duration, + /// Unfortunately the ports have to be the same on both sides, so + /// since we’re tunneling through the WebSocket, and our hosts are + /// both 127.0.0.1, the Gateway has to propose the port on the + /// Bridge, too (as both sides open both ports). + pub proposed_platform_h2h_port: u16, + pub gateway_h2h_port: u16, + /// This being set to `true` means that the ceremony is successful, and the + /// Gateway is going to start its own `hydra-node`, and the Bridge should too. + pub kex_done: bool, + pub commit_ada: f64, + pub lovelace_per_request: u64, + pub requests_per_microtransaction: u64, + pub microtransactions_per_fanout: u64, +} + +pub struct TerminateRequest; + +impl HydraController { + #[allow(clippy::too_many_arguments)] + pub async fn spawn( + config: HydraConfig, + kex_requests: mpsc::Sender, + kex_responses: mpsc::Receiver, + terminate_reqs: mpsc::Receiver, + ) -> Result { + let credits_available = Arc::new(AtomicU64::new(0)); + let head_open = Arc::new(AtomicBool::new(false)); + let event_tx = State::spawn( + config, + kex_requests, + kex_responses, + terminate_reqs, + credits_available.clone(), + head_open.clone(), + ) + .await?; + Ok(Self { + event_tx, + credits_available, + head_open, + }) + } + + pub fn try_reserve_credit(&self) -> Result<(), CreditError> { + if !self.head_open.load(Ordering::SeqCst) { + return Err(CreditError::HeadNotOpen); + } + + let mut current = self.credits_available.load(Ordering::SeqCst); + loop { + if current == 0 { + return Err(CreditError::InsufficientCredits); + } + + match self.credits_available.compare_exchange( + current, + current - 1, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => return Ok(()), + Err(next) => current = next, + } + } + } + + pub async fn account_one_request(&self) { + self.event_tx + .send(Event::AccountOneRequest) + .await + .unwrap_or_else(|_| { + error!("hydra-controller: failed to account one request: event channel closed") + }) + } +} + +#[derive(Clone, Debug)] +struct PaymentParams { + commit_ada: f64, + lovelace_per_request: u64, + requests_per_microtransaction: u64, + microtransactions_per_fanout: u64, +} + +enum Event { + Restart, + Terminate, + KeyExchangeResponse(KeyExchangeResponse), + TryToInitHead, + FundCommitAddr, + TryToCommit, + WaitForOpen, + MonitorStates, + AccountOneRequest, + MonitorCredits, +} + +// FIXME: don’t construct all key and other paths manually, keep them in a single place +struct State { + config: HydraConfig, + hydra_node_exe: String, + cardano_cli_exe: String, + config_dir: PathBuf, + platform_cardano_vkey: serde_json::Value, + gateway_payment_addr: String, + payment_params: Option, + event_tx: mpsc::Sender, + kex_requests: mpsc::Sender, + api_port: u16, + metrics_port: u16, + last_hydra_head_state: String, + hydra_pid: Option, + hydra_head_open: bool, + credits_available: Arc, + head_open_flag: Arc, + credits_last_balance: u64, + accounted_requests: u64, + sent_microtransactions: u64, + commit_wallet_skey: PathBuf, + commit_wallet_addr: String, + prepay_sent: bool, +} + +impl State { + const RESTART_DELAY: Duration = Duration::from_secs(5); + + #[allow(clippy::too_many_arguments)] + async fn spawn( + config: HydraConfig, + kex_requests: mpsc::Sender, + kex_responses: mpsc::Receiver, + terminate_reqs: mpsc::Receiver, + credits_available: Arc, + head_open_flag: Arc, + ) -> Result> { + let hydra_node_exe = + crate::find_libexec::find_libexec("hydra-node", "HYDRA_NODE_PATH", &["--version"]) + .map_err(|e| anyhow!(e))?; + let cardano_cli_exe = + crate::find_libexec::find_libexec("cardano-cli", "CARDANO_CLI_PATH", &["version"]) + .map_err(|e| anyhow!(e))?; + + let config_dir = dirs::config_dir() + .expect("Could not determine config directory") + .join("blockfrost-sdk-bridge") + .join("hydra") + .join(config.network.as_str()) + .join("_default"); + + let (event_tx, mut event_rx) = mpsc::channel::(32); + + let mut self_ = Self { + config, + hydra_node_exe, + cardano_cli_exe, + config_dir, + platform_cardano_vkey: serde_json::Value::Null, + gateway_payment_addr: String::new(), + payment_params: None, + event_tx: event_tx.clone(), + kex_requests, + api_port: 0, + metrics_port: 0, + last_hydra_head_state: String::new(), + hydra_pid: None, + hydra_head_open: false, + credits_available, + head_open_flag, + credits_last_balance: 0, + accounted_requests: 0, + sent_microtransactions: 0, + commit_wallet_skey: PathBuf::new(), + commit_wallet_addr: String::new(), + prepay_sent: false, + }; + + let platform_cardano_vkey = self_ + .derive_vkey_from_skey(&self_.config.cardano_signing_key) + .await?; + self_.platform_cardano_vkey = platform_cardano_vkey; + + self_.send(Event::Restart).await; + + let event_tx_ = event_tx.clone(); + tokio::spawn(async move { + let mut kex_responses = kex_responses; + while let Some(resp) = kex_responses.recv().await { + event_tx_ + .send(Event::KeyExchangeResponse(resp)) + .await + .expect("we never close the event receiver"); + } + }); + + let event_tx_ = event_tx.clone(); + tokio::spawn(async move { + let mut terminate_reqs = terminate_reqs; + while terminate_reqs.recv().await.is_some() { + event_tx_ + .send(Event::Terminate) + .await + .expect("we never close the event receiver"); + } + }); + + tokio::spawn(async move { + while let Some(event) = event_rx.recv().await { + match self_.process_event(event).await { + Ok(()) => (), + Err(err) => { + error!( + "hydra-controller: error: {}; will restart in {:?}…", + err, + Self::RESTART_DELAY + ); + tokio::time::sleep(Self::RESTART_DELAY).await; + self_.send(Event::Restart).await; + }, + } + } + }); + + Ok(event_tx) + } + + async fn send(&self, event: Event) { + self.event_tx + .send(event) + .await + .expect("we never close the event receiver"); + } + + async fn send_delayed(&self, event: Event, delay: Duration) { + let event_tx = self.event_tx.clone(); + tokio::spawn(async move { + tokio::time::sleep(delay).await; + event_tx.send(event).await + }); + } + + async fn process_event(&mut self, event: Event) -> Result<()> { + match event { + Event::Restart => { + info!("hydra-controller: starting…"); + + let potential_fuel = self + .lovelace_on_payment_skey(&self.config.cardano_signing_key) + .await?; + if potential_fuel < MIN_FUEL_LOVELACE { + Err(anyhow!( + "hydra-controller: {} ADA is too little for the Hydra L1 fees on the enterprise address associated with {:?}. Please provide at least {} ADA", + potential_fuel as f64 / 1_000_000.0, + self.config.cardano_signing_key, + MIN_FUEL_LOVELACE as f64 / 1_000_000.0, + ))? + } + + info!( + "hydra-controller: fuel on cardano_signing_key: {:?} lovelace", + potential_fuel + ); + + self.gen_hydra_keys().await?; + + self.kex_requests + .send(KeyExchangeRequest { + machine_id: verifications::hashed_machine_id(), + platform_cardano_vkey: self.platform_cardano_vkey.clone(), + platform_hydra_vkey: verifications::read_json_file( + &self.config_dir.join("hydra.vk"), + )?, + accepted_platform_h2h_port: None, + }) + .await?; + + self.hydra_head_open = false; + self.head_open_flag.store(false, Ordering::SeqCst); + self.credits_available.store(0, Ordering::SeqCst); + self.credits_last_balance = 0; + self.accounted_requests = 0; + self.sent_microtransactions = 0; + self.prepay_sent = false; + self.last_hydra_head_state = String::new(); + }, + + Event::Terminate => { + if let Some(pid) = self.hydra_pid { + verifications::sigterm(pid)? + } + }, + + Event::KeyExchangeResponse( + kex_resp @ KeyExchangeResponse { + kex_done: false, .. + }, + ) => { + let params = PaymentParams { + commit_ada: kex_resp.commit_ada, + lovelace_per_request: kex_resp.lovelace_per_request, + requests_per_microtransaction: kex_resp.requests_per_microtransaction, + microtransactions_per_fanout: kex_resp.microtransactions_per_fanout, + }; + info!( + "hydra-controller: payment params commit_ada={} lovelace_per_request={} requests_per_microtransaction={} microtransactions_per_fanout={}", + params.commit_ada, + params.lovelace_per_request, + params.requests_per_microtransaction, + params.microtransactions_per_fanout + ); + self.payment_params = Some(params); + + if self.gateway_payment_addr.is_empty() { + let addr = self + .derive_enterprise_address_from_vkey_json(&kex_resp.gateway_cardano_vkey) + .await?; + info!("hydra-controller: gateway payment address: {}", addr); + self.gateway_payment_addr = addr; + } + + if !(matches!( + verifications::is_tcp_port_free(kex_resp.gateway_h2h_port).await, + Ok(true) + ) && matches!( + verifications::is_tcp_port_free(kex_resp.proposed_platform_h2h_port).await, + Ok(true) + )) { + warn!( + "hydra-controller: the ports proposed by the Gateway are not free locally, will ask again" + ); + self.send(Event::Restart).await + } else { + self.kex_requests + .send(KeyExchangeRequest { + machine_id: verifications::hashed_machine_id(), + platform_cardano_vkey: self.platform_cardano_vkey.clone(), + platform_hydra_vkey: verifications::read_json_file( + &self.config_dir.join("hydra.vk"), + )?, + accepted_platform_h2h_port: Some(kex_resp.proposed_platform_h2h_port), + }) + .await?; + } + }, + + Event::KeyExchangeResponse(kex_resp @ KeyExchangeResponse { kex_done: true, .. }) => { + if self.gateway_payment_addr.is_empty() { + let addr = self + .derive_enterprise_address_from_vkey_json(&kex_resp.gateway_cardano_vkey) + .await?; + info!("hydra-controller: gateway payment address: {}", addr); + self.gateway_payment_addr = addr; + } + + if self.payment_params.is_none() { + self.payment_params = Some(PaymentParams { + commit_ada: kex_resp.commit_ada, + lovelace_per_request: kex_resp.lovelace_per_request, + requests_per_microtransaction: kex_resp.requests_per_microtransaction, + microtransactions_per_fanout: kex_resp.microtransactions_per_fanout, + }); + } + + self.start_hydra_node(kex_resp).await?; + self.send_delayed(Event::TryToInitHead, Duration::from_secs(1)) + .await + }, + + Event::TryToInitHead => { + let ready = verifications::prometheus_metric_at_least( + &format!("http://127.0.0.1:{}/metrics", self.metrics_port), + "hydra_head_peers_connected", + 1.0, + ) + .await; + + info!( + "hydra-controller: waiting for hydras to connect: ready={:?}", + ready + ); + + if matches!(ready, Ok(true)) { + verifications::send_one_websocket_msg( + &format!("ws://127.0.0.1:{}/", self.api_port), + serde_json::json!({"tag":"Init"}), + Duration::from_secs(2), + ) + .await?; + + self.send_delayed(Event::FundCommitAddr, Duration::from_secs(3)) + .await + } else { + self.send_delayed(Event::TryToInitHead, Duration::from_secs(1)) + .await + } + }, + + Event::FundCommitAddr => { + let status = verifications::fetch_head_tag(self.api_port).await?; + + info!( + "hydra-controller: waiting for the Initial head status: status={:?}", + status + ); + + if status == "Initial" || status == "Open" { + let commit_wallet = self.config_dir.join("commit-funds"); + self.commit_wallet_skey = commit_wallet.with_extension("sk"); + + if !self.commit_wallet_skey.exists() { + if status == "Open" { + Err(anyhow!( + "Head status is Open, but there’s no commit wallet anymore; this shouldn’t really happen" + ))? + } + + self.new_cardano_keypair(&commit_wallet).await?; + } + + self.commit_wallet_addr = self + .derive_enterprise_address_from_skey(&self.commit_wallet_skey) + .await?; + + if status == "Initial" { + let params = self.payment_params.clone().ok_or(anyhow!( + "payment parameters not set before funding commit address" + ))?; + self.fund_address( + &self + .derive_enterprise_address_from_skey( + &self.config.cardano_signing_key, + ) + .await?, + &self.commit_wallet_addr, + (params.commit_ada * 1_000_000.0).round() as u64, + &self.config.cardano_signing_key, + ) + .await?; + + self.send_delayed(Event::TryToCommit, Duration::from_secs(3)) + .await + } else if status == "Open" { + warn!( + "hydra-controller: turns out the Head is already Open, skipping Commit" + ); + self.send_delayed(Event::WaitForOpen, Duration::from_secs(3)) + .await + } + } else { + self.send_delayed(Event::FundCommitAddr, Duration::from_secs(3)) + .await + } + }, + + Event::TryToCommit => { + let commit_wallet_lovelace = self + .lovelace_on_payment_skey(&self.commit_wallet_skey) + .await?; + + let params = self + .payment_params + .clone() + .ok_or(anyhow!("payment parameters not set before commit"))?; + + let lovelace_needed = 0.99 * params.commit_ada * 1_000_000.0; + + info!( + "hydra-controller: waiting for enough lovelace (> {}) to appear on the commit address: lovelace={:?}", + lovelace_needed.round(), + commit_wallet_lovelace + ); + + if commit_wallet_lovelace as f64 >= lovelace_needed { + info!( + "hydra-controller: submitting a Commit transaction to join the Hydra Head" + ); + self.commit_all_utxo_to_hydra( + &self.commit_wallet_addr, + self.api_port, + &self.commit_wallet_skey, + ) + .await?; + + self.send_delayed(Event::WaitForOpen, Duration::from_secs(3)) + .await + } else { + self.send_delayed(Event::TryToCommit, Duration::from_secs(3)) + .await + } + }, + + Event::WaitForOpen => { + let status = verifications::fetch_head_tag(self.api_port).await?; + info!( + "hydra-controller: waiting for the Open head status: status={:?}", + status + ); + if status == "Open" { + self.hydra_head_open = true; + self.head_open_flag.store(true, Ordering::SeqCst); + self.credits_available.store(0, Ordering::SeqCst); + self.credits_last_balance = 0; + self.prepay_sent = false; + self.last_hydra_head_state = status.clone(); + self.send_delayed(Event::MonitorCredits, CREDIT_POLL_INTERVAL) + .await; + self.send_delayed(Event::MonitorStates, Duration::from_secs(5)) + .await; + self.send_prepay_microtransaction().await?; + } else { + self.send_delayed(Event::WaitForOpen, Duration::from_secs(3)) + .await + } + }, + + Event::MonitorStates => { + let new_status = verifications::fetch_head_tag(self.api_port).await?; + + if new_status != self.last_hydra_head_state { + let old = self.last_hydra_head_state.clone(); + let new = new_status.clone(); + self.last_hydra_head_state = new_status.clone(); + + info!("hydra-controller: state changed from {old} to {new}"); + + if new == "Initial" { + self.send_delayed(Event::TryToCommit, Duration::from_secs(1)) + .await; + } + } + + if new_status == "Open" { + self.hydra_head_open = true; + self.head_open_flag.store(true, Ordering::SeqCst); + } else { + self.hydra_head_open = false; + self.head_open_flag.store(false, Ordering::SeqCst); + self.credits_available.store(0, Ordering::SeqCst); + self.credits_last_balance = 0; + } + + self.send_delayed(Event::MonitorStates, Duration::from_secs(5)) + .await; + }, + + Event::MonitorCredits => { + if self.hydra_head_open { + if self.gateway_payment_addr.is_empty() { + warn!("hydra-controller: gateway payment address not set yet"); + } else if let Some(params) = &self.payment_params { + match verifications::lovelace_in_snapshot_for_address( + self.api_port, + &self.gateway_payment_addr, + ) + .await + { + Ok(current_balance) => { + if current_balance < self.credits_last_balance { + warn!( + "hydra-controller: snapshot balance decreased ({} -> {}), resetting", + self.credits_last_balance, current_balance + ); + self.credits_last_balance = current_balance; + } else { + let delta = current_balance - self.credits_last_balance; + if delta > 0 { + let microtransaction_lovelace = params.lovelace_per_request + * params.requests_per_microtransaction; + if microtransaction_lovelace == 0 { + warn!( + "hydra-controller: microtransaction value is zero; ignoring credits" + ); + } else if delta >= microtransaction_lovelace { + let new_microtransactions = + delta / microtransaction_lovelace; + let new_credits = new_microtransactions + * params.requests_per_microtransaction; + self.credits_available + .fetch_add(new_credits, Ordering::SeqCst); + info!( + "hydra-controller: req. credits +{} ({} microtransaction(s))", + new_credits, new_microtransactions + ); + } else { + warn!( + "hydra-controller: snapshot delta {} is below expected microtransaction size {}", + delta, microtransaction_lovelace + ); + } + self.credits_last_balance = current_balance; + } + } + }, + Err(err) => { + warn!("hydra-controller: failed to read snapshot/utxo: {err}") + }, + } + } + + self.send_delayed(Event::MonitorCredits, CREDIT_POLL_INTERVAL) + .await; + } + }, + + Event::AccountOneRequest => { + let params = match &self.payment_params { + Some(p) => p.clone(), + None => { + warn!("hydra-controller: payment parameters not set yet"); + return Ok(()); + }, + }; + + if !self.hydra_head_open { + warn!( + "hydra-controller: would account a request, but the Hydra Head is not Open" + ); + return Ok(()); + } + + if self.gateway_payment_addr.is_empty() { + warn!("hydra-controller: gateway payment address not set yet"); + return Ok(()); + } + + self.accounted_requests += 1; + + if self.accounted_requests >= params.requests_per_microtransaction { + info!("hydra-controller: sending a microtransaction"); + let amount_lovelace: u64 = + self.accounted_requests * params.lovelace_per_request; + self.send_hydra_transaction( + self.api_port, + &self.commit_wallet_addr, + &self.gateway_payment_addr, + &self.commit_wallet_skey, + amount_lovelace, + ) + .await?; + + self.accounted_requests = 0; + self.sent_microtransactions += 1; + } + }, + } + Ok(()) + } + + async fn send_prepay_microtransaction(&mut self) -> Result<()> { + if self.prepay_sent { + return Ok(()); + } + + let params = self + .payment_params + .clone() + .ok_or(anyhow!("payment parameters not set before prepay"))?; + + if self.gateway_payment_addr.is_empty() { + warn!("hydra-controller: gateway payment address not set yet"); + return Ok(()); + } + + let amount_lovelace: u64 = + params.requests_per_microtransaction * params.lovelace_per_request; + self.send_hydra_transaction( + self.api_port, + &self.commit_wallet_addr, + &self.gateway_payment_addr, + &self.commit_wallet_skey, + amount_lovelace, + ) + .await?; + + self.sent_microtransactions += 1; + self.prepay_sent = true; + Ok(()) + } + + async fn start_hydra_node(&mut self, kex_response: KeyExchangeResponse) -> Result<()> { + use std::process::Stdio; + use tokio::io::{AsyncBufReadExt, BufReader}; + + self.api_port = verifications::find_free_tcp_port().await?; + self.metrics_port = verifications::find_free_tcp_port().await?; + + let protocol_parameters_path = self.config_dir.join("protocol-parameters.json"); + verifications::write_json_if_changed( + &protocol_parameters_path, + &kex_response.protocol_parameters, + )?; + + let gateway_hydra_vkey_path = self.config_dir.join("gateway-hydra.vk"); + verifications::write_json_if_changed( + &gateway_hydra_vkey_path, + &kex_response.gateway_hydra_vkey, + )?; + + let gateway_cardano_vkey_path = self.config_dir.join("gateway-payment.vk"); + verifications::write_json_if_changed( + &gateway_cardano_vkey_path, + &kex_response.gateway_cardano_vkey, + )?; + + let mut child = tokio::process::Command::new(&self.hydra_node_exe) + .arg("--node-id") + .arg("bridge-node") + .arg("--persistence-dir") + .arg(self.config_dir.join("persistence")) + .arg("--cardano-signing-key") + .arg(&self.config.cardano_signing_key) + .arg("--hydra-signing-key") + .arg(self.config_dir.join("hydra.sk")) + .arg("--hydra-scripts-tx-id") + .arg(&kex_response.hydra_scripts_tx_id) + .arg("--ledger-protocol-parameters") + .arg(&protocol_parameters_path) + .arg("--contestation-period") + .arg(format!("{}s", kex_response.contestation_period.as_secs())) + .args(if self.config.network == Network::Mainnet { + vec!["-mainnet".to_string()] + } else { + vec![ + "--testnet-magic".to_string(), + format!("{}", self.config.network.network_magic()), + ] + }) + .arg("--node-socket") + .arg(&self.config.node_socket_path) + .arg("--api-port") + .arg(format!("{}", self.api_port)) + .arg("--api-host") + .arg("127.0.0.1") + .arg("--listen") + .arg(format!( + "127.0.0.1:{}", + kex_response.proposed_platform_h2h_port + )) + .arg("--peer") + .arg(format!("127.0.0.1:{}", kex_response.gateway_h2h_port)) + .arg("--monitoring-port") + .arg(format!("{}", self.metrics_port)) + .arg("--hydra-verification-key") + .arg(gateway_hydra_vkey_path) + .arg("--cardano-verification-key") + .arg(gateway_cardano_vkey_path) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + self.hydra_pid = child.id(); + + let stdout = child.stdout.take().expect("child stdout"); + let stderr = child.stderr.take().expect("child stderr"); + + tokio::spawn(async move { + let mut lines = BufReader::new(stdout).lines(); + while let Ok(Some(line)) = lines.next_line().await { + debug!("hydra-node: {}", line); + } + debug!("hydra-node: stdout closed"); + }); + + tokio::spawn(async move { + let mut lines = BufReader::new(stderr).lines(); + while let Ok(Some(line)) = lines.next_line().await { + info!("hydra-node: {}", line); + } + info!("hydra-node: stderr closed"); + }); + + let event_tx = self.event_tx.clone(); + tokio::spawn(async move { + match child.wait().await { + Ok(status) => { + warn!("hydra-node: exited: {}", status); + tokio::time::sleep(Self::RESTART_DELAY).await; + event_tx + .send(Event::Restart) + .await + .expect("we never close the event receiver"); + }, + Err(e) => { + error!("hydra-node: failed to wait: {e}"); + }, + } + }); + + Ok(()) + } +} diff --git a/crates/sdk_bridge/src/hydra_client/tunnel2.rs b/crates/sdk_bridge/src/hydra_client/tunnel2.rs new file mode 100644 index 00000000..f5abeb8d --- /dev/null +++ b/crates/sdk_bridge/src/hydra_client/tunnel2.rs @@ -0,0 +1,337 @@ +use anyhow::Result; +use base64::{Engine as _, engine::general_purpose::STANDARD as B64}; +use bytes::{Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{TcpListener, TcpStream}, + sync::{Mutex, mpsc}, +}; +use tokio_util::sync::CancellationToken; + +/// JSON-serializable tunnel messages (base64 for buffers). +/// +/// Plug into a WebSocket protocol as e.g. `WsProto::HydraTunnel(TunnelMsg)`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", rename_all = "snake_case")] +pub enum TunnelMsg { + /// Ask peer to open its *configured* local service port for stream `id`. + Open { id: u64 }, + + /// Bytes for connection `id` encoded as base64 string. + Data { id: u64, b64: String }, + + /// Close stream `id`. `code` is small+stable, `msg` is optional. + Close { + id: u64, + code: u8, + msg: Option, + }, +} + +pub mod close_code { + pub const CLEAN: u8 = 0; + pub const IO: u8 = 1; + pub const CANCELLED: u8 = 2; + pub const PROTOCOL: u8 = 3; +} + +/// Tunnel config. +#[derive(Debug, Clone)] +pub struct TunnelConfig { + /// Host used for local TCP connects when peer sends Open. + pub local_connect_host: IpAddr, + + /// The *only* local port that the peer is allowed to connect to (via Open). + pub expose_port: u16, + + /// If true, set bit 63 in all locally-allocated IDs. + /// Set opposite values on the two peers to avoid ID collisions. + pub id_prefix_bit: bool, + + /// Outbound TunnelMsg buffer (what the WebSocket event loop drains). + pub outbound_capacity: usize, + + /// Per-connection command channel capacity. + pub per_conn_cmd_capacity: usize, + + /// Max bytes per TCP read. + pub read_chunk: usize, +} + +impl Default for TunnelConfig { + fn default() -> Self { + Self { + local_connect_host: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + expose_port: 0, + id_prefix_bit: false, + outbound_capacity: 256, + per_conn_cmd_capacity: 128, + read_chunk: 8 * 1024, + } + } +} + +enum ConnCmd { + Write(Bytes), + /// Close local TCP. If notify_peer=false, don’t emit a Close back. + Close { + notify_peer: bool, + }, +} + +struct Inner { + cfg: TunnelConfig, + cancel: CancellationToken, + out_tx: mpsc::Sender, + conns: Mutex>>, + next_id: AtomicU64, +} + +/// Cloneable handle kept in a WebSocket connection state/event-loop. +#[derive(Clone)] +pub struct Tunnel { + inner: Arc, +} + +impl Tunnel { + /// Create tunnel + outbound receiver (to drain in the WebSocket event loop). + pub fn new(cfg: TunnelConfig, cancel: CancellationToken) -> (Self, mpsc::Receiver) { + let (out_tx, out_rx) = mpsc::channel(cfg.outbound_capacity); + + let prefix = if cfg.id_prefix_bit { 1u64 << 63 } else { 0 }; + let next_id = AtomicU64::new(1 | prefix); + + let inner = Arc::new(Inner { + cfg, + cancel, + out_tx, + conns: Mutex::new(HashMap::new()), + next_id, + }); + + (Self { inner }, out_rx) + } + + pub fn cancel(&self) { + self.inner.cancel.cancel(); + } + + /// Call this from the WebSocket event loop when it receives a tunnel message. + pub async fn on_msg(&self, msg: TunnelMsg) -> Result<()> { + match msg { + TunnelMsg::Open { id } => { + // Always connect to the single configured local port. + let addr = SocketAddr::new( + self.inner.cfg.local_connect_host, + self.inner.cfg.expose_port, + ); + match TcpStream::connect(addr).await { + Ok(sock) => self.attach_stream_with_id(id, sock).await?, + Err(e) => { + let _ = self + .inner + .out_tx + .send(TunnelMsg::Close { + id, + code: close_code::IO, + msg: Some(e.to_string()), + }) + .await; + }, + } + }, + + TunnelMsg::Data { id, b64 } => { + let bytes = match B64.decode(b64.as_bytes()) { + Ok(v) => Bytes::from(v), + Err(_) => { + let _ = self + .inner + .out_tx + .send(TunnelMsg::Close { + id, + code: close_code::PROTOCOL, + msg: Some("invalid base64".into()), + }) + .await; + return Ok(()); + }, + }; + + let tx = { self.inner.conns.lock().await.get(&id).cloned() }; + if let Some(tx) = tx { + let _ = tx.send(ConnCmd::Write(bytes)).await; + } + }, + + TunnelMsg::Close { id, .. } => { + let tx = { self.inner.conns.lock().await.remove(&id) }; + if let Some(tx) = tx { + let _ = tx.send(ConnCmd::Close { notify_peer: false }).await; + } + }, + } + + Ok(()) + } + + /// Spawn a TCP listener on *this* side. Each accepted local TCP connection becomes + /// a tunneled stream to the peer’s configured `expose_port`. + pub async fn spawn_listener(&self, listen_port: u16) -> Result<()> { + let listener = TcpListener::bind((self.inner.cfg.local_connect_host, listen_port)).await?; + let this = self.clone(); + + tokio::spawn(async move { + loop { + let (mut sock, _) = tokio::select! { + _ = this.inner.cancel.cancelled() => break, + res = listener.accept() => match res { Ok(v) => v, Err(_) => break } + }; + + let id = this.alloc_local_id(); + + // Ask peer to open its configured port. + if this + .inner + .out_tx + .send(TunnelMsg::Open { id }) + .await + .is_err() + { + let _ = sock.shutdown().await; + break; + } + + // Attach local accepted socket. + if this.attach_stream_with_id(id, sock).await.is_err() { + let _ = this + .inner + .out_tx + .send(TunnelMsg::Close { + id, + code: close_code::PROTOCOL, + msg: Some("attach failed".into()), + }) + .await; + } + } + }); + + Ok(()) + } + + /// If you already accepted a TcpStream elsewhere and want to tunnel it: + /// sends Open (no port) and returns the allocated id. + /// + /// *Warning*: it’s a little controversial, but trivial to add. Probably + /// shouldn’t be used. + #[allow(dead_code)] + pub async fn attach_stream(&self, sock: TcpStream) -> Result { + let id = self.alloc_local_id(); + self.inner.out_tx.send(TunnelMsg::Open { id }).await?; + self.attach_stream_with_id(id, sock).await?; + Ok(id) + } + + fn alloc_local_id(&self) -> u64 { + let prefix = if self.inner.cfg.id_prefix_bit { + 1u64 << 63 + } else { + 0 + }; + let base = self.inner.next_id.fetch_add(1, Ordering::Relaxed) & !(1u64 << 63); + base | prefix + } + + async fn attach_stream_with_id(&self, id: u64, sock: TcpStream) -> Result<()> { + let (cmd_tx, mut cmd_rx) = mpsc::channel::(self.inner.cfg.per_conn_cmd_capacity); + + // Insert route, replacing duplicates (and closing old). + { + let mut m = self.inner.conns.lock().await; + if let Some(old) = m.insert(id, cmd_tx) { + let _ = old.send(ConnCmd::Close { notify_peer: false }).await; + } + } + + let out_tx = self.inner.out_tx.clone(); + let cancel = self.inner.cancel.clone(); + let cfg = self.inner.cfg.clone(); + let inner = Arc::clone(&self.inner); + + tokio::spawn(async move { + let mut sock = sock; + let mut buf = BytesMut::with_capacity(cfg.read_chunk); + let mut notify_peer_close = true; + + let close_reason: Option<(u8, Option)> = loop { + tokio::select! { + _ = cancel.cancelled() => { + break Some((close_code::CANCELLED, Some("cancelled".into()))); + } + + // TCP -> WS (encode bytes as base64) + rv = async { + buf.clear(); + buf.reserve(cfg.read_chunk); + sock.read_buf(&mut buf).await + } => { + match rv { + Ok(0) => break Some((close_code::CLEAN, None)), // EOF + Ok(_) => { + let chunk = buf.split().freeze(); + let b64 = B64.encode(&chunk); + if out_tx.send(TunnelMsg::Data { id, b64 }).await.is_err() { + notify_peer_close = false; + break None; + } + } + Err(e) => break Some((close_code::IO, Some(e.to_string()))), + } + } + + // WS -> TCP (decode already done in on_msg) + cmd = cmd_rx.recv() => { + match cmd { + Some(ConnCmd::Write(chunk)) => { + if let Err(e) = sock.write_all(&chunk).await { + break Some((close_code::IO, Some(e.to_string()))); + } + } + Some(ConnCmd::Close { notify_peer }) => { + notify_peer_close = notify_peer; + break Some((close_code::CLEAN, None)); + } + None => { + notify_peer_close = false; + break None; + } + } + } + } + }; + + // Remove route + let _ = inner.conns.lock().await.remove(&id); + + if notify_peer_close { + let (code, msg) = + close_reason.unwrap_or((close_code::CANCELLED, Some("closed".into()))); + let _ = out_tx.send(TunnelMsg::Close { id, code, msg }).await; + } + + let _ = sock.shutdown().await; + }); + + Ok(()) + } +} diff --git a/crates/sdk_bridge/src/hydra_client/verifications.rs b/crates/sdk_bridge/src/hydra_client/verifications.rs new file mode 100644 index 00000000..623672ac --- /dev/null +++ b/crates/sdk_bridge/src/hydra_client/verifications.rs @@ -0,0 +1,768 @@ +use anyhow::{Result, anyhow}; +use serde_json::Value; +use std::path::Path; +use tracing::info; + +use crate::types::Network; + +/// FIXME: don’t use `cardano-cli`. +/// +/// FIXME: proper errors, not `anyhow!` +impl super::State { + /// Generates Hydra keys if they don’t exist. + pub(super) async fn gen_hydra_keys(&self) -> Result<()> { + std::fs::create_dir_all(&self.config_dir)?; + + let key_path = self.config_dir.join("hydra.sk"); + + if !key_path.exists() { + info!("hydra-controller: generating hydra keys"); + + let status = tokio::process::Command::new(&self.hydra_node_exe) + .arg("gen-hydra-key") + .arg("--output-file") + .arg(self.config_dir.join("hydra")) + .status() + .await?; + + if !status.success() { + Err(anyhow!("gen-hydra-key failed with status: {status}"))?; + } + } else { + info!("hydra-controller: hydra keys already exist"); + } + + Ok(()) + } + + fn cardano_cli_env(&self) -> Vec<(&str, String)> { + vec![ + ( + "CARDANO_NODE_SOCKET_PATH", + self.config.node_socket_path.to_string_lossy().to_string(), + ), + ( + "CARDANO_NODE_NETWORK_ID", + match &self.config.network { + Network::Mainnet => self.config.network.as_str().to_string(), + other => other.network_magic().to_string(), + }, + ), + ] + } + + /// Check how much lovelace is on an enterprise address associated with a + /// given `payment.skey`. + pub(super) async fn lovelace_on_payment_skey(&self, skey_path: &Path) -> Result { + let address = self.derive_enterprise_address_from_skey(skey_path).await?; + let utxo_json = self.query_utxo_json(&address).await?; + Self::sum_lovelace_from_utxo_json(&utxo_json) + } + + pub(super) async fn derive_vkey_from_skey( + &self, + skey_path: &Path, + ) -> Result { + let vkey_output = tokio::process::Command::new(&self.cardano_cli_exe) + .envs(self.cardano_cli_env()) + .args(["key", "verification-key", "--signing-key-file"]) + .arg(skey_path) + .args(["--verification-key-file", "/dev/stdout"]) + .output() + .await?; + Ok(serde_json::from_slice(&vkey_output.stdout)?) + } + + pub(super) async fn derive_enterprise_address_from_skey( + &self, + skey_path: &Path, + ) -> Result { + let vkey_output = tokio::process::Command::new(&self.cardano_cli_exe) + .envs(self.cardano_cli_env()) + .args(["key", "verification-key", "--signing-key-file"]) + .arg(skey_path) + .args(["--verification-key-file", "/dev/stdout"]) + .output() + .await?; + + if !vkey_output.status.success() { + return Err(anyhow!( + "cardano-cli key verification-key failed: {}", + String::from_utf8_lossy(&vkey_output.stderr) + )); + } + + self.derive_enterprise_address_from_vkey_json(&serde_json::from_slice(&vkey_output.stdout)?) + .await + } + + pub(super) async fn derive_enterprise_address_from_vkey_json( + &self, + vkey_json: &serde_json::Value, + ) -> Result { + let mut child = tokio::process::Command::new(&self.cardano_cli_exe) + .envs(self.cardano_cli_env()) + .args([ + "address", + "build", + "--payment-verification-key-file", + "/dev/stdin", + ]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn()?; + + { + let stdin = child.stdin.as_mut().ok_or(anyhow!( + "failed to open stdin for cardano-cli address build" + ))?; + use tokio::io::AsyncWriteExt; + stdin.write_all(&serde_json::to_vec(vkey_json)?).await?; + } + + let addr_output = child.wait_with_output().await?; + if !addr_output.status.success() { + Err(anyhow!( + "cardano-cli address build failed: {}", + String::from_utf8_lossy(&addr_output.stderr) + ))?; + } + + let address = String::from_utf8(addr_output.stdout)?.trim().to_string(); + if address.is_empty() { + return Err(anyhow!("derived address is empty")); + } + + Ok(address) + } + + pub(super) async fn query_utxo_json(&self, address: &str) -> Result { + let utxo_json = self + .cardano_cli_capture( + &[ + "query", + "utxo", + "--address", + address, + "--out-file", + "/dev/stdout", + ], + None, + ) + .await? + .0; + Ok(utxo_json) + } + + fn sum_lovelace_from_utxo_json(json: &serde_json::Value) -> Result { + let obj = json + .as_object() + .ok_or(anyhow!("UTxO JSON root is not an object"))?; + + let mut total: u64 = 0; + + for (_txin, utxo) in obj { + if let Some(value_obj) = utxo.get("value").and_then(|v| v.as_object()) { + if let Some(lovelace_val) = value_obj.get("lovelace") { + total = total + .checked_add(Self::as_u64(lovelace_val)?) + .ok_or(anyhow!("cannot add"))?; + continue; + } + } + + if let Some(amount_arr) = utxo.get("amount").and_then(|v| v.as_array()) { + if let Some(lovelace_val) = amount_arr.first() { + total = total + .checked_add(Self::as_u64(lovelace_val)?) + .ok_or(anyhow!("cannot add"))?; + } + } + } + + Ok(total) + } + + /// Convert a JSON value into u64, allowing either number or string. + fn as_u64(v: &Value) -> Result { + if let Some(n) = v.as_u64() { + return Ok(n); + } + if let Some(s) = v.as_str() { + return Ok(s.parse()?); + } + Err(anyhow!("lovelace value is neither u64 nor string")) + } + + async fn cardano_cli_capture( + &self, + args: &[&str], + stdin_bytes: Option<&[u8]>, + ) -> Result<(serde_json::Value, Vec)> { + use tokio::io::AsyncWriteExt; + + let mut cmd = tokio::process::Command::new(&self.cardano_cli_exe); + cmd.envs(self.cardano_cli_env()); + cmd.args(args) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + + if stdin_bytes.is_some() { + cmd.stdin(std::process::Stdio::piped()); + } else { + cmd.stdin(std::process::Stdio::null()); + } + + let mut child = cmd.spawn()?; + + if let Some(bytes) = stdin_bytes { + let mut stdin = child + .stdin + .take() + .ok_or_else(|| anyhow!("failed to open stdin pipe"))?; + stdin.write_all(bytes).await?; + stdin.shutdown().await?; + } + + let out = child.wait_with_output().await?; + + if !out.status.success() { + return Err(anyhow!( + "cardano-cli failed (exit={}):\nstdout: {}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stdout).trim(), + String::from_utf8_lossy(&out.stderr).trim(), + )); + } + + let (json, rest) = parse_first_json_and_rest(&out.stdout)?; + Ok((json, rest)) + } + + pub(super) async fn new_cardano_keypair(&self, base_path: &Path) -> Result<()> { + let output = tokio::process::Command::new(&self.cardano_cli_exe) + .envs(self.cardano_cli_env()) + .args(["address", "key-gen", "--verification-key-file"]) + .arg(base_path.with_extension("vk")) + .arg("--signing-key-file") + .arg(base_path.with_extension("sk")) + .output() + .await?; + + if !output.status.success() { + return Err(anyhow!( + "cardano-cli address key-gen failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + + Ok(()) + } + + pub(super) async fn fund_address( + &self, + addr_from: &str, + addr_to: &str, + amount_lovelace: u64, + payment_skey_path: &Path, + ) -> Result<()> { + let utxo_json = self + .cardano_cli_capture( + &[ + "query", + "utxo", + "--address", + addr_from, + "--out-file", + "/dev/stdout", + ], + None, + ) + .await? + .0; + + // XXX: we’re only taking the first 200 UTxOs below, because the test address on + // CI has too many of them, and we’d hit `MaxTxSizeUTxO`. + let obj = utxo_json + .as_object() + .ok_or_else(|| anyhow!("UTxO JSON is not an object"))?; + + let tx_in_keys: Vec<&str> = obj.keys().take(200).map(|k| k.as_str()).collect(); + if tx_in_keys.is_empty() { + Err(anyhow!("no UTxOs found for addr_from"))? + } + + let tx_out = format!("{addr_to}+{amount_lovelace}"); + let mut build_args: Vec = + vec!["latest".into(), "transaction".into(), "build".into()]; + for k in &tx_in_keys { + build_args.push("--tx-in".into()); + build_args.push((*k).into()); + } + build_args.extend([ + "--change-address".into(), + addr_from.into(), + "--tx-out".into(), + tx_out, + "--out-file".into(), + "/dev/stdout".into(), + ]); + + let build_args_ref: Vec<&str> = build_args.iter().map(|s| s.as_str()).collect(); + let tx_json = self.cardano_cli_capture(&build_args_ref, None).await?.0; + + let skey = payment_skey_path + .to_str() + .ok_or_else(|| anyhow!("payment_skey_path is not valid UTF-8"))?; + + let tx_signed = self + .cardano_cli_capture( + &[ + "latest", + "transaction", + "sign", + "--tx-file", + "/dev/stdin", + "--signing-key-file", + skey, + "--out-file", + "/dev/stdout", + ], + Some(&serde_json::to_vec(&tx_json)?), + ) + .await? + .0; + + let _ = self + .cardano_cli_capture( + &["latest", "transaction", "submit", "--tx-file", "/dev/stdin"], + Some(&serde_json::to_vec(&tx_signed)?), + ) + .await? + .0; + + Ok(()) + } + + pub(super) async fn commit_all_utxo_to_hydra( + &self, + from_addr: &str, + hydra_api_port: u16, + commit_funds_skey: &Path, + ) -> Result<()> { + use anyhow::Context; + use reqwest::header; + + let utxo_json = self.query_utxo_json(from_addr).await?; + let utxo_body = serde_json::to_vec(&utxo_json).context("failed to serialize utxo JSON")?; + + let url = format!("http://127.0.0.1:{hydra_api_port}/commit"); + let client = reqwest::Client::new(); + let resp = client + .post(url) + .header(header::CONTENT_TYPE, "application/json") + .body(utxo_body) + .send() + .await + .context("failed to POST /commit to hydra-node")?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.bytes().await.unwrap_or_default(); + return Err(anyhow!( + "hydra /commit failed with {}: {}", + status, + String::from_utf8_lossy(&body) + )); + } + + let commit_tx_bytes = resp + .bytes() + .await + .context("failed to read hydra /commit response body")? + .to_vec(); + + let _: serde_json::Value = serde_json::from_slice(&commit_tx_bytes) + .context("hydra /commit response was not valid JSON")?; + + let signed_tx = self + .cardano_cli_capture( + &[ + "latest", + "transaction", + "sign", + "--tx-file", + "/dev/stdin", + "--signing-key-file", + commit_funds_skey + .to_str() + .ok_or_else(|| anyhow!("commit_funds_skey is not valid UTF-8"))?, + "--out-file", + "/dev/stdout", + ], + Some(&commit_tx_bytes), + ) + .await? + .0; + + let _ = self + .cardano_cli_capture( + &["latest", "transaction", "submit", "--tx-file", "/dev/stdin"], + Some(&serde_json::to_vec(&signed_tx)?), + ) + .await? + .0; + Ok(()) + } + + pub(super) async fn send_hydra_transaction( + &self, + hydra_api_port: u16, + sender_addr: &str, + receiver_addr: &str, + sender_skey_path: &Path, + amount_lovelace: u64, + ) -> Result<()> { + use anyhow::Context; + + let snapshot_url = format!("http://127.0.0.1:{hydra_api_port}/snapshot/utxo"); + let utxo: Value = reqwest::Client::new() + .get(&snapshot_url) + .send() + .await? + .error_for_status()? + .json() + .await + .context("snapshot/utxo: failed to decode JSON")?; + + let utxo_obj = utxo + .as_object() + .context("snapshot/utxo: expected top-level JSON object")?; + + let mut filtered: serde_json::Map = serde_json::Map::new(); + for (k, v) in utxo_obj.iter() { + if v.get("address").and_then(Value::as_str) == Some(sender_addr) { + filtered.insert(k.clone(), v.clone()); + } + } + + let (tx_in, chosen_entry) = filtered + .iter() + .next() + .map(|(k, v)| (k.clone(), v.clone())) + .ok_or_else(|| anyhow!("no UTxO found for sender address"))?; + + let lovelace_total = chosen_entry + .pointer("/value/lovelace") + .and_then(Value::as_u64) + .context("utxo entry: expected .value.lovelace as integer")?; + + if lovelace_total < amount_lovelace { + return Err(anyhow!( + "insufficient lovelace in selected UTxO: {lovelace_total} < {amount_lovelace}" + )); + } + + let change = lovelace_total - amount_lovelace; + + let tx_body: serde_json::Value = { + let args: &[&str] = &[ + "latest", + "transaction", + "build-raw", + "--tx-in", + &tx_in, + "--tx-out", + &format!("{receiver_addr}+{amount_lovelace}"), + "--tx-out", + &format!("{sender_addr}+{change}"), + "--fee", + "0", + "--out-file", + "/dev/stdout", + ]; + self.cardano_cli_capture(args, None).await?.0 + }; + + let tx_signed: serde_json::Value = { + let skey_str = sender_skey_path + .to_str() + .context("sender_skey_path is not valid UTF-8")?; + + let args: &[&str] = &[ + "latest", + "transaction", + "sign", + "--tx-body-file", + "/dev/stdin", + "--signing-key-file", + skey_str, + "--out-file", + "/dev/stdout", + ]; + + self.cardano_cli_capture(args, Some(&serde_json::to_vec(&tx_body)?)) + .await? + .0 + }; + + let payload = serde_json::json!({ + "tag": "NewTx", + "transaction": tx_signed, + }); + + tracing::info!( + "hydra-controller: sending WebSocket payload: {}", + serde_json::to_string(&payload)? + ); + + let ws_url = format!("ws://127.0.0.1:{hydra_api_port}/"); + send_one_websocket_msg(&ws_url, payload, std::time::Duration::from_secs(2)).await?; + + Ok(()) + } +} + +pub async fn lovelace_in_snapshot_for_address(hydra_api_port: u16, address: &str) -> Result { + use anyhow::Context; + + let snapshot_url = format!("http://127.0.0.1:{hydra_api_port}/snapshot/utxo"); + let utxo: Value = reqwest::Client::new() + .get(&snapshot_url) + .send() + .await? + .error_for_status()? + .json() + .await + .context("snapshot/utxo: failed to decode JSON")?; + + let utxo_obj = utxo + .as_object() + .context("snapshot/utxo: expected top-level JSON object")?; + + let mut filtered: serde_json::Map = serde_json::Map::new(); + for (k, v) in utxo_obj.iter() { + if v.get("address").and_then(Value::as_str) == Some(address) { + filtered.insert(k.clone(), v.clone()); + } + } + + let filtered_json = Value::Object(filtered); + super::State::sum_lovelace_from_utxo_json(&filtered_json) +} + +/// Reads a JSON file from disk. +pub fn read_json_file(path: &Path) -> Result { + let contents = std::fs::read_to_string(path)?; + let json: serde_json::Value = serde_json::from_str(&contents)?; + Ok(json) +} + +/// Writes `json` to `path` (pretty-printed) **only if** the JSON content differs +/// from what is already on disk. Returns `true` if the file was written. +pub fn write_json_if_changed(path: &Path, json: &serde_json::Value) -> Result { + use std::fs::File; + use std::io::Write; + + if path.exists() { + if let Ok(existing_str) = std::fs::read_to_string(path) { + if let Ok(existing_json) = serde_json::from_str::(&existing_str) { + if existing_json == *json { + return Ok(false); + } + } + } + } + + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent)?; + } + } + + let mut file = File::create(path)?; + serde_json::to_writer_pretty(&mut file, json)?; + file.write_all(b"\n")?; + + Ok(true) +} + +/// Finds a free port by bind to port 0, to let the OS pick a free port. +pub async fn find_free_tcp_port() -> std::io::Result { + let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)).await?; + let port = listener.local_addr()?.port(); + drop(listener); + Ok(port) +} + +/// Returns `Ok(true)` if `port` can be bound on 127.0.0.1 (so it's free), +/// `Ok(false)` if it's already in use, and `Err(_)` for other IO errors. +pub async fn is_tcp_port_free(port: u16) -> std::io::Result { + match tokio::net::TcpListener::bind(("127.0.0.1", port)).await { + Ok(listener) => { + drop(listener); + Ok(true) + }, + Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => Ok(false), + Err(e) => Err(e), + } +} + +/// Checks if a Prometheus `metric` at `url` is greater or equal to `threshold`. +pub async fn prometheus_metric_at_least(url: &str, metric: &str, threshold: f64) -> Result { + let client = reqwest::Client::new(); + let body = client + .get(url) + .send() + .await? + .error_for_status()? + .text() + .await?; + + let mut found_any = false; + let mut max_value: Option = None; + + for line in body.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + // {labels} [timestamp] + let mut parts = line.split_whitespace(); + let name_and_labels = match parts.next() { + Some(x) => x, + None => continue, + }; + + let name = name_and_labels.split('{').next().unwrap_or(name_and_labels); + if name != metric { + continue; + } + + let value_str = match parts.next() { + Some(v) => v, + None => continue, + }; + + let value: f64 = value_str.parse()?; + found_any = true; + max_value = Some(max_value.map_or(value, |m| m.max(value))); + } + + if !found_any { + return Err(anyhow!("metric {metric} not found in /metrics output")); + } + + Ok(max_value.unwrap_or(f64::NEG_INFINITY) >= threshold) +} + +/// Sends a single WebSocket message, and waits a bit before closing the +/// connection cleanly. Particularly useful for Hydra. +pub async fn send_one_websocket_msg( + url: &str, + payload: serde_json::Value, + wait_before_close: std::time::Duration, +) -> Result<()> { + use futures_util::{SinkExt, StreamExt}; + use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; + + let (ws_stream, _resp) = connect_async(url).await?; + let (mut write, mut read) = ws_stream.split(); + + write + .send(Message::Text(payload.to_string().into())) + .await?; + + tokio::time::sleep(wait_before_close).await; + + write.send(Message::Close(None)).await?; + + // Drain until we observe the close handshake (or the peer drops): + while let Some(msg) = read.next().await { + match msg? { + Message::Close(_) => break, + Message::Text(msg) => { + tracing::info!("hydra-controller: got WebSocket message: {}", msg) + }, + msg => tracing::info!("hydra-controller: got WebSocket message: {:?}", msg), + } + } + + Ok(()) +} + +pub async fn fetch_head_tag(hydra_api_port: u16) -> Result { + let url = format!("http://127.0.0.1:{hydra_api_port}/head"); + + let v: serde_json::Value = reqwest::get(url).await?.error_for_status()?.json().await?; + + v.get("tag") + .ok_or(anyhow!("missing tag")) + .and_then(|a| a.as_str().ok_or(anyhow!("tag is not a string"))) + .map(|a| a.to_string()) +} + +/// Parse the first JSON value from e.g. stdout, and return the remainder. +fn parse_first_json_and_rest(stdout: &[u8]) -> Result<(serde_json::Value, Vec)> { + let mut start = stdout + .iter() + .position(|b| !b.is_ascii_whitespace()) + .unwrap_or(0); + + if !matches!(stdout.get(start), Some(b'{') | Some(b'[')) { + if let Some(i) = stdout.iter().position(|&b| b == b'{' || b == b'[') { + start = i; + } + } + + let mut it = serde_json::Deserializer::from_slice(&stdout[start..]).into_iter::(); + + let first = it + .next() + .ok_or_else(|| anyhow!("no JSON value found in stdout"))? + .map_err(|e| anyhow!("failed to parse first JSON value from stdout: {e}"))?; + + let consumed = it.byte_offset(); + let rest = stdout[start + consumed..].to_vec(); + + Ok((first, rest)) +} + +#[cfg(unix)] +pub fn sigterm(pid: u32) -> Result<()> { + use nix::sys::signal::{Signal, kill}; + use nix::unistd::Pid; + Ok(kill(Pid::from_raw(pid as i32), Signal::SIGTERM)?) +} + +#[cfg(windows)] +pub fn sigterm(_pid: u32) -> Result<()> { + unreachable!() +} + +/// We use it for `localhost` tests, to detect if the Gateway and Bridge are +/// running on the same host. Then we cannot set up a +/// `[crate::hydra_client::tunnel2::Tunnel]`, because the ports are already taken. +pub fn hashed_machine_id() -> String { + const MACHINE_ID_NAMESPACE: &str = "blockfrost.machine-id.v1"; + + let mut hasher = blake3::Hasher::new(); + hasher.update(MACHINE_ID_NAMESPACE.as_bytes()); + hasher.update(b":"); + + match machine_uid::get() { + Ok(id) => { + hasher.update(id.as_bytes()); + }, + Err(e) => { + tracing::warn!(error = ?e, "machine_uid::get() failed; falling back to random bytes"); + let mut fallback = [0u8; 32]; + getrandom::fill(&mut fallback) + .expect("getrandom::fill shouldn’t fail in normal circumstances"); + hasher.update(&fallback); + }, + } + + hasher.finalize().to_hex().to_string() +} diff --git a/crates/sdk_bridge/src/main.rs b/crates/sdk_bridge/src/main.rs new file mode 100644 index 00000000..43640e06 --- /dev/null +++ b/crates/sdk_bridge/src/main.rs @@ -0,0 +1,50 @@ +mod config; +mod find_libexec; +mod http_proxy; +mod hydra_client; +mod protocol; +mod types; +mod ws_client; + +use anyhow::Result; +use clap::Parser; +use tracing::info; +use tracing_subscriber::fmt::format::Format; + +#[tokio::main] +async fn main() -> Result<()> { + let args = config::Args::parse(); + let config = config::BridgeConfig::from_args(args)?; + + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .event_format( + Format::default() + .with_ansi(true) + .with_level(true) + .with_target(false) + .compact(), + ) + .init(); + + let hydra_config = hydra_client::HydraConfig { + cardano_signing_key: config.cardano_signing_key.clone(), + node_socket_path: config.node_socket_path.clone(), + network: config.network.clone(), + }; + + let bridge = ws_client::start(ws_client::BridgeWsConfig { + ws_url: config.gateway_ws_url.clone(), + hydra: hydra_config, + }) + .await?; + + info!( + "sdk-bridge: proxying HTTP on {} -> {}", + config.listen_address, config.gateway_ws_url + ); + + http_proxy::serve(config.listen_address, bridge).await?; + + Ok(()) +} diff --git a/crates/sdk_bridge/src/protocol.rs b/crates/sdk_bridge/src/protocol.rs new file mode 100644 index 00000000..31f4bd12 --- /dev/null +++ b/crates/sdk_bridge/src/protocol.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct RequestId(pub Uuid); + +#[derive(Serialize, Deserialize, Debug)] +pub struct JsonRequest { + pub id: RequestId, + pub method: JsonRequestMethod, + pub path: String, + pub header: Vec, + pub body_base64: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct JsonResponse { + pub id: RequestId, + pub code: u16, + pub header: Vec, + pub body_base64: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct JsonHeader { + pub name: String, + pub value: String, +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Serialize, Deserialize, Debug)] +pub enum JsonRequestMethod { + GET, + POST, +} diff --git a/crates/sdk_bridge/src/types.rs b/crates/sdk_bridge/src/types.rs new file mode 100644 index 00000000..86a5821c --- /dev/null +++ b/crates/sdk_bridge/src/types.rs @@ -0,0 +1,28 @@ +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, ValueEnum, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum Network { + Mainnet, + Preprod, + Preview, +} + +impl Network { + pub fn network_magic(&self) -> u64 { + match self { + Self::Mainnet => 764824073, + Self::Preprod => 1, + Self::Preview => 2, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Mainnet => "mainnet", + Self::Preprod => "preprod", + Self::Preview => "preview", + } + } +} diff --git a/crates/sdk_bridge/src/ws_client.rs b/crates/sdk_bridge/src/ws_client.rs new file mode 100644 index 00000000..644d17f4 --- /dev/null +++ b/crates/sdk_bridge/src/ws_client.rs @@ -0,0 +1,457 @@ +use crate::hydra_client; +use crate::protocol::{JsonRequest, JsonResponse, RequestId}; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::Duration; +use tokio::sync::{Mutex, mpsc, oneshot}; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); +const WS_PING_TIMEOUT: Duration = Duration::from_secs(13); + +#[derive(Clone)] +pub struct BridgeHandle { + request_tx: mpsc::Sender, + hydra: hydra_client::HydraController, +} + +impl BridgeHandle { + pub async fn forward_request(&self, request: JsonRequest) -> Result { + let (tx, rx) = oneshot::channel(); + self.request_tx + .send(BridgeRequest { + request, + respond_to: tx, + }) + .await + .map_err(|_| BridgeError::ConnectionClosed)?; + + match tokio::time::timeout(REQUEST_TIMEOUT, rx).await { + Ok(Ok(response)) => Ok(response), + Ok(Err(_)) => Err(BridgeError::ResponseDropped), + Err(_) => Err(BridgeError::Timeout), + } + } + + pub fn hydra(&self) -> &hydra_client::HydraController { + &self.hydra + } +} + +#[derive(Debug)] +pub enum BridgeError { + ConnectionClosed, + Timeout, + ResponseDropped, +} + +pub struct BridgeWsConfig { + pub ws_url: String, + pub hydra: hydra_client::HydraConfig, +} + +pub async fn start(config: BridgeWsConfig) -> Result { + let (request_tx, request_rx) = mpsc::channel(64); + let (kex_request_tx, kex_request_rx) = mpsc::channel(32); + let (kex_response_tx, kex_response_rx) = mpsc::channel(32); + let (terminate_tx, terminate_rx) = mpsc::channel(1); + + let hydra = hydra_client::HydraController::spawn( + config.hydra, + kex_request_tx, + kex_response_rx, + terminate_rx, + ) + .await?; + + tokio::spawn(run_ws_loop( + config.ws_url, + request_rx, + kex_request_rx, + kex_response_tx, + terminate_tx, + )); + + Ok(BridgeHandle { request_tx, hydra }) +} + +struct BridgeRequest { + request: JsonRequest, + respond_to: oneshot::Sender, +} + +/// The WebSocket messages that we receive. +#[derive(Serialize, Deserialize, Debug)] +enum GatewayMessage { + Response(JsonResponse), + HydraKExResponse(hydra_client::KeyExchangeResponse), + HydraTunnel(hydra_client::tunnel2::TunnelMsg), + Ping(u64), + Pong(u64), + Error { code: u64, msg: String }, +} + +/// The WebSocket messages that we send. +#[derive(Serialize, Deserialize, Debug)] +enum BridgeMessage { + Request(JsonRequest), + HydraKExRequest(hydra_client::KeyExchangeRequest), + HydraTunnel(hydra_client::tunnel2::TunnelMsg), + Ping(u64), + Pong(u64), +} + +async fn run_ws_loop( + ws_url: String, + mut request_rx: mpsc::Receiver, + mut kex_request_rx: mpsc::Receiver, + kex_response_tx: mpsc::Sender, + terminate_tx: mpsc::Sender, +) { + let connect_result = tokio_tungstenite::connect_async(&ws_url).await; + let (ws_stream, _response) = match connect_result { + Ok(ok) => ok, + Err(err) => { + error!("sdk-bridge: failed to connect to {}: {}", ws_url, err); + let _ = terminate_tx.send(hydra_client::TerminateRequest).await; + return; + }, + }; + + info!("sdk-bridge: connected to {}", ws_url); + + let (event_tx, mut event_rx) = mpsc::channel::(64); + let (socket_tx, request_task, arbitrary_msg_task) = + wire_socket(event_tx.clone(), ws_stream, ws_url.clone()).await; + + let inflight: std::sync::Arc>>> = + std::sync::Arc::new(Mutex::new(HashMap::new())); + + let kex_fwd_task = { + let event_tx = event_tx.clone(); + tokio::spawn(async move { + while let Some(req) = kex_request_rx.recv().await { + let _ = event_tx.send(BridgeEvent::HydraKExRequest(req)).await; + } + }) + }; + + let request_fwd_task = { + let event_tx = event_tx.clone(); + tokio::spawn(async move { + while let Some(req) = request_rx.recv().await { + if event_tx.send(BridgeEvent::NewRequest(req)).await.is_err() { + break; + } + } + }) + }; + + let schedule_ping_tick = { + let event_tx = event_tx.clone(); + move || { + let tx = event_tx.clone(); + tokio::spawn(async move { + tokio::time::sleep(WS_PING_TIMEOUT).await; + let _ignored_failure: Result<_, _> = tx.send(BridgeEvent::PingTick).await; + }) + } + }; + + let mut last_ping_sent_at: Option = None; + let mut last_ping_id: u64 = 0; + let mut loop_error: Result<(), String> = Ok(()); + + // Schedule the first `PingTick` immediately, otherwise we won’t start + // checking for ping timeout: + schedule_ping_tick(); + + let tunnel_cancellation = CancellationToken::new(); + let mut tunnel_controller: Option = None; + + 'event_loop: while let Some(msg) = event_rx.recv().await { + match msg { + BridgeEvent::SocketError(err) => { + loop_error = Err(err); + break 'event_loop; + }, + + BridgeEvent::NewGatewayMessage(GatewayMessage::HydraTunnel(tun_msg)) => { + if let Some(tunnel_ctl) = &tunnel_controller { + match tunnel_ctl.on_msg(tun_msg).await { + Ok(()) => (), + Err(err) => error!( + "hydra-tunnel: got an error when passing message through WebSocket: {err}; ignoring" + ), + } + } + }, + + BridgeEvent::NewGatewayMessage(GatewayMessage::HydraKExResponse(resp)) => { + if resp.machine_id != hydra_client::verifications::hashed_machine_id() { + let (tunnel_ctl, mut tunnel_rx) = hydra_client::tunnel2::Tunnel::new( + hydra_client::tunnel2::TunnelConfig { + expose_port: resp.proposed_platform_h2h_port, + id_prefix_bit: true, + ..(hydra_client::tunnel2::TunnelConfig::default()) + }, + tunnel_cancellation.clone(), + ); + + if let Err(err) = tunnel_ctl.spawn_listener(resp.gateway_h2h_port).await { + warn!("hydra-tunnel: failed to spawn listener: {err}"); + } else { + let socket_tx_ = socket_tx.clone(); + tokio::spawn(async move { + while let Some(tun_msg) = tunnel_rx.recv().await { + if send_json_msg(&socket_tx_, &BridgeMessage::HydraTunnel(tun_msg)) + .await + .is_err() + { + break; + } + } + }); + + tunnel_controller = Some(tunnel_ctl); + } + } + + let _ = kex_response_tx.send(resp).await; + }, + + BridgeEvent::NewGatewayMessage(GatewayMessage::Response(response)) => { + let request_id = response.id.clone(); + let sender = inflight.lock().await.remove(&request_id); + if let Some(sender) = sender { + let _ = sender.send(response); + } else { + warn!( + "sdk-bridge: received response for unknown request: {:?}", + request_id + ); + } + }, + + BridgeEvent::NewGatewayMessage(GatewayMessage::Error { code, msg }) => { + warn!("sdk-bridge: gateway error {}: {}", code, msg); + }, + + BridgeEvent::NewGatewayMessage(GatewayMessage::Ping(ping_id)) => { + if let Err(err) = send_json_msg(&socket_tx, &BridgeMessage::Pong(ping_id)).await { + loop_error = Err(err); + break 'event_loop; + } + }, + + BridgeEvent::NewGatewayMessage(GatewayMessage::Pong(pong_id)) => { + if pong_id == last_ping_id { + last_ping_sent_at = None; + } + }, + + BridgeEvent::PingTick => { + if let Some(_sent_at) = last_ping_sent_at { + loop_error = Err("ping timeout".to_string()); + break 'event_loop; + } else { + schedule_ping_tick(); + last_ping_id += 1; + last_ping_sent_at = Some(std::time::Instant::now()); + if let Err(err) = + send_json_msg(&socket_tx, &BridgeMessage::Ping(last_ping_id)).await + { + loop_error = Err(err); + break 'event_loop; + } + } + }, + + BridgeEvent::HydraKExRequest(req) => { + if send_json_msg(&socket_tx, &BridgeMessage::HydraKExRequest(req)) + .await + .is_err() + { + break 'event_loop; + } + }, + + BridgeEvent::NewRequest(req) => { + let request_id = req.request.id.clone(); + inflight + .lock() + .await + .insert(request_id.clone(), req.respond_to); + if let Err(err) = + send_json_msg(&socket_tx, &BridgeMessage::Request(req.request)).await + { + loop_error = Err(err); + break 'event_loop; + } + }, + } + } + + if let Err(err) = loop_error { + warn!("sdk-bridge: WebSocket loop finished with error: {err}"); + } + + if let Some(tunnel_ctl) = &tunnel_controller { + tunnel_ctl.cancel(); + } + tunnel_cancellation.cancel(); + + let _ = terminate_tx.send(hydra_client::TerminateRequest).await; + + let inflight_keys: Vec = inflight.lock().await.keys().cloned().collect(); + for request_id in inflight_keys { + if let Some(sender) = inflight.lock().await.remove(&request_id) { + let _ = sender.send(error_response( + request_id, + 503, + "gateway WebSocket disconnected".to_string(), + )); + } + } + + let children = [ + request_task, + arbitrary_msg_task, + kex_fwd_task, + request_fwd_task, + ]; + children.iter().for_each(|t| t.abort()); + futures::future::join_all(children).await; +} + +enum BridgeEvent { + NewGatewayMessage(GatewayMessage), + NewRequest(BridgeRequest), + HydraKExRequest(hydra_client::KeyExchangeRequest), + PingTick, + SocketError(String), +} + +async fn wire_socket( + event_tx: mpsc::Sender, + socket: tokio_tungstenite::WebSocketStream< + tokio_tungstenite::MaybeTlsStream, + >, + ws_url: String, +) -> ( + mpsc::Sender, + JoinHandle<()>, + JoinHandle<()>, +) { + use futures_util::{SinkExt, StreamExt}; + + let (msg_tx, mut msg_rx) = mpsc::channel::(64); + let (mut sock_tx, mut sock_rx) = socket.split(); + + let request_task = tokio::spawn(async move { + 'read_loop: loop { + match sock_rx.next().await { + None => { + let _ignored_failure: Result<_, _> = event_tx + .send(BridgeEvent::SocketError("connection closed".to_string())) + .await; + break 'read_loop; + }, + Some(Err(err)) => { + let _ignored_failure: Result<_, _> = event_tx + .send(BridgeEvent::SocketError(format!("stream error: {err:?}"))) + .await; + break 'read_loop; + }, + Some(Ok(tungstenite::protocol::Message::Close(frame))) => { + warn!("sdk-bridge: gateway disconnected (CloseFrame: {:?})", frame); + let _ignored_failure: Result<_, _> = event_tx + .send(BridgeEvent::SocketError("gateway disconnected".to_string())) + .await; + break 'read_loop; + }, + Some(Ok( + tungstenite::protocol::Message::Frame(_) + | tungstenite::protocol::Message::Ping(_) + | tungstenite::protocol::Message::Pong(_), + )) => {}, + Some(Ok(tungstenite::protocol::Message::Binary(bin))) => { + warn!( + "sdk-bridge: received unexpected binary message: {:?}", + hex::encode(bin), + ); + }, + Some(Ok(tungstenite::protocol::Message::Text(text))) => { + match serde_json::from_str::(&text) { + Ok(msg) => { + if event_tx + .send(BridgeEvent::NewGatewayMessage(msg)) + .await + .is_err() + { + break 'read_loop; + } + }, + Err(err) => warn!( + "sdk-bridge: received unparsable text message from {}: {:?}: {:?}", + ws_url, text, err, + ), + }; + }, + } + } + }); + + let arbitrary_msg_task = tokio::spawn(async move { + while let Some(msg) = msg_rx.recv().await { + match sock_tx.send(msg).await { + Ok(()) => (), + Err(err) => { + error!("sdk-bridge: error when sending a message: {:?}", err); + break; + }, + } + } + }); + + (msg_tx, request_task, arbitrary_msg_task) +} + +async fn send_json_msg( + socket_tx: &mpsc::Sender, + msg: &J, +) -> Result<(), String> +where + J: ?Sized + serde::ser::Serialize, +{ + match serde_json::to_string(msg) { + Ok(msg) => match socket_tx + .send(tungstenite::protocol::Message::Text(msg.into())) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("sdk-bridge: error when sending a message: {:?}", err); + Err("broken connection with the gateway".to_string()) + }, + }, + Err(err) => { + let err = + format!("error when serializing request to JSON (this will never happen): {err:?}"); + error!("sdk-bridge: {}", err); + Err(err) + }, + } +} + +fn error_response(request_id: RequestId, code: u16, msg: String) -> JsonResponse { + JsonResponse { + id: request_id, + code, + header: vec![], + body_base64: msg, + } +} From 09d681e44359796cda8223a566546b492443c6ab Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 19 Feb 2026 19:18:22 +0100 Subject: [PATCH 08/23] fix: the minimal top-up bug in the Bridge --- crates/sdk_bridge/src/hydra_client/mod.rs | 46 +++++++++++++++++------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/crates/sdk_bridge/src/hydra_client/mod.rs b/crates/sdk_bridge/src/hydra_client/mod.rs index 44690056..ab0a61dc 100644 --- a/crates/sdk_bridge/src/hydra_client/mod.rs +++ b/crates/sdk_bridge/src/hydra_client/mod.rs @@ -13,6 +13,7 @@ pub mod tunnel2; pub mod verifications; const MIN_FUEL_LOVELACE: u64 = 15_000_000; +const MIN_COMMIT_TOPUP_LOVELACE: u64 = 1_000_000; const CREDIT_POLL_INTERVAL: Duration = Duration::from_secs(1); #[derive(Clone, Debug)] @@ -491,17 +492,38 @@ impl State { let params = self.payment_params.clone().ok_or(anyhow!( "payment parameters not set before funding commit address" ))?; - self.fund_address( - &self - .derive_enterprise_address_from_skey( - &self.config.cardano_signing_key, - ) - .await?, - &self.commit_wallet_addr, - (params.commit_ada * 1_000_000.0).round() as u64, - &self.config.cardano_signing_key, - ) - .await?; + + let target_lovelace = (params.commit_ada * 1_000_000.0).round() as u64; + let current_lovelace = self + .lovelace_on_payment_skey(&self.commit_wallet_skey) + .await?; + + if current_lovelace < target_lovelace { + let mut top_up = target_lovelace - current_lovelace; + if top_up < MIN_COMMIT_TOPUP_LOVELACE { + top_up = MIN_COMMIT_TOPUP_LOVELACE; + } + info!( + "hydra-controller: topping up commit address by {} lovelace (current={}, target={})", + top_up, current_lovelace, target_lovelace + ); + self.fund_address( + &self + .derive_enterprise_address_from_skey( + &self.config.cardano_signing_key, + ) + .await?, + &self.commit_wallet_addr, + top_up, + &self.config.cardano_signing_key, + ) + .await?; + } else { + info!( + "hydra-controller: commit address already funded (current={}, target={})", + current_lovelace, target_lovelace + ); + } self.send_delayed(Event::TryToCommit, Duration::from_secs(3)) .await @@ -590,7 +612,7 @@ impl State { info!("hydra-controller: state changed from {old} to {new}"); if new == "Initial" { - self.send_delayed(Event::TryToCommit, Duration::from_secs(1)) + self.send_delayed(Event::FundCommitAddr, Duration::from_secs(1)) .await; } } From 5dc07a170795f9a6c8769e937a391bbf5badaa77 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 19 Feb 2026 19:58:05 +0100 Subject: [PATCH 09/23] fix: get rid of the dust causing `BabbageOutputTooSmallUTxO` --- crates/sdk_bridge/src/hydra_client/mod.rs | 37 +++-- .../src/hydra_client/verifications.rs | 132 ++++++++++++++---- 2 files changed, 132 insertions(+), 37 deletions(-) diff --git a/crates/sdk_bridge/src/hydra_client/mod.rs b/crates/sdk_bridge/src/hydra_client/mod.rs index ab0a61dc..75be72cd 100644 --- a/crates/sdk_bridge/src/hydra_client/mod.rs +++ b/crates/sdk_bridge/src/hydra_client/mod.rs @@ -179,6 +179,7 @@ struct State { last_hydra_head_state: String, hydra_pid: Option, hydra_head_open: bool, + head_open_initialized: bool, credits_available: Arc, head_open_flag: Arc, credits_last_balance: u64, @@ -232,6 +233,7 @@ impl State { last_hydra_head_state: String::new(), hydra_pid: None, hydra_head_open: false, + head_open_initialized: false, credits_available, head_open_flag, credits_last_balance: 0, @@ -348,6 +350,7 @@ impl State { self.accounted_requests = 0; self.sent_microtransactions = 0; self.prepay_sent = false; + self.head_open_initialized = false; self.last_hydra_head_state = String::new(); }, @@ -584,17 +587,10 @@ impl State { status ); if status == "Open" { - self.hydra_head_open = true; - self.head_open_flag.store(true, Ordering::SeqCst); - self.credits_available.store(0, Ordering::SeqCst); - self.credits_last_balance = 0; - self.prepay_sent = false; self.last_hydra_head_state = status.clone(); - self.send_delayed(Event::MonitorCredits, CREDIT_POLL_INTERVAL) - .await; self.send_delayed(Event::MonitorStates, Duration::from_secs(5)) .await; - self.send_prepay_microtransaction().await?; + self.on_head_open().await?; } else { self.send_delayed(Event::WaitForOpen, Duration::from_secs(3)) .await @@ -618,13 +614,13 @@ impl State { } if new_status == "Open" { - self.hydra_head_open = true; - self.head_open_flag.store(true, Ordering::SeqCst); + self.on_head_open().await?; } else { self.hydra_head_open = false; self.head_open_flag.store(false, Ordering::SeqCst); self.credits_available.store(0, Ordering::SeqCst); self.credits_last_balance = 0; + self.head_open_initialized = false; } self.send_delayed(Event::MonitorStates, Duration::from_secs(5)) @@ -765,6 +761,27 @@ impl State { Ok(()) } + async fn on_head_open(&mut self) -> Result<()> { + if self.head_open_initialized { + self.hydra_head_open = true; + self.head_open_flag.store(true, Ordering::SeqCst); + return Ok(()); + } + + self.head_open_initialized = true; + self.hydra_head_open = true; + self.head_open_flag.store(true, Ordering::SeqCst); + self.credits_available.store(0, Ordering::SeqCst); + self.credits_last_balance = 0; + self.accounted_requests = 0; + self.sent_microtransactions = 0; + self.prepay_sent = false; + self.send_delayed(Event::MonitorCredits, CREDIT_POLL_INTERVAL) + .await; + self.send_prepay_microtransaction().await?; + Ok(()) + } + async fn start_hydra_node(&mut self, kex_response: KeyExchangeResponse) -> Result<()> { use std::process::Stdio; use tokio::io::{AsyncBufReadExt, BufReader}; diff --git a/crates/sdk_bridge/src/hydra_client/verifications.rs b/crates/sdk_bridge/src/hydra_client/verifications.rs index 623672ac..563dc2f2 100644 --- a/crates/sdk_bridge/src/hydra_client/verifications.rs +++ b/crates/sdk_bridge/src/hydra_client/verifications.rs @@ -5,6 +5,8 @@ use tracing::info; use crate::types::Network; +const MIN_OUTPUT_LOVELACE: u64 = 840_450; + /// FIXME: don’t use `cardano-cli`. /// /// FIXME: proper errors, not `anyhow!` @@ -425,6 +427,34 @@ impl super::State { ) -> Result<()> { use anyhow::Context; + fn utxo_lovelace(entry: &Value) -> Option { + if let Some(v) = entry.pointer("/value/lovelace") { + if let Some(n) = v.as_u64() { + return Some(n); + } + if let Some(s) = v.as_str() { + return s.parse().ok(); + } + } + + if let Some(amounts) = entry.get("amount").and_then(Value::as_array) { + for item in amounts { + if item.get("unit").and_then(Value::as_str) == Some("lovelace") { + if let Some(q) = item.get("quantity") { + if let Some(n) = q.as_u64() { + return Some(n); + } + if let Some(s) = q.as_str() { + return s.parse().ok(); + } + } + } + } + } + + None + } + let snapshot_url = format!("http://127.0.0.1:{hydra_api_port}/snapshot/utxo"); let utxo: Value = reqwest::Client::new() .get(&snapshot_url) @@ -446,42 +476,90 @@ impl super::State { } } - let (tx_in, chosen_entry) = filtered - .iter() - .next() - .map(|(k, v)| (k.clone(), v.clone())) - .ok_or_else(|| anyhow!("no UTxO found for sender address"))?; + if amount_lovelace < MIN_OUTPUT_LOVELACE { + return Err(anyhow!( + "amount_lovelace {amount_lovelace} is below minimum output lovelace {MIN_OUTPUT_LOVELACE}" + )); + } + + let mut candidates: Vec<(String, u64)> = Vec::new(); + for (k, v) in filtered.iter() { + let lovelace_total = utxo_lovelace(v).context("utxo entry: expected lovelace value")?; + candidates.push((k.clone(), lovelace_total)); + } + + if candidates.is_empty() { + return Err(anyhow!("no UTxO found for sender address")); + } + + candidates.sort_by_key(|(_, value)| *value); + + let min_total = amount_lovelace + .checked_add(MIN_OUTPUT_LOVELACE) + .ok_or_else(|| anyhow!("amount_lovelace overflow"))?; - let lovelace_total = chosen_entry - .pointer("/value/lovelace") - .and_then(Value::as_u64) - .context("utxo entry: expected .value.lovelace as integer")?; + let mut selected: Vec = Vec::new(); + let mut selected_total: u64 = 0; - if lovelace_total < amount_lovelace { + if let Some((tx_in, total)) = candidates + .iter() + .find(|(_, total)| *total == amount_lovelace) + { + selected.push(tx_in.clone()); + selected_total = *total; + } else if let Some((tx_in, total)) = candidates + .iter() + .find(|(_, total)| *total > amount_lovelace && *total >= min_total) + { + selected.push(tx_in.clone()); + selected_total = *total; + } else { + for (tx_in, total) in &candidates { + selected_total = selected_total + .checked_add(*total) + .ok_or_else(|| anyhow!("utxo sum overflow"))?; + selected.push(tx_in.clone()); + if selected_total == amount_lovelace || selected_total >= min_total { + break; + } + } + } + + if selected_total < amount_lovelace { return Err(anyhow!( - "insufficient lovelace in selected UTxO: {lovelace_total} < {amount_lovelace}" + "insufficient lovelace in available UTxOs: {selected_total} < {amount_lovelace}" )); } - let change = lovelace_total - amount_lovelace; + let change = selected_total - amount_lovelace; + if change > 0 && change < MIN_OUTPUT_LOVELACE { + return Err(anyhow!( + "change output {change} is below minimum output lovelace {MIN_OUTPUT_LOVELACE}" + )); + } let tx_body: serde_json::Value = { - let args: &[&str] = &[ - "latest", - "transaction", - "build-raw", - "--tx-in", - &tx_in, - "--tx-out", - &format!("{receiver_addr}+{amount_lovelace}"), - "--tx-out", - &format!("{sender_addr}+{change}"), - "--fee", - "0", - "--out-file", - "/dev/stdout", + let mut args = vec![ + "latest".to_string(), + "transaction".to_string(), + "build-raw".to_string(), ]; - self.cardano_cli_capture(args, None).await?.0 + for tx_in in &selected { + args.push("--tx-in".to_string()); + args.push(tx_in.clone()); + } + args.push("--tx-out".to_string()); + args.push(format!("{receiver_addr}+{amount_lovelace}")); + if change > 0 { + args.push("--tx-out".to_string()); + args.push(format!("{sender_addr}+{change}")); + } + args.push("--fee".to_string()); + args.push("0".to_string()); + args.push("--out-file".to_string()); + args.push("/dev/stdout".to_string()); + let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + self.cardano_cli_capture(&args_ref, None).await?.0 }; let tx_signed: serde_json::Value = { From b73e9acd26a1af7515424831296b06b9e5b433be Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 19 Feb 2026 22:44:34 +0100 Subject: [PATCH 10/23] fix: allow Bridge request with non-Open head, as long as there are credits --- crates/gateway/src/hydra_server_bridge/mod.rs | 19 +---------------- crates/gateway/src/sdk_bridge_ws.rs | 7 ------- crates/sdk_bridge/src/http_proxy.rs | 3 --- crates/sdk_bridge/src/hydra_client/mod.rs | 21 +------------------ 4 files changed, 2 insertions(+), 48 deletions(-) diff --git a/crates/gateway/src/hydra_server_bridge/mod.rs b/crates/gateway/src/hydra_server_bridge/mod.rs index 761f09f5..8d441a9b 100644 --- a/crates/gateway/src/hydra_server_bridge/mod.rs +++ b/crates/gateway/src/hydra_server_bridge/mod.rs @@ -5,7 +5,7 @@ use serde::Deserialize; use std::path::PathBuf; use std::sync::{ Arc, - atomic::{AtomicBool, AtomicU64, Ordering}, + atomic::{AtomicU64, Ordering}, }; use std::time::Duration; use tokio::sync::mpsc; @@ -241,20 +241,17 @@ impl HydraConfig { pub struct HydraController { event_tx: mpsc::Sender, credits_available: Arc, - head_open: Arc, _controller_counter: Arc<()>, } #[derive(Debug)] pub enum CreditError { - HeadNotOpen, InsufficientCredits, } impl std::fmt::Display for CreditError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - CreditError::HeadNotOpen => write!(f, "hydra head is not open"), CreditError::InsufficientCredits => write!(f, "insufficient prepaid credits"), } } @@ -304,20 +301,17 @@ impl HydraController { kex_resp: KeyExchangeResponse, ) -> Result { let credits_available = Arc::new(AtomicU64::new(0)); - let head_open = Arc::new(AtomicBool::new(false)); let event_tx = State::spawn( config, customer_id.clone(), kex_req, kex_resp, credits_available.clone(), - head_open.clone(), ) .await?; Ok(Self { event_tx, credits_available, - head_open, _controller_counter: controller_counter, }) } @@ -328,10 +322,6 @@ impl HydraController { } pub fn try_consume_credit(&self) -> Result<(), CreditError> { - if !self.head_open.load(Ordering::SeqCst) { - return Err(CreditError::HeadNotOpen); - } - let mut current = self.credits_available.load(Ordering::SeqCst); loop { if current == 0 { @@ -392,7 +382,6 @@ struct State { hydra_peers_connected: bool, hydra_head_open: bool, credits_available: Arc, - head_open_flag: Arc, credits_last_balance: u64, received_microtransactions: u64, is_closing: bool, @@ -408,7 +397,6 @@ impl State { kex_req: KeyExchangeRequest, kex_resp: KeyExchangeResponse, credits_available: Arc, - head_open_flag: Arc, ) -> Result> { let config_dir = mk_config_dir(&config.network, &customer_id)?; let customer_log_id = format!("customer-{customer_id}"); @@ -427,7 +415,6 @@ impl State { hydra_peers_connected: false, hydra_head_open: false, credits_available, - head_open_flag, credits_last_balance: 0, received_microtransactions: 0, is_closing: false, @@ -477,7 +464,6 @@ impl State { Event::Restart => { info!("hydra-controller: {}: starting…", self.customer_log_id); self.hydra_head_open = false; - self.head_open_flag.store(false, Ordering::SeqCst); self.credits_available.store(0, Ordering::SeqCst); self.credits_last_balance = 0; self.received_microtransactions = 0; @@ -570,8 +556,6 @@ impl State { ); if status == "Open" { self.hydra_head_open = true; - self.head_open_flag.store(true, Ordering::SeqCst); - self.credits_available.store(0, Ordering::SeqCst); self.credits_last_balance = 0; self.received_microtransactions = 0; self.send_delayed(Event::MonitorCredits, CREDIT_POLL_INTERVAL) @@ -655,7 +639,6 @@ impl State { Event::TryToClose => { self.hydra_head_open = false; - self.head_open_flag.store(false, Ordering::SeqCst); verifications::send_one_websocket_msg( &format!("ws://127.0.0.1:{}", self.api_port), serde_json::json!({"tag":"Close"}), diff --git a/crates/gateway/src/sdk_bridge_ws.rs b/crates/gateway/src/sdk_bridge_ws.rs index 6c511e16..96c20371 100644 --- a/crates/gateway/src/sdk_bridge_ws.rs +++ b/crates/gateway/src/sdk_bridge_ws.rs @@ -261,13 +261,6 @@ pub mod event_loop { } else { match ctl.try_consume_credit() { Ok(()) => None, - Err(hydra_server_bridge::CreditError::HeadNotOpen) => { - Some(error_response( - request_id, - StatusCode::SERVICE_UNAVAILABLE, - "Hydra head is not open".to_string(), - )) - }, Err(hydra_server_bridge::CreditError::InsufficientCredits) => { Some(error_response( request_id, diff --git a/crates/sdk_bridge/src/http_proxy.rs b/crates/sdk_bridge/src/http_proxy.rs index 20b86d0a..4e1d1d43 100644 --- a/crates/sdk_bridge/src/http_proxy.rs +++ b/crates/sdk_bridge/src/http_proxy.rs @@ -38,9 +38,6 @@ async fn proxy_route( match state.bridge.hydra().try_reserve_credit() { Ok(()) => (), - Err(crate::hydra_client::CreditError::HeadNotOpen) => { - return (StatusCode::SERVICE_UNAVAILABLE, "Hydra head is not open").into_response(); - }, Err(crate::hydra_client::CreditError::InsufficientCredits) => { return (StatusCode::PAYMENT_REQUIRED, "Prepaid credits exhausted").into_response(); }, diff --git a/crates/sdk_bridge/src/hydra_client/mod.rs b/crates/sdk_bridge/src/hydra_client/mod.rs index 75be72cd..24dbb044 100644 --- a/crates/sdk_bridge/src/hydra_client/mod.rs +++ b/crates/sdk_bridge/src/hydra_client/mod.rs @@ -3,7 +3,7 @@ use anyhow::{Result, anyhow}; use std::path::PathBuf; use std::sync::{ Arc, - atomic::{AtomicBool, AtomicU64, Ordering}, + atomic::{AtomicU64, Ordering}, }; use std::time::Duration; use tokio::sync::mpsc; @@ -30,19 +30,16 @@ pub struct HydraConfig { pub struct HydraController { event_tx: mpsc::Sender, credits_available: Arc, - head_open: Arc, } #[derive(Debug)] pub enum CreditError { - HeadNotOpen, InsufficientCredits, } impl std::fmt::Display for CreditError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - CreditError::HeadNotOpen => write!(f, "hydra head is not open"), CreditError::InsufficientCredits => write!(f, "insufficient prepaid credits"), } } @@ -92,28 +89,21 @@ impl HydraController { terminate_reqs: mpsc::Receiver, ) -> Result { let credits_available = Arc::new(AtomicU64::new(0)); - let head_open = Arc::new(AtomicBool::new(false)); let event_tx = State::spawn( config, kex_requests, kex_responses, terminate_reqs, credits_available.clone(), - head_open.clone(), ) .await?; Ok(Self { event_tx, credits_available, - head_open, }) } pub fn try_reserve_credit(&self) -> Result<(), CreditError> { - if !self.head_open.load(Ordering::SeqCst) { - return Err(CreditError::HeadNotOpen); - } - let mut current = self.credits_available.load(Ordering::SeqCst); loop { if current == 0 { @@ -181,7 +171,6 @@ struct State { hydra_head_open: bool, head_open_initialized: bool, credits_available: Arc, - head_open_flag: Arc, credits_last_balance: u64, accounted_requests: u64, sent_microtransactions: u64, @@ -200,7 +189,6 @@ impl State { kex_responses: mpsc::Receiver, terminate_reqs: mpsc::Receiver, credits_available: Arc, - head_open_flag: Arc, ) -> Result> { let hydra_node_exe = crate::find_libexec::find_libexec("hydra-node", "HYDRA_NODE_PATH", &["--version"]) @@ -235,7 +223,6 @@ impl State { hydra_head_open: false, head_open_initialized: false, credits_available, - head_open_flag, credits_last_balance: 0, accounted_requests: 0, sent_microtransactions: 0, @@ -344,7 +331,6 @@ impl State { .await?; self.hydra_head_open = false; - self.head_open_flag.store(false, Ordering::SeqCst); self.credits_available.store(0, Ordering::SeqCst); self.credits_last_balance = 0; self.accounted_requests = 0; @@ -617,8 +603,6 @@ impl State { self.on_head_open().await?; } else { self.hydra_head_open = false; - self.head_open_flag.store(false, Ordering::SeqCst); - self.credits_available.store(0, Ordering::SeqCst); self.credits_last_balance = 0; self.head_open_initialized = false; } @@ -764,14 +748,11 @@ impl State { async fn on_head_open(&mut self) -> Result<()> { if self.head_open_initialized { self.hydra_head_open = true; - self.head_open_flag.store(true, Ordering::SeqCst); return Ok(()); } self.head_open_initialized = true; self.hydra_head_open = true; - self.head_open_flag.store(true, Ordering::SeqCst); - self.credits_available.store(0, Ordering::SeqCst); self.credits_last_balance = 0; self.accounted_requests = 0; self.sent_microtransactions = 0; From 9403975d3b018c8337639c8502c87882bab36283 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 19 Feb 2026 22:50:40 +0100 Subject: [PATCH 11/23] chore: edit the default Bridge port for the videos --- crates/sdk_bridge/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sdk_bridge/src/config.rs b/crates/sdk_bridge/src/config.rs index 31358053..f44b1d0e 100644 --- a/crates/sdk_bridge/src/config.rs +++ b/crates/sdk_bridge/src/config.rs @@ -14,7 +14,7 @@ pub struct Args { )] pub gateway_ws_url: String, - #[arg(long, default_value = "127.0.0.1:3001")] + #[arg(long, default_value = "127.0.0.1:3002")] pub listen_address: String, #[arg(long, value_enum)] From 473bb17e84f3378eb2ee4f598ef9efc352388f92 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Fri, 20 Feb 2026 00:18:20 +0100 Subject: [PATCH 12/23] =?UTF-8?q?chore:=20using=203=20=C2=B5transactions/f?= =?UTF-8?q?anout=20looks=20better?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/gateway/config/development.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gateway/config/development.toml b/crates/gateway/config/development.toml index e20b7caa..92e450a3 100644 --- a/crates/gateway/config/development.toml +++ b/crates/gateway/config/development.toml @@ -23,7 +23,7 @@ microtransactions_per_fanout = 2 max_concurrent_hydra_nodes = 2 cardano_signing_key = "/home/mw/.config/blockfrost-gateway/hydra/preview/_our-keys/payment/payment.sk" node_socket_path = "/home/mw/.local/share/blockfrost-platform/preview/node.socket" -commit_ada = 3.0 +commit_ada = 4.0 lovelace_per_request = 100_000 requests_per_microtransaction = 10 -microtransactions_per_fanout = 2 +microtransactions_per_fanout = 3 From 88b450d1d98cedcad63f71cf1cbdd7d6d8621929 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Mon, 9 Feb 2026 23:46:58 +0100 Subject: [PATCH 13/23] fix(ci): try to fix the macOS build (missing `libpq`) --- .github/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index af6a0d42..c1ca58cd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -90,6 +90,12 @@ jobs: toolchain: ${{ matrix.rust }} components: rustfmt, rust-src, clippy + - name: Install libpq (macOS) + if: runner.os == 'macOS' + run: | + brew install libpq + brew link --force libpq + - name: Build run: cargo build --workspace --release --verbose From bd858322290f15072a03308451e038b1bc472b7a Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Mon, 9 Feb 2026 23:55:33 +0100 Subject: [PATCH 14/23] =?UTF-8?q?fix(ci):=20try=20to=20fix=20the=20macOS?= =?UTF-8?q?=20build=20(missing=20`libpq`),=20take=202=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c1ca58cd..59220319 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -95,6 +95,9 @@ jobs: run: | brew install libpq brew link --force libpq + echo "LIBRARY_PATH=$(brew --prefix libpq)/lib:$LIBRARY_PATH" >> "$GITHUB_ENV" + echo "CPATH=$(brew --prefix libpq)/include:$CPATH" >> "$GITHUB_ENV" + echo "PKG_CONFIG_PATH=$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" >> "$GITHUB_ENV" - name: Build run: cargo build --workspace --release --verbose From 639c3dfa8a3b746ef55c0c5242694eca7af7bf66 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Mon, 23 Feb 2026 09:34:37 +0100 Subject: [PATCH 15/23] fix: Nix archives --- flake.nix | 4 ++-- nix/internal/darwin.nix | 18 +++++++++--------- nix/internal/linux.nix | 10 +++++----- nix/internal/windows.nix | 18 +++++++++--------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/flake.nix b/flake.nix index 85223692..9b20c5f2 100644 --- a/flake.nix +++ b/flake.nix @@ -95,7 +95,7 @@ inherit (internal) tx-build cardano-address testgen-hs; } // (lib.optionalAttrs (system == "x86_64-linux") { - blockfrost-platform-x86_64-windows = inputs.self.internal.x86_64-windows.package; + blockfrost-platform-x86_64-windows = inputs.self.internal.x86_64-windows.blockfrost-platform; }); devshells.default = import ./nix/devshells.nix {inherit inputs;}; @@ -173,7 +173,7 @@ crossSystems = ["x86_64-windows"]; allJobs = { blockfrost-platform = lib.genAttrs (config.systems ++ crossSystems) ( - targetSystem: inputs.self.internal.${targetSystem}.package + targetSystem: inputs.self.internal.${targetSystem}.blockfrost-platform ); devshell = lib.genAttrs config.systems ( targetSystem: inputs.self.devShells.${targetSystem}.default diff --git a/nix/internal/darwin.nix b/nix/internal/darwin.nix index 405e9c51..c9fd31bf 100644 --- a/nix/internal/darwin.nix +++ b/nix/internal/darwin.nix @@ -11,15 +11,15 @@ in unix // rec { archive = let - outFileName = "${unix.package.pname}-${unix.package.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.tar.bz2"; + outFileName = "${unix.blockfrost-platform.pname}-${unix.blockfrost-platform.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.tar.bz2"; in - pkgs.runCommandNoCC "${unix.package.pname}-archive" { + pkgs.runCommandNoCC "${unix.blockfrost-platform.pname}-archive" { passthru = {inherit outFileName;}; } '' - cp -r ${bundle} ${unix.package.pname} + cp -r ${bundle} ${unix.blockfrost-platform.pname} mkdir -p $out - tar -cjvf $out/${outFileName} ${unix.package.pname}/ + tar -cjvf $out/${outFileName} ${unix.blockfrost-platform.pname}/ # Make it downloadable from Hydra: mkdir -p $out/nix-support @@ -40,7 +40,7 @@ in }; # Portable directory that can be run on any modern Darwin: - bundle = (nix-bundle-exe-lib-subdir "${unix.package}/libexec/${unix.packageName.pname}") + bundle = (nix-bundle-exe-lib-subdir "${unix.blockfrost-platform}/libexec/${unix.packageName.pname}") .overrideAttrs (drv: { name = unix.packageName.pname; buildCommand = @@ -72,7 +72,7 @@ in # repo. We replace that workdir on each release. homebrew-tap = pkgs.runCommandNoCC "homebrew-repo" { - inherit (unix.package) version; + inherit (unix.blockfrost-platform) version; url_x86_64 = "${unix.releaseBaseUrl}/${inputs.self.internal.x86_64-darwin.archive.outFileName}"; url_aarch64 = "${unix.releaseBaseUrl}/${inputs.self.internal.aarch64-darwin.archive.outFileName}"; } '' @@ -127,9 +127,9 @@ in CFBundleDisplayName ${appName} CFBundleVersion - ${unix.package.version}-${inputs.self.shortRev or "dirty"} + ${unix.blockfrost-platform.version}-${inputs.self.shortRev or "dirty"} CFBundleShortVersionString - ${unix.package.version} + ${unix.blockfrost-platform.version} CFBundleIconFile iconset LSMinimumSystemVersion @@ -244,7 +244,7 @@ in }; make-dmg = {doSign ? false}: let - outFileName = "${unix.package.pname}-${unix.package.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.dmg"; + outFileName = "${unix.blockfrost-platform.pname}-${unix.blockfrost-platform.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.dmg"; credentials = "/var/lib/buildkite-agent-default/signing.sh"; codeSigningConfig = "/var/lib/buildkite-agent-default/code-signing-config.json"; signingConfig = "/var/lib/buildkite-agent-default/signing-config.json"; diff --git a/nix/internal/linux.nix b/nix/internal/linux.nix index c75d70f6..5597fc30 100644 --- a/nix/internal/linux.nix +++ b/nix/internal/linux.nix @@ -10,13 +10,13 @@ in unix // rec { archive = let - outFileName = "${unix.package.pname}-${unix.package.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.tar.bz2"; + outFileName = "${unix.blockfrost-platform.pname}-${unix.blockfrost-platform.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.tar.bz2"; in - pkgs.runCommandNoCC "${unix.package.pname}-archive" {} '' - cp -r ${bundle} ${unix.package.pname} + pkgs.runCommandNoCC "${unix.blockfrost-platform.pname}-archive" {} '' + cp -r ${bundle} ${unix.blockfrost-platform.pname} mkdir -p $out - tar -cjvf $out/${outFileName} ${unix.package.pname}/ + tar -cjvf $out/${outFileName} ${unix.blockfrost-platform.pname}/ # Make it downloadable from Hydra: mkdir -p $out/nix-support @@ -32,7 +32,7 @@ in bin_dir = "bin"; exe_dir = "exe"; lib_dir = "lib"; - } "${unix.package}/libexec/${unix.packageName.pname}") + } "${unix.blockfrost-platform}/libexec/${unix.packageName.pname}") .overrideAttrs (drv: { name = unix.packageName.pname; buildCommand = diff --git a/nix/internal/windows.nix b/nix/internal/windows.nix index 85e62c2b..7c5cdada 100644 --- a/nix/internal/windows.nix +++ b/nix/internal/windows.nix @@ -47,7 +47,7 @@ in rec { GIT_REVISION = inputs.self.rev or "dirty"; - package = craneLib.buildPackage (commonArgs + blockfrost-platform = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts GIT_REVISION; doCheck = false; # we run Windows tests on real Windows on GHA @@ -81,8 +81,8 @@ in rec { uninstaller = pkgs.runCommandNoCC "uninstaller" { buildInputs = [nsis pkgs.wine]; - projectName = package.pname; - projectVersion = package.version; + projectName = blockfrost-platform.pname; + projectVersion = blockfrost-platform.version; WINEDEBUG = "-all"; # comment out to get normal output (err,fixme), or set to +all for a flood } '' mkdir home @@ -116,12 +116,12 @@ in rec { }; make-installer = {doSign ? false}: let - outFileName = "${package.pname}-${package.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.exe"; + outFileName = "${blockfrost-platform.pname}-${blockfrost-platform.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.exe"; installer-nsi = pkgs.runCommandNoCC "installer.nsi" { inherit outFileName; - projectName = package.pname; - projectVersion = package.version; + projectName = blockfrost-platform.pname; + projectVersion = blockfrost-platform.version; installerIconPath = "icon.ico"; lockfileName = "lockfile"; } '' @@ -188,7 +188,7 @@ in rec { archive = pkgs.runCommandNoCC "archive" { buildInputs = with pkgs; [zip]; - outFileName = "${package.pname}-${package.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.zip"; + outFileName = "${blockfrost-platform.pname}-${blockfrost-platform.version}-${inputs.self.shortRev or "dirty"}-${targetSystem}.zip"; } '' cp -r ${bundle} ${packageName.pname} mkdir -p $out @@ -245,7 +245,7 @@ in rec { }; packageWithIcon = - pkgs.runCommand package.name { + pkgs.runCommand blockfrost-platform.name { buildInputs = with pkgs; [ wine winetricks @@ -262,7 +262,7 @@ in rec { set +e wine ${resource-hacker}/ResourceHacker.exe \ -log res-hack.log \ - -open "$(winepath -w ${package}/bin/*.exe)" \ + -open "$(winepath -w ${blockfrost-platform}/bin/*.exe)" \ -save with-icon.exe \ -action addoverwrite \ -res "$(winepath -w ${icon})" \ From e394f56c02c3c7e684fb7f4f30925a07422f2998 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Mon, 23 Feb 2026 10:21:13 +0100 Subject: [PATCH 16/23] fix(build): Windows --- crates/gateway/Cargo.toml | 8 +++-- crates/gateway/src/db.rs | 34 ++++++++++++++++--- crates/gateway/src/errors.rs | 4 +++ .../hydra_server_platform/verifications.rs | 2 +- crates/gateway/src/lib.rs | 1 + crates/gateway/src/models.rs | 25 ++++++++------ nix/internal/windows.nix | 27 ++++++++++++++- 7 files changed, 82 insertions(+), 19 deletions(-) diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml index 1ad14f1b..80522851 100644 --- a/crates/gateway/Cargo.toml +++ b/crates/gateway/Cargo.toml @@ -24,9 +24,6 @@ tokio-tungstenite = "0.24" tungstenite = "0.24" tracing = "0.1.40" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } -deadpool-diesel = { version = "0.6.1", features = ["postgres"] } -diesel = { version = "2.2.3", features = ["chrono", "postgres"] } -diesel_migrations = "2" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.122" colored = "2.1.0" @@ -50,6 +47,11 @@ getrandom = "0.3" [target.'cfg(unix)'.dependencies] nix = { version = "0.30", default-features = false, features = ["signal"] } +[target.'cfg(not(target_os = "windows"))'.dependencies] +deadpool-diesel = { version = "0.6.1", features = ["postgres"] } +diesel = { version = "2.2.3", features = ["chrono", "postgres"] } +diesel_migrations = "2" + [lib] name = "blockfrost_gateway" path = "src/lib.rs" diff --git a/crates/gateway/src/db.rs b/crates/gateway/src/db.rs index a78e270b..a4bf27ba 100644 --- a/crates/gateway/src/db.rs +++ b/crates/gateway/src/db.rs @@ -1,20 +1,31 @@ use crate::errors::APIError; -use crate::{ - models::{Request, RequestNewItem, User}, - schema, -}; +use crate::models::{Request, RequestNewItem, User}; + +#[cfg(not(target_os = "windows"))] +use crate::schema; +#[cfg(not(target_os = "windows"))] use deadpool_diesel::postgres::{Manager, Pool}; +#[cfg(not(target_os = "windows"))] use diesel::prelude::*; +#[cfg(not(target_os = "windows"))] use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; +#[cfg(not(target_os = "windows"))] use schema::users::dsl::*; +#[cfg(not(target_os = "windows"))] pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/"); +#[cfg(not(target_os = "windows"))] #[derive(Clone)] pub struct DB { pool: Pool, } +#[cfg(target_os = "windows")] +#[derive(Clone)] +pub struct DB; + +#[cfg(not(target_os = "windows"))] impl DB { pub async fn new(database_url: &str) -> Self { let manager = Manager::new(database_url, deadpool_diesel::Runtime::Tokio1); @@ -95,3 +106,18 @@ impl DB { } } } + +#[cfg(target_os = "windows")] +impl DB { + pub async fn new(_database_url: &str) -> Self { + unimplemented!("Postgres-backed DB is not available for Windows targets"); + } + + pub async fn insert_request(&self, _request: RequestNewItem) -> Result { + unimplemented!("Postgres-backed DB is not available for Windows targets"); + } + + pub async fn authorize_user(&self, _secret_param: String) -> Result { + unimplemented!("Postgres-backed DB is not available for Windows targets"); + } +} diff --git a/crates/gateway/src/errors.rs b/crates/gateway/src/errors.rs index a347d80b..512dc979 100644 --- a/crates/gateway/src/errors.rs +++ b/crates/gateway/src/errors.rs @@ -27,12 +27,15 @@ pub enum APIError { #[error("Unauthorized registration access")] Unauthorized(), + #[cfg(not(target_os = "windows"))] #[error("Database connection error: {0}")] DatabaseConnection(#[from] deadpool_diesel::PoolError), + #[cfg(not(target_os = "windows"))] #[error("Database interaction error: {0}")] DatabaseInteraction(#[from] deadpool_diesel::InteractError), + #[cfg(not(target_os = "windows"))] #[error("Database query error: {0}")] DatabaseQuery(#[from] diesel::result::Error), } @@ -78,6 +81,7 @@ impl IntoResponse for APIError { details: "You are not authorized to access the registration.".to_string(), }, ), + #[cfg(not(target_os = "windows"))] APIError::DatabaseConnection(_) | APIError::DatabaseQuery(_) | APIError::DatabaseInteraction(_) => ( diff --git a/crates/gateway/src/hydra_server_platform/verifications.rs b/crates/gateway/src/hydra_server_platform/verifications.rs index 90b4df00..b5195945 100644 --- a/crates/gateway/src/hydra_server_platform/verifications.rs +++ b/crates/gateway/src/hydra_server_platform/verifications.rs @@ -760,7 +760,7 @@ pub fn sigterm(pid: u32) -> Result<()> { } #[cfg(windows)] -pub fn sigterm(pid: u32) -> Result<()> { +pub fn sigterm(_pid: u32) -> Result<()> { unreachable!() } diff --git a/crates/gateway/src/lib.rs b/crates/gateway/src/lib.rs index bc28168f..4dfe8117 100644 --- a/crates/gateway/src/lib.rs +++ b/crates/gateway/src/lib.rs @@ -9,6 +9,7 @@ pub mod hydra_server_platform; pub mod load_balancer; pub mod models; pub mod payload; +#[cfg(not(target_os = "windows"))] pub mod schema; pub mod sdk_bridge_ws; pub mod types; diff --git a/crates/gateway/src/models.rs b/crates/gateway/src/models.rs index 2e2fd149..0614f602 100644 --- a/crates/gateway/src/models.rs +++ b/crates/gateway/src/models.rs @@ -1,10 +1,13 @@ use chrono::NaiveDateTime; -use diesel::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Queryable, Selectable, Insertable, Deserialize, Serialize, Debug)] -#[diesel(table_name = crate::schema::requests)] -#[diesel(check_for_backend(diesel::pg::Pg))] +#[cfg(not(target_os = "windows"))] +use diesel::prelude::*; + +#[derive(Deserialize, Serialize, Debug)] +#[cfg_attr(not(target_os = "windows"), derive(Queryable, Selectable, Insertable))] +#[cfg_attr(not(target_os = "windows"), diesel(table_name = crate::schema::requests))] +#[cfg_attr(not(target_os = "windows"), diesel(check_for_backend(diesel::pg::Pg)))] pub struct Request { pub id: i32, pub route: String, @@ -14,9 +17,10 @@ pub struct Request { pub reward_address: String, } -#[derive(Selectable, Insertable, Deserialize, Serialize)] -#[diesel(table_name = crate::schema::requests)] -#[diesel(check_for_backend(diesel::pg::Pg))] +#[derive(Deserialize, Serialize, Debug)] +#[cfg_attr(not(target_os = "windows"), derive(Selectable, Insertable))] +#[cfg_attr(not(target_os = "windows"), diesel(table_name = crate::schema::requests))] +#[cfg_attr(not(target_os = "windows"), diesel(check_for_backend(diesel::pg::Pg)))] pub struct RequestNewItem { pub route: String, pub user_id: i32, @@ -27,9 +31,10 @@ pub struct RequestNewItem { pub asset_name: Option, } -#[derive(Selectable, Insertable, Queryable, Deserialize, Serialize)] -#[diesel(table_name = crate::schema::users)] -#[diesel(check_for_backend(diesel::pg::Pg))] +#[derive(Deserialize, Serialize, Debug)] +#[cfg_attr(not(target_os = "windows"), derive(Selectable, Insertable, Queryable))] +#[cfg_attr(not(target_os = "windows"), diesel(table_name = crate::schema::users))] +#[cfg_attr(not(target_os = "windows"), diesel(check_for_backend(diesel::pg::Pg)))] pub struct User { pub id: i32, pub created_at: NaiveDateTime, diff --git a/nix/internal/windows.nix b/nix/internal/windows.nix index 7c5cdada..a06b9700 100644 --- a/nix/internal/windows.nix +++ b/nix/internal/windows.nix @@ -16,7 +16,14 @@ in rec { craneLib = (inputs.crane.mkLib pkgs).overrideToolchain toolchain; - src = craneLib.cleanCargoSource ../../.; + src = lib.cleanSourceWith { + src = lib.cleanSource ../../.; + filter = path: type: + craneLib.filterCargoSources path type + || lib.hasSuffix ".sql" path + || lib.hasSuffix "/LICENSE" path; + name = "source"; + }; pkgsCross = pkgs.pkgsCross.mingwW64; @@ -36,6 +43,8 @@ in rec { OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib"; OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include/"; + # Unfortunately, `pkgsCross.postgresql` is broken on Windows, so making the + # `blockfrost-gateway` work there will be much more tinkering. depsBuildBuild = [ pkgsCross.stdenv.cc pkgsCross.windows.pthreads @@ -59,6 +68,22 @@ in rec { ''; }); + gatewayCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/crates/gateway/Cargo.toml";})); + blockfrost-gateway = craneLib.buildPackage (commonArgs + // { + inherit cargoArtifacts GIT_REVISION; + pname = gatewayCargoToml.package.name; + doCheck = false; # we run Windows tests on real Windows on GHA + cargoExtraArgs = "--package blockfrost-gateway"; + postPatch = '' + find -name 'Cargo.toml' | while IFS= read -r cargo_toml ; do + sed -r '/^build = .*/d' -i "$cargo_toml" + done + find -name 'build.rs' -delete + ''; + } + // (builtins.listToAttrs inputs.self.internal.x86_64-linux.hydraScriptsEnvVars)); + testgen-hs = let inherit (inputs.self.internal.x86_64-linux.testgen-hs) version; in From 1bffe03de6788e2951a34dd25aa615b29aeb976c Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Fri, 13 Mar 2026 20:05:21 +0100 Subject: [PATCH 17/23] chore: add `blockfrost-sdk-bridge` to Nix packages --- flake.nix | 6 +++++- nix/internal/unix.nix | 15 +++++++++++++++ nix/internal/windows.nix | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 0fa130f6..c3b6f138 100644 --- a/flake.nix +++ b/flake.nix @@ -91,12 +91,13 @@ packages = { default = internal.blockfrost-platform; - inherit (internal) blockfrost-platform blockfrost-gateway; + inherit (internal) blockfrost-platform blockfrost-gateway blockfrost-sdk-bridge; inherit (internal) tx-build cardano-address testgen-hs; } // (lib.optionalAttrs (system == "x86_64-linux") { blockfrost-platform-x86_64-windows = inputs.self.internal.x86_64-windows.blockfrost-platform; blockfrost-gateway-x86_64-windows = inputs.self.internal.x86_64-windows.blockfrost-gateway; + blockfrost-sdk-bridge-x86_64-windows = inputs.self.internal.x86_64-windows.blockfrost-sdk-bridge; }); devshells.default = import ./nix/devshells.nix {inherit inputs;}; @@ -190,6 +191,9 @@ blockfrost-gateway = lib.genAttrs (config.systems ++ crossSystems) ( targetSystem: inputs.self.internal.${targetSystem}.blockfrost-gateway ); + blockfrost-sdk-bridge = lib.genAttrs (config.systems ++ crossSystems) ( + targetSystem: inputs.self.internal.${targetSystem}.blockfrost-sdk-bridge + ); devshell = lib.genAttrs config.systems ( targetSystem: inputs.self.devShells.${targetSystem}.default ); diff --git a/nix/internal/unix.nix b/nix/internal/unix.nix index 95d25e97..397d6f2d 100644 --- a/nix/internal/unix.nix +++ b/nix/internal/unix.nix @@ -68,6 +68,7 @@ in workspaceCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/Cargo.toml";})); platformCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/crates/platform/Cargo.toml";})); gatewayCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/crates/gateway/Cargo.toml";})); + sdkBridgeCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/crates/sdk_bridge/Cargo.toml";})); GIT_REVISION = inputs.self.rev or "dirty"; @@ -107,6 +108,20 @@ in } // (builtins.listToAttrs hydraScriptsEnvVars)); + blockfrost-sdk-bridge = craneLib.buildPackage (commonArgs + // { + inherit cargoArtifacts GIT_REVISION; + pname = sdkBridgeCargoToml.package.name; + doCheck = false; # we run tests with `cargo-nextest` below + meta.mainProgram = sdkBridgeCargoToml.package.name; + postInstall = '' + mv $out/bin $out/libexec + mkdir -p $out/bin + ( cd $out/bin && ln -s ../libexec/${sdkBridgeCargoToml.package.name} ./ ; ) + ''; + cargoExtraArgs = "--package blockfrost-sdk-bridge"; + }); + cargoChecks = let # `cargo-udeps` and `cargo-shear --expand` require the Nightly toolchain: nightlyToolchain = inputs.fenix.packages.${pkgs.system}.complete.toolchain; diff --git a/nix/internal/windows.nix b/nix/internal/windows.nix index 8f17b59d..5964968d 100644 --- a/nix/internal/windows.nix +++ b/nix/internal/windows.nix @@ -74,6 +74,7 @@ in rec { }); gatewayCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/crates/gateway/Cargo.toml";})); + sdkBridgeCargoToml = builtins.fromTOML (builtins.readFile (builtins.path {path = src + "/crates/sdk_bridge/Cargo.toml";})); blockfrost-gateway = craneLib.buildPackage (commonArgs // { inherit cargoArtifacts GIT_REVISION; @@ -89,6 +90,20 @@ in rec { } // (builtins.listToAttrs hydraScriptsEnvVars)); + blockfrost-sdk-bridge = craneLib.buildPackage (commonArgs + // { + inherit cargoArtifacts GIT_REVISION; + pname = sdkBridgeCargoToml.package.name; + doCheck = false; # we run Windows tests on real Windows on GHA + cargoExtraArgs = "--package blockfrost-sdk-bridge"; + postPatch = '' + find -name 'Cargo.toml' | while IFS= read -r cargo_toml ; do + sed -r '/^build = .*/d' -i "$cargo_toml" + done + find -name 'build.rs' -delete + ''; + }); + testgen-hs = let inherit (inputs.self.internal.x86_64-linux.testgen-hs) version; in From 7a5aa6c6839b6d6a2710df72b2b83bd39d509c82 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Fri, 13 Mar 2026 20:05:54 +0100 Subject: [PATCH 18/23] chore: relax coverage % --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 35e60ecd..cfc40b3a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -229,7 +229,7 @@ jobs: - name: Check coverage timeout-minutes: 10 - run: cargo tarpaulin --workspace --all --lib --features tarpaulin --fail-under 9 + run: cargo tarpaulin --workspace --all --lib --features tarpaulin --fail-under 8 oci_build: name: OCI Build (${{ matrix.arch }}) From 92c0e6398a89a6f357f4133fcd168854efe4f158 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Fri, 13 Mar 2026 20:07:54 +0100 Subject: [PATCH 19/23] fix: anonymize `node.socket` and key paths --- crates/gateway/config/development.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/gateway/config/development.toml b/crates/gateway/config/development.toml index 92e450a3..7f35334f 100644 --- a/crates/gateway/config/development.toml +++ b/crates/gateway/config/development.toml @@ -12,8 +12,8 @@ nft_asset = '4213fc3eac8c781ac85514dd1de9aaabcd5a3a81cc2df4f413b9b295' [hydra_platform] max_concurrent_hydra_nodes = 2 -cardano_signing_key = "/home/mw/.config/blockfrost-gateway/hydra/preview/_our-keys/payment/payment.sk" -node_socket_path = "/home/mw/.local/share/blockfrost-platform/preview/node.socket" +cardano_signing_key = "./hydra-config/preview/_our-keys/payment/payment.sk" +node_socket_path = "/run/cardano-node/node.socket" commit_ada = 3.0 lovelace_per_request = 100_000 requests_per_microtransaction = 10 @@ -21,8 +21,8 @@ microtransactions_per_fanout = 2 [hydra_bridge] max_concurrent_hydra_nodes = 2 -cardano_signing_key = "/home/mw/.config/blockfrost-gateway/hydra/preview/_our-keys/payment/payment.sk" -node_socket_path = "/home/mw/.local/share/blockfrost-platform/preview/node.socket" +cardano_signing_key = "./hydra-config/preview/_our-keys/payment/payment.sk" +node_socket_path = "/run/cardano-node/node.socket" commit_ada = 4.0 lovelace_per_request = 100_000 requests_per_microtransaction = 10 From 28f23b86aa40a5942019a5d4758977a3d49ed2aa Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 2 Apr 2026 14:03:09 +0200 Subject: [PATCH 20/23] feat(hydra): add an integration test: SDK Bridge + Gateway Hydra micropayments --- crates/sdk_bridge/src/hydra_client/mod.rs | 174 +++--- nix/internal/hydra-bridge-gateway-test.sh | 658 ++++++++++++++++++++++ nix/internal/unix.nix | 35 ++ 3 files changed, 777 insertions(+), 90 deletions(-) create mode 100644 nix/internal/hydra-bridge-gateway-test.sh diff --git a/crates/sdk_bridge/src/hydra_client/mod.rs b/crates/sdk_bridge/src/hydra_client/mod.rs index c9208b7e..7cedea32 100644 --- a/crates/sdk_bridge/src/hydra_client/mod.rs +++ b/crates/sdk_bridge/src/hydra_client/mod.rs @@ -417,115 +417,109 @@ impl State { } self.start_hydra_node(kex_resp).await?; - self.send_delayed(Event::TryToInitHead, Duration::from_secs(1)) + // Fund the commit wallet *before* `Init` so that the fund tx + // and hydra-node's `Init` tx don't race for the same + // signing-key UTxOs. + self.send_delayed(Event::FundCommitAddr, Duration::from_secs(1)) .await }, Event::TryToInitHead => { - let ready = verifications::prometheus_metric_at_least( - &format!("http://127.0.0.1:{}/metrics", self.metrics_port), - "hydra_head_peers_connected", - 1.0, - ) - .await; - - info!( - "hydra-controller: waiting for hydras to connect: ready={:?}", - ready - ); - - if matches!(ready, Ok(true)) { - verifications::send_one_websocket_msg( - &format!("ws://127.0.0.1:{}/", self.api_port), - serde_json::json!({"tag":"Init"}), - Duration::from_secs(2), - ) - .await?; + // During re-initialisation the Gateway sends `Init`; the + // Bridge's hydra-node will transition to "Initial" on its own. + // In that case we skip straight to commit. + let status = verifications::fetch_head_tag(self.api_port).await?; - self.send_delayed(Event::FundCommitAddr, Duration::from_secs(3)) + if status == "Initial" { + info!("hydra-controller: head already Initial, proceeding to commit"); + self.send_delayed(Event::TryToCommit, Duration::from_secs(1)) .await - } else { - self.send_delayed(Event::TryToInitHead, Duration::from_secs(1)) + } else if status == "Open" { + info!("hydra-controller: head already Open, skipping init + commit"); + self.send_delayed(Event::WaitForOpen, Duration::from_secs(1)) .await + } else { + let ready = verifications::prometheus_metric_at_least( + &format!("http://127.0.0.1:{}/metrics", self.metrics_port), + "hydra_head_peers_connected", + 1.0, + ) + .await; + + info!( + "hydra-controller: waiting for hydras to connect: ready={:?}", + ready + ); + + if matches!(ready, Ok(true)) { + verifications::send_one_websocket_msg( + &format!("ws://127.0.0.1:{}/", self.api_port), + serde_json::json!({"tag":"Init"}), + Duration::from_secs(2), + ) + .await?; + + // Commit wallet was already funded before we got here, + // so proceed directly to commit. + self.send_delayed(Event::TryToCommit, Duration::from_secs(3)) + .await + } else { + self.send_delayed(Event::TryToInitHead, Duration::from_secs(1)) + .await + } } }, Event::FundCommitAddr => { - let status = verifications::fetch_head_tag(self.api_port).await?; - - info!( - "hydra-controller: waiting for the Initial head status: status={:?}", - status - ); + let commit_wallet = self.config_dir.join("commit-funds"); + self.commit_wallet_skey = commit_wallet.with_extension("sk"); - if status == "Initial" || status == "Open" { - let commit_wallet = self.config_dir.join("commit-funds"); - self.commit_wallet_skey = commit_wallet.with_extension("sk"); + if !self.commit_wallet_skey.exists() { + self.new_cardano_keypair(&commit_wallet).await?; + } - if !self.commit_wallet_skey.exists() { - if status == "Open" { - Err(anyhow!( - "Head status is Open, but there’s no commit wallet anymore; this shouldn’t really happen" - ))? - } + self.commit_wallet_addr = self + .derive_enterprise_address_from_skey(&self.commit_wallet_skey) + .await?; - self.new_cardano_keypair(&commit_wallet).await?; - } + let params = self.payment_params.clone().ok_or(anyhow!( + "payment parameters not set before funding commit address" + ))?; - self.commit_wallet_addr = self - .derive_enterprise_address_from_skey(&self.commit_wallet_skey) - .await?; - - if status == "Initial" { - let params = self.payment_params.clone().ok_or(anyhow!( - "payment parameters not set before funding commit address" - ))?; - - let target_lovelace = (params.commit_ada * 1_000_000.0).round() as u64; - let current_lovelace = self - .lovelace_on_payment_skey(&self.commit_wallet_skey) - .await?; - - if current_lovelace < target_lovelace { - let mut top_up = target_lovelace - current_lovelace; - if top_up < MIN_COMMIT_TOPUP_LOVELACE { - top_up = MIN_COMMIT_TOPUP_LOVELACE; - } - info!( - "hydra-controller: topping up commit address by {} lovelace (current={}, target={})", - top_up, current_lovelace, target_lovelace - ); - self.fund_address( - &self - .derive_enterprise_address_from_skey( - &self.config.cardano_signing_key, - ) - .await?, - &self.commit_wallet_addr, - top_up, - &self.config.cardano_signing_key, - ) - .await?; - } else { - info!( - "hydra-controller: commit address already funded (current={}, target={})", - current_lovelace, target_lovelace - ); - } + let target_lovelace = (params.commit_ada * 1_000_000.0).round() as u64; + let current_lovelace = self + .lovelace_on_payment_skey(&self.commit_wallet_skey) + .await?; - self.send_delayed(Event::TryToCommit, Duration::from_secs(3)) - .await - } else if status == "Open" { - warn!( - "hydra-controller: turns out the Head is already Open, skipping Commit" - ); - self.send_delayed(Event::WaitForOpen, Duration::from_secs(3)) - .await + if current_lovelace < target_lovelace { + let mut top_up = target_lovelace - current_lovelace; + if top_up < MIN_COMMIT_TOPUP_LOVELACE { + top_up = MIN_COMMIT_TOPUP_LOVELACE; } + info!( + "hydra-controller: topping up commit address by {} lovelace (current={}, target={})", + top_up, current_lovelace, target_lovelace + ); + self.fund_address( + &self + .derive_enterprise_address_from_skey(&self.config.cardano_signing_key) + .await?, + &self.commit_wallet_addr, + top_up, + &self.config.cardano_signing_key, + ) + .await?; } else { - self.send_delayed(Event::FundCommitAddr, Duration::from_secs(3)) - .await + info!( + "hydra-controller: commit address already funded (current={}, target={})", + current_lovelace, target_lovelace + ); } + + // Proceed to Init (or straight to Commit if the head + // is already in "Initial" state, e.g. re-init). + self.send_delayed(Event::TryToInitHead, Duration::from_secs(1)) + .await }, Event::TryToCommit => { diff --git a/nix/internal/hydra-bridge-gateway-test.sh b/nix/internal/hydra-bridge-gateway-test.sh new file mode 100644 index 00000000..2d8c0295 --- /dev/null +++ b/nix/internal/hydra-bridge-gateway-test.sh @@ -0,0 +1,658 @@ +#!/usr/bin/env bash + +# Integration test: SDK Bridge + Gateway Hydra Micropayments +# +# This test verifies the full Hydra micropayment cycle between +# `blockfrost-sdk-bridge` (sender) and `blockfrost-gateway` (with +# `dev_mock_db`, receiver): +# +# 1. Start the Gateway with a `[hydra_bridge]` config section +# 2. Start the SDK Bridge pointing at the local Gateway (`--gateway-ws-url`) +# 3. Wait for Hydra key exchange, Head Init, Commit, and Open +# 4. Send API requests through the SDK Bridge -> Gateway +# 5. Verify that the Gateway triggers Close -> Fanout after enough micropayments +# 6. Verify the cycle restarts ± indefinitely (2nd and 3rd fanout) + +set -euo pipefail + +# ---------------------------------------------------------------------------- # + +work_dir="" +test_passed=false +gateway_pid="" +bridge_pid="" +cleanup() { + # Prevent re-entry on repeated Ctrl-C or cascading signals: + trap '' INT TERM + trap - EXIT + + local exit_code=$? + echo >&2 "Cleaning up… (exit_code=$exit_code)" + if [ "$test_passed" = false ] && [ -n "$work_dir" ]; then + echo >&2 "=== Test FAILED ===" + fi + + for pid in $gateway_pid $bridge_pid; do + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + echo >&2 "Sending SIGTERM to pid $pid" + kill -TERM "$pid" 2>/dev/null || true + fi + done + wait 2>/dev/null || true + + if [ -n "$work_dir" ]; then + cd / + rm -rf -- "$work_dir" + fi + exit "$exit_code" +} +trap cleanup INT TERM EXIT + +# ---------------------------------------------------------------------------- # + +log() { + local level="${1}" + shift + level=$(printf '%5s' "${level^^}") + local timestamp + timestamp=$(date -u +'%Y-%m-%dT%H:%M:%S.%6NZ') + if [[ -t 2 ]]; then + local color_reset=$'\e[0m' + local color_grey=$'\e[90m' + local color_red=$'\e[1;91m' + local color_yellow=$'\e[93m' + local color_green=$'\e[92m' + case "$level" in + "FATAL") level="${color_red}${level}${color_reset}" ;; + " WARN") level="${color_yellow}${level}${color_reset}" ;; + " INFO") level="${color_green}${level}${color_reset}" ;; + esac + timestamp="${color_grey}${timestamp}${color_reset}" + fi + echo >&2 "test: $timestamp" "$level" "$@" +} + +require_env() { + local name="$1" + local val="${!name-}" + if [[ -z $val ]]; then + log fatal "$name is not set." + missing=1 + fi +} +missing=0 +for v in NETWORK BLOCKFROST_PROJECT_ID CARDANO_NODE_SOCKET_PATH SUBMIT_MNEMONIC CARDANO_NODE_NETWORK_ID; do + require_env "$v" +done +if ((missing)); then + exit 1 +fi + +lovelace_to_ada() { + printf '%d.%06d' $(($1 / (1000 * 1000))) $(($1 % (1000 * 1000))) +} + +# ---------------------------------------------------------------------------- # + +# How much the Bridge commits to the Hydra Head: +commit_ada=7.0 +commit_lovelace=$(printf '%.0f' "$(echo "$commit_ada * 1000 * 1000" | bc)") +# How much each request is worth: +lovelace_per_request=$((1 * 1000 * 1000)) +# How many requests to bundle for a microtransaction: +requests_per_microtransaction=2 +# How many microtransactions until a fanout (the Gateway counts the prepay as +# microtransaction #1, so only microtransactions_per_fanout-1 are request-driven): +microtransactions_per_fanout=3 +# How many fanout cycles to test (each is ~7 minutes): +num_fanout_cycles=3 + +# The prepay counts as microtransaction #1 for the Gateway's fanout trigger, +# so the number of HTTP-request-driven microtransactions per cycle is one fewer: +requests_per_fanout=$((requests_per_microtransaction * (microtransactions_per_fanout - 1))) + +# ---------------------------------------------------------------------------- # + +work_dir=$(mktemp -d) +cd "$work_dir" + +export HOME="$work_dir" +unset XDG_CONFIG_HOME XDG_CACHE_HOME XDG_DATA_HOME XDG_STATE_HOME + +log info "Working directory (and HOME): $work_dir" +log info "Network tip: $(cardano-cli query tip | jq --compact-output .)" + +mkdir -p credentials + +# ---------------------------------------------------------------------------- # + +# Derive keys using cardano-address +log info "Deriving keys from the 'SUBMIT_MNEMONIC'" + +( + mkdir -p credentials/submit-mnemonic + cd credentials/submit-mnemonic + + echo "$SUBMIT_MNEMONIC" | cardano-address key from-recovery-phrase Shelley >"root.prv" + + # Derive payment key (m/1852'/1815'/0'/0/0) + cardano-address key child 1852H/1815H/0H/0/0 <"root.prv" >"payment.prv" + cardano-address key public --with-chain-code <"payment.prv" >"payment.pub" + + # Derive stake key (m/1852'/1815'/0'/2/0) + cardano-address key child 1852H/1815H/0H/2/0 <"root.prv" >"stake.prv" + cardano-address key public --with-chain-code <"stake.prv" >"stake.pub" + + # Convert payment signing key to cardano-cli format + cardano-cli key convert-cardano-address-key \ + --shelley-payment-key --signing-key-file "payment.prv" --out-file "payment.sk" + + # Extract the payment verification key + cardano-cli key verification-key --signing-key-file "payment.sk" \ + --verification-key-file "payment.evkey" + + # Convert the extended payment verification key to a non-extended key + cardano-cli key non-extended-key \ + --extended-verification-key-file "payment.evkey" \ + --verification-key-file "payment.vk" + + # Convert stake signing key to cardano-cli format + cardano-cli key convert-cardano-address-key \ + --shelley-stake-key --signing-key-file "stake.prv" --out-file "stake.sk" + + # Extract the stake verification key + cardano-cli key verification-key --signing-key-file "stake.sk" \ + --verification-key-file "stake.evkey" + + # Convert the extended stake verification key to a non-extended key + cardano-cli key non-extended-key \ + --extended-verification-key-file "stake.evkey" \ + --verification-key-file "stake.vk" + + # Generate base address using non-extended verification keys + cardano-cli address build \ + --payment-verification-key-file "payment.vk" \ + --stake-verification-key-file "stake.vk" >"payment.addr" + + log info "'SUBMIT_MNEMONIC' address: $(cat "payment.addr")" +) + +# ---------------------------------------------------------------------------- # + +# We need 2 funded addresses: +# - bridge-hydra: pays L1 fees for the Bridge's hydra-node + commits funds to L2 +# - gateway-hydra: pays L1 fees for the Gateway's hydra-node (empty commit) + +log info "Verifying that 'SUBMIT_MNEMONIC' has enough funds…" + +# MIN_FUEL_LOVELACE in Rust is 15 ADA. Each fanout cycle burns roughly one +# MIN_FUEL_LOVELACE in L1 fees for the Bridge and a bit less for the Gateway, +# so fuel must scale with the number of cycles (plus one extra for headroom): +min_fuel_lovelace=$((15 * 1000 * 1000)) +# Total micropayments per cycle includes the prepay (microtransactions_per_fanout +# counts it), so use the full microtransactions_per_fanout here: +micropayments_per_cycle=$((microtransactions_per_fanout * requests_per_microtransaction * lovelace_per_request)) +micropayments_total=$((num_fanout_cycles * micropayments_per_cycle)) + +declare -A lovelace_fund +lovelace_fund["bridge-hydra"]=$((commit_lovelace + micropayments_total + (num_fanout_cycles + 1) * min_fuel_lovelace)) +lovelace_fund["gateway-hydra"]=$(((num_fanout_cycles + 1) * min_fuel_lovelace)) + +submit_mnemonic_funds=$(cardano-cli query utxo \ + --address "$(cat credentials/submit-mnemonic/payment.addr)" \ + --out-file /dev/stdout | + jq '[.[] | .value.lovelace] | add // 0') + +# 1 ADA extra for tx fees: +required_funds=$((1 * 1000 * 1000)) +for k in "${!lovelace_fund[@]}"; do + ((required_funds += lovelace_fund[$k])) +done + +if ((submit_mnemonic_funds < required_funds)); then + log fatal "… insufficient funds on 'SUBMIT_MNEMONIC': have $(lovelace_to_ada "$submit_mnemonic_funds") ADA, need $(lovelace_to_ada "$required_funds") ADA." + exit 1 +else + log info "… OK, have $(lovelace_to_ada "$submit_mnemonic_funds") ADA, need $(lovelace_to_ada "$required_funds") ADA." +fi + +unset submit_mnemonic_funds +unset required_funds + +# ---------------------------------------------------------------------------- # + +log info "Generating L1 credentials…" + +for participant in gateway-hydra bridge-hydra; do + log info "Generating L1 credentials for: $participant" + + mkdir -p credentials/"$participant" + + cardano-cli address key-gen \ + --verification-key-file credentials/"$participant"/payment.vk \ + --signing-key-file credentials/"$participant"/payment.sk + + cardano-cli address build \ + --verification-key-file credentials/"$participant"/payment.vk \ + --out-file credentials/"$participant"/payment.addr +done + +# ---------------------------------------------------------------------------- # + +log info "Funding L1 participants: bridge-hydra, gateway-hydra" + +txdir=tx-01-fund-participants +mkdir -p $txdir + +max_funding_attempts=5 +funding_retry_delay=25 +for funding_attempt in $(seq 1 $max_funding_attempts); do + log info "Funding attempt $funding_attempt/$max_funding_attempts" + + cardano-cli query utxo \ + --address "$(cat credentials/submit-mnemonic/payment.addr)" \ + --out-file $txdir/input-utxo.json + + # shellcheck disable=SC2046 + if cardano-cli latest transaction build \ + $(jq -r 'keys[]' <$txdir/input-utxo.json | shuf | head -n 200 | sed 's/^/--tx-in /') \ + --change-address "$(cat credentials/submit-mnemonic/payment.addr)" \ + --tx-out "$(cat credentials/bridge-hydra/payment.addr)"+"${lovelace_fund["bridge-hydra"]}" \ + --tx-out "$(cat credentials/gateway-hydra/payment.addr)"+"${lovelace_fund["gateway-hydra"]}" \ + --out-file $txdir/tx.json && + cardano-cli latest transaction sign \ + --tx-file $txdir/tx.json \ + --signing-key-file credentials/submit-mnemonic/payment.sk \ + --out-file $txdir/tx-signed.json && + cardano-cli latest transaction submit --tx-file $txdir/tx-signed.json; then + log info "Funding transaction submitted successfully." + break + fi + + if ((funding_attempt == max_funding_attempts)); then + log fatal "All $max_funding_attempts funding attempts failed." + exit 1 + fi + + log warn "Funding attempt $funding_attempt failed, retrying in ${funding_retry_delay}s (waiting for a new block)…" + sleep "$funding_retry_delay" +done + +# ---------------------------------------------------------------------------- # + +for participant in bridge-hydra gateway-hydra; do + while true; do + funds=$(cardano-cli query utxo --address "$(cat credentials/"$participant"/payment.addr)" --out-file /dev/stdout | jq --compact-output .) + log info "Verifying L1 participant funds: $participant: $funds" + if [ "$funds" != '{}' ]; then + break + fi + sleep 5 + done +done + +# ---------------------------------------------------------------------------- # + +log_level=info + +gateway_port=$(python3 -m portpicker) +bridge_port=$(python3 -m portpicker) + +gateway_url="http://127.0.0.1:${gateway_port}" +bridge_url="http://127.0.0.1:${bridge_port}" + +# ---------------------------------------------------------------------------- # + +log info "Writing Gateway config…" + +cat >gateway-config.toml < >(tee "$gateway_log" | sed -u 's/^/gateway: /' >&2) 2>&1 & +gateway_pid=$! + +sleep 1 +wait4x http "${gateway_url}" --expect-status-code 200 --timeout 60s --interval 1s +log info "Gateway is up at ${gateway_url}" + +# ---------------------------------------------------------------------------- # + +log info "Starting the SDK Bridge (blockfrost-sdk-bridge)…" + +blockfrost-sdk-bridge \ + --gateway-ws-url "${gateway_url}" \ + --listen-address "127.0.0.1:${bridge_port}" \ + --network "${NETWORK}" \ + --node-socket-path "${CARDANO_NODE_SOCKET_PATH}" \ + --cardano-signing-key "$(realpath credentials/bridge-hydra/payment.sk)" \ + > >(tee "$bridge_log" | sed -u 's/^/bridge: /' >&2) 2>&1 & +bridge_pid=$! + +sleep 1 +wait4x tcp "127.0.0.1:${bridge_port}" --timeout 60s --interval 1s +log info "SDK Bridge is listening at ${bridge_url}" + +# ---------------------------------------------------------------------------- # + +# Helper: count occurrences of a pattern in the Bridge log. +bridge_log_count() { + local n + n=$(grep -c "$1" "$bridge_log" 2>/dev/null) || true + echo "${n:-0}" +} + +# Helper: wait for a pattern to appear at least N times in the Bridge log. +# Usage: wait_for_bridge_log_count +wait_for_bridge_log_count() { + local pattern="$1" + local min_count="$2" + local timeout="$3" + local desc="$4" + local start elapsed count + + start=$(date +%s) + while true; do + count=$(bridge_log_count "$pattern") + elapsed=$(($(date +%s) - start)) + log info "$desc: occurrences=$count, need=$min_count (elapsed: ${elapsed}s)" + if ((count >= min_count)); then + return 0 + fi + if ((elapsed > timeout)); then + log fatal "$desc: timed out after ${timeout}s (occurrences=$count, need=$min_count)" + return 1 + fi + sleep 1 + done +} + +# Helper: count occurrences of a pattern in the Gateway log. +gw_log_count() { + local n + n=$(grep -c "$1" "$gateway_log" 2>/dev/null) || true + echo "${n:-0}" +} + +# Helper: wait for a pattern to appear at least N times in the Gateway log. +# Usage: wait_for_gw_log_count +wait_for_gw_log_count() { + local pattern="$1" + local min_count="$2" + local timeout="$3" + local desc="$4" + local start elapsed count + + start=$(date +%s) + while true; do + count=$(gw_log_count "$pattern") + elapsed=$(($(date +%s) - start)) + log info "$desc: occurrences=$count, need=$min_count (elapsed: ${elapsed}s)" + if ((count >= min_count)); then + return 0 + fi + if ((elapsed > timeout)); then + log fatal "$desc: timed out after ${timeout}s (occurrences=$count, need=$min_count)" + return 1 + fi + sleep 5 + done +} + +# ---------------------------------------------------------------------------- # + +log info "Waiting for the Hydra Head to become Open…" + +# The initial Head opening can take several minutes on the Cardano L1 (init + +# commit + open transactions need to be confirmed): + +wait_for_gw_log_count 'waiting for the Open head status: status="Open"' 1 600 \ + "Hydra Head open (initial)" || exit 1 + +log info "Hydra Head is Open! Waiting for the Bridge to have request credits…" + +# After the head opens the Bridge sends a prepay microtransaction and then polls +# its local hydra-node snapshot every 1 s to detect the confirmed balance change. +# It logs "req. credits +N" once credits are granted. We wait for that before +# sending traffic so we never fire a request that would get a 402. +bridge_credits_seen=1 +wait_for_bridge_log_count "req. credits +" "$bridge_credits_seen" 30 \ + "Bridge credits (initial)" || exit 1 + +log info "Bridge has credits. Ready to send traffic." + +# Track how many credits the Bridge currently has so we can wait for +# replenishment before sending the next request when credits are exhausted. +# Each "req. credits +" log grants requests_per_microtransaction credits. +test_credits=$requests_per_microtransaction + +# ---------------------------------------------------------------------------- # + +# Track total requests sent so far. +total_requests_sent=0 + +# Number of head-open events seen so far (the initial open := 1). +head_opens_seen=1 + +# Number of "re-initializing" events expected (starts at 0). +reinits_seen=0 + +perform_fanout_cycle() { + local fanout_num="$1" + local is_last="$2" # 1 for the last cycle, empty otherwise + + log info "=== Fanout cycle $fanout_num: sending $requests_per_fanout requests ===" + + for nth in $(seq 1 "$requests_per_fanout"); do + # The Bridge only has requests_per_microtransaction credits at a time. + # When exhausted a new microtransaction is sent but takes ~1-2s to confirm + # in the snapshot before the Bridge grants itself fresh credits. Wait for + # the next "req. credits +" log rather than hitting a 402. + if ((test_credits <= 0)); then + bridge_credits_seen=$((bridge_credits_seen + 1)) + log info "Fanout $fanout_num: waiting for Bridge credit grant #$bridge_credits_seen before request $nth…" + wait_for_bridge_log_count "req. credits +" "$bridge_credits_seen" 30 \ + "Fanout $fanout_num: credits before request $nth" || exit 1 + test_credits=$((test_credits + requests_per_microtransaction)) + fi + + log info "Fanout $fanout_num: sending request $nth/$requests_per_fanout (test_credits=$test_credits)" + + resp=$(curl -sS -w '\n%{http_code}' "${bridge_url}/" 2>/dev/null || true) + code="${resp##*$'\n'}" + + if [ "$code" != "200" ]; then + log fatal "Fanout $fanout_num: request $nth failed: http/$code" + exit 1 + fi + + log info "Fanout $fanout_num: request $nth: http/$code OK" + total_requests_sent=$((total_requests_sent + 1)) + test_credits=$((test_credits - 1)) + + # Small delay between requests to let the L2 transactions settle: + sleep 1 + done + + log info "Fanout $fanout_num: all $requests_per_fanout requests sent (total_requests_sent=$total_requests_sent)." + + # The contestation period is 60s, the invalidity period is (2+1)*60 = 180s. + # We wait much longer for: Close + Fanout + Idle (180s) + re-Init + re-Commit + re-Open. + fanout_wait_timeout=600 + + # Wait for the Gateway log to show "re-initializing the Hydra Head" for the + # Nth time, which means the Nth fanout completed and it's cycling back: + reinits_seen=$((reinits_seen + 1)) + log info "Fanout $fanout_num: waiting for re-initialization #$reinits_seen in Gateway log (up to ${fanout_wait_timeout}s)…" + wait_for_gw_log_count "re-initializing the Hydra Head" "$reinits_seen" "$fanout_wait_timeout" \ + "Fanout $fanout_num: Close → Fanout → Re-init" || exit 1 + + log info "Fanout $fanout_num: fanout cycle completed on L2!" + + # For non-last cycles, also wait for the head to reopen, so we can send the + # next batch of requests: + if [[ -z $is_last ]]; then + head_opens_seen=$((head_opens_seen + 1)) + log info "Fanout $fanout_num: waiting for the head to reopen (Open #$head_opens_seen)…" + wait_for_gw_log_count 'waiting for the Open head status: status="Open"' "$head_opens_seen" "$fanout_wait_timeout" \ + "Fanout $fanout_num: head reopen" || exit 1 + log info "Fanout $fanout_num: head is Open again. Waiting for Bridge credits…" + bridge_credits_seen=$((bridge_credits_seen + 1)) + wait_for_bridge_log_count "req. credits +" "$bridge_credits_seen" 30 \ + "Fanout $fanout_num: Bridge credits" || exit 1 + log info "Fanout $fanout_num: Bridge has credits, ready for next cycle." + test_credits=$requests_per_microtransaction + fi +} + +# ---------------------------------------------------------------------------- # + +for cycle in $(seq 1 "$num_fanout_cycles"); do + if ((cycle == num_fanout_cycles)); then + is_last=1 + else + is_last= + fi + log info "=== Starting fanout cycle $cycle/$num_fanout_cycles ===" + perform_fanout_cycle "$cycle" "$is_last" +done + +# ---------------------------------------------------------------------------- # + +log info "All $num_fanout_cycles fanout cycles completed successfully!" + +log info "Stopping Gateway (pid $gateway_pid) and SDK Bridge (pid $bridge_pid)…" +kill "$gateway_pid" "$bridge_pid" 2>/dev/null || true +wait "$gateway_pid" "$bridge_pid" 2>/dev/null || true +log info "Gateway and SDK Bridge stopped." + +log info "Waiting 30s for in-flight L1 transactions to clear the mempool…" +sleep 30 + +# ---------------------------------------------------------------------------- # + +log info "Verifying that funds were transferred on L1 to the Gateway…" + +# The minimum expected amount is the total micropayments received (including +# prepays). The Gateway's signing key address also holds remaining L1 fuel, +# so the actual balance will be higher: +expected_lovelace=$micropayments_total + +# The third fanout settles on L1 asynchronously; let's wait longer for the UTxOs to appear: +l1_verify_timeout=300 +l1_verify_start=$(date +%s) +gateway_l1_lovelace=0 + +while true; do + gateway_l1_lovelace=$(cardano-cli query utxo \ + --address "$(cat credentials/gateway-hydra/payment.addr)" \ + --out-file /dev/stdout | + jq '[.[] | .value.lovelace] | add // 0') + + log info "Gateway address has $(lovelace_to_ada "$gateway_l1_lovelace") ADA ($gateway_l1_lovelace lovelace), expected at least $(lovelace_to_ada "$expected_lovelace") ADA ($expected_lovelace lovelace)" + + if ((gateway_l1_lovelace >= expected_lovelace)); then + log info "… OK, L1 fund transfer verified! Gateway has at least $(lovelace_to_ada "$expected_lovelace") ADA." + break + fi + + elapsed=$(($(date +%s) - l1_verify_start)) + if ((elapsed > l1_verify_timeout)); then + log fatal "Timed out waiting for L1 funds at Gateway address (${l1_verify_timeout}s). Got: $gateway_l1_lovelace lovelace, expected: $expected_lovelace" + exit 1 + fi + + sleep 10 +done + +# ---------------------------------------------------------------------------- # + +log info "Returning all funds to 'SUBMIT_MNEMONIC'…" + +txdir=tx-02-return-test-ada +mkdir -p "$txdir" +change_address=$(cat credentials/submit-mnemonic/payment.addr) + +declare -A lovelace_remaining + +for participant in bridge-hydra gateway-hydra; do + addr=$(cat credentials/"$participant"/payment.addr) + utxo_json=$(cardano-cli query utxo --address "$addr" --out-file /dev/stdout) + funds=$(echo "$utxo_json" | jq '[.[] | .value.lovelace] | add // 0') + lovelace_remaining["$participant"]=$funds + log info "Returning funds from $participant ($funds lovelace)…" + + if ((funds == 0)); then + log warn "$participant has no funds to return; skipping." + continue + fi + + tx_ins=$(echo "$utxo_json" | jq -j 'to_entries[].key | "--tx-in ", ., " "') + + # shellcheck disable=SC2086 + cardano-cli latest transaction build \ + $tx_ins \ + --change-address "$change_address" \ + --out-file "$txdir/tx-$participant.json" + cardano-cli latest transaction sign \ + --tx-file "$txdir/tx-$participant.json" \ + --signing-key-file "credentials/$participant/payment.sk" \ + --out-file "$txdir/tx-signed-$participant.json" + cardano-cli latest transaction submit --tx-file "$txdir/tx-signed-$participant.json" + log info "Returned funds from $participant." +done + +# ---------------------------------------------------------------------------- # + +log info "Calculating how much was lost in Hydra transaction fees (excluding L1 fees from and to 'SUBMIT_MNEMONIC')…" + +total_cost=0 + +for participant in bridge-hydra gateway-hydra; do + cost=$((lovelace_fund["$participant"] - lovelace_remaining["$participant"])) + total_cost=$((total_cost + cost)) + if ((cost >= 0)); then + log info "Address '$participant' spent $(lovelace_to_ada "$cost") ADA (net)." + else + gain=$((-cost)) + log info "Address '$participant' gained $(lovelace_to_ada "$gain") ADA (net, from micropayments)." + fi +done + +# The net cost across both participants equals the total L1 transaction and +# Hydra fees, since micropayments transfer from bridge-hydra to gateway-hydra +# and cancel out: +log warn "In total, we lost $(lovelace_to_ada "$total_cost") ADA (in Hydra and L1 transaction fees)." + +# ---------------------------------------------------------------------------- # + +test_passed=true +log info "Test passed! Exiting." diff --git a/nix/internal/unix.nix b/nix/internal/unix.nix index 477452d5..5b1dcf87 100644 --- a/nix/internal/unix.nix +++ b/nix/internal/unix.nix @@ -944,6 +944,41 @@ in text = builtins.readFile ./hydra-platform-gateway-test.sh; }; + hydra-bridge-gateway-test = pkgs.writeShellApplication { + name = "test-hydra-bridge-gateway"; + meta.description = "Tests the Hydra micropayments between blockfrost-sdk-bridge and blockfrost-gateway"; + runtimeInputs = with pkgs; [ + bash + bc + coreutils + gnused + gnugrep + gawk + procps + jq + curl + hydra-node + cardano-cli + cardano-address + (python3.withPackages (ps: with ps; [portpicker])) + wait4x + blockfrost-sdk-bridge + blockfrost-gateway--dev-mock-db + ]; + runtimeEnv = rec { + NETWORK = "preview"; + CARDANO_NODE_NETWORK_ID = + { + mainnet = "mainnet"; + preprod = 1; + preview = 2; + }.${ + NETWORK + }; + }; + text = builtins.readFile ./hydra-bridge-gateway-test.sh; + }; + midnight = let fenix = inputs.fenix.packages.${pkgs.system}; From 8eeca714cc97d9bc20490a6057e92d42c1f83a2a Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 2 Apr 2026 14:04:36 +0200 Subject: [PATCH 21/23] chore(hydra): add the new test to CI --- .github/workflows/ci.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 68996f92..f60edbd3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -240,6 +240,23 @@ jobs: run: | nix run -L .#internal.x86_64-linux.hydra-platform-gateway-test + hydra_bridge_gateway_tests: + name: Hydra micropayments tests between Bridge↔Gateway + runs-on: [self-hosted, Linux, X64, nixos] + needs: check_flake_lock + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Run Hydra Tests + timeout-minutes: 45 + env: + CARDANO_NODE_SOCKET_PATH: /run/cardano-node/node_preview.socket + BLOCKFROST_PROJECT_ID: ${{ secrets.BLOCKFROST_TESTS_PROJECT_ID }} + SUBMIT_MNEMONIC: ${{ secrets.BLOCKFROST_TESTS_SUBMIT_MNEMONIC }} + run: | + nix run -L .#internal.x86_64-linux.hydra-bridge-gateway-test + coverage: name: Coverage runs-on: ubuntu-latest From 31af299bc900b93b9b12d058d548c5bfd46848a5 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Thu, 2 Apr 2026 16:12:50 +0200 Subject: [PATCH 22/23] fix(test): only ever exit with 0 if `test_passed == true` --- nix/internal/hydra-bridge-gateway-test.sh | 8 ++++---- nix/internal/hydra-platform-gateway-test.sh | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nix/internal/hydra-bridge-gateway-test.sh b/nix/internal/hydra-bridge-gateway-test.sh index 2d8c0295..2b59d2e1 100644 --- a/nix/internal/hydra-bridge-gateway-test.sh +++ b/nix/internal/hydra-bridge-gateway-test.sh @@ -26,9 +26,9 @@ cleanup() { trap '' INT TERM trap - EXIT - local exit_code=$? - echo >&2 "Cleaning up… (exit_code=$exit_code)" - if [ "$test_passed" = false ] && [ -n "$work_dir" ]; then + if [ "$test_passed" = true ]; then + echo >&2 "=== Test PASSED ===" + else echo >&2 "=== Test FAILED ===" fi @@ -44,7 +44,7 @@ cleanup() { cd / rm -rf -- "$work_dir" fi - exit "$exit_code" + if [ "$test_passed" = true ]; then exit 0; else exit 1; fi } trap cleanup INT TERM EXIT diff --git a/nix/internal/hydra-platform-gateway-test.sh b/nix/internal/hydra-platform-gateway-test.sh index d9b49119..32557af9 100644 --- a/nix/internal/hydra-platform-gateway-test.sh +++ b/nix/internal/hydra-platform-gateway-test.sh @@ -25,9 +25,9 @@ cleanup() { trap '' INT TERM trap - EXIT - local exit_code=$? - echo >&2 "Cleaning up… (exit_code=$exit_code)" - if [ "$test_passed" = false ] && [ -n "$work_dir" ]; then + if [ "$test_passed" = true ]; then + echo >&2 "=== Test PASSED ===" + else echo >&2 "=== Test FAILED ===" fi @@ -43,7 +43,7 @@ cleanup() { cd / rm -rf -- "$work_dir" fi - exit "$exit_code" + if [ "$test_passed" = true ]; then exit 0; else exit 1; fi } trap cleanup INT TERM EXIT From f3b7fa4a074dc6a1b863d86a06208a540c9c5229 Mon Sep 17 00:00:00 2001 From: Michal Rus Date: Fri, 3 Apr 2026 01:03:21 +0200 Subject: [PATCH 23/23] chore: drop the tracing prefixes in accordance with #501 --- crates/gateway/src/hydra_server_bridge/mod.rs | 46 +++++------ .../src/hydra_server_bridge/verifications.rs | 8 +- crates/sdk_bridge/src/hydra_client/mod.rs | 79 +++++++------------ .../src/hydra_client/verifications.rs | 10 +-- crates/sdk_bridge/src/main.rs | 2 +- 5 files changed, 59 insertions(+), 86 deletions(-) diff --git a/crates/gateway/src/hydra_server_bridge/mod.rs b/crates/gateway/src/hydra_server_bridge/mod.rs index 9684a99a..d802b112 100644 --- a/crates/gateway/src/hydra_server_bridge/mod.rs +++ b/crates/gateway/src/hydra_server_bridge/mod.rs @@ -44,7 +44,7 @@ impl HydrasManager { / 1_000_000.0; if config.commit_ada < minimal_commit { Err(anyhow!( - "hydras-manager: Please make sure that configured commit_ada ≥ lovelace_per_request * requests_per_microtransaction * microtransactions_per_fanout + {}.", + "Please make sure that configured commit_ada ≥ lovelace_per_request * requests_per_microtransaction * microtransactions_per_fanout + {}.", MIN_LOVELACE_PER_TRANSACTION as f64 / 1_000_000.0 ))? } @@ -53,7 +53,7 @@ impl HydrasManager { config.lovelace_per_request * config.requests_per_microtransaction; if microtransaction_lovelace < MIN_LOVELACE_PER_TRANSACTION { Err(anyhow!( - "hydras-manager: Please make sure that each microtransaction will be larger than {MIN_LOVELACE_PER_TRANSACTION} lovelace. Currently it would be {microtransaction_lovelace}." + "Please make sure that each microtransaction will be larger than {MIN_LOVELACE_PER_TRANSACTION} lovelace. Currently it would be {microtransaction_lovelace}." ))? } @@ -90,7 +90,7 @@ impl HydrasManager { let required_funds_ada: f64 = MIN_FUEL_LOVELACE as f64 / 1_000_000.0; if have_funds < required_funds_ada { let err = anyhow!( - "hydra-controller: {} ADA is too little for the Hydra L1 fees on the enterprise address associated with {:?}. Please provide at least {} ADA", + "{} ADA is too little for the Hydra L1 fees on the enterprise address associated with {:?}. Please provide at least {} ADA", have_funds, self.config.toml.cardano_signing_key, required_funds_ada, @@ -98,10 +98,7 @@ impl HydrasManager { error!("{err}"); Err(err)? } - info!( - "hydra-controller: funds on cardano_signing_key: {:?} ADA", - have_funds - ); + info!("funds on cardano_signing_key: {:?} ADA", have_funds); use verifications::{find_free_tcp_port, read_json_file}; @@ -428,7 +425,7 @@ impl State { Ok(()) => (), Err(err) => { error!( - "hydra-controller: {}: error: {}; will restart in {:?}…", + "{}: error: {}; will restart in {:?}…", self_.customer_log_id, err, Self::RESTART_DELAY @@ -461,7 +458,7 @@ impl State { async fn process_event(&mut self, event: Event) -> Result<()> { match event { Event::Restart => { - info!("hydra-controller: {}: starting…", self.customer_log_id); + info!("{}: starting…", self.customer_log_id); self.hydra_head_open = false; self.credits_available.store(0, Ordering::SeqCst); self.credits_last_balance = 0; @@ -487,7 +484,7 @@ impl State { .await; info!( - "hydra-controller: {}: waiting for hydras to connect: ready={:?}", + "{}: waiting for hydras to connect: ready={:?}", self.customer_log_id, ready ); @@ -513,7 +510,7 @@ impl State { let status = verifications::fetch_head_tag(self.api_port).await; info!( - "hydra-controller: {}: waiting for the Initial head status: status={:?}", + "{}: waiting for the Initial head status: status={:?}", self.customer_log_id, status ); @@ -525,7 +522,7 @@ impl State { Ok(status) => { if status == "Initial" { info!( - "hydra-controller: {}: submitting an empty Commit transaction to join the Hydra Head", + "{}: submitting an empty Commit transaction to join the Hydra Head", self.customer_log_id ); self.config @@ -550,7 +547,7 @@ impl State { Event::WaitForOpen => { let status = verifications::fetch_head_tag(self.api_port).await?; info!( - "hydra-controller: {}: waiting for the Open head status: status={:?}", + "{}: waiting for the Open head status: status={:?}", self.customer_log_id, status ); if status == "Open" { @@ -576,7 +573,7 @@ impl State { Ok(current_balance) => { if current_balance < self.credits_last_balance { warn!( - "hydra-controller: {}: snapshot balance decreased ({} -> {}), resetting", + "{}: snapshot balance decreased ({} -> {}), resetting", self.customer_log_id, self.credits_last_balance, current_balance @@ -590,7 +587,7 @@ impl State { * self.config.toml.requests_per_microtransaction; if microtransaction_lovelace == 0 { warn!( - "hydra-controller: {}: microtransaction value is zero; ignoring credits", + "{}: microtransaction value is zero; ignoring credits", self.customer_log_id ); } else if delta >= microtransaction_lovelace { @@ -602,14 +599,14 @@ impl State { .fetch_add(new_credits, Ordering::SeqCst); self.received_microtransactions += new_microtransactions; info!( - "hydra-controller: {}: received {} microtransaction(s), req. credits +{}", + "{}: received {} microtransaction(s), req. credits +{}", self.customer_log_id, new_microtransactions, new_credits ); } else { warn!( - "hydra-controller: {}: snapshot delta {} is below expected microtransaction size {}", + "{}: snapshot delta {} is below expected microtransaction size {}", self.customer_log_id, delta, microtransaction_lovelace ); } @@ -627,7 +624,7 @@ impl State { } }, Err(err) => warn!( - "hydra-controller: {}: failed to read snapshot/utxo: {err}", + "{}: failed to read snapshot/utxo: {err}", self.customer_log_id ), } @@ -658,13 +655,13 @@ impl State { } => { let status = verifications::fetch_head_tag(self.api_port).await?; info!( - "hydra-controller: {}: waiting for the Closed head status: status={:?}", + "{}: waiting for the Closed head status: status={:?}", self.customer_log_id, status ); if status == "Closed" { let invalidity_period = (2 + 1) * CONTESTATION_PERIOD_SECONDS; info!( - "hydra-controller: {}: will wait through the invalidity period ({:?}) before requesting `Fanout`", + "{}: will wait through the invalidity period ({:?}) before requesting `Fanout`", self.customer_log_id, invalidity_period, ); self.send_delayed(Event::DoFanout, invalidity_period).await @@ -684,10 +681,7 @@ impl State { }, Event::DoFanout => { - info!( - "hydra-controller: {}: requesting `Fanout`", - self.customer_log_id, - ); + info!("{}: requesting `Fanout`", self.customer_log_id,); verifications::send_one_websocket_msg( &format!("ws://127.0.0.1:{}", self.api_port), serde_json::json!({"tag":"Fanout"}), @@ -701,12 +695,12 @@ impl State { Event::WaitForIdleAfterClose => { let status = verifications::fetch_head_tag(self.api_port).await?; info!( - "hydra-controller: {}: waiting for the Idle head status (after Fanout): status={:?}", + "{}: waiting for the Idle head status (after Fanout): status={:?}", self.customer_log_id, status ); if status == "Idle" { info!( - "hydra-controller: {}: re-initializing the Hydra Head for another L2 session", + "{}: re-initializing the Hydra Head for another L2 session", self.customer_log_id, ); diff --git a/crates/gateway/src/hydra_server_bridge/verifications.rs b/crates/gateway/src/hydra_server_bridge/verifications.rs index fab1673b..83412efa 100644 --- a/crates/gateway/src/hydra_server_bridge/verifications.rs +++ b/crates/gateway/src/hydra_server_bridge/verifications.rs @@ -16,7 +16,7 @@ impl super::HydraConfig { let key_path = target_dir.join("hydra.sk"); if !key_path.exists() { - info!("hydra-controller: generating hydra keys"); + info!("generating hydra keys"); let status = tokio::process::Command::new(&self.hydra_node_exe) .arg("gen-hydra-key") @@ -29,7 +29,7 @@ impl super::HydraConfig { Err(anyhow!("gen-hydra-key failed with status: {status}"))?; } } else { - info!("hydra-controller: hydra keys already exist"); + info!("hydra keys already exist"); } Ok(()) @@ -498,9 +498,9 @@ pub async fn send_one_websocket_msg( match msg? { Message::Close(_) => break, Message::Text(msg) => { - tracing::info!("hydra-controller: got WebSocket message: {}", msg) + tracing::info!("got WebSocket message: {}", msg) }, - msg => tracing::info!("hydra-controller: got WebSocket message: {:?}", msg), + msg => tracing::info!("got WebSocket message: {:?}", msg), } } diff --git a/crates/sdk_bridge/src/hydra_client/mod.rs b/crates/sdk_bridge/src/hydra_client/mod.rs index 7cedea32..3c3cb288 100644 --- a/crates/sdk_bridge/src/hydra_client/mod.rs +++ b/crates/sdk_bridge/src/hydra_client/mod.rs @@ -125,9 +125,7 @@ impl HydraController { self.event_tx .send(Event::AccountOneRequest) .await - .unwrap_or_else(|_| { - error!("hydra-controller: failed to account one request: event channel closed") - }) + .unwrap_or_else(|_| error!("failed to account one request: event channel closed")) } } @@ -264,11 +262,7 @@ impl State { match self_.process_event(event).await { Ok(()) => (), Err(err) => { - error!( - "hydra-controller: error: {}; will restart in {:?}…", - err, - Self::RESTART_DELAY - ); + error!("error: {}; will restart in {:?}…", err, Self::RESTART_DELAY); tokio::time::sleep(Self::RESTART_DELAY).await; self_.send(Event::Restart).await; }, @@ -297,24 +291,21 @@ impl State { async fn process_event(&mut self, event: Event) -> Result<()> { match event { Event::Restart => { - info!("hydra-controller: starting…"); + info!("starting…"); let potential_fuel = self .lovelace_on_payment_skey(&self.config.cardano_signing_key) .await?; if potential_fuel < MIN_FUEL_LOVELACE { Err(anyhow!( - "hydra-controller: {} ADA is too little for the Hydra L1 fees on the enterprise address associated with {:?}. Please provide at least {} ADA", + "{} ADA is too little for the Hydra L1 fees on the enterprise address associated with {:?}. Please provide at least {} ADA", potential_fuel as f64 / 1_000_000.0, self.config.cardano_signing_key, MIN_FUEL_LOVELACE as f64 / 1_000_000.0, ))? } - info!( - "hydra-controller: fuel on cardano_signing_key: {:?} lovelace", - potential_fuel - ); + info!("fuel on cardano_signing_key: {:?} lovelace", potential_fuel); self.gen_hydra_keys().await?; @@ -357,7 +348,7 @@ impl State { microtransactions_per_fanout: kex_resp.microtransactions_per_fanout, }; info!( - "hydra-controller: payment params commit_ada={} lovelace_per_request={} requests_per_microtransaction={} microtransactions_per_fanout={}", + "payment params commit_ada={} lovelace_per_request={} requests_per_microtransaction={} microtransactions_per_fanout={}", params.commit_ada, params.lovelace_per_request, params.requests_per_microtransaction, @@ -369,7 +360,7 @@ impl State { let addr = self .derive_enterprise_address_from_vkey_json(&kex_resp.gateway_cardano_vkey) .await?; - info!("hydra-controller: gateway payment address: {}", addr); + info!("gateway payment address: {}", addr); self.gateway_payment_addr = addr; } @@ -380,9 +371,7 @@ impl State { verifications::is_tcp_port_free(kex_resp.proposed_platform_h2h_port).await, Ok(true) )) { - warn!( - "hydra-controller: the ports proposed by the Gateway are not free locally, will ask again" - ); + warn!("the ports proposed by the Gateway are not free locally, will ask again"); self.send(Event::Restart).await } else { self.kex_requests @@ -403,7 +392,7 @@ impl State { let addr = self .derive_enterprise_address_from_vkey_json(&kex_resp.gateway_cardano_vkey) .await?; - info!("hydra-controller: gateway payment address: {}", addr); + info!("gateway payment address: {}", addr); self.gateway_payment_addr = addr; } @@ -431,11 +420,11 @@ impl State { let status = verifications::fetch_head_tag(self.api_port).await?; if status == "Initial" { - info!("hydra-controller: head already Initial, proceeding to commit"); + info!("head already Initial, proceeding to commit"); self.send_delayed(Event::TryToCommit, Duration::from_secs(1)) .await } else if status == "Open" { - info!("hydra-controller: head already Open, skipping init + commit"); + info!("head already Open, skipping init + commit"); self.send_delayed(Event::WaitForOpen, Duration::from_secs(1)) .await } else { @@ -446,10 +435,7 @@ impl State { ) .await; - info!( - "hydra-controller: waiting for hydras to connect: ready={:?}", - ready - ); + info!("waiting for hydras to connect: ready={:?}", ready); if matches!(ready, Ok(true)) { verifications::send_one_websocket_msg( @@ -497,7 +483,7 @@ impl State { top_up = MIN_COMMIT_TOPUP_LOVELACE; } info!( - "hydra-controller: topping up commit address by {} lovelace (current={}, target={})", + "topping up commit address by {} lovelace (current={}, target={})", top_up, current_lovelace, target_lovelace ); self.fund_address( @@ -511,7 +497,7 @@ impl State { .await?; } else { info!( - "hydra-controller: commit address already funded (current={}, target={})", + "commit address already funded (current={}, target={})", current_lovelace, target_lovelace ); } @@ -535,15 +521,13 @@ impl State { let lovelace_needed = 0.99 * params.commit_ada * 1_000_000.0; info!( - "hydra-controller: waiting for enough lovelace (> {}) to appear on the commit address: lovelace={:?}", + "waiting for enough lovelace (> {}) to appear on the commit address: lovelace={:?}", lovelace_needed.round(), commit_wallet_lovelace ); if commit_wallet_lovelace as f64 >= lovelace_needed { - info!( - "hydra-controller: submitting a Commit transaction to join the Hydra Head" - ); + info!("submitting a Commit transaction to join the Hydra Head"); self.commit_all_utxo_to_hydra( &self.commit_wallet_addr, self.api_port, @@ -561,10 +545,7 @@ impl State { Event::WaitForOpen => { let status = verifications::fetch_head_tag(self.api_port).await?; - info!( - "hydra-controller: waiting for the Open head status: status={:?}", - status - ); + info!("waiting for the Open head status: status={:?}", status); if status == "Open" { self.last_hydra_head_state = status.clone(); self.send_delayed(Event::MonitorStates, Duration::from_secs(5)) @@ -584,7 +565,7 @@ impl State { let new = new_status.clone(); self.last_hydra_head_state = new_status.clone(); - info!("hydra-controller: state changed from {old} to {new}"); + info!("state changed from {old} to {new}"); if new == "Initial" { self.send_delayed(Event::FundCommitAddr, Duration::from_secs(1)) @@ -607,7 +588,7 @@ impl State { Event::MonitorCredits => { if self.hydra_head_open { if self.gateway_payment_addr.is_empty() { - warn!("hydra-controller: gateway payment address not set yet"); + warn!("gateway payment address not set yet"); } else if let Some(params) = &self.payment_params { match verifications::lovelace_in_snapshot_for_address( self.api_port, @@ -618,7 +599,7 @@ impl State { Ok(current_balance) => { if current_balance < self.credits_last_balance { warn!( - "hydra-controller: snapshot balance decreased ({} -> {}), resetting", + "snapshot balance decreased ({} -> {}), resetting", self.credits_last_balance, current_balance ); self.credits_last_balance = current_balance; @@ -629,7 +610,7 @@ impl State { * params.requests_per_microtransaction; if microtransaction_lovelace == 0 { warn!( - "hydra-controller: microtransaction value is zero; ignoring credits" + "microtransaction value is zero; ignoring credits" ); } else if delta >= microtransaction_lovelace { let new_microtransactions = @@ -639,12 +620,12 @@ impl State { self.credits_available .fetch_add(new_credits, Ordering::SeqCst); info!( - "hydra-controller: req. credits +{} ({} microtransaction(s))", + "req. credits +{} ({} microtransaction(s))", new_credits, new_microtransactions ); } else { warn!( - "hydra-controller: snapshot delta {} is below expected microtransaction size {}", + "snapshot delta {} is below expected microtransaction size {}", delta, microtransaction_lovelace ); } @@ -653,7 +634,7 @@ impl State { } }, Err(err) => { - warn!("hydra-controller: failed to read snapshot/utxo: {err}") + warn!("failed to read snapshot/utxo: {err}") }, } } @@ -667,27 +648,25 @@ impl State { let params = match &self.payment_params { Some(p) => p.clone(), None => { - warn!("hydra-controller: payment parameters not set yet"); + warn!("payment parameters not set yet"); return Ok(()); }, }; if !self.hydra_head_open { - warn!( - "hydra-controller: would account a request, but the Hydra Head is not Open" - ); + warn!("would account a request, but the Hydra Head is not Open"); return Ok(()); } if self.gateway_payment_addr.is_empty() { - warn!("hydra-controller: gateway payment address not set yet"); + warn!("gateway payment address not set yet"); return Ok(()); } self.accounted_requests += 1; if self.accounted_requests >= params.requests_per_microtransaction { - info!("hydra-controller: sending a microtransaction"); + info!("sending a microtransaction"); let amount_lovelace: u64 = self.accounted_requests * params.lovelace_per_request; self.send_hydra_transaction( @@ -718,7 +697,7 @@ impl State { .ok_or(anyhow!("payment parameters not set before prepay"))?; if self.gateway_payment_addr.is_empty() { - warn!("hydra-controller: gateway payment address not set yet"); + warn!("gateway payment address not set yet"); return Ok(()); } diff --git a/crates/sdk_bridge/src/hydra_client/verifications.rs b/crates/sdk_bridge/src/hydra_client/verifications.rs index e84cd85e..7bc0bcf4 100644 --- a/crates/sdk_bridge/src/hydra_client/verifications.rs +++ b/crates/sdk_bridge/src/hydra_client/verifications.rs @@ -18,7 +18,7 @@ impl super::State { let key_path = self.config_dir.join("hydra.sk"); if !key_path.exists() { - info!("hydra-controller: generating hydra keys"); + info!("generating hydra keys"); let status = tokio::process::Command::new(&self.hydra_node_exe) .arg("gen-hydra-key") @@ -31,7 +31,7 @@ impl super::State { Err(anyhow!("gen-hydra-key failed with status: {status}"))?; } } else { - info!("hydra-controller: hydra keys already exist"); + info!("hydra keys already exist"); } Ok(()) @@ -590,7 +590,7 @@ impl super::State { }); tracing::info!( - "hydra-controller: sending WebSocket payload: {}", + "sending WebSocket payload: {}", serde_json::to_string(&payload)? ); @@ -761,9 +761,9 @@ pub async fn send_one_websocket_msg( match msg? { Message::Close(_) => break, Message::Text(msg) => { - tracing::info!("hydra-controller: got WebSocket message: {}", msg) + tracing::info!("got WebSocket message: {}", msg) }, - msg => tracing::info!("hydra-controller: got WebSocket message: {:?}", msg), + msg => tracing::info!("got WebSocket message: {:?}", msg), } } diff --git a/crates/sdk_bridge/src/main.rs b/crates/sdk_bridge/src/main.rs index 43640e06..a1ea0678 100644 --- a/crates/sdk_bridge/src/main.rs +++ b/crates/sdk_bridge/src/main.rs @@ -22,7 +22,7 @@ async fn main() -> Result<()> { Format::default() .with_ansi(true) .with_level(true) - .with_target(false) + .with_target(true) .compact(), ) .init();