diff --git a/Cargo.lock b/Cargo.lock index a7eb2e6ce..3cbcc5f87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,7 +200,7 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http", + "http 1.2.0", "http-body", "http-body-util", "itoa", @@ -227,7 +227,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http", + "http 1.2.0", "http-body", "http-body-util", "hyper", @@ -260,7 +260,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 1.2.0", "http-body", "http-body-util", "mime", @@ -279,7 +279,7 @@ checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" dependencies = [ "bytes", "futures-util", - "http", + "http 1.2.0", "http-body", "http-body-util", "mime", @@ -315,6 +315,12 @@ dependencies = [ "backtrace", ] +[[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" @@ -574,6 +580,37 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[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" version = "0.10.0" @@ -852,6 +889,31 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fantoccini" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7722aeee9c2be6fa131166990295089d73d973012b758a2208b9ba51af5dd024" +dependencies = [ + "base64 0.22.1", + "cookie 0.18.1", + "futures-core", + "futures-util", + "http 1.2.0", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "mime", + "openssl", + "serde", + "serde_json", + "time", + "tokio", + "url", + "webdriver", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -886,6 +948,21 @@ 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" @@ -1055,7 +1132,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.2.0", "indexmap 2.7.1", "slab", "tokio", @@ -1111,6 +1188,17 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.2.0" @@ -1129,7 +1217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.2.0", ] [[package]] @@ -1140,7 +1228,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http", + "http 1.2.0", "http-body", "pin-project-lite", ] @@ -1167,7 +1255,7 @@ dependencies = [ "futures-channel", "futures-util", "h2", - "http", + "http 1.2.0", "http-body", "httparse", "httpdate", @@ -1191,6 +1279,22 @@ dependencies = [ "tower-service", ] +[[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" @@ -1200,7 +1304,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.2.0", "http-body", "hyper", "pin-project-lite", @@ -1753,7 +1857,8 @@ dependencies = [ "anyhow", "axum 0.8.1", "clap", - "http", + "fantoccini", + "http 1.2.0", "http-body-util", "miden-lib", "miden-node-proto", @@ -1764,11 +1869,14 @@ dependencies = [ "rand", "rand_chacha", "serde", + "serde_json", "static-files", "thiserror 2.0.11", "tokio", + "tokio-stream", "toml", "tonic", + "tonic-web", "tower 0.5.2", "tower-http 0.6.2", "tracing", @@ -1962,7 +2070,7 @@ version = "0.8.0" dependencies = [ "anyhow", "figment", - "http", + "http 1.2.0", "itertools 0.14.0", "miden-objects", "opentelemetry", @@ -2153,6 +2261,23 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2304,12 +2429,50 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-sys" +version = "0.9.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.28.0" @@ -2332,7 +2495,7 @@ checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91" dependencies = [ "async-trait", "futures-core", - "http", + "http 1.2.0", "opentelemetry", "opentelemetry-proto", "opentelemetry_sdk", @@ -2939,7 +3102,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", ] [[package]] @@ -3022,6 +3185,19 @@ 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 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -3029,7 +3205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -3489,6 +3665,16 @@ dependencies = [ "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.1" @@ -3566,10 +3752,10 @@ dependencies = [ "async-stream", "async-trait", "axum 0.7.9", - "base64", + "base64 0.22.1", "bytes", "h2", - "http", + "http 1.2.0", "http-body", "http-body-util", "hyper", @@ -3610,9 +3796,9 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5299dd20801ad736dccb4a5ea0da7376e59cd98f213bf1c3d478cf53f4834b58" dependencies = [ - "base64", + "base64 0.22.1", "bytes", - "http", + "http 1.2.0", "http-body", "http-body-util", "pin-project", @@ -3668,7 +3854,7 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "bitflags", "bytes", - "http", + "http 1.2.0", "http-body", "http-body-util", "pin-project-lite", @@ -3684,7 +3870,7 @@ checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "bitflags", "bytes", - "http", + "http 1.2.0", "http-body", "pin-project-lite", "tower-layer", @@ -3871,6 +4057,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" @@ -4103,6 +4295,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webdriver" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144ab979b12d36d65065635e646549925de229954de2eb3b47459b432a42db71" +dependencies = [ + "base64 0.21.7", + "bytes", + "cookie 0.16.2", + "http 0.2.12", + "log", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "time", + "unicode-segmentation", + "url", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/bin/faucet/Cargo.toml b/bin/faucet/Cargo.toml index 83194cc0f..a1b66b565 100644 --- a/bin/faucet/Cargo.toml +++ b/bin/faucet/Cargo.toml @@ -43,3 +43,9 @@ url = { workspace = true } # Required to inject build metadata. miden-node-utils = { workspace = true, features = ["vergen"] } static-files = "0.2" + +[dev-dependencies] +fantoccini = { version = "0.21" } +serde_json = { version = "1.0" } +tokio-stream = { workspace = true, features = ["net"] } +tonic-web = { version = "0.12" } diff --git a/bin/faucet/src/main.rs b/bin/faucet/src/main.rs index 64e223d0b..c6dce5eed 100644 --- a/bin/faucet/src/main.rs +++ b/bin/faucet/src/main.rs @@ -5,6 +5,9 @@ mod handlers; mod state; mod store; +#[cfg(test)] +mod stub_rpc_api; + use std::path::PathBuf; use anyhow::Context; @@ -96,11 +99,14 @@ async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); + run_faucet_command(cli).await +} + +async fn run_faucet_command(cli: Cli) -> anyhow::Result<()> { match &cli.command { Command::Start { config } => { let config: FaucetConfig = load_config(config).context("failed to load configuration file")?; - let faucet_state = FaucetState::new(config.clone()).await?; info!(target: COMPONENT, %config, "Initializing server"); @@ -128,9 +134,12 @@ async fn main() -> anyhow::Result<()> { ) .with_state(faucet_state); - let socket_addr = config.endpoint.socket_addrs(|| None)?.into_iter().next().ok_or( - anyhow::anyhow!("Couldn't get any socket addrs for endpoint: {}", config.endpoint), - )?; + let socket_addr = config + .endpoint + .socket_addrs(|| None)? + .into_iter() + .next() + .with_context(|| format!("no sockets available on {}", config.endpoint))?; let listener = TcpListener::bind(socket_addr).await.context("failed to bind TCP listener")?; @@ -229,3 +238,121 @@ fn long_version() -> LongVersion { debug: option_env!("VERGEN_CARGO_DEBUG").unwrap_or_default(), } } + +#[cfg(test)] +mod test { + use std::{ + env::temp_dir, + io::{BufRead, BufReader}, + process::{Command, Stdio}, + str::FromStr, + }; + + use fantoccini::ClientBuilder; + use serde_json::{json, Map}; + use url::Url; + + use crate::{config::FaucetConfig, run_faucet_command, stub_rpc_api::serve_stub, Cli}; + + /// This test starts a stub node, a faucet connected to the stub node, and a chromedriver + /// to test the faucet website. It then loads the website and checks that all the requests + /// made return status 200. + #[tokio::test] + async fn test_website() { + let stub_node_url = Url::from_str("http://localhost:50051").unwrap(); + + // Start the stub node + tokio::spawn({ + let stub_node_url = stub_node_url.clone(); + async move { serve_stub(&stub_node_url).await.unwrap() } + }); + + let config_path = temp_dir().join("faucet.toml"); + let faucet_account_path = temp_dir().join("account.mac"); + + // Create config + let config = FaucetConfig { + node_url: stub_node_url, + faucet_account_path: faucet_account_path.clone(), + ..FaucetConfig::default() + }; + let config_as_toml_string = toml::to_string(&config).unwrap(); + std::fs::write(&config_path, config_as_toml_string).unwrap(); + + // Create faucet account + run_faucet_command(Cli { + command: crate::Command::CreateFaucetAccount { + config_path: config_path.clone(), + output_path: faucet_account_path.clone(), + token_symbol: "TEST".to_string(), + decimals: 2, + max_supply: 1000, + }, + }) + .await + .unwrap(); + + // Start the faucet connected to the stub + let website_url = config.endpoint.clone(); + tokio::spawn(async move { + run_faucet_command(Cli { + command: crate::Command::Start { config: config_path }, + }) + .await + .unwrap(); + }); + + // Start chromedriver. This requires having chromedriver and chrome installed + let chromedriver_port = "57709"; + #[expect(clippy::zombie_processes)] + let mut chromedriver = Command::new("chromedriver") + .arg(format!("--port={chromedriver_port}")) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to start chromedriver"); + // Wait for chromedriver to be running + let stdout = chromedriver.stdout.take().unwrap(); + for line in BufReader::new(stdout).lines() { + if line.unwrap().contains("ChromeDriver was started successfully") { + break; + } + } + + // Start fantoccini client + let client = ClientBuilder::native() + .capabilities( + [( + "goog:chromeOptions".to_string(), + json!({"args": ["--headless", "--disable-gpu", "--no-sandbox"]}), + )] + .into_iter() + .collect::>(), + ) + .connect(&format!("http://localhost:{chromedriver_port}")) + .await + .expect("failed to connect to WebDriver"); + + // Open the website + client.goto(website_url.as_str()).await.unwrap(); + + let title = client.title().await.unwrap(); + assert_eq!(title, "Miden Faucet"); + + // Execute a script to get all the failed requests + let script = r" + let errors = []; + performance.getEntriesByType('resource').forEach(entry => { + if (entry.responseStatus && entry.responseStatus >= 400) { + errors.push({url: entry.name, status: entry.responseStatus}); + } + }); + return errors; + "; + let failed_requests = client.execute(script, vec![]).await.unwrap(); + assert!(failed_requests.as_array().unwrap().is_empty()); + + // Close the client and kill chromedriver + client.close().await.unwrap(); + chromedriver.kill().unwrap(); + } +} diff --git a/bin/faucet/src/stub_rpc_api.rs b/bin/faucet/src/stub_rpc_api.rs new file mode 100644 index 000000000..0a89336ff --- /dev/null +++ b/bin/faucet/src/stub_rpc_api.rs @@ -0,0 +1,166 @@ +use miden_node_proto::generated::{ + block::BlockHeader, + digest::Digest, + requests::{ + CheckNullifiersByPrefixRequest, CheckNullifiersRequest, GetAccountDetailsRequest, + GetAccountProofsRequest, GetAccountStateDeltaRequest, GetBlockByNumberRequest, + GetBlockHeaderByNumberRequest, GetNotesByIdRequest, SubmitProvenTransactionRequest, + SyncNoteRequest, SyncStateRequest, + }, + responses::{ + CheckNullifiersByPrefixResponse, CheckNullifiersResponse, GetAccountDetailsResponse, + GetAccountProofsResponse, GetAccountStateDeltaResponse, GetBlockByNumberResponse, + GetBlockHeaderByNumberResponse, GetNotesByIdResponse, SubmitProvenTransactionResponse, + SyncNoteResponse, SyncStateResponse, + }, + rpc::api_server, +}; +use miden_node_utils::errors::ApiError; +use tokio::net::TcpListener; +use tokio_stream::wrappers::TcpListenerStream; +use tonic::{Request, Response, Status}; +use url::Url; + +#[derive(Clone)] +pub struct StubRpcApi; + +#[tonic::async_trait] +impl api_server::Api for StubRpcApi { + async fn check_nullifiers( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!(); + } + + async fn check_nullifiers_by_prefix( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!(); + } + + async fn get_block_header_by_number( + &self, + _request: Request, + ) -> Result, Status> { + // Values are taken from the default genesis block as at v0.7 + Ok(Response::new(GetBlockHeaderByNumberResponse { + block_header: Some(BlockHeader { + version: 1, + prev_hash: Some(Digest { d0: 0, d1: 0, d2: 0, d3: 0 }), + block_num: 0, + chain_root: Some(Digest { + d0: 0x9729_9D39_2DA8_DC69, + d1: 0x674_44AF_6294_0719, + d2: 0x7B97_0BC7_07A0_F7D6, + d3: 0xE423_8D7C_78F3_9D8B, + }), + account_root: Some(Digest { + d0: 0x9666_5D75_8487_401A, + d1: 0xB7BF_DF8B_379F_ED71, + d2: 0xFCA7_82CB_2406_2222, + d3: 0x8D0C_B80F_6377_4E9A, + }), + nullifier_root: Some(Digest { + d0: 0xD4A0_CFF6_578C_123E, + d1: 0xF11A_1794_8930_B14A, + d2: 0xD128_DD2A_4213_B53C, + d3: 0x2DF8_FE54_F23F_6B91, + }), + note_root: Some(Digest { + d0: 0x93CE_DDC8_A187_24FE, + d1: 0x4E32_9917_2E91_30ED, + d2: 0x8022_9E0E_1808_C860, + d3: 0x13F4_7934_7EB7_FD78, + }), + tx_hash: Some(Digest { d0: 0, d1: 0, d2: 0, d3: 0 }), + kernel_root: Some(Digest { + d0: 0x7B6F_43E5_2910_C8C3, + d1: 0x99B3_2868_577E_5779, + d2: 0xAF9E_6424_57CD_B8C1, + d3: 0xB1DD_E61B_F983_2DBD, + }), + proof_hash: Some(Digest { d0: 0, d1: 0, d2: 0, d3: 0 }), + timestamp: 0x63B0_CD00, + }), + mmr_path: None, + chain_length: None, + })) + } + + async fn sync_state( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!(); + } + + async fn sync_notes( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!(); + } + + async fn get_notes_by_id( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!(); + } + + async fn submit_proven_transaction( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(SubmitProvenTransactionResponse { block_height: 0 })) + } + + async fn get_account_details( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::not_found("account not found")) + } + + async fn get_block_by_number( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } + + async fn get_account_state_delta( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } + + async fn get_account_proofs( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } +} + +pub async fn serve_stub(endpoint: &Url) -> Result<(), ApiError> { + let addr = endpoint + .socket_addrs(|| None) + .map_err(ApiError::EndpointToSocketFailed)? + .into_iter() + .next() + .unwrap(); + + let listener = TcpListener::bind(addr).await?; + let api_service = api_server::ApiServer::new(StubRpcApi); + + tonic::transport::Server::builder() + .accept_http1(true) + .add_service(tonic_web::enable(api_service)) + .serve_with_incoming(TcpListenerStream::new(listener)) + .await + .map_err(ApiError::ApiServeFailed) +}