diff --git a/Cargo.lock b/Cargo.lock index 6b6bb0df2ade..3713ccc80c95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,9 +89,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fe14fb6dded4be3f44d053e59402a405bb231561e36a88bc2283a9829d81fe" +checksum = "fafc3b20c6d069d9db47037f34acfb0e079c050fa5c1ff9e79855609b359b92b" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -110,9 +110,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cbbc8fcae58f39dbfbdc94ead48de0779910528889aebc074aed75959bffe7" +checksum = "8d32061da2f184e5defab8e65a3057f88b7017cfe1ea9e2d6b413edb5ca76a54" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -123,7 +123,7 @@ dependencies = [ [[package]] name = "alloy-json-rpc" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#d6ec3478d8c68e3c12b0eb9bdb65870a113e46a6" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" dependencies = [ "alloy-primitives", "serde", @@ -134,7 +134,7 @@ dependencies = [ [[package]] name = "alloy-networks" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#d6ec3478d8c68e3c12b0eb9bdb65870a113e46a6" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5def4b5e1bb8fe7ea37eeac1063246d4ef26f56cbdccf864a5a6bdcb80e91f4" +checksum = "08ca2c09d5911548a5cb620382ea0e1af99d3c898ce0efecbbd274a4676cf53e" dependencies = [ "alloy-rlp", "arbitrary", @@ -169,7 +169,7 @@ dependencies = [ [[package]] name = "alloy-providers" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#d6ec3478d8c68e3c12b0eb9bdb65870a113e46a6" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" dependencies = [ "alloy-networks", "alloy-primitives", @@ -184,6 +184,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "alloy-pubsub" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "bimap", + "futures", + "serde_json", + "tokio", + "tower", + "tracing", +] + [[package]] name = "alloy-rlp" version = "0.3.3" @@ -210,7 +226,7 @@ dependencies = [ [[package]] name = "alloy-rpc-client" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#d6ec3478d8c68e3c12b0eb9bdb65870a113e46a6" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -227,11 +243,11 @@ dependencies = [ [[package]] name = "alloy-rpc-types" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#d6ec3478d8c68e3c12b0eb9bdb65870a113e46a6" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" dependencies = [ "alloy-primitives", "alloy-rlp", - "itertools 0.11.0", + "itertools 0.12.0", "jsonrpsee-types", "serde", "serde_json", @@ -241,9 +257,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0acd5b8d2699b095a57a0ecea6a6a2045b8e5ed6f2607bfa3382961d2889e82" +checksum = "e40cea54ac58080a1b88ea6556866eac1902b321186c77d53ad2b5ebbbf0e038" dependencies = [ "alloy-json-abi", "const-hex", @@ -261,18 +277,18 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc77699fb68c8a2cf18efb2c51df43e09b42b53886c6eb552951b19c41ebfc84" +checksum = "f23cb462613b2046da46dbf69ebaee458b7bfd3e9d7fe05adcce38a8d4b8a14f" dependencies = [ "winnow", ] [[package]] name = "alloy-sol-types" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d483e9c6db659cdb24fc736684ef68b743705fbdb0677c6404815361871b92" +checksum = "f81aa34725607be118c395d62c1d8d97c8a343dd1ada5370ed508ed5232eab6a" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -284,7 +300,7 @@ dependencies = [ [[package]] name = "alloy-transport" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#d6ec3478d8c68e3c12b0eb9bdb65870a113e46a6" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" dependencies = [ "alloy-json-rpc", "base64 0.21.5", @@ -300,7 +316,7 @@ dependencies = [ [[package]] name = "alloy-transport-http" version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#d6ec3478d8c68e3c12b0eb9bdb65870a113e46a6" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -310,6 +326,39 @@ dependencies = [ "url", ] +[[package]] +name = "alloy-transport-ipc" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" +dependencies = [ + "alloy-json-rpc", + "alloy-pubsub", + "alloy-transport", + "bytes", + "futures", + "interprocess", + "pin-project", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "alloy-transport-ws" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy/?branch=onbjerg/alloy-temp-provider-trait#e359e2722d3aa17a014a07a932f52453824f8bb2" +dependencies = [ + "alloy-pubsub", + "alloy-transport", + "futures", + "http", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "ws_stream_wasm", +] + [[package]] name = "ammonia" version = "3.3.0" @@ -663,13 +712,37 @@ dependencies = [ "term", ] +[[package]] +name = "async-channel" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +dependencies = [ + "concurrent-queue", + "event-listener 4.0.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-lock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +dependencies = [ + "event-listener 4.0.0", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-priority-channel" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c21678992e1b21bebfe2bc53ab5f5f68c106eddab31b24e0bb06e9b715a86640" dependencies = [ - "event-listener", + "event-listener 2.5.3", ] [[package]] @@ -683,6 +756,12 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + [[package]] name = "async-trait" version = "0.1.74" @@ -720,6 +799,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "aurora-engine-modexp" version = "1.0.0" @@ -854,6 +939,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bindgen" version = "0.66.1" @@ -939,6 +1030,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + [[package]] name = "blst" version = "0.3.11" @@ -1494,6 +1601,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.7" @@ -2490,6 +2606,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.0", + "pin-project-lite", +] + [[package]] name = "evm-disassembler" version = "0.3.0" @@ -2891,12 +3028,16 @@ version = "0.2.0" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", + "alloy-json-rpc", "alloy-primitives", "alloy-providers", + "alloy-pubsub", "alloy-rpc-types", "alloy-sol-types", "alloy-transport", "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", "async-trait", "auto_impl", "clap", @@ -2924,6 +3065,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tower", "tracing", "url", "walkdir", @@ -3270,6 +3412,16 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +[[package]] +name = "futures-lite" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-locks" version = "0.7.1" @@ -4133,6 +4285,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "blocking", + "cfg-if", + "futures-core", + "futures-io", + "intmap", + "libc", + "once_cell", + "rustc_version 0.4.0", + "spinning", + "thiserror", + "to_method", + "tokio", + "winapi", +] + +[[package]] +name = "intmap" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9" + [[package]] name = "ipnet" version = "2.9.0" @@ -4168,6 +4347,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -5075,6 +5263,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.1" @@ -5374,6 +5568,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -6878,6 +7083,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spinning" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -7030,9 +7244,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e349ed2ca56eff703d7c3ea013771bcbab9ad2ad39dddf863fc51d820329dc41" +checksum = "e2c7ad08db24862d5b787a94714ff6b047935c3e3f60af944ac969404debd7ff" dependencies = [ "paste", "proc-macro2", @@ -7256,6 +7470,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + [[package]] name = "tokio" version = "1.34.0" diff --git a/Cargo.toml b/Cargo.toml index 30cc5deb5507..0e6b6a87ec3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,11 +145,16 @@ ethers-solc = { version = "2.0", default-features = false } alloy-providers = "0.1.0" alloy-transport = "0.1.0" alloy-transport-http = "0.1.0" +alloy-transport-ws = "0.1.0" +alloy-transport-ipc = "0.1.0" alloy-rpc-types = "0.1.0" -alloy-primitives = "0.5.0" -alloy-dyn-abi = "0.5.0" -alloy-json-abi = "0.5.0" -alloy-sol-types = "0.5.0" +alloy-json-rpc = "0.1.0" +alloy-pubsub = "0.1.0" +alloy-rpc-client = "0.1.0" +alloy-primitives = "0.5.1" +alloy-dyn-abi = "0.5.1" +alloy-json-abi = "0.5.1" +alloy-sol-types = "0.5.1" syn-solidity = "0.5.0" alloy-chains = "0.1.3" @@ -203,7 +208,12 @@ ethers-solc = { git = "https://github.com/gakonst/ethers-rs", rev = "2e93fa7e0f2 alloy-providers = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } alloy-transport = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } alloy-transport-http = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } +alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } +alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } +alloy-pubsub = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } +alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } +alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy/", branch = "onbjerg/alloy-temp-provider-trait" } revm = { git = "https://github.com/bluealloy/revm", branch = "reth_freeze" } revm-interpreter = { git = "https://github.com/bluealloy/revm", branch = "reth_freeze" } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 582ed8dcb010..12cf0c151160 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -25,8 +25,14 @@ alloy-rpc-types.workspace = true alloy-providers.workspace = true alloy-transport.workspace = true alloy-transport-http.workspace = true +alloy-transport-ws.workspace = true +alloy-transport-ipc.workspace = true +alloy-json-rpc.workspace = true +alloy-pubsub.workspace = true alloy-sol-types.workspace = true +tower.workspace = true + async-trait = "0.1" auto_impl = "1.1.0" clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } diff --git a/crates/common/src/alloy_runtime_transport.rs b/crates/common/src/alloy_runtime_transport.rs new file mode 100644 index 000000000000..22844ccc4a01 --- /dev/null +++ b/crates/common/src/alloy_runtime_transport.rs @@ -0,0 +1,314 @@ +//! Runtime transport that connects on first request, which can take either of an HTTP, +//! WebSocket, or IPC transport. +use alloy_json_rpc::{RequestPacket, ResponsePacket}; +use alloy_pubsub::{PubSubConnect, PubSubFrontend}; +use alloy_transport::{ + Authorization, BoxTransport, TransportError, TransportErrorKind, TransportFut, +}; +use alloy_transport_http::Http; +use alloy_transport_ipc::IpcConnect; +use alloy_transport_ws::WsConnect; +use ethers_providers::{JwtAuth, JwtKey}; +use reqwest::header::{HeaderName, HeaderValue}; +use std::{path::PathBuf, str::FromStr, sync::Arc}; +use thiserror::Error; +use tokio::sync::RwLock; +use tower::Service; +use url::Url; + +/// An enum representing the different transports that can be used to connect to a runtime. +/// Only meant to be used internally by [RuntimeTransport]. +#[derive(Clone, Debug)] +pub enum InnerTransport { + /// HTTP transport + Http(Http), + /// WebSocket transport + Ws(PubSubFrontend), + // TODO: IPC + /// IPC transport + Ipc(PubSubFrontend), +} + +/// Error type for the runtime transport. +#[derive(Error, Debug)] +pub enum RuntimeTransportError { + /// Internal transport error + #[error(transparent)] + TransportError(TransportError), + + /// Failed to lock the transport + #[error("Failed to lock the transport")] + LockError, + + /// Invalid URL scheme + #[error("URL scheme is not supported: {0}")] + BadScheme(String), + + /// Invalid HTTP header + #[error("Invalid HTTP header: {0}")] + BadHeader(String), + + /// Invalid file path + #[error("Invalid IPC file path: {0}")] + BadPath(String), + + /// Invalid construction of Http provider + #[error(transparent)] + HttpConstructionError(#[from] reqwest::Error), + + /// Invalid JWT + #[error("Invalid JWT: {0}")] + InvalidJwt(String), +} + +/// A runtime transport is a custom [alloy_transport::Transport] that only connects when the *first* +/// request is made. When the first request is made, it will connect to the runtime using either an +/// HTTP WebSocket, or IPC transport depending on the URL used. +#[derive(Clone, Debug, Error)] +pub struct RuntimeTransport { + /// The inner actual transport used. + inner: Arc>>, + /// The URL to connect to. + url: Url, + /// The headers to use for requests. + headers: Vec, + /// The JWT to use for requests. + jwt: Option, + /// The timeout for requests. + timeout: std::time::Duration, +} + +/// A builder for [RuntimeTransport]. +pub struct RuntimeTransportBuilder { + url: Url, + headers: Vec, + jwt: Option, + timeout: std::time::Duration, +} + +impl RuntimeTransportBuilder { + /// Create a new builder with the given URL. + pub fn new(url: Url) -> Self { + Self { url, headers: vec![], jwt: None, timeout: std::time::Duration::from_secs(30) } + } + + /// Set the URL for the transport. + pub fn with_headers(mut self, headers: Vec) -> Self { + self.headers = headers; + self + } + + /// Set the JWT for the transport. + pub fn with_jwt(mut self, jwt: Option) -> Self { + self.jwt = jwt; + self + } + + /// Set the timeout for the transport. + pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self { + self.timeout = timeout; + self + } + + /// Builds the [RuntimeTransport] and returns it in a disconnected state. + /// The runtime transport will then connect when the first request happens. + pub fn build(self) -> RuntimeTransport { + RuntimeTransport { + inner: Arc::new(RwLock::new(None)), + url: self.url, + headers: self.headers, + jwt: self.jwt, + timeout: self.timeout, + } + } +} + +impl ::core::fmt::Display for RuntimeTransport { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "RuntimeTransport {}", self.url) + } +} + +impl RuntimeTransport { + /// Create a new [RuntimeTransport]. + pub fn new( + url: Url, + headers: Vec, + jwt: Option, + timeout: std::time::Duration, + ) -> Self { + Self { inner: Arc::new(RwLock::new(None)), url, headers, jwt, timeout } + } + + /// Connects the underlying transport, depending on the URL scheme. + pub async fn connect(&self) -> Result { + match self.url.scheme() { + "http" | "https" => self.connect_http().await, + "ws" | "wss" => self.connect_ws().await, + "file" => self.connect_ipc().await, + _ => Err(RuntimeTransportError::BadScheme(self.url.scheme().to_string())), + } + } + + /// Connects to an HTTP [alloy_transport_http::Http] transport. + async fn connect_http(&self) -> Result { + let mut client_builder = reqwest::Client::builder().timeout(self.timeout); + let mut headers = reqwest::header::HeaderMap::new(); + + // If there's a JWT, add it to the headers if we can decode it. + if let Some(jwt) = self.jwt.clone() { + let auth = + build_auth(jwt).map_err(|e| RuntimeTransportError::InvalidJwt(e.to_string()))?; + + let mut auth_value: HeaderValue = + HeaderValue::from_str(&auth.to_string()).expect("Header should be valid string"); + auth_value.set_sensitive(true); + + headers.insert(reqwest::header::AUTHORIZATION, auth_value); + }; + + // Add any custom headers. + for header in self.headers.iter() { + let make_err = || RuntimeTransportError::BadHeader(header.to_string()); + + let (key, val) = header.split_once(':').ok_or_else(make_err)?; + + headers.insert( + HeaderName::from_str(key.trim()).map_err(|_| make_err())?, + HeaderValue::from_str(val.trim()).map_err(|_| make_err())?, + ); + } + + client_builder = client_builder.default_headers(headers); + + let client = + client_builder.build().map_err(RuntimeTransportError::HttpConstructionError)?; + + // todo: retry tower layer + Ok(InnerTransport::Http(Http::with_client(client, self.url.clone()))) + } + + /// Connects to a WS transport. + async fn connect_ws(&self) -> Result { + let auth = self.jwt.as_ref().and_then(|jwt| build_auth(jwt.clone()).ok()); + let ws = WsConnect { url: self.url.to_string(), auth } + .into_service() + .await + .map_err(RuntimeTransportError::TransportError)?; + Ok(InnerTransport::Ws(ws)) + } + + /// Connects to an IPC transport. + async fn connect_ipc(&self) -> Result { + let path = url_to_file_path(&self.url) + .map_err(|_| RuntimeTransportError::BadPath(self.url.to_string()))?; + let ipc_connector: IpcConnect = path.into(); + let ipc = + ipc_connector.into_service().await.map_err(RuntimeTransportError::TransportError)?; + Ok(InnerTransport::Ipc(ipc)) + } + + /// Sends a request using the underlying transport. + /// If this is the first request, it will connect to the appropiate transport depending on the + /// URL scheme. For sending the actual request, this action is delegated down to the + /// underlying transport through Tower's call. See tower's [tower::Service] trait for more + /// information. + pub fn request(&self, req: RequestPacket) -> TransportFut<'static> { + let this = self.clone(); + Box::pin(async move { + if this.inner.read().await.is_none() { + let mut inner = this.inner.write().await; + *inner = Some(this.connect().await.map_err(TransportErrorKind::custom)?) + } + + let mut inner = this.inner.write().await; + // SAFETY: We just checked that the inner transport exists. + let inner_mut = inner.as_mut().expect("We should have an inner transport."); + + match inner_mut { + InnerTransport::Http(http) => http.call(req).await, + InnerTransport::Ws(ws) => ws.call(req).await, + InnerTransport::Ipc(ipc) => ipc.call(req).await, + } + }) + } + + /// Convert this transport into a boxed trait object. + pub fn boxed(self) -> BoxTransport + where + Self: Sized + Clone + Send + Sync + 'static, + { + BoxTransport::new(self) + } +} + +impl tower::Service for RuntimeTransport { + type Response = ResponsePacket; + type Error = TransportError; + type Future = TransportFut<'static>; + + #[inline] + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, req: RequestPacket) -> Self::Future { + self.request(req) + } +} + +impl Service for &RuntimeTransport { + type Response = ResponsePacket; + type Error = TransportError; + type Future = TransportFut<'static>; + + #[inline] + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, req: RequestPacket) -> Self::Future { + self.request(req) + } +} + +fn build_auth(jwt: String) -> eyre::Result { + // Decode jwt from hex, then generate claims (iat with current timestamp) + let jwt = hex::decode(jwt)?; + let secret = JwtKey::from_slice(&jwt).map_err(|err| eyre::eyre!("Invalid JWT: {}", err))?; + let auth = JwtAuth::new(secret, None, None); + let token = auth.generate_token()?; + + // Essentially unrolled ethers-rs new_with_auth to accomodate the custom timeout + let auth = Authorization::Bearer(token); + + Ok(auth) +} + +#[cfg(windows)] +fn url_to_file_path(url: &Url) -> Result { + const PREFIX: &str = "file:///pipe/"; + + let url_str = url.as_str(); + + if url_str.starts_with(PREFIX) { + let pipe_name = &url_str[PREFIX.len()..]; + let pipe_path = format!(r"\\.\pipe\{}", pipe_name); + return Ok(PathBuf::from(pipe_path)); + } + + url.to_file_path() +} + +#[cfg(not(windows))] +fn url_to_file_path(url: &Url) -> Result { + url.to_file_path() +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index ef8eb88528c0..a75ffcbb75a0 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -8,6 +8,7 @@ extern crate self as foundry_common; extern crate tracing; pub mod abi; +pub mod alloy_runtime_transport; pub mod calc; pub mod clap_helpers; pub mod compile; diff --git a/crates/common/src/provider/alloy.rs b/crates/common/src/provider/alloy.rs index 4289f07cea59..a2c29869ab16 100644 --- a/crates/common/src/provider/alloy.rs +++ b/crates/common/src/provider/alloy.rs @@ -1,16 +1,16 @@ //! Commonly used helpers to construct `Provider`s -use crate::{ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT}; +use crate::{ + alloy_runtime_transport::RuntimeTransportBuilder, ALCHEMY_FREE_TIER_CUPS, REQUEST_TIMEOUT, +}; use alloy_primitives::U256; use alloy_providers::provider::{Provider, TempProvider}; -use alloy_transport::{Authorization, BoxTransport, Transport}; -use alloy_transport_http::Http; +use alloy_transport::BoxTransport; use ethers_middleware::gas_oracle::{GasCategory, GasOracle, Polygon}; -use ethers_providers::{JwtAuth, JwtKey}; use eyre::{Result, WrapErr}; use foundry_common::types::ToAlloy; use foundry_config::NamedChain; -use reqwest::{header::HeaderValue, Url}; +use reqwest::Url; use std::{ path::{Path, PathBuf}, time::Duration, @@ -220,45 +220,21 @@ impl ProviderBuilder { max_retry: _, timeout_retry: _, initial_backoff: _, - timeout: _, + timeout, compute_units_per_second: _, jwt, - headers: _, + headers, } = self; let url = url?; - // todo: ipc - // todo: ws // todo: port alchemy compute units logic? // todo: provider polling interval - let transport = match url.scheme() { - "http" | "https" => { - let mut client_builder = reqwest::Client::builder().timeout(self.timeout); - - if let Some(jwt) = jwt { - // todo: wrap err - let auth = build_auth(jwt)?; - - let mut auth_value: HeaderValue = HeaderValue::from_str(&auth.to_string()) - .expect("Header should be valid string"); - auth_value.set_sensitive(true); - - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert(reqwest::header::AUTHORIZATION, auth_value); - - client_builder = client_builder.default_headers(headers); - }; - - // todo: wrap err - let client = client_builder.build()?; + let transport_builder = RuntimeTransportBuilder::new(url.clone()) + .with_timeout(timeout) + .with_headers(headers) + .with_jwt(jwt); - // todo: retry tower layer - Http::with_client(client, url).boxed() - } - _ => unimplemented!(), - }; - - Ok(Provider::new(transport)) + Ok(Provider::new(transport_builder.build().boxed())) } } @@ -317,20 +293,6 @@ fn resolve_path(path: &Path) -> Result { Err(()) } -// todo: docs -fn build_auth(jwt: String) -> eyre::Result { - // Decode jwt from hex, then generate claims (iat with current timestamp) - let jwt = hex::decode(jwt)?; - let secret = JwtKey::from_slice(&jwt).map_err(|err| eyre::eyre!("Invalid JWT: {}", err))?; - let auth = JwtAuth::new(secret, None, None); - let token = auth.generate_token()?; - - // Essentially unrolled ethers-rs new_with_auth to accomodate the custom timeout - let auth = Authorization::Bearer(token); - - Ok(auth) -} - #[cfg(test)] mod tests { use super::*;