From 0e614ba1df7132044155f3c467bea240d33aa682 Mon Sep 17 00:00:00 2001 From: keinberger Date: Tue, 9 Dec 2025 18:54:34 +0200 Subject: [PATCH 01/23] feat: implement mint command --- Cargo.lock | 122 ++++++++++++ bin/faucet/Cargo.toml | 3 +- bin/faucet/src/main.rs | 217 +++++++++++++++++++- bin/faucet/src/mint.rs | 438 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 775 insertions(+), 5 deletions(-) create mode 100644 bin/faucet/src/mint.rs diff --git a/Cargo.lock b/Cargo.lock index 4942ec6e..07075cb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,6 +386,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -1338,6 +1344,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1910,6 +1933,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -2136,6 +2165,7 @@ dependencies = [ "base64", "clap", "fantoccini", + "hex", "http 1.4.0", "humantime", "miden-client", @@ -3309,6 +3339,61 @@ dependencies = [ "pulldown-cmark", ] +[[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", + "rustls", + "socket2", + "thiserror 2.0.17", + "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.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "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", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.42" @@ -3460,16 +3545,21 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -3477,6 +3567,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -3533,6 +3624,12 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[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.2.3" @@ -3610,6 +3707,7 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ + "web-time", "zeroize", ] @@ -4191,6 +4289,21 @@ dependencies = [ "zerovec 0.11.5", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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" @@ -4917,6 +5030,15 @@ dependencies = [ "url", ] +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" diff --git a/bin/faucet/Cargo.toml b/bin/faucet/Cargo.toml index ecfd1e28..3cd17cdc 100644 --- a/bin/faucet/Cargo.toml +++ b/bin/faucet/Cargo.toml @@ -29,6 +29,7 @@ axum = { features = ["tokio"], version = "0.8" } axum-extra = { version = "0.10" } base64 = { version = "0.22" } clap = { features = ["derive", "env", "string"], version = "4.5" } +hex = { version = "0.4" } http = { workspace = true } humantime = { workspace = true } opentelemetry = { workspace = true } @@ -36,6 +37,7 @@ opentelemetry-otlp = { workspace = true } opentelemetry_sdk = { workspace = true } rand = { features = ["thread_rng"], workspace = true } rand_chacha = { version = "0.9" } +reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } @@ -54,7 +56,6 @@ url = { workspace = true } fantoccini = { version = "0.22" } miden-node-proto = { version = "0.12" } miden-testing = { version = "0.12" } -reqwest = { default-features = false, features = ["json"], version = "0.12" } serde_json = { workspace = true } tokio = { features = ["macros", "process"], workspace = true } tonic-web = { version = "0.14" } diff --git a/bin/faucet/src/main.rs b/bin/faucet/src/main.rs index 67795498..def4c72f 100644 --- a/bin/faucet/src/main.rs +++ b/bin/faucet/src/main.rs @@ -2,6 +2,7 @@ mod api; mod api_key; mod frontend; mod logging; +mod mint; mod network; #[cfg(test)] mod testing; @@ -205,6 +206,9 @@ pub enum Command { #[arg(long = "batch-size", value_name = "USIZE", default_value = "32", env = ENV_BATCH_SIZE)] batch_size: usize, }, + + /// Request tokens from a remote faucet (public note; does not consume). + Mint(mint::MintCmd), } /// Configuration for the faucet client. @@ -437,6 +441,10 @@ async fn run_faucet_command(cli: Cli) -> anyhow::Result<()> { }, }?; }, + + Command::Mint(cmd) => { + cmd.execute().await.map_err(anyhow::Error::new)?; + }, } Ok(()) @@ -501,17 +509,218 @@ mod tests { use std::str::FromStr; use std::time::{Duration, Instant}; + use clap::Parser; use fantoccini::ClientBuilder; - use miden_client::account::{AccountId, Address, NetworkId}; + use miden_client::account::{AccountFile, AccountId, Address, NetworkId}; use serde_json::{Map, json}; use tokio::io::AsyncBufReadExt; use tokio::net::TcpListener; use tokio::time::sleep; use url::Url; + use uuid::Uuid; use crate::network::FaucetNetwork; use crate::testing::stub_rpc_api::serve_stub; - use crate::{Cli, ClientConfig, run_faucet_command}; + use crate::{Cli, ClientConfig, Command, create_faucet_account, run_faucet_command}; + + // CLI TESTS + // --------------------------------------------------------------------------------------------- + + #[tokio::test] + async fn init_with_new_token() { + let stub_node_url = run_stub_node().await; + let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); + let result = Box::pin(run_faucet_command(Cli::parse_from([ + "miden-faucet", + "init", + "--token-symbol", + "TEST", + "--decimals", + "6", + "--max-supply", + "100000000000000000", + "--node-url", + stub_node_url.to_string().as_str(), + "--store", + store_path.to_str().unwrap(), + ]))) + .await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn init_importing_account_file() { + let stub_node_url = run_stub_node().await; + let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); + let account_path = temp_dir().join("test_account.mac"); + let (account, secret) = create_faucet_account("TEST", 100_000_000, 3).unwrap(); + let account_data = AccountFile::new(account, vec![secret]); + account_data.write(&account_path).unwrap(); + + let result = Box::pin(run_faucet_command(Cli::parse_from([ + "miden-faucet", + "init", + "--import", + account_path.to_str().unwrap(), + "--node-url", + stub_node_url.to_string().as_str(), + "--store", + store_path.to_str().unwrap(), + ]))) + .await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn init_with_deploy() { + let stub_node_url = run_stub_node().await; + let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); + let result = Box::pin(run_faucet_command(Cli::parse_from([ + "miden-faucet", + "init", + "--token-symbol", + "TEST", + "--decimals", + "6", + "--max-supply", + "100000000000000000", + "--node-url", + stub_node_url.to_string().as_str(), + "--store", + store_path.to_str().unwrap(), + "--deploy", + ]))) + .await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn serve_fails_without_init() { + let stub_node_url = run_stub_node().await; + let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); + + let result = Box::pin(run_faucet_command(Cli::parse_from([ + "miden-faucet", + "start", + "--api-bind-url", + "http://0.0.0.0:8000", + "--frontend-url", + "http://0.0.0.0:8081", + "--node-url", + stub_node_url.to_string().as_str(), + "--store", + store_path.to_str().unwrap(), + ]))) + .await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn mint_command_against_local_faucet() { + use crate::mint::MintCmd; + + let stub_node_url = run_stub_node().await; + let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); + + // Initialize faucet account + Box::pin(run_faucet_command(Cli::parse_from([ + "miden-faucet", + "init", + "--token-symbol", + "TEST", + "--decimals", + "6", + "--max-supply", + "100000000000000000", + "--node-url", + stub_node_url.to_string().as_str(), + "--store", + store_path.to_str().unwrap(), + ]))) + .await + .expect("failed to init faucet"); + + // Reserve an open port for the API server + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let api_addr = listener.local_addr().unwrap(); + drop(listener); + let api_url = Url::from_str(&format!("http://{api_addr}")).unwrap(); + + // Start faucet server on a dedicated thread with low PoW difficulty for fast testing + let config = ClientConfig { + node_url: Some(stub_node_url.clone()), + timeout: Duration::from_millis(5_000), + network: FaucetNetwork::Localhost, + store_path: store_path.clone(), + remote_tx_prover_url: None, + }; + + let api_url_for_server = api_url.clone(); + let start_handle = std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("runtime"); + + rt.block_on(async { + Box::pin(run_faucet_command(Cli { + command: Command::Start { + config, + api_bind_url: api_url_for_server, + api_public_url: None, + frontend_url: None, + max_claimable_amount: 1_000_000_000, + api_keys: vec![], + pow_secret: "test".to_string(), + pow_challenge_lifetime: Duration::from_secs(30), + pow_cleanup_interval: Duration::from_secs(1), + pow_growth_rate: 0.5, + pow_baseline: 8, + base_amount: 100_000, + open_telemetry: false, + explorer_url: None, + batch_size: 8, + }, + })) + .await + .expect("failed to start faucet"); + }); + }); + + // Wait for faucet API to become reachable + let addrs = api_url.socket_addrs(|| None).unwrap(); + let start = Instant::now(); + let timeout = Duration::from_secs(10); + loop { + match tokio::net::TcpStream::connect(&addrs[..]).await { + Ok(_) => break, + Err(_) if start.elapsed() < timeout => { + sleep(Duration::from_millis(200)).await; + }, + Err(e) => panic!("faucet never became reachable: {e}"), + } + } + + // Should mint a public P2ID note without consuming + MintCmd::parse_from([ + "mint", + "--url", + api_url.to_string().as_str(), + "--account", + "0xca8203e8e58cf72049b061afca78ce", + "--amount", + "1000", + ]) + .execute() + .await + .expect("mint command should succeed"); + + // Drop server after test + drop(start_handle); + } + + // INTEGRATION TEST + // --------------------------------------------------------------------------------------------- /// 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, mints tokens, and checks that all the @@ -588,7 +797,7 @@ mod tests { // TESTING HELPERS // --------------------------------------------------------------------------------------------- - async fn run_stub_node() -> Url { + pub async fn run_stub_node() -> Url { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener_addr = listener.local_addr().unwrap(); let stub_node_url = Url::from_str(&format!("http://{listener_addr}")).unwrap(); @@ -604,7 +813,7 @@ mod tests { node_url: Some(stub_node_url.clone()), timeout: Duration::from_millis(5000), network: FaucetNetwork::Localhost, - store_path: temp_dir().join("test_store.sqlite3"), + store_path: temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())), remote_tx_prover_url: None, }; diff --git a/bin/faucet/src/mint.rs b/bin/faucet/src/mint.rs new file mode 100644 index 00000000..58f348a1 --- /dev/null +++ b/bin/faucet/src/mint.rs @@ -0,0 +1,438 @@ +//! CLI command to mint tokens from a remote faucet + +use std::time::Duration; + +use clap::Parser; +use miden_client::account::{AccountId, Address}; +use miden_client::address::AddressId; +use miden_client::note::NoteId; +use rand::Rng; +use reqwest::{Client as HttpClient, Url}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use tokio::task; + +// CONSTANTS +// ================================================================================================= + +const DEFAULT_FAUCET_URL: &str = "https://faucet-api.testnet.miden.io"; +const DEFAULT_TIMEOUT_MS: u64 = 30_000; + +// CLI +// ================================================================================================= + +/// Mint tokens from a remote faucet by solving its `PoW` challenge and requesting a **public** +/// P2ID note. +#[derive(Debug, Parser, Clone)] +pub struct MintCmd { + /// Faucet API base URL. Defaults to the public testnet faucet. + #[arg(long = "url", default_value = DEFAULT_FAUCET_URL, value_name = "URL")] + api_url: String, + + /// Account ID or address to receive the minted tokens. + #[arg(short = 'a', long = "account", value_name = "ACCOUNT")] + account: String, + + /// Amount to mint (in base units). + #[arg(short = 'm', long = "amount", value_name = "U64")] + amount: u64, + + /// Optional faucet API key. + #[arg(long = "api-key", value_name = "STRING")] + api_key: Option, +} + +impl MintCmd { + /// Executes the mint command. + pub async fn execute(&self) -> Result<(), MintClientError> { + if self.amount == 0 { + return Err(MintClientError::AmountZero); + } + + let account_id = parse_account_id(&self.account)?; + let faucet_client = + FaucetHttpClient::new(&self.api_url, DEFAULT_TIMEOUT_MS, self.api_key.clone())?; + + println!( + "Requesting PoW challenge for account {} from faucet at {}...", + account_id.to_hex(), + faucet_client.base_url + ); + + let (challenge, target) = faucet_client.request_pow(&account_id, self.amount).await?; + + println!("Solving faucet PoW challenge, this can take some time..."); + let nonce = solve_challenge(&challenge, target).await?; + + println!("Submitting mint request for a public P2ID note..."); + let minted_note = faucet_client + .request_tokens(&challenge, nonce, &account_id, self.amount) + .await?; + + println!("Mint request accepted. Transaction: {}", minted_note.tx_id); + println!("Public P2ID note commitment: {}", minted_note.note_id.to_hex()); + + Ok(()) + } +} + +// HTTP CLIENT +// ================================================================================================= + +/// HTTP client for interacting with the faucet API. +#[derive(Clone)] +struct FaucetHttpClient { + http_client: HttpClient, + base_url: Url, + api_key: Option, +} + +impl FaucetHttpClient { + /// Creates a new `FaucetHttpClient` instance. + fn new( + endpoint: &str, + timeout_ms: u64, + api_key: Option, + ) -> Result { + let base_url = Url::parse(endpoint) + .map_err(|err| MintClientError::InvalidUrl(endpoint.to_owned(), err))?; + + let http_client = HttpClient::builder() + .timeout(Duration::from_millis(timeout_ms)) + .build() + .map_err(MintClientError::HttpClient)?; + + Ok(Self { http_client, base_url, api_key }) + } + + /// Requests a `PoW` challenge from the faucet API. + async fn request_pow( + &self, + account_id: &AccountId, + amount: u64, + ) -> Result<(String, u64), MintClientError> { + let pow_url = self + .base_url + .join("pow") + .map_err(|err| MintClientError::InvalidUrl(self.base_url.to_string(), err))?; + + let mut request = self + .http_client + .get(pow_url) + .query(&[("account_id", account_id.to_hex()), ("amount", amount.to_string())]); + + if let Some(key) = &self.api_key { + request = request.query(&[("api_key", key)]); + } + + let response = request.send().await.map_err(|err| MintClientError::Request("PoW", err))?; + + if !response.status().is_success() { + let status = response.status(); + let body = + response.text().await.map_err(|err| MintClientError::ResponseBody("pow", err))?; + return Err(MintClientError::UnexpectedStatus("pow", status, body)); + } + + let body = + response.text().await.map_err(|err| MintClientError::ResponseBody("pow", err))?; + let parsed = serde_json::from_str::(&body) + .map_err(|err| MintClientError::ParseResponse("PoW", err, body.clone()))?; + + Ok((parsed.challenge, parsed.target)) + } + + /// Requests tokens from the faucet API. + async fn request_tokens( + &self, + challenge: &str, + nonce: u64, + account_id: &AccountId, + amount: u64, + ) -> Result { + let url = self + .base_url + .join("get_tokens") + .map_err(|err| MintClientError::InvalidUrl(self.base_url.to_string(), err))?; + + let mut request = self.http_client.get(url).query(&[ + ("account_id", account_id.to_hex()), + ("asset_amount", amount.to_string()), + ("is_private_note", false.to_string()), + ("challenge", challenge.to_owned()), + ("nonce", nonce.to_string()), + ]); + + if let Some(key) = &self.api_key { + request = request.query(&[("api_key", key)]); + } + + let response = request + .send() + .await + .map_err(|err| MintClientError::Request("get_tokens", err))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response + .text() + .await + .map_err(|err| MintClientError::ResponseBody("get_tokens", err))?; + return Err(MintClientError::UnexpectedStatus("get_tokens", status, body)); + } + + let body = response + .text() + .await + .map_err(|err| MintClientError::ResponseBody("get_tokens", err))?; + let parsed = serde_json::from_str::(&body) + .map_err(|err| MintClientError::ParseResponse("get_tokens", err, body.clone()))?; + + let note_id = NoteId::try_from_hex(&parsed.note_id).map_err(|err| { + MintClientError::InvalidNoteId(parsed.note_id.clone(), err.to_string()) + })?; + + Ok(MintNote { note_id, tx_id: parsed.tx_id }) + } +} + +// RESPONSES +// ================================================================================================= + +/// Response from the `/pow` endpoint. +#[derive(Debug, Deserialize, Serialize, Clone)] +struct PowResponse { + challenge: String, + target: u64, +} + +/// Response from the `/get_tokens` endpoint. +#[derive(Debug, Deserialize, Serialize, Clone)] +struct GetTokensResponse { + note_id: String, + tx_id: String, +} + +/// Represents a minted note with its ID and transaction ID. +#[derive(Debug, Clone)] +struct MintNote { + note_id: NoteId, + tx_id: String, +} + +// ERRORS +// ================================================================================================= + +/// Errors that can occur while interacting with the faucet API. +#[derive(Debug, thiserror::Error)] +pub enum MintClientError { + #[error("amount must be greater than zero")] + AmountZero, + #[error("invalid account `{0}`: {1}")] + InvalidAccount(String, String), + #[error("invalid faucet URL `{0}`: {1}")] + InvalidUrl(String, url::ParseError), + #[error("failed to build HTTP client: {0}")] + HttpClient(#[source] reqwest::Error), + #[error("{0} request failed: {1}")] + Request(&'static str, #[source] reqwest::Error), + #[error("{0} request failed with status {1}: {2}")] + UnexpectedStatus(&'static str, reqwest::StatusCode, String), + #[error("failed to parse {0} response: {1}. Body: {2}")] + ParseResponse(&'static str, #[source] serde_json::Error, String), + #[error("failed to read {0} response body: {1}")] + ResponseBody(&'static str, #[source] reqwest::Error), + #[error("faucet returned a PoW target of 0")] + ZeroTarget, + #[error("invalid challenge bytes returned by faucet: {0}")] + InvalidChallenge(#[source] hex::FromHexError), + #[error("PoW solving task failed: {0}")] + PowTask(String), + #[error("invalid note id `{0}`: {1}")] + InvalidNoteId(String, String), +} + +// HELPERS +// ================================================================================================= + +/// Parses a user provided account ID string and returns the corresponding `AccountId` +fn parse_account_id(input: &str) -> Result { + if input.starts_with("0x") { + AccountId::from_hex(input) + .map_err(|err| MintClientError::InvalidAccount(input.to_owned(), err.to_string())) + } else { + Address::decode(input) + .map_err(|err| MintClientError::InvalidAccount(input.to_owned(), err.to_string())) + .and_then(|(_, address)| match address.id() { + AddressId::AccountId(account_id) => Ok(account_id), + _ => Err(MintClientError::InvalidAccount( + input.to_owned(), + "address is not account-based".to_owned(), + )), + }) + } +} + +/// Solves the `PoW` challenge and returns the nonce that satisfies the target. +/// +/// The faucet expects the first 8 bytes of the SHA-256 digest (big endian) to be lower than +/// the target. +/// +/// Heavy work runs on a blocking thread so we don't stall the async runtime +async fn solve_challenge(challenge_hex: &str, target: u64) -> Result { + if target == 0 { + return Err(MintClientError::ZeroTarget); + } + + let challenge_bytes = hex::decode(challenge_hex).map_err(MintClientError::InvalidChallenge)?; + + task::spawn_blocking(move || -> Result { + let mut rng = rand::rng(); + + loop { + let nonce: u64 = rng.random(); + + let mut hasher = Sha256::new(); + hasher.update(&challenge_bytes); + hasher.update(nonce.to_be_bytes()); + let hash = hasher.finalize(); + let digest = + u64::from_be_bytes(hash[..8].try_into().expect("hash should be 32 bytes long")); + + if digest < target { + return Ok(nonce); + } + } + }) + .await + .map_err(|err| MintClientError::PowTask(err.to_string()))? +} + +// TESTS +// ================================================================================================= + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use axum::extract::{Query, State}; + use axum::routing::get; + use axum::{Json, Router}; + use miden_client::account::AccountId; + use miden_client::note::NoteId; + use serde::Deserialize; + use tokio::net::TcpListener; + use tokio::sync::Mutex; + + use super::{GetTokensResponse, MintCmd, MintNote, PowResponse}; + + #[derive(Clone, Default)] + struct RecordedRequest { + account_id: Option, + amount: Option, + is_private_note: Option, + api_key: Option, + challenge: Option, + } + + #[derive(Clone)] + struct AppState { + pow_response: PowResponse, + note: MintNote, + recorded: Arc>, + } + + #[derive(Deserialize)] + struct PowQuery { + amount: u64, + account_id: String, + api_key: Option, + } + + #[derive(Deserialize)] + struct TokensQuery { + account_id: String, + is_private_note: String, + asset_amount: u64, + challenge: String, + #[allow(dead_code)] + nonce: u64, + api_key: Option, + } + + #[tokio::test] + async fn mint_command_requests_public_note() { + let account_hex = "0xca8203e8e58cf72049b061afca78ce"; + let account_id = AccountId::from_hex(account_hex).unwrap(); + let expected_amount = 123_000; + let pow_response = PowResponse { + challenge: "00".repeat(32), + target: u64::MAX, + }; + let note_id_hex = format!("0x{}", "00".repeat(32)); + let note_id = + NoteId::try_from_hex(¬e_id_hex).expect("hex string should produce a note id"); + let app_state = AppState { + pow_response, + note: MintNote { note_id, tx_id: "0xdeadbeef".to_string() }, + recorded: Arc::new(Mutex::new(RecordedRequest::default())), + }; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let app = Router::new() + .route("/pow", get(pow_handler)) + .route("/get_tokens", get(tokens_handler)) + .with_state(app_state.clone()); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let cli = MintCmd { + api_url: format!("http://{addr}"), + account: account_id.to_hex(), + amount: expected_amount, + api_key: Some("test-key".to_owned()), + }; + + cli.execute().await.unwrap(); + + let recorded = app_state.recorded.lock().await.clone(); + assert_eq!(recorded.account_id, Some(account_id.to_hex())); + assert_eq!(recorded.amount, Some(expected_amount)); + assert_eq!(recorded.is_private_note.as_deref(), Some("false")); + assert_eq!(recorded.api_key.as_deref(), Some("test-key")); + assert_eq!(recorded.challenge, Some("00".repeat(32))); + } + + async fn pow_handler( + State(state): State, + Query(params): Query, + ) -> Json { + { + let mut recorded = state.recorded.lock().await; + recorded.account_id = Some(params.account_id); + recorded.amount = Some(params.amount); + recorded.api_key = params.api_key; + } + Json(state.pow_response.clone()) + } + + async fn tokens_handler( + State(state): State, + Query(params): Query, + ) -> Json { + { + let mut recorded = state.recorded.lock().await; + recorded.account_id = Some(params.account_id.clone()); + recorded.amount = Some(params.asset_amount); + recorded.is_private_note = Some(params.is_private_note.clone()); + recorded.api_key = params.api_key.clone(); + recorded.challenge = Some(params.challenge); + } + Json(GetTokensResponse { + note_id: state.note.note_id.to_hex(), + tx_id: state.note.tx_id.clone(), + }) + } +} From f5ce75629e1af8831249e40f1960e076f0d404b9 Mon Sep 17 00:00:00 2001 From: keinberger Date: Wed, 10 Dec 2025 13:28:43 +0200 Subject: [PATCH 02/23] chore: update changelog --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61eb3ad4..b269ed4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.13.0 (TBD) + +- [BREAKING] Replaced the `api-bind-url` param for `api-bind-port` ([#156](https://github.com/0xMiden/miden-faucet/pull/156)). +- [BREAKING] Replaced the `frontend-url` param for `frontend-bind-port` ([#156](https://github.com/0xMiden/miden-faucet/pull/156)). +- [BREAKING] Added `no-frontend` param to optionally disable the frontend server ([#156](https://github.com/0xMiden/miden-faucet/pull/156)). +## 0.12.5 (TBD) + +- Added `mint` CLI command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). + ## 0.12.4 (2025-12-04) - Added version to the metadata endpoint ([#169](https://github.com/0xMiden/miden-faucet/pull/169)). @@ -76,7 +85,7 @@ - Redesigned the home frontend ([#20](https://github.com/0xMiden/miden-faucet/pull/20)). - Redesigned the tokens request flows ([#25](https://github.com/0xMiden/miden-faucet/pull/25)). - Added faucet supply amounts to the metadata ([#30](https://github.com/0xMiden/miden-faucet/pull/30)). -- Added supply exceeded check ([#31](https://github.com/0xMiden/miden-faucet/pull/31)). +- Added supply exceeded check ([#31](https://github.com/0xMiden/miden-faucet/pull/31)). - Use HTTP 429 status code for rate limited error ([#51](https://github.com/0xMiden/miden-faucet/pull/51)). - Replace amount options validation for maximum claimable amount ([#52](https://github.com/0xMiden/miden-faucet/pull/52)). - Added `mdbook` documentation ([#61](https://github.com/0xMiden/miden-faucet/pull/61)). From c72de749ce78dc67d6735e7dd50f1171d50c1a7d Mon Sep 17 00:00:00 2001 From: keinberger Date: Thu, 11 Dec 2025 19:10:10 +0200 Subject: [PATCH 03/23] feat: implement new miden-faucet-operator binary feat: rename existing miden-faucet binary to miden-faucet-client docs: update docs to include information about new miden-faucet-operator binary chore: move tests of miden-faucet-operator binary into dedicated folder docs(README): update README for binary separation chore: update changelog --- CHANGELOG.md | 7 +- Cargo.lock | 61 +++++--- Cargo.toml | 2 +- README.md | 15 +- bin/faucet-operator/Cargo.toml | 33 +++++ bin/faucet-operator/src/lib.rs | 1 + bin/faucet-operator/src/main.rs | 29 ++++ bin/{faucet => faucet-operator}/src/mint.rs | 155 ++------------------ bin/faucet-operator/tests/mint.rs | 128 ++++++++++++++++ bin/faucet/Cargo.toml | 12 +- bin/faucet/src/bin/miden-faucet.rs | 2 + bin/faucet/src/main.rs | 124 +--------------- docs/src/getting-started/cli.md | 22 ++- docs/src/getting-started/installation.md | 12 +- docs/src/getting-started/quick-start.md | 33 +++-- packaging/faucet/miden-faucet.service | 4 +- 16 files changed, 322 insertions(+), 318 deletions(-) create mode 100644 bin/faucet-operator/Cargo.toml create mode 100644 bin/faucet-operator/src/lib.rs create mode 100644 bin/faucet-operator/src/main.rs rename bin/{faucet => faucet-operator}/src/mint.rs (69%) create mode 100644 bin/faucet-operator/tests/mint.rs create mode 100644 bin/faucet/src/bin/miden-faucet.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b269ed4d..3d917df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,8 @@ # Changelog -## 0.13.0 (TBD) - -- [BREAKING] Replaced the `api-bind-url` param for `api-bind-port` ([#156](https://github.com/0xMiden/miden-faucet/pull/156)). -- [BREAKING] Replaced the `frontend-url` param for `frontend-bind-port` ([#156](https://github.com/0xMiden/miden-faucet/pull/156)). -- [BREAKING] Added `no-frontend` param to optionally disable the frontend server ([#156](https://github.com/0xMiden/miden-faucet/pull/156)). ## 0.12.5 (TBD) -- Added `mint` CLI command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). +- Renamed the faucet CLI to `miden-faucet-client` and kept a `miden-faucet` alias for compatibility; added a new `miden-faucet-operator` binary with the `mint` command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). ## 0.12.4 (2025-12-04) diff --git a/Cargo.lock b/Cargo.lock index 07075cb6..86cfb548 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,9 +285,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "bech32" @@ -370,9 +370,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.48" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -1510,9 +1510,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections 2.1.1", "icu_locale_core", @@ -1524,9 +1524,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -2096,9 +2096,9 @@ dependencies = [ [[package]] name = "miden-crypto" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0048d2d987f215bc9633ced499a8c488d0e2474350c765f904b87cae3462acb7" +checksum = "395e5cc76b64e24533ee55c8d1ff90305b8cad372bdbea4f4f324239e36a895f" dependencies = [ "blake3", "cc", @@ -2128,9 +2128,9 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3b38aace84e157fb02aba8f8ae85bbf8c3afdcdbdf8190fbe7476f3be7ef44" +checksum = "c89641b257eb395cf03105ac1c6cbdf3fd9a5450749696af9835c3c47fc6806e" dependencies = [ "quote", "syn", @@ -2155,7 +2155,7 @@ dependencies = [ ] [[package]] -name = "miden-faucet" +name = "miden-faucet-client" version = "0.12.4" dependencies = [ "anyhow", @@ -2165,7 +2165,6 @@ dependencies = [ "base64", "clap", "fantoccini", - "hex", "http 1.4.0", "humantime", "miden-client", @@ -2179,7 +2178,6 @@ dependencies = [ "opentelemetry_sdk", "rand", "rand_chacha", - "reqwest", "serde", "serde_json", "sha2", @@ -2210,6 +2208,25 @@ dependencies = [ "url", ] +[[package]] +name = "miden-faucet-operator" +version = "0.12.4" +dependencies = [ + "anyhow", + "axum", + "clap", + "hex", + "miden-client", + "rand", + "reqwest", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.17", + "tokio", + "url", +] + [[package]] name = "miden-formatting" version = "0.1.1" @@ -3391,7 +3408,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -3534,9 +3551,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64", "bytes", @@ -4143,9 +4160,9 @@ dependencies = [ [[package]] name = "term" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2111ef44dae28680ae9752bb89409e7310ca33a8c621ebe7b106cf5c928b3ac0" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ "windows-sys 0.61.2", ] @@ -4603,9 +4620,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", diff --git a/Cargo.toml b/Cargo.toml index d005a77e..1eae2dac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bin/faucet", "crates/faucet"] +members = ["bin/faucet", "bin/faucet-operator", "crates/faucet"] resolver = "2" diff --git a/README.md b/README.md index 1909cb8d..6aaeb874 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,15 @@ For comprehensive guides, API reference, and examples, see the [Miden Faucet Doc ## Running the faucet -1. Install the faucet: +1. Install the faucet binaries: ```bash make install-faucet ``` -2. Initialize the faucet. This will generate a new account with the specified token configuration and save the account data to a local SQLite store: +2. Initialize the faucet server. This will generate a new account with the specified token configuration and save the account data to a local SQLite store: ```bash -miden-faucet init \ +miden-faucet-client init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ @@ -25,15 +25,22 @@ miden-faucet init \ > [!TIP] > This account will not be created on chain yet, creation on chain will happen on the first minting transaction. +> You can also run the legacy alias `miden-faucet` for backwards compatibility; it runs the same `miden-faucet-client` binary. + 3. Start the faucet: ```bash -miden-faucet start \ +miden-faucet-client start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --explorer-url https://testnet.midenscan.com \ --network testnet ``` +Requesting tokens from a faucet endpoint is handled by the separate `miden-faucet-operator` binary: +```bash +miden-faucet-operator mint --url --account --amount +``` + After a few seconds you may go to `http://localhost:8080` and see the faucet UI. ## Faucet security features: diff --git a/bin/faucet-operator/Cargo.toml b/bin/faucet-operator/Cargo.toml new file mode 100644 index 00000000..f77ad04a --- /dev/null +++ b/bin/faucet-operator/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors.workspace = true +description = "Operator CLI for interacting with a Miden faucet" +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "miden-faucet-operator" +readme = "../../README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { features = ["derive", "env", "string"], version = "4.5" } +hex = { version = "0.4" } +miden-client = { workspace = true } +rand = { features = ["thread_rng"], workspace = true } +reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } +tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true } +url = { workspace = true } + +[dev-dependencies] +axum = { features = ["tokio"], version = "0.8" } +serde = { workspace = true } +tokio = { features = ["macros", "net", "rt-multi-thread", "time"], workspace = true } diff --git a/bin/faucet-operator/src/lib.rs b/bin/faucet-operator/src/lib.rs new file mode 100644 index 00000000..7e5e8b22 --- /dev/null +++ b/bin/faucet-operator/src/lib.rs @@ -0,0 +1 @@ +pub mod mint; diff --git a/bin/faucet-operator/src/main.rs b/bin/faucet-operator/src/main.rs new file mode 100644 index 00000000..f028a8ca --- /dev/null +++ b/bin/faucet-operator/src/main.rs @@ -0,0 +1,29 @@ +use clap::{Parser, Subcommand}; +use miden_faucet_operator::mint; + +/// Operator CLI for interacting with a live faucet. +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Request tokens from a remote faucet (does not consume the resulting note). + Mint(mint::MintCmd), +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Command::Mint(cmd) => { + cmd.execute().await.map_err(anyhow::Error::new)?; + }, + } + + Ok(()) +} diff --git a/bin/faucet/src/mint.rs b/bin/faucet-operator/src/mint.rs similarity index 69% rename from bin/faucet/src/mint.rs rename to bin/faucet-operator/src/mint.rs index 58f348a1..2e38ade3 100644 --- a/bin/faucet/src/mint.rs +++ b/bin/faucet-operator/src/mint.rs @@ -1,4 +1,4 @@ -//! CLI command to mint tokens from a remote faucet +//! CLI command to mint tokens from a remote faucet by solving its `PoW` challenge. use std::time::Duration; @@ -33,9 +33,9 @@ pub struct MintCmd { #[arg(short = 'a', long = "account", value_name = "ACCOUNT")] account: String, - /// Amount to mint (in base units). - #[arg(short = 'm', long = "amount", value_name = "U64")] - amount: u64, + /// Quantity to mint (in base units). + #[arg(short = 'q', long = "quantity", value_name = "U64", alias = "amount")] + quantity: u64, /// Optional faucet API key. #[arg(long = "api-key", value_name = "STRING")] @@ -45,7 +45,7 @@ pub struct MintCmd { impl MintCmd { /// Executes the mint command. pub async fn execute(&self) -> Result<(), MintClientError> { - if self.amount == 0 { + if self.quantity == 0 { return Err(MintClientError::AmountZero); } @@ -59,14 +59,14 @@ impl MintCmd { faucet_client.base_url ); - let (challenge, target) = faucet_client.request_pow(&account_id, self.amount).await?; + let (challenge, target) = faucet_client.request_pow(&account_id, self.quantity).await?; println!("Solving faucet PoW challenge, this can take some time..."); let nonce = solve_challenge(&challenge, target).await?; println!("Submitting mint request for a public P2ID note..."); let minted_note = faucet_client - .request_tokens(&challenge, nonce, &account_id, self.amount) + .request_tokens(&challenge, nonce, &account_id, self.quantity) .await?; println!("Mint request accepted. Transaction: {}", minted_note.tx_id); @@ -201,16 +201,16 @@ impl FaucetHttpClient { /// Response from the `/pow` endpoint. #[derive(Debug, Deserialize, Serialize, Clone)] -struct PowResponse { - challenge: String, - target: u64, +pub struct PowResponse { + pub challenge: String, + pub target: u64, } /// Response from the `/get_tokens` endpoint. #[derive(Debug, Deserialize, Serialize, Clone)] -struct GetTokensResponse { - note_id: String, - tx_id: String, +pub struct GetTokensResponse { + pub note_id: String, + pub tx_id: String, } /// Represents a minted note with its ID and transaction ID. @@ -307,132 +307,3 @@ async fn solve_challenge(challenge_hex: &str, target: u64) -> Result, - amount: Option, - is_private_note: Option, - api_key: Option, - challenge: Option, - } - - #[derive(Clone)] - struct AppState { - pow_response: PowResponse, - note: MintNote, - recorded: Arc>, - } - - #[derive(Deserialize)] - struct PowQuery { - amount: u64, - account_id: String, - api_key: Option, - } - - #[derive(Deserialize)] - struct TokensQuery { - account_id: String, - is_private_note: String, - asset_amount: u64, - challenge: String, - #[allow(dead_code)] - nonce: u64, - api_key: Option, - } - - #[tokio::test] - async fn mint_command_requests_public_note() { - let account_hex = "0xca8203e8e58cf72049b061afca78ce"; - let account_id = AccountId::from_hex(account_hex).unwrap(); - let expected_amount = 123_000; - let pow_response = PowResponse { - challenge: "00".repeat(32), - target: u64::MAX, - }; - let note_id_hex = format!("0x{}", "00".repeat(32)); - let note_id = - NoteId::try_from_hex(¬e_id_hex).expect("hex string should produce a note id"); - let app_state = AppState { - pow_response, - note: MintNote { note_id, tx_id: "0xdeadbeef".to_string() }, - recorded: Arc::new(Mutex::new(RecordedRequest::default())), - }; - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - let app = Router::new() - .route("/pow", get(pow_handler)) - .route("/get_tokens", get(tokens_handler)) - .with_state(app_state.clone()); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - - let cli = MintCmd { - api_url: format!("http://{addr}"), - account: account_id.to_hex(), - amount: expected_amount, - api_key: Some("test-key".to_owned()), - }; - - cli.execute().await.unwrap(); - - let recorded = app_state.recorded.lock().await.clone(); - assert_eq!(recorded.account_id, Some(account_id.to_hex())); - assert_eq!(recorded.amount, Some(expected_amount)); - assert_eq!(recorded.is_private_note.as_deref(), Some("false")); - assert_eq!(recorded.api_key.as_deref(), Some("test-key")); - assert_eq!(recorded.challenge, Some("00".repeat(32))); - } - - async fn pow_handler( - State(state): State, - Query(params): Query, - ) -> Json { - { - let mut recorded = state.recorded.lock().await; - recorded.account_id = Some(params.account_id); - recorded.amount = Some(params.amount); - recorded.api_key = params.api_key; - } - Json(state.pow_response.clone()) - } - - async fn tokens_handler( - State(state): State, - Query(params): Query, - ) -> Json { - { - let mut recorded = state.recorded.lock().await; - recorded.account_id = Some(params.account_id.clone()); - recorded.amount = Some(params.asset_amount); - recorded.is_private_note = Some(params.is_private_note.clone()); - recorded.api_key = params.api_key.clone(); - recorded.challenge = Some(params.challenge); - } - Json(GetTokensResponse { - note_id: state.note.note_id.to_hex(), - tx_id: state.note.tx_id.clone(), - }) - } -} diff --git a/bin/faucet-operator/tests/mint.rs b/bin/faucet-operator/tests/mint.rs new file mode 100644 index 00000000..dc34b163 --- /dev/null +++ b/bin/faucet-operator/tests/mint.rs @@ -0,0 +1,128 @@ +use std::sync::Arc; + +use axum::extract::{Query, State}; +use axum::routing::get; +use axum::{Json, Router}; +use clap::Parser; +use miden_client::account::AccountId; +use miden_client::note::NoteId; +use miden_faucet_operator::mint::{GetTokensResponse, MintCmd, PowResponse}; +use serde::Deserialize; +use tokio::net::TcpListener; +use tokio::sync::Mutex; + +#[derive(Clone, Default)] +struct RecordedRequest { + account_id: Option, + amount: Option, + is_private_note: Option, + api_key: Option, + challenge: Option, +} + +#[derive(Clone)] +struct AppState { + pow_response: PowResponse, + note_id_hex: String, + tx_id: String, + recorded: Arc>, +} + +#[derive(Deserialize)] +struct PowQuery { + amount: u64, + account_id: String, + api_key: Option, +} + +#[derive(Deserialize)] +struct TokensQuery { + account_id: String, + is_private_note: String, + asset_amount: u64, + challenge: String, + #[allow(dead_code)] + nonce: u64, + api_key: Option, +} + +#[tokio::test] +async fn mint_command_requests_public_note() { + let account_hex = "0xca8203e8e58cf72049b061afca78ce"; + let account_id = AccountId::from_hex(account_hex).unwrap(); + let expected_amount = 123_000; + let pow_response = PowResponse { + challenge: "00".repeat(32), + target: u64::MAX, + }; + let note_id_hex = format!("0x{}", "00".repeat(32)); + let _note_id = NoteId::try_from_hex(¬e_id_hex).expect("hex string should produce a note id"); + let app_state = AppState { + pow_response, + note_id_hex, + tx_id: "0xdeadbeef".to_string(), + recorded: Arc::new(Mutex::new(RecordedRequest::default())), + }; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let app = Router::new() + .route("/pow", get(pow_handler)) + .route("/get_tokens", get(tokens_handler)) + .with_state(app_state.clone()); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let cli = MintCmd::parse_from([ + "mint", + "--url", + format!("http://{addr}").as_str(), + "--account", + account_id.to_hex().as_str(), + "--quantity", + &expected_amount.to_string(), + "--api-key", + "test-key", + ]); + + cli.execute().await.unwrap(); + + let recorded = app_state.recorded.lock().await.clone(); + assert_eq!(recorded.account_id, Some(account_id.to_hex())); + assert_eq!(recorded.amount, Some(expected_amount)); + assert_eq!(recorded.is_private_note.as_deref(), Some("false")); + assert_eq!(recorded.api_key.as_deref(), Some("test-key")); + assert_eq!(recorded.challenge, Some("00".repeat(32))); +} + +async fn pow_handler( + State(state): State, + Query(params): Query, +) -> Json { + { + let mut recorded = state.recorded.lock().await; + recorded.account_id = Some(params.account_id); + recorded.amount = Some(params.amount); + recorded.api_key = params.api_key; + } + Json(state.pow_response.clone()) +} + +async fn tokens_handler( + State(state): State, + Query(params): Query, +) -> Json { + { + let mut recorded = state.recorded.lock().await; + recorded.account_id = Some(params.account_id.clone()); + recorded.amount = Some(params.asset_amount); + recorded.is_private_note = Some(params.is_private_note.clone()); + recorded.api_key.clone_from(¶ms.api_key); + recorded.challenge = Some(params.challenge); + } + Json(GetTokensResponse { + note_id: state.note_id_hex.clone(), + tx_id: state.tx_id.clone(), + }) +} diff --git a/bin/faucet/Cargo.toml b/bin/faucet/Cargo.toml index 3cd17cdc..612fc92c 100644 --- a/bin/faucet/Cargo.toml +++ b/bin/faucet/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true homepage.workspace = true keywords = ["faucet", "miden", "node"] license.workspace = true -name = "miden-faucet" +name = "miden-faucet-client" readme = "README.md" repository.workspace = true rust-version.workspace = true @@ -14,6 +14,14 @@ version.workspace = true [lints] workspace = true +[[bin]] +name = "miden-faucet-client" +path = "src/main.rs" + +[[bin]] +name = "miden-faucet" +path = "src/bin/miden-faucet.rs" + [dependencies] miden-faucet-lib = { workspace = true } miden-pow-rate-limiter = { workspace = true } @@ -29,7 +37,6 @@ axum = { features = ["tokio"], version = "0.8" } axum-extra = { version = "0.10" } base64 = { version = "0.22" } clap = { features = ["derive", "env", "string"], version = "4.5" } -hex = { version = "0.4" } http = { workspace = true } humantime = { workspace = true } opentelemetry = { workspace = true } @@ -37,7 +44,6 @@ opentelemetry-otlp = { workspace = true } opentelemetry_sdk = { workspace = true } rand = { features = ["thread_rng"], workspace = true } rand_chacha = { version = "0.9" } -reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/bin/faucet/src/bin/miden-faucet.rs b/bin/faucet/src/bin/miden-faucet.rs new file mode 100644 index 00000000..0123888b --- /dev/null +++ b/bin/faucet/src/bin/miden-faucet.rs @@ -0,0 +1,2 @@ +// Backwards-compatibility alias for the legacy `miden-faucet` binary name. +include!("../main.rs"); diff --git a/bin/faucet/src/main.rs b/bin/faucet/src/main.rs index def4c72f..96c24efe 100644 --- a/bin/faucet/src/main.rs +++ b/bin/faucet/src/main.rs @@ -2,7 +2,6 @@ mod api; mod api_key; mod frontend; mod logging; -mod mint; mod network; #[cfg(test)] mod testing; @@ -48,7 +47,7 @@ use crate::network::FaucetNetwork; // ================================================================================================= pub const REQUESTS_QUEUE_SIZE: usize = 1000; -const COMPONENT: &str = "miden-faucet-server"; +const COMPONENT: &str = "miden-faucet-client"; const ENV_API_BIND_URL: &str = "MIDEN_FAUCET_API_BIND_URL"; const ENV_API_PUBLIC_URL: &str = "MIDEN_FAUCET_API_PUBLIC_URL"; @@ -206,9 +205,6 @@ pub enum Command { #[arg(long = "batch-size", value_name = "USIZE", default_value = "32", env = ENV_BATCH_SIZE)] batch_size: usize, }, - - /// Request tokens from a remote faucet (public note; does not consume). - Mint(mint::MintCmd), } /// Configuration for the faucet client. @@ -441,10 +437,6 @@ async fn run_faucet_command(cli: Cli) -> anyhow::Result<()> { }, }?; }, - - Command::Mint(cmd) => { - cmd.execute().await.map_err(anyhow::Error::new)?; - }, } Ok(()) @@ -521,7 +513,7 @@ mod tests { use crate::network::FaucetNetwork; use crate::testing::stub_rpc_api::serve_stub; - use crate::{Cli, ClientConfig, Command, create_faucet_account, run_faucet_command}; + use crate::{Cli, ClientConfig, create_faucet_account, run_faucet_command}; // CLI TESTS // --------------------------------------------------------------------------------------------- @@ -531,7 +523,7 @@ mod tests { let stub_node_url = run_stub_node().await; let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); let result = Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet", + "miden-faucet-client", "init", "--token-symbol", "TEST", @@ -558,7 +550,7 @@ mod tests { account_data.write(&account_path).unwrap(); let result = Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet", + "miden-faucet-client", "init", "--import", account_path.to_str().unwrap(), @@ -576,7 +568,7 @@ mod tests { let stub_node_url = run_stub_node().await; let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); let result = Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet", + "miden-faucet-client", "init", "--token-symbol", "TEST", @@ -600,7 +592,7 @@ mod tests { let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); let result = Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet", + "miden-faucet-client", "start", "--api-bind-url", "http://0.0.0.0:8000", @@ -615,110 +607,6 @@ mod tests { assert!(result.is_err()); } - #[tokio::test] - async fn mint_command_against_local_faucet() { - use crate::mint::MintCmd; - - let stub_node_url = run_stub_node().await; - let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); - - // Initialize faucet account - Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet", - "init", - "--token-symbol", - "TEST", - "--decimals", - "6", - "--max-supply", - "100000000000000000", - "--node-url", - stub_node_url.to_string().as_str(), - "--store", - store_path.to_str().unwrap(), - ]))) - .await - .expect("failed to init faucet"); - - // Reserve an open port for the API server - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let api_addr = listener.local_addr().unwrap(); - drop(listener); - let api_url = Url::from_str(&format!("http://{api_addr}")).unwrap(); - - // Start faucet server on a dedicated thread with low PoW difficulty for fast testing - let config = ClientConfig { - node_url: Some(stub_node_url.clone()), - timeout: Duration::from_millis(5_000), - network: FaucetNetwork::Localhost, - store_path: store_path.clone(), - remote_tx_prover_url: None, - }; - - let api_url_for_server = api_url.clone(); - let start_handle = std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("runtime"); - - rt.block_on(async { - Box::pin(run_faucet_command(Cli { - command: Command::Start { - config, - api_bind_url: api_url_for_server, - api_public_url: None, - frontend_url: None, - max_claimable_amount: 1_000_000_000, - api_keys: vec![], - pow_secret: "test".to_string(), - pow_challenge_lifetime: Duration::from_secs(30), - pow_cleanup_interval: Duration::from_secs(1), - pow_growth_rate: 0.5, - pow_baseline: 8, - base_amount: 100_000, - open_telemetry: false, - explorer_url: None, - batch_size: 8, - }, - })) - .await - .expect("failed to start faucet"); - }); - }); - - // Wait for faucet API to become reachable - let addrs = api_url.socket_addrs(|| None).unwrap(); - let start = Instant::now(); - let timeout = Duration::from_secs(10); - loop { - match tokio::net::TcpStream::connect(&addrs[..]).await { - Ok(_) => break, - Err(_) if start.elapsed() < timeout => { - sleep(Duration::from_millis(200)).await; - }, - Err(e) => panic!("faucet never became reachable: {e}"), - } - } - - // Should mint a public P2ID note without consuming - MintCmd::parse_from([ - "mint", - "--url", - api_url.to_string().as_str(), - "--account", - "0xca8203e8e58cf72049b061afca78ce", - "--amount", - "1000", - ]) - .execute() - .await - .expect("mint command should succeed"); - - // Drop server after test - drop(start_handle); - } - // INTEGRATION TEST // --------------------------------------------------------------------------------------------- diff --git a/docs/src/getting-started/cli.md b/docs/src/getting-started/cli.md index 47349558..61b3e4ec 100644 --- a/docs/src/getting-started/cli.md +++ b/docs/src/getting-started/cli.md @@ -23,7 +23,7 @@ The Miden Faucet can be configured using: ### Basic Configuration ```bash -miden-faucet init \ +miden-faucet-client init \ --token-symbol \ --decimals \ --max-supply \ @@ -32,7 +32,7 @@ miden-faucet init \ ``` ```bash -miden-faucet start \ +miden-faucet-client start \ --api-bind-url \ --frontend-url \ --node-url \ @@ -177,7 +177,7 @@ export MIDEN_FAUCET_API_KEYS=key1,key2,key3 ### Generate API Keys ```bash -miden-faucet create-api-key +miden-faucet-client create-api-key ``` This generates an API key that can be used for authentication. It is printed to stdout. @@ -210,17 +210,27 @@ Enable OpenTelemetry for production monitoring: ## Configuration Example ```bash -miden-faucet init \ +miden-faucet-client init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --node-url http://localhost:57291 -miden-faucet start \ +miden-faucet-client start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --node-url http://localhost:57291 \ --network localhost ``` -For detailed options, run `miden-faucet [COMMAND] --help`. +For detailed options, run `miden-faucet-client [COMMAND] --help`. The legacy alias `miden-faucet` is still available for backwards compatibility. + +To request tokens from a remote faucet, use the separate operator CLI: +```bash +miden-faucet-operator mint --url --account --amount +``` + +To see available options: +```bash +miden-faucet-operator mint --help +``` diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 473387c6..261e2547 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -38,13 +38,13 @@ sudo apt install llvm clang bindgen pkg-config libssl-dev libsqlite3-dev Install the latest faucet binary: ```sh -cargo install miden-faucet --locked +cargo install miden-faucet-client --locked ``` This will install the latest official version of the faucet. You can install a specific version `x.y.z` using ```sh -cargo install miden-faucet --locked --version x.y.z +cargo install miden-faucet-client --locked --version x.y.z ``` You can also use `cargo` to compile the node from the source code if for some reason you need a specific git revision. @@ -53,13 +53,15 @@ this for advanced use only. The incantation is a little different as you'll be t ```sh # Install from a specific branch -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet --branch +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --branch # Install a specific tag -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet --tag +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --tag # Install a specific git revision -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet --rev +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --rev + +> The legacy `miden-faucet` binary name is still available as an alias for backwards compatibility. ``` More information on the various `cargo install` options can be found diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index e3fdbac7..e9f9ca5b 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -12,7 +12,7 @@ Get the Miden Faucet running in minutes. First, we need to initialize the faucet with a new account that will hold the tokens to be distributed. This command generates a new account with the specified token configuration and saves the account data to a local SQLite store. The account is not yet deployed to the network - that will happen when the faucet is running and the first transaction is sent to the node. ```bash -miden-faucet init \ +miden-faucet-client init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ @@ -24,7 +24,7 @@ miden-faucet init \ Next, start the faucet by specifying the addresses where the API and the frontend will be served, the address of the Miden node, and the network configuration. The API server will handle incoming token requests and manage the minting process. ```bash -miden-faucet start \ +miden-faucet-client start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --node-url https://rpc.testnet.miden.io \ @@ -33,7 +33,20 @@ miden-faucet start \ ## Step 3: Request Test Tokens -Once the faucet is running, you can request test tokens through either the web interface or the REST API. +Once the faucet is running, you can request test tokens through either the web interface, the operator CLI, or the REST API. + +### Via Operator CLI + +Use the dedicated mint binary: + +```bash +miden-faucet-operator mint \ + --url http://localhost:8000 \ + --account \ + --amount 1000 +``` + +This solves the PoW challenge and mints a public P2ID note. (The legacy `miden-faucet mint` subcommand is removed.) ### Via Web Interface (if frontend is enabled) @@ -57,12 +70,12 @@ You can also programmatically interact with the REST API to mint tokens. Check o If you have a Miden Node running locally, you can run the faucet against that node. ```bash -miden-faucet init \ +miden-faucet-client init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 -miden-faucet start \ +miden-faucet-client start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 ``` @@ -72,13 +85,13 @@ miden-faucet start \ Connect to the node deployed in Miden Devnet. ```bash -miden-faucet init \ +miden-faucet-client init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --network devnet -miden-faucet start \ +miden-faucet-client start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --network devnet @@ -89,13 +102,13 @@ miden-faucet start \ Connect to the node deployed in Miden Testnet. ```bash -miden-faucet init \ +miden-faucet-client init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --network testnet -miden-faucet start \ +miden-faucet-client start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --explorer-url https://testnet.midenscan.com \ @@ -111,3 +124,5 @@ miden-faucet start \ --api-bind-url http://localhost:8000 \ --network testnet ``` + +If you need to mint from a remote faucet instance, use `miden-faucet-operator mint ...`. The legacy `miden-faucet` alias still works for the client commands. diff --git a/packaging/faucet/miden-faucet.service b/packaging/faucet/miden-faucet.service index babbda70..8ebef46c 100644 --- a/packaging/faucet/miden-faucet.service +++ b/packaging/faucet/miden-faucet.service @@ -7,9 +7,9 @@ WantedBy=multi-user.target [Service] Type=exec -Environment="OTEL_SERVICE_NAME=miden-faucet" +Environment="OTEL_SERVICE_NAME=miden-faucet-client" EnvironmentFile=/lib/systemd/system/miden-faucet.env -ExecStart=/usr/bin/miden-faucet start +ExecStart=/usr/bin/miden-faucet-client start WorkingDirectory=/opt/miden-faucet User=miden-faucet RestartSec=5 From ede1df1d6e8254866b5ebc014f5c9ea3a7c49874 Mon Sep 17 00:00:00 2001 From: keinberger Date: Fri, 12 Dec 2025 10:59:46 +0200 Subject: [PATCH 04/23] chore(Makefile): update Makefile to include miden-faucet-operator binary installation --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index c27c2c6a..11f60061 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,7 @@ check: ## Check all targets and features for errors without code generation .PHONY: install-faucet install-faucet: ## Installs faucet ${BUILD_PROTO} cargo install --path bin/faucet --locked + ${BUILD_PROTO} cargo install --path bin/faucet-operator --locked .PHONY: check-tools check-tools: ## Checks if development tools are installed From 06f1dd96bbab100c0c285c5c686786df9e7f0978 Mon Sep 17 00:00:00 2001 From: keinberger Date: Fri, 12 Dec 2025 11:00:21 +0200 Subject: [PATCH 05/23] docs: update README to include correct client start command docs: update instructions to include correct client start command and mint command usage --- docs/src/getting-started/installation.md | 5 +++++ docs/src/getting-started/quick-start.md | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 261e2547..e6f1f502 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -39,12 +39,14 @@ Install the latest faucet binary: ```sh cargo install miden-faucet-client --locked +cargo install miden-faucet-operator --locked ``` This will install the latest official version of the faucet. You can install a specific version `x.y.z` using ```sh cargo install miden-faucet-client --locked --version x.y.z +cargo install miden-faucet-operator --locked --version x.y.z ``` You can also use `cargo` to compile the node from the source code if for some reason you need a specific git revision. @@ -54,12 +56,15 @@ this for advanced use only. The incantation is a little different as you'll be t ```sh # Install from a specific branch cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --branch +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --branch # Install a specific tag cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --tag +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --tag # Install a specific git revision cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --rev +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --rev > The legacy `miden-faucet` binary name is still available as an alias for backwards compatibility. ``` diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index e9f9ca5b..2b92c911 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -28,6 +28,7 @@ miden-faucet-client start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --node-url https://rpc.testnet.miden.io \ + --explorer-url https://testnet.midenscan.com \ --network testnet ``` From b26a00853e3a417edc2ca5687451c728451b752d Mon Sep 17 00:00:00 2001 From: keinberger Date: Fri, 12 Dec 2025 17:55:39 +0200 Subject: [PATCH 06/23] feat: rename faucet to faucet-operator and faucet-operator to faucet-client (reverse naming) --- Cargo.lock | 49 +- Cargo.toml | 2 +- README.md | 10 +- bin/faucet-client/Cargo.toml | 33 + .../src/lib.rs | 0 bin/faucet-client/src/main.rs | 29 + .../src/mint.rs | 0 .../tests/mint.rs | 2 +- bin/{faucet => faucet-operator}/.env | 0 bin/faucet-operator/Cargo.toml | 66 +- bin/{faucet => faucet-operator}/README.md | 0 bin/{faucet => faucet-operator}/build.rs | 0 .../frontend/api.js | 0 .../frontend/app.js | 0 .../frontend/background.png | Bin .../frontend/favicon.ico | Bin .../frontend/index.css | 0 .../frontend/index.html | 0 .../frontend/index.js | 0 .../frontend/not_found.html | 0 .../frontend/package.json | 0 .../frontend/ui.js | 0 .../frontend/utils.js | 0 .../frontend/wallet-icon.png | Bin .../src/api/get_metadata.rs | 0 .../src/api/get_note.rs | 0 .../src/api/get_pow.rs | 0 .../src/api/get_tokens.rs | 0 .../src/api/mod.rs | 0 .../src/api_key.rs | 0 .../src/frontend.rs | 0 .../src/logging.rs | 0 bin/faucet-operator/src/main.rs | 703 ++++++++++++++- .../src/network.rs | 0 .../src/testing/mod.rs | 0 .../src/testing/stub_rpc_api.rs | 0 bin/faucet/Cargo.toml | 72 -- bin/faucet/src/bin/miden-faucet.rs | 2 - bin/faucet/src/main.rs | 811 ------------------ docs/src/getting-started/cli.md | 20 +- docs/src/getting-started/installation.md | 2 +- docs/src/getting-started/quick-start.md | 22 +- packaging/faucet/miden-faucet.service | 4 +- 43 files changed, 860 insertions(+), 967 deletions(-) create mode 100644 bin/faucet-client/Cargo.toml rename bin/{faucet-operator => faucet-client}/src/lib.rs (100%) create mode 100644 bin/faucet-client/src/main.rs rename bin/{faucet-operator => faucet-client}/src/mint.rs (100%) rename bin/{faucet-operator => faucet-client}/tests/mint.rs (97%) rename bin/{faucet => faucet-operator}/.env (100%) rename bin/{faucet => faucet-operator}/README.md (100%) rename bin/{faucet => faucet-operator}/build.rs (100%) rename bin/{faucet => faucet-operator}/frontend/api.js (100%) rename bin/{faucet => faucet-operator}/frontend/app.js (100%) rename bin/{faucet => faucet-operator}/frontend/background.png (100%) rename bin/{faucet => faucet-operator}/frontend/favicon.ico (100%) rename bin/{faucet => faucet-operator}/frontend/index.css (100%) rename bin/{faucet => faucet-operator}/frontend/index.html (100%) rename bin/{faucet => faucet-operator}/frontend/index.js (100%) rename bin/{faucet => faucet-operator}/frontend/not_found.html (100%) rename bin/{faucet => faucet-operator}/frontend/package.json (100%) rename bin/{faucet => faucet-operator}/frontend/ui.js (100%) rename bin/{faucet => faucet-operator}/frontend/utils.js (100%) rename bin/{faucet => faucet-operator}/frontend/wallet-icon.png (100%) rename bin/{faucet => faucet-operator}/src/api/get_metadata.rs (100%) rename bin/{faucet => faucet-operator}/src/api/get_note.rs (100%) rename bin/{faucet => faucet-operator}/src/api/get_pow.rs (100%) rename bin/{faucet => faucet-operator}/src/api/get_tokens.rs (100%) rename bin/{faucet => faucet-operator}/src/api/mod.rs (100%) rename bin/{faucet => faucet-operator}/src/api_key.rs (100%) rename bin/{faucet => faucet-operator}/src/frontend.rs (100%) rename bin/{faucet => faucet-operator}/src/logging.rs (100%) rename bin/{faucet => faucet-operator}/src/network.rs (100%) rename bin/{faucet => faucet-operator}/src/testing/mod.rs (100%) rename bin/{faucet => faucet-operator}/src/testing/stub_rpc_api.rs (100%) delete mode 100644 bin/faucet/Cargo.toml delete mode 100644 bin/faucet/src/bin/miden-faucet.rs delete mode 100644 bin/faucet/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 86cfb548..1e8cb0ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2159,38 +2159,17 @@ name = "miden-faucet-client" version = "0.12.4" dependencies = [ "anyhow", - "async-trait", "axum", - "axum-extra", - "base64", "clap", - "fantoccini", - "http 1.4.0", - "humantime", + "hex", "miden-client", - "miden-client-sqlite-store", - "miden-faucet-lib", - "miden-node-proto", - "miden-pow-rate-limiter", - "miden-testing", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", "rand", - "rand_chacha", + "reqwest", "serde", "serde_json", "sha2", "thiserror 2.0.17", "tokio", - "tokio-stream", - "tonic", - "tonic-web", - "tower", - "tower-http", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", "url", ] @@ -2213,17 +2192,39 @@ name = "miden-faucet-operator" version = "0.12.4" dependencies = [ "anyhow", + "async-trait", "axum", + "axum-extra", + "base64", "clap", - "hex", + "fantoccini", + "http 1.4.0", + "humantime", "miden-client", + "miden-client-sqlite-store", + "miden-faucet-lib", + "miden-node-proto", + "miden-pow-rate-limiter", + "miden-testing", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "rand", + "rand_chacha", "reqwest", "serde", "serde_json", "sha2", "thiserror 2.0.17", "tokio", + "tokio-stream", + "tonic", + "tonic-web", + "tower", + "tower-http", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 1eae2dac..b2f87a62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bin/faucet", "bin/faucet-operator", "crates/faucet"] +members = ["bin/faucet-client", "bin/faucet-operator", "crates/faucet"] resolver = "2" diff --git a/README.md b/README.md index 6aaeb874..0da6b751 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ make install-faucet 2. Initialize the faucet server. This will generate a new account with the specified token configuration and save the account data to a local SQLite store: ```bash -miden-faucet-client init \ +miden-faucet-operator init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ @@ -25,20 +25,20 @@ miden-faucet-client init \ > [!TIP] > This account will not be created on chain yet, creation on chain will happen on the first minting transaction. -> You can also run the legacy alias `miden-faucet` for backwards compatibility; it runs the same `miden-faucet-client` binary. +> You can also run the legacy alias `miden-faucet` for backwards compatibility; it runs the same `miden-faucet-operator` binary. 3. Start the faucet: ```bash -miden-faucet-client start \ +miden-faucet-operator start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --explorer-url https://testnet.midenscan.com \ --network testnet ``` -Requesting tokens from a faucet endpoint is handled by the separate `miden-faucet-operator` binary: +Requesting tokens from a faucet endpoint is handled by the separate `miden-faucet-client` binary: ```bash -miden-faucet-operator mint --url --account --amount +miden-faucet-client mint --url --account --amount ``` After a few seconds you may go to `http://localhost:8080` and see the faucet UI. diff --git a/bin/faucet-client/Cargo.toml b/bin/faucet-client/Cargo.toml new file mode 100644 index 00000000..de137887 --- /dev/null +++ b/bin/faucet-client/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors.workspace = true +description = "Client CLI to request tokens from a remote Miden faucet (PoW mint)" +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "miden-faucet-client" +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { features = ["derive", "env", "string"], version = "4.5" } +hex = { version = "0.4" } +miden-client = { workspace = true } +rand = { features = ["thread_rng"], workspace = true } +reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } +tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true } +url = { workspace = true } + +[dev-dependencies] +axum = { features = ["tokio"], version = "0.8" } +serde = { workspace = true } +tokio = { features = ["macros", "net", "rt-multi-thread", "time"], workspace = true } diff --git a/bin/faucet-operator/src/lib.rs b/bin/faucet-client/src/lib.rs similarity index 100% rename from bin/faucet-operator/src/lib.rs rename to bin/faucet-client/src/lib.rs diff --git a/bin/faucet-client/src/main.rs b/bin/faucet-client/src/main.rs new file mode 100644 index 00000000..a0569f16 --- /dev/null +++ b/bin/faucet-client/src/main.rs @@ -0,0 +1,29 @@ +use clap::{Parser, Subcommand}; +use miden_faucet_client::mint; + +/// Operator CLI for interacting with a live faucet. +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Request tokens from a remote faucet (does not consume the resulting note). + Mint(mint::MintCmd), +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Command::Mint(cmd) => { + cmd.execute().await.map_err(anyhow::Error::from)?; + }, + } + + Ok(()) +} diff --git a/bin/faucet-operator/src/mint.rs b/bin/faucet-client/src/mint.rs similarity index 100% rename from bin/faucet-operator/src/mint.rs rename to bin/faucet-client/src/mint.rs diff --git a/bin/faucet-operator/tests/mint.rs b/bin/faucet-client/tests/mint.rs similarity index 97% rename from bin/faucet-operator/tests/mint.rs rename to bin/faucet-client/tests/mint.rs index dc34b163..09b1de92 100644 --- a/bin/faucet-operator/tests/mint.rs +++ b/bin/faucet-client/tests/mint.rs @@ -6,7 +6,7 @@ use axum::{Json, Router}; use clap::Parser; use miden_client::account::AccountId; use miden_client::note::NoteId; -use miden_faucet_operator::mint::{GetTokensResponse, MintCmd, PowResponse}; +use miden_faucet_client::mint::{GetTokensResponse, MintCmd, PowResponse}; use serde::Deserialize; use tokio::net::TcpListener; use tokio::sync::Mutex; diff --git a/bin/faucet/.env b/bin/faucet-operator/.env similarity index 100% rename from bin/faucet/.env rename to bin/faucet-operator/.env diff --git a/bin/faucet-operator/Cargo.toml b/bin/faucet-operator/Cargo.toml index f77ad04a..94951a7f 100644 --- a/bin/faucet-operator/Cargo.toml +++ b/bin/faucet-operator/Cargo.toml @@ -1,11 +1,12 @@ [package] authors.workspace = true -description = "Operator CLI for interacting with a Miden faucet" +description = "Token faucet operator (init/start) for Miden faucet" edition.workspace = true homepage.workspace = true +keywords = ["faucet", "miden", "node", "operator"] license.workspace = true name = "miden-faucet-operator" -readme = "../../README.md" +readme = "README.md" repository.workspace = true rust-version.workspace = true version.workspace = true @@ -14,20 +15,51 @@ version.workspace = true workspace = true [dependencies] -anyhow = { workspace = true } -clap = { features = ["derive", "env", "string"], version = "4.5" } -hex = { version = "0.4" } -miden-client = { workspace = true } -rand = { features = ["thread_rng"], workspace = true } -reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } -serde = { workspace = true } -serde_json = { workspace = true } -sha2 = { workspace = true } -thiserror = { workspace = true } -tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true } -url = { workspace = true } +miden-faucet-lib = { workspace = true } +miden-pow-rate-limiter = { workspace = true } + +# Miden dependencies. +miden-client = { features = ["tonic"], workspace = true } +miden-client-sqlite-store = { workspace = true } + +# External dependencies. +anyhow = { workspace = true } +async-trait = { version = "0.1" } +axum = { features = ["tokio"], version = "0.8" } +axum-extra = { version = "0.10" } +base64 = { version = "0.22" } +clap = { features = ["derive", "env", "string"], version = "4.5" } +http = { workspace = true } +humantime = { workspace = true } +opentelemetry = { workspace = true } +opentelemetry-otlp = { workspace = true } +opentelemetry_sdk = { workspace = true } +rand = { features = ["thread_rng"], workspace = true } +rand_chacha = { version = "0.9" } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } +tokio = { features = ["fs"], workspace = true } +tokio-stream = { features = ["net"], workspace = true } +tonic = { features = ["tls-native-roots"], workspace = true } +tower = { workspace = true } +tower-http = { features = ["cors", "set-header", "trace"], workspace = true } +tracing = { workspace = true } +tracing-opentelemetry = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } [dev-dependencies] -axum = { features = ["tokio"], version = "0.8" } -serde = { workspace = true } -tokio = { features = ["macros", "net", "rt-multi-thread", "time"], workspace = true } +fantoccini = { version = "0.22" } +miden-node-proto = { version = "0.12" } +miden-testing = { version = "0.12" } +reqwest = { default-features = false, features = ["json"], version = "0.12" } +serde_json = { workspace = true } +tokio = { features = ["macros", "process"], workspace = true } +tonic-web = { version = "0.14" } + +# Required to avoid false positives in cargo-machete +# This is due to the `winter-maybe-async` crate not working standalone. +[package.metadata.cargo-machete] +ignored = ["async-trait"] diff --git a/bin/faucet/README.md b/bin/faucet-operator/README.md similarity index 100% rename from bin/faucet/README.md rename to bin/faucet-operator/README.md diff --git a/bin/faucet/build.rs b/bin/faucet-operator/build.rs similarity index 100% rename from bin/faucet/build.rs rename to bin/faucet-operator/build.rs diff --git a/bin/faucet/frontend/api.js b/bin/faucet-operator/frontend/api.js similarity index 100% rename from bin/faucet/frontend/api.js rename to bin/faucet-operator/frontend/api.js diff --git a/bin/faucet/frontend/app.js b/bin/faucet-operator/frontend/app.js similarity index 100% rename from bin/faucet/frontend/app.js rename to bin/faucet-operator/frontend/app.js diff --git a/bin/faucet/frontend/background.png b/bin/faucet-operator/frontend/background.png similarity index 100% rename from bin/faucet/frontend/background.png rename to bin/faucet-operator/frontend/background.png diff --git a/bin/faucet/frontend/favicon.ico b/bin/faucet-operator/frontend/favicon.ico similarity index 100% rename from bin/faucet/frontend/favicon.ico rename to bin/faucet-operator/frontend/favicon.ico diff --git a/bin/faucet/frontend/index.css b/bin/faucet-operator/frontend/index.css similarity index 100% rename from bin/faucet/frontend/index.css rename to bin/faucet-operator/frontend/index.css diff --git a/bin/faucet/frontend/index.html b/bin/faucet-operator/frontend/index.html similarity index 100% rename from bin/faucet/frontend/index.html rename to bin/faucet-operator/frontend/index.html diff --git a/bin/faucet/frontend/index.js b/bin/faucet-operator/frontend/index.js similarity index 100% rename from bin/faucet/frontend/index.js rename to bin/faucet-operator/frontend/index.js diff --git a/bin/faucet/frontend/not_found.html b/bin/faucet-operator/frontend/not_found.html similarity index 100% rename from bin/faucet/frontend/not_found.html rename to bin/faucet-operator/frontend/not_found.html diff --git a/bin/faucet/frontend/package.json b/bin/faucet-operator/frontend/package.json similarity index 100% rename from bin/faucet/frontend/package.json rename to bin/faucet-operator/frontend/package.json diff --git a/bin/faucet/frontend/ui.js b/bin/faucet-operator/frontend/ui.js similarity index 100% rename from bin/faucet/frontend/ui.js rename to bin/faucet-operator/frontend/ui.js diff --git a/bin/faucet/frontend/utils.js b/bin/faucet-operator/frontend/utils.js similarity index 100% rename from bin/faucet/frontend/utils.js rename to bin/faucet-operator/frontend/utils.js diff --git a/bin/faucet/frontend/wallet-icon.png b/bin/faucet-operator/frontend/wallet-icon.png similarity index 100% rename from bin/faucet/frontend/wallet-icon.png rename to bin/faucet-operator/frontend/wallet-icon.png diff --git a/bin/faucet/src/api/get_metadata.rs b/bin/faucet-operator/src/api/get_metadata.rs similarity index 100% rename from bin/faucet/src/api/get_metadata.rs rename to bin/faucet-operator/src/api/get_metadata.rs diff --git a/bin/faucet/src/api/get_note.rs b/bin/faucet-operator/src/api/get_note.rs similarity index 100% rename from bin/faucet/src/api/get_note.rs rename to bin/faucet-operator/src/api/get_note.rs diff --git a/bin/faucet/src/api/get_pow.rs b/bin/faucet-operator/src/api/get_pow.rs similarity index 100% rename from bin/faucet/src/api/get_pow.rs rename to bin/faucet-operator/src/api/get_pow.rs diff --git a/bin/faucet/src/api/get_tokens.rs b/bin/faucet-operator/src/api/get_tokens.rs similarity index 100% rename from bin/faucet/src/api/get_tokens.rs rename to bin/faucet-operator/src/api/get_tokens.rs diff --git a/bin/faucet/src/api/mod.rs b/bin/faucet-operator/src/api/mod.rs similarity index 100% rename from bin/faucet/src/api/mod.rs rename to bin/faucet-operator/src/api/mod.rs diff --git a/bin/faucet/src/api_key.rs b/bin/faucet-operator/src/api_key.rs similarity index 100% rename from bin/faucet/src/api_key.rs rename to bin/faucet-operator/src/api_key.rs diff --git a/bin/faucet/src/frontend.rs b/bin/faucet-operator/src/frontend.rs similarity index 100% rename from bin/faucet/src/frontend.rs rename to bin/faucet-operator/src/frontend.rs diff --git a/bin/faucet/src/logging.rs b/bin/faucet-operator/src/logging.rs similarity index 100% rename from bin/faucet/src/logging.rs rename to bin/faucet-operator/src/logging.rs diff --git a/bin/faucet-operator/src/main.rs b/bin/faucet-operator/src/main.rs index f028a8ca..67795498 100644 --- a/bin/faucet-operator/src/main.rs +++ b/bin/faucet-operator/src/main.rs @@ -1,29 +1,714 @@ +mod api; +mod api_key; +mod frontend; +mod logging; +mod network; +#[cfg(test)] +mod testing; + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; use clap::{Parser, Subcommand}; -use miden_faucet_operator::mint; +use miden_client::account::component::{AuthRpoFalcon512, BasicFungibleFaucet}; +use miden_client::account::{ + Account, + AccountBuilder, + AccountFile, + AccountStorageMode, + AccountType, +}; +use miden_client::asset::TokenSymbol; +use miden_client::auth::AuthSecretKey; +use miden_client::crypto::RpoRandomCoin; +use miden_client::crypto::rpo_falcon512::SecretKey; +use miden_client::rpc::Endpoint; +use miden_client::{Felt, Word}; +use miden_client_sqlite_store::SqliteStore; +use miden_faucet_lib::types::AssetAmount; +use miden_faucet_lib::{Faucet, FaucetConfig}; +use miden_pow_rate_limiter::PoWRateLimiterConfig; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use tokio::sync::mpsc; +use tokio::task::JoinSet; +use url::Url; + +use crate::api::{ApiServer, Metadata}; +use crate::api_key::ApiKey; +use crate::frontend::serve_frontend; +use crate::logging::OpenTelemetry; +use crate::network::FaucetNetwork; + +// CONSTANTS +// ================================================================================================= + +pub const REQUESTS_QUEUE_SIZE: usize = 1000; +const COMPONENT: &str = "miden-faucet-server"; + +const ENV_API_BIND_URL: &str = "MIDEN_FAUCET_API_BIND_URL"; +const ENV_API_PUBLIC_URL: &str = "MIDEN_FAUCET_API_PUBLIC_URL"; +const ENV_FRONTEND_URL: &str = "MIDEN_FAUCET_FRONTEND_URL"; +const ENV_NETWORK: &str = "MIDEN_FAUCET_NETWORK"; +const ENV_NODE_URL: &str = "MIDEN_FAUCET_NODE_URL"; +const ENV_TIMEOUT: &str = "MIDEN_FAUCET_TIMEOUT"; +const ENV_MAX_CLAIMABLE_AMOUNT: &str = "MIDEN_FAUCET_MAX_CLAIMABLE_AMOUNT"; +const ENV_REMOTE_TX_PROVER_URL: &str = "MIDEN_FAUCET_REMOTE_TX_PROVER_URL"; +const ENV_POW_SECRET: &str = "MIDEN_FAUCET_POW_SECRET"; +const ENV_POW_CHALLENGE_LIFETIME: &str = "MIDEN_FAUCET_POW_CHALLENGE_LIFETIME"; +const ENV_POW_CLEANUP_INTERVAL: &str = "MIDEN_FAUCET_POW_CLEANUP_INTERVAL"; +const ENV_POW_GROWTH_RATE: &str = "MIDEN_FAUCET_POW_GROWTH_RATE"; +const ENV_POW_BASELINE: &str = "MIDEN_FAUCET_POW_BASELINE"; +const ENV_BASE_AMOUNT: &str = "MIDEN_FAUCET_BASE_AMOUNT"; +const ENV_API_KEYS: &str = "MIDEN_FAUCET_API_KEYS"; +const ENV_ENABLE_OTEL: &str = "MIDEN_FAUCET_ENABLE_OTEL"; +const ENV_STORE: &str = "MIDEN_FAUCET_STORE"; +const ENV_EXPLORER_URL: &str = "MIDEN_FAUCET_EXPLORER_URL"; +const ENV_BATCH_SIZE: &str = "MIDEN_FAUCET_BATCH_SIZE"; +const ENV_IMPORT_ACCOUNT_PATH: &str = "MIDEN_FAUCET_IMPORT_ACCOUNT_PATH"; +const ENV_DEPLOY: &str = "MIDEN_FAUCET_DEPLOY"; +const ENV_TOKEN_SYMBOL: &str = "MIDEN_FAUCET_TOKEN_SYMBOL"; +const ENV_DECIMALS: &str = "MIDEN_FAUCET_DECIMALS"; +const ENV_MAX_SUPPLY: &str = "MIDEN_FAUCET_MAX_SUPPLY"; + +// COMMANDS +// ================================================================================================ -/// Operator CLI for interacting with a live faucet. #[derive(Parser)] #[command(version, about, long_about = None)] -struct Cli { +pub struct Cli { #[command(subcommand)] - command: Command, + pub command: Command, } +#[allow(clippy::large_enum_variant)] #[derive(Subcommand)] -enum Command { - /// Request tokens from a remote faucet (does not consume the resulting note). - Mint(mint::MintCmd), +pub enum Command { + /// Initialize the faucet with a new or existing account. + Init { + #[clap(flatten)] + config: ClientConfig, + + /// Symbol of the new token. + #[arg( + short, + long, + value_name = "STRING", + required_unless_present = "import_account_path", + env = ENV_TOKEN_SYMBOL + )] + token_symbol: Option, + + /// Decimals of the new token. + #[arg(short, long, value_name = "U8", required_unless_present = "import_account_path", env = ENV_DECIMALS)] + decimals: Option, + + /// Max supply of the new token (in base units). + #[arg(short, long, value_name = "U64", required_unless_present = "import_account_path", env = ENV_MAX_SUPPLY)] + max_supply: Option, + + /// Set an existing faucet account file to use, instead of creating a new account. + #[arg(long = "import", value_name = "FILE", conflicts_with_all = ["token_symbol", "decimals", "max_supply"], env = ENV_IMPORT_ACCOUNT_PATH)] + import_account_path: Option, + + /// Whether to deploy the faucet account to the node. + #[arg(long, value_name = "BOOL", default_value_t = false, env = ENV_DEPLOY)] + deploy: bool, + }, + + /// Generate an API key that can be used by the faucet. + /// + /// Prints out the generated API key to stdout. Keys can then be supplied to the faucet via the + /// `--api-keys` flag or `MIDEN_FAUCET_API_KEYS` env var of the `start` command. + CreateApiKey, + + /// Start the faucet server + Start { + #[clap(flatten)] + config: ClientConfig, + + /// URL to bind the API server. + #[arg(long = "api-bind-url", value_name = "URL", env = ENV_API_BIND_URL)] + api_bind_url: Url, + + /// Public URL to access the API server. If not set, the bind url will be used. + #[arg(long = "api-public-url", value_name = "URL", env = ENV_API_PUBLIC_URL)] + api_public_url: Option, + + /// URL to bind the frontend server. If not set, the frontend will not be served. + #[arg(long = "frontend-url", value_name = "URL", env = ENV_FRONTEND_URL)] + frontend_url: Option, + + /// The maximum amount of assets' base units that can be dispersed on each request. + #[arg(long = "max-claimable-amount", value_name = "U64", env = ENV_MAX_CLAIMABLE_AMOUNT, default_value = "1000000000")] + max_claimable_amount: u64, + + /// The secret to be used by the server to sign the `PoW` challenges. This should NOT be + /// shared. + #[arg(long = "pow-secret", value_name = "STRING", default_value = "", env = ENV_POW_SECRET)] + pow_secret: String, + + /// The duration during which the `PoW` challenges are valid. Changing this will affect the + /// rate limiting, since it works by rejecting new submissions while the previous submitted + /// challenge is still valid. + #[arg(long = "pow-challenge-lifetime", value_name = "DURATION", env = ENV_POW_CHALLENGE_LIFETIME, default_value = "30s", value_parser = humantime::parse_duration)] + pow_challenge_lifetime: Duration, + + /// Defines how quickly the `PoW` difficulty grows with the number of requests. The number + /// of active challenges gets multiplied by the growth rate to compute the load + /// difficulty. + /// + /// Meaning, the difficulty bits of the challenge will increase approximately by + /// `log2(growth_rate * num_active_challenges)`. + #[arg(long = "pow-growth-rate", value_name = "F64", env = ENV_POW_GROWTH_RATE, default_value = "0.1")] + pow_growth_rate: f64, + + /// The interval at which the `PoW` challenge cache is cleaned up. + #[arg(long = "pow-cleanup-interval", value_name = "DURATION", env = ENV_POW_CLEANUP_INTERVAL, default_value = "2s", value_parser = humantime::parse_duration)] + pow_cleanup_interval: Duration, + + /// The baseline for the `PoW` challenges. This sets the `PoW` difficulty (in bits) that a + /// a challenge will have when there are no requests against the faucet. It must be between + /// 0 and 32. + #[arg(value_parser = clap::value_parser!(u8).range(0..=32))] + #[arg(long = "pow-baseline", value_name = "U8", env = ENV_POW_BASELINE, default_value = "16")] + pow_baseline: u8, + + /// The baseline amount for token requests (in base units). Requests for greater amounts + /// would require higher level of `PoW`. + /// + /// The request complexity for challenges is computed as: `request_complexity = (amount / + /// base_amount) + 1` + #[arg(long = "base-amount", value_name = "U64", env = ENV_BASE_AMOUNT, default_value = "100000000")] + base_amount: u64, + + /// Comma-separated list of API keys. + #[arg(long = "api-keys", value_name = "STRING", env = ENV_API_KEYS, num_args = 1.., value_delimiter = ',')] + api_keys: Vec, + + /// Enables the exporting of traces for OpenTelemetry. + /// + /// This can be further configured using environment variables as defined in the official + /// OpenTelemetry documentation. See our operator manual for further details. + #[arg(long = "enable-otel", value_name = "BOOL", default_value_t = false, env = ENV_ENABLE_OTEL)] + open_telemetry: bool, + + /// Explorer URL. + #[arg(long = "explorer-url", value_name = "URL", env = ENV_EXPLORER_URL)] + explorer_url: Option, + + /// The maximum number of requests to process in each batch. Each batch is processed in a + /// single transaction. + #[arg(long = "batch-size", value_name = "USIZE", default_value = "32", env = ENV_BATCH_SIZE)] + batch_size: usize, + }, +} + +/// Configuration for the faucet client. +#[derive(Parser, Debug, Clone)] +pub struct ClientConfig { + /// Path to the `SQLite` store. + #[arg(long = "store", value_name = "FILE", default_value = "faucet_client_store.sqlite3", env = ENV_STORE)] + store_path: PathBuf, + + /// Timeout for attempting to connect to the node. + #[arg(long = "timeout", value_name = "DURATION", default_value = "5s", env = ENV_TIMEOUT, value_parser = humantime::parse_duration)] + timeout: Duration, + + /// Network configuration to use. Options are `devnet`, `testnet`, `localhost` or a custom + /// network. It is used to display the correct bech32 addresses in the UI. + #[arg(long = "network", value_name = "NETWORK", default_value = "localhost", env = ENV_NETWORK)] + network: FaucetNetwork, + + /// Endpoint of the remote transaction prover in the format `://[:]`. + #[arg(long = "remote-tx-prover-url", value_name = "URL", env = ENV_REMOTE_TX_PROVER_URL)] + remote_tx_prover_url: Option, + + /// Node RPC gRPC endpoint in the format `http://[:]`. If not set, the url is derived + /// from the specified network. + #[arg(long = "node-url", value_name = "URL", env = ENV_NODE_URL)] + node_url: Option, +} + +impl Command { + fn open_telemetry(&self) -> OpenTelemetry { + if match *self { + Command::Start { open_telemetry, .. } => open_telemetry, + _ => false, + } { + OpenTelemetry::Enabled + } else { + OpenTelemetry::Disabled + } + } } +// MAIN +// ================================================================================================= + #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); + // Configure tracing with optional OpenTelemetry exporting support. + let _otel_guard = logging::setup_tracing(cli.command.open_telemetry()) + .context("failed to initialize logging")?; + + Box::pin(run_faucet_command(cli)).await +} + +#[allow(clippy::too_many_lines)] +async fn run_faucet_command(cli: Cli) -> anyhow::Result<()> { + // Note: open-telemetry is handled in main. match cli.command { - Command::Mint(cmd) => { - cmd.execute().await.map_err(anyhow::Error::new)?; + Command::Init { + config: + ClientConfig { + node_url, + timeout, + remote_tx_prover_url, + network, + store_path, + }, + token_symbol, + decimals, + max_supply, + import_account_path, + deploy, + } => { + let (account, secret) = if let Some(account_path) = import_account_path { + // Import existing faucet account + let account_data = AccountFile::read(account_path) + .context("failed to read account data from file")?; + let secret = account_data + .auth_secret_keys + .first() + .context("auth secret key is required")? + .clone(); + (account_data.account, secret) + } else { + println!("Generating new faucet account. This may take a few seconds..."); + let token_symbol = + token_symbol.expect("token_symbol should be present when not importing"); + let decimals = decimals.expect("decimals should be present when not importing"); + let max_supply = + max_supply.expect("max_supply should be present when not importing"); + create_faucet_account(token_symbol.as_str(), max_supply, decimals)? + }; + let node_endpoint = parse_node_endpoint(node_url, &network)?; + let faucet_config = FaucetConfig { + store_path, + node_endpoint, + network_id: network.to_network_id()?, + timeout, + remote_tx_prover_url, + }; + Box::pin(Faucet::init(&faucet_config, account, &secret, deploy)) + .await + .context("failed to initialize faucet")?; + + println!("Faucet account successfully initialized"); + }, + + Command::CreateApiKey => { + let mut rng = ChaCha20Rng::from_seed(rand::random()); + let key = ApiKey::generate(&mut rng).encode(); + println!("{key}"); + }, + + Command::Start { + config: + ClientConfig { + node_url, + timeout, + remote_tx_prover_url, + network, + store_path, + }, + api_bind_url, + api_public_url, + frontend_url, + max_claimable_amount, + pow_secret, + pow_challenge_lifetime, + pow_cleanup_interval, + pow_growth_rate, + pow_baseline, + base_amount, + api_keys, + open_telemetry: _, + explorer_url, + batch_size, + } => { + let node_endpoint = parse_node_endpoint(node_url, &network)?; + let config = FaucetConfig { + store_path: store_path.clone(), + node_endpoint: node_endpoint.clone(), + network_id: network.to_network_id()?, + timeout, + remote_tx_prover_url, + }; + let faucet = Faucet::load(&config).await.context("failed to load faucet")?; + + let store = + Arc::new(SqliteStore::new(store_path).await.context("failed to create store")?); + + // Maximum of 1000 requests in-queue at once. Overflow is rejected for faster feedback. + let (tx_mint_requests, rx_mint_requests) = mpsc::channel(REQUESTS_QUEUE_SIZE); + + let api_keys = api_keys + .iter() + .map(|k| ApiKey::decode(k)) + .collect::, _>>() + .context("failed to decode API keys")?; + let max_claimable_amount = AssetAmount::new(max_claimable_amount)?; + let rate_limiter_config = PoWRateLimiterConfig { + challenge_lifetime: pow_challenge_lifetime, + cleanup_interval: pow_cleanup_interval, + growth_rate: pow_growth_rate, + baseline: pow_baseline, + }; + let faucet_account = faucet.faucet_account().await?; + let faucet_component = BasicFungibleFaucet::try_from(&faucet_account)?; + let max_supply = AssetAmount::new(faucet_component.max_supply().as_int())?; + let decimals = faucet_component.decimals(); + + let metadata = Metadata { + id: faucet.faucet_id(), + issuance: faucet.issuance(), + max_supply, + decimals, + explorer_url, + base_amount, + }; + + // We keep a channel sender open in the main thread to avoid the faucet closing before + // servers can propagate any errors. + let tx_mint_requests_clone = tx_mint_requests.clone(); + let api_server = ApiServer::new( + metadata, + max_claimable_amount, + tx_mint_requests_clone, + pow_secret.as_str(), + rate_limiter_config, + &api_keys, + store, + ); + + // Use select to concurrently: + // - Run and wait for the faucet (on current thread) + // - Run and wait for API server (in a spawned task) + // - Run and wait for frontend server (in a spawned task, only if set) + let faucet_future = faucet.run(rx_mint_requests, batch_size); + + let mut tasks = JoinSet::new(); + let mut tasks_ids = HashMap::new(); + + let api_id = tasks.spawn(api_server.serve(api_bind_url.clone())).id(); + tasks_ids.insert(api_id, "api"); + + if let Some(frontend_url) = frontend_url { + let frontend_id = tasks + .spawn(serve_frontend( + frontend_url, + api_public_url.unwrap_or(api_bind_url), + node_endpoint.to_string(), + )) + .id(); + tasks_ids.insert(frontend_id, "frontend"); + } + + tokio::select! { + serve_result = tasks.join_next_with_id() => { + let (id, err) = match serve_result.unwrap() { + Ok((id, Ok(_))) => (id, Err(anyhow::anyhow!("completed unexpectedly"))), + Ok((id, Err(err))) => (id, Err(err)), + Err(join_err) => (join_err.id(), Err(join_err).context("failed to join task")), + }; + let component = tasks_ids.get(&id).unwrap_or(&"unknown"); + err.context(format!("{component} server failed")) + }, + faucet_result = faucet_future => { + // Faucet completed, return its result + faucet_result.context("faucet failed") + }, + }?; }, } Ok(()) } + +// UTILITIES +// ================================================================================================= + +/// Parses the node endpoint from the cli arguments. If an explicit url is provided, it is used. +/// Otherwise, it is derived from the specified network. +fn parse_node_endpoint(node_url: Option, network: &FaucetNetwork) -> anyhow::Result { + let url = if let Some(node_url) = node_url { + node_url.to_string() + } else { + network + .to_rpc_endpoint() + .context("no node url provided for the custom network")? + }; + + Endpoint::try_from(url.as_str()) + .map_err(anyhow::Error::msg) + .with_context(|| format!("failed to parse node url: {url}")) +} + +/// Creates a new faucet account from the given parameters. +fn create_faucet_account( + token_symbol: &str, + max_supply: u64, + decimals: u8, +) -> anyhow::Result<(Account, AuthSecretKey)> { + let mut rng = ChaCha20Rng::from_seed(rand::random()); + let secret = { + let auth_seed: [u64; 4] = rng.random(); + let rng_seed = Word::from(auth_seed.map(Felt::new)); + SecretKey::with_rng(&mut RpoRandomCoin::new(rng_seed)) + }; + + let symbol = TokenSymbol::try_from(token_symbol).context("failed to parse token symbol")?; + let max_supply = Felt::try_from(max_supply) + .map_err(anyhow::Error::msg) + .context("max supply value is greater than or equal to the field modulus")?; + let auth_component = AuthRpoFalcon512::new(secret.public_key().to_commitment().into()); + + let account = AccountBuilder::new(rng.random()) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?) + .with_auth_component(auth_component) + .build() + .context("failed to create basic fungible faucet account")?; + + Ok((account, AuthSecretKey::RpoFalcon512(secret))) +} + +// INTEGRATION TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use std::env::temp_dir; + use std::process::Stdio; + use std::str::FromStr; + use std::time::{Duration, Instant}; + + use fantoccini::ClientBuilder; + use miden_client::account::{AccountId, Address, NetworkId}; + use serde_json::{Map, json}; + use tokio::io::AsyncBufReadExt; + use tokio::net::TcpListener; + use tokio::time::sleep; + use url::Url; + + use crate::network::FaucetNetwork; + use crate::testing::stub_rpc_api::serve_stub; + use crate::{Cli, ClientConfig, run_faucet_command}; + + /// 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, mints tokens, and checks that all the + /// requests returned status 200. + #[tokio::test] + async fn frontend_mint_tokens() { + let stub_node_url = run_stub_node().await; + let website_url = run_faucet_server(stub_node_url).await; + let client = start_fantoccini_client().await; + + // Open the website + client.goto(website_url.as_str()).await.unwrap(); + + let title = client.title().await.unwrap(); + assert_eq!(title, "Miden Faucet"); + + let network_id = NetworkId::Testnet; + let account_id = AccountId::try_from(0).unwrap(); + let address = Address::new(account_id); + let address_bech32 = address.encode(network_id); + + // Fill in the account address + client + .find(fantoccini::Locator::Css("#recipient-address")) + .await + .unwrap() + .send_keys(&address_bech32) + .await + .unwrap(); + + // Select the first asset amount option + client + .find(fantoccini::Locator::Css("#token-amount")) + .await + .unwrap() + .click() + .await + .unwrap(); + client + .find(fantoccini::Locator::Css("#token-amount option")) + .await + .unwrap() + .click() + .await + .unwrap(); + + // Click the public note button + client + .find(fantoccini::Locator::Css("#send-public-button")) + .await + .unwrap() + .click() + .await + .unwrap(); + + // 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(); + + // Verify all requests are successful + assert!(failed_requests.as_array().unwrap().is_empty()); + + client.close().await.unwrap(); + } + + // TESTING HELPERS + // --------------------------------------------------------------------------------------------- + + async fn run_stub_node() -> Url { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let listener_addr = listener.local_addr().unwrap(); + let stub_node_url = Url::from_str(&format!("http://{listener_addr}")).unwrap(); + tokio::spawn({ + let stub_node_url = stub_node_url.clone(); + async move { serve_stub(&stub_node_url).await.unwrap() } + }); + stub_node_url + } + + async fn run_faucet_server(stub_node_url: Url) -> String { + let config = ClientConfig { + node_url: Some(stub_node_url.clone()), + timeout: Duration::from_millis(5000), + network: FaucetNetwork::Localhost, + store_path: temp_dir().join("test_store.sqlite3"), + remote_tx_prover_url: None, + }; + + Box::pin(run_faucet_command(Cli { + command: crate::Command::Init { + config: config.clone(), + token_symbol: Some("TEST".to_owned()), + decimals: Some(6), + max_supply: Some(1_000_000_000_000), + import_account_path: None, + deploy: false, + }, + })) + .await + .expect("failed to create faucet account"); + + let api_url = "http://localhost:8000"; + let frontend_url = "http://localhost:8080"; + + // Use std::thread to launch faucet - avoids Send requirements + std::thread::spawn(move || { + // Create a new runtime for this thread + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to build runtime"); + + // Run the faucet on this thread's runtime + rt.block_on(async { + Box::pin(run_faucet_command(Cli { + command: crate::Command::Start { + config, + api_bind_url: Url::try_from(api_url).unwrap(), + api_public_url: None, + frontend_url: Some(Url::parse(frontend_url).unwrap()), + max_claimable_amount: 1_000_000_000, + api_keys: vec![], + pow_secret: "test".to_string(), + pow_challenge_lifetime: Duration::from_secs(30), + pow_cleanup_interval: Duration::from_secs(1), + pow_growth_rate: 1.0, + pow_baseline: 12, + base_amount: 100_000, + open_telemetry: false, + explorer_url: None, + batch_size: 8, + }, + })) + .await + .expect("failed to start faucet"); + }); + }); + + // Wait for faucet to be up + let api_url = Url::parse(api_url).unwrap(); + let addrs = api_url.socket_addrs(|| None).unwrap(); + let start = Instant::now(); + let timeout = Duration::from_secs(10); + loop { + match tokio::net::TcpStream::connect(&addrs[..]).await { + Ok(_) => break, + Err(_) if start.elapsed() < timeout => { + sleep(Duration::from_millis(200)).await; + }, + Err(e) => panic!("faucet never became reachable: {e}"), + } + } + + frontend_url.to_string() + } + + async fn start_fantoccini_client() -> fantoccini::Client { + // Start chromedriver. This requires having chromedriver and chrome installed + let chromedriver_port = "57708"; + let mut chromedriver = tokio::process::Command::new("chromedriver") + .arg(format!("--port={chromedriver_port}")) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn() + .expect("failed to start chromedriver"); + let stdout = chromedriver.stdout.take().unwrap(); + tokio::spawn( + async move { chromedriver.wait().await.expect("chromedriver process failed") }, + ); + // Wait for chromedriver to be running + let mut reader = tokio::io::BufReader::new(stdout).lines(); + while let Some(line) = reader.next_line().await.unwrap() { + if line.contains("ChromeDriver was started successfully") { + break; + } + } + + // Start fantoccini 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") + } +} diff --git a/bin/faucet/src/network.rs b/bin/faucet-operator/src/network.rs similarity index 100% rename from bin/faucet/src/network.rs rename to bin/faucet-operator/src/network.rs diff --git a/bin/faucet/src/testing/mod.rs b/bin/faucet-operator/src/testing/mod.rs similarity index 100% rename from bin/faucet/src/testing/mod.rs rename to bin/faucet-operator/src/testing/mod.rs diff --git a/bin/faucet/src/testing/stub_rpc_api.rs b/bin/faucet-operator/src/testing/stub_rpc_api.rs similarity index 100% rename from bin/faucet/src/testing/stub_rpc_api.rs rename to bin/faucet-operator/src/testing/stub_rpc_api.rs diff --git a/bin/faucet/Cargo.toml b/bin/faucet/Cargo.toml deleted file mode 100644 index 612fc92c..00000000 --- a/bin/faucet/Cargo.toml +++ /dev/null @@ -1,72 +0,0 @@ -[package] -authors.workspace = true -description = "Token faucet application for Miden testnet" -edition.workspace = true -homepage.workspace = true -keywords = ["faucet", "miden", "node"] -license.workspace = true -name = "miden-faucet-client" -readme = "README.md" -repository.workspace = true -rust-version.workspace = true -version.workspace = true - -[lints] -workspace = true - -[[bin]] -name = "miden-faucet-client" -path = "src/main.rs" - -[[bin]] -name = "miden-faucet" -path = "src/bin/miden-faucet.rs" - -[dependencies] -miden-faucet-lib = { workspace = true } -miden-pow-rate-limiter = { workspace = true } - -# Miden dependencies. -miden-client = { features = ["tonic"], workspace = true } -miden-client-sqlite-store = { workspace = true } - -# External dependencies. -anyhow = { workspace = true } -async-trait = { version = "0.1" } -axum = { features = ["tokio"], version = "0.8" } -axum-extra = { version = "0.10" } -base64 = { version = "0.22" } -clap = { features = ["derive", "env", "string"], version = "4.5" } -http = { workspace = true } -humantime = { workspace = true } -opentelemetry = { workspace = true } -opentelemetry-otlp = { workspace = true } -opentelemetry_sdk = { workspace = true } -rand = { features = ["thread_rng"], workspace = true } -rand_chacha = { version = "0.9" } -serde = { workspace = true } -serde_json = { workspace = true } -sha2 = { workspace = true } -thiserror = { workspace = true } -tokio = { features = ["fs"], workspace = true } -tokio-stream = { features = ["net"], workspace = true } -tonic = { features = ["tls-native-roots"], workspace = true } -tower = { workspace = true } -tower-http = { features = ["cors", "set-header", "trace"], workspace = true } -tracing = { workspace = true } -tracing-opentelemetry = { workspace = true } -tracing-subscriber = { workspace = true } -url = { workspace = true } - -[dev-dependencies] -fantoccini = { version = "0.22" } -miden-node-proto = { version = "0.12" } -miden-testing = { version = "0.12" } -serde_json = { workspace = true } -tokio = { features = ["macros", "process"], workspace = true } -tonic-web = { version = "0.14" } - -# Required to avoid false positives in cargo-machete -# This is due to the `winter-maybe-async` crate not working standalone. -[package.metadata.cargo-machete] -ignored = ["async-trait"] diff --git a/bin/faucet/src/bin/miden-faucet.rs b/bin/faucet/src/bin/miden-faucet.rs deleted file mode 100644 index 0123888b..00000000 --- a/bin/faucet/src/bin/miden-faucet.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Backwards-compatibility alias for the legacy `miden-faucet` binary name. -include!("../main.rs"); diff --git a/bin/faucet/src/main.rs b/bin/faucet/src/main.rs deleted file mode 100644 index 96c24efe..00000000 --- a/bin/faucet/src/main.rs +++ /dev/null @@ -1,811 +0,0 @@ -mod api; -mod api_key; -mod frontend; -mod logging; -mod network; -#[cfg(test)] -mod testing; - -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::Context; -use clap::{Parser, Subcommand}; -use miden_client::account::component::{AuthRpoFalcon512, BasicFungibleFaucet}; -use miden_client::account::{ - Account, - AccountBuilder, - AccountFile, - AccountStorageMode, - AccountType, -}; -use miden_client::asset::TokenSymbol; -use miden_client::auth::AuthSecretKey; -use miden_client::crypto::RpoRandomCoin; -use miden_client::crypto::rpo_falcon512::SecretKey; -use miden_client::rpc::Endpoint; -use miden_client::{Felt, Word}; -use miden_client_sqlite_store::SqliteStore; -use miden_faucet_lib::types::AssetAmount; -use miden_faucet_lib::{Faucet, FaucetConfig}; -use miden_pow_rate_limiter::PoWRateLimiterConfig; -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha20Rng; -use tokio::sync::mpsc; -use tokio::task::JoinSet; -use url::Url; - -use crate::api::{ApiServer, Metadata}; -use crate::api_key::ApiKey; -use crate::frontend::serve_frontend; -use crate::logging::OpenTelemetry; -use crate::network::FaucetNetwork; - -// CONSTANTS -// ================================================================================================= - -pub const REQUESTS_QUEUE_SIZE: usize = 1000; -const COMPONENT: &str = "miden-faucet-client"; - -const ENV_API_BIND_URL: &str = "MIDEN_FAUCET_API_BIND_URL"; -const ENV_API_PUBLIC_URL: &str = "MIDEN_FAUCET_API_PUBLIC_URL"; -const ENV_FRONTEND_URL: &str = "MIDEN_FAUCET_FRONTEND_URL"; -const ENV_NETWORK: &str = "MIDEN_FAUCET_NETWORK"; -const ENV_NODE_URL: &str = "MIDEN_FAUCET_NODE_URL"; -const ENV_TIMEOUT: &str = "MIDEN_FAUCET_TIMEOUT"; -const ENV_MAX_CLAIMABLE_AMOUNT: &str = "MIDEN_FAUCET_MAX_CLAIMABLE_AMOUNT"; -const ENV_REMOTE_TX_PROVER_URL: &str = "MIDEN_FAUCET_REMOTE_TX_PROVER_URL"; -const ENV_POW_SECRET: &str = "MIDEN_FAUCET_POW_SECRET"; -const ENV_POW_CHALLENGE_LIFETIME: &str = "MIDEN_FAUCET_POW_CHALLENGE_LIFETIME"; -const ENV_POW_CLEANUP_INTERVAL: &str = "MIDEN_FAUCET_POW_CLEANUP_INTERVAL"; -const ENV_POW_GROWTH_RATE: &str = "MIDEN_FAUCET_POW_GROWTH_RATE"; -const ENV_POW_BASELINE: &str = "MIDEN_FAUCET_POW_BASELINE"; -const ENV_BASE_AMOUNT: &str = "MIDEN_FAUCET_BASE_AMOUNT"; -const ENV_API_KEYS: &str = "MIDEN_FAUCET_API_KEYS"; -const ENV_ENABLE_OTEL: &str = "MIDEN_FAUCET_ENABLE_OTEL"; -const ENV_STORE: &str = "MIDEN_FAUCET_STORE"; -const ENV_EXPLORER_URL: &str = "MIDEN_FAUCET_EXPLORER_URL"; -const ENV_BATCH_SIZE: &str = "MIDEN_FAUCET_BATCH_SIZE"; -const ENV_IMPORT_ACCOUNT_PATH: &str = "MIDEN_FAUCET_IMPORT_ACCOUNT_PATH"; -const ENV_DEPLOY: &str = "MIDEN_FAUCET_DEPLOY"; -const ENV_TOKEN_SYMBOL: &str = "MIDEN_FAUCET_TOKEN_SYMBOL"; -const ENV_DECIMALS: &str = "MIDEN_FAUCET_DECIMALS"; -const ENV_MAX_SUPPLY: &str = "MIDEN_FAUCET_MAX_SUPPLY"; - -// COMMANDS -// ================================================================================================ - -#[derive(Parser)] -#[command(version, about, long_about = None)] -pub struct Cli { - #[command(subcommand)] - pub command: Command, -} - -#[allow(clippy::large_enum_variant)] -#[derive(Subcommand)] -pub enum Command { - /// Initialize the faucet with a new or existing account. - Init { - #[clap(flatten)] - config: ClientConfig, - - /// Symbol of the new token. - #[arg( - short, - long, - value_name = "STRING", - required_unless_present = "import_account_path", - env = ENV_TOKEN_SYMBOL - )] - token_symbol: Option, - - /// Decimals of the new token. - #[arg(short, long, value_name = "U8", required_unless_present = "import_account_path", env = ENV_DECIMALS)] - decimals: Option, - - /// Max supply of the new token (in base units). - #[arg(short, long, value_name = "U64", required_unless_present = "import_account_path", env = ENV_MAX_SUPPLY)] - max_supply: Option, - - /// Set an existing faucet account file to use, instead of creating a new account. - #[arg(long = "import", value_name = "FILE", conflicts_with_all = ["token_symbol", "decimals", "max_supply"], env = ENV_IMPORT_ACCOUNT_PATH)] - import_account_path: Option, - - /// Whether to deploy the faucet account to the node. - #[arg(long, value_name = "BOOL", default_value_t = false, env = ENV_DEPLOY)] - deploy: bool, - }, - - /// Generate an API key that can be used by the faucet. - /// - /// Prints out the generated API key to stdout. Keys can then be supplied to the faucet via the - /// `--api-keys` flag or `MIDEN_FAUCET_API_KEYS` env var of the `start` command. - CreateApiKey, - - /// Start the faucet server - Start { - #[clap(flatten)] - config: ClientConfig, - - /// URL to bind the API server. - #[arg(long = "api-bind-url", value_name = "URL", env = ENV_API_BIND_URL)] - api_bind_url: Url, - - /// Public URL to access the API server. If not set, the bind url will be used. - #[arg(long = "api-public-url", value_name = "URL", env = ENV_API_PUBLIC_URL)] - api_public_url: Option, - - /// URL to bind the frontend server. If not set, the frontend will not be served. - #[arg(long = "frontend-url", value_name = "URL", env = ENV_FRONTEND_URL)] - frontend_url: Option, - - /// The maximum amount of assets' base units that can be dispersed on each request. - #[arg(long = "max-claimable-amount", value_name = "U64", env = ENV_MAX_CLAIMABLE_AMOUNT, default_value = "1000000000")] - max_claimable_amount: u64, - - /// The secret to be used by the server to sign the `PoW` challenges. This should NOT be - /// shared. - #[arg(long = "pow-secret", value_name = "STRING", default_value = "", env = ENV_POW_SECRET)] - pow_secret: String, - - /// The duration during which the `PoW` challenges are valid. Changing this will affect the - /// rate limiting, since it works by rejecting new submissions while the previous submitted - /// challenge is still valid. - #[arg(long = "pow-challenge-lifetime", value_name = "DURATION", env = ENV_POW_CHALLENGE_LIFETIME, default_value = "30s", value_parser = humantime::parse_duration)] - pow_challenge_lifetime: Duration, - - /// Defines how quickly the `PoW` difficulty grows with the number of requests. The number - /// of active challenges gets multiplied by the growth rate to compute the load - /// difficulty. - /// - /// Meaning, the difficulty bits of the challenge will increase approximately by - /// `log2(growth_rate * num_active_challenges)`. - #[arg(long = "pow-growth-rate", value_name = "F64", env = ENV_POW_GROWTH_RATE, default_value = "0.1")] - pow_growth_rate: f64, - - /// The interval at which the `PoW` challenge cache is cleaned up. - #[arg(long = "pow-cleanup-interval", value_name = "DURATION", env = ENV_POW_CLEANUP_INTERVAL, default_value = "2s", value_parser = humantime::parse_duration)] - pow_cleanup_interval: Duration, - - /// The baseline for the `PoW` challenges. This sets the `PoW` difficulty (in bits) that a - /// a challenge will have when there are no requests against the faucet. It must be between - /// 0 and 32. - #[arg(value_parser = clap::value_parser!(u8).range(0..=32))] - #[arg(long = "pow-baseline", value_name = "U8", env = ENV_POW_BASELINE, default_value = "16")] - pow_baseline: u8, - - /// The baseline amount for token requests (in base units). Requests for greater amounts - /// would require higher level of `PoW`. - /// - /// The request complexity for challenges is computed as: `request_complexity = (amount / - /// base_amount) + 1` - #[arg(long = "base-amount", value_name = "U64", env = ENV_BASE_AMOUNT, default_value = "100000000")] - base_amount: u64, - - /// Comma-separated list of API keys. - #[arg(long = "api-keys", value_name = "STRING", env = ENV_API_KEYS, num_args = 1.., value_delimiter = ',')] - api_keys: Vec, - - /// Enables the exporting of traces for OpenTelemetry. - /// - /// This can be further configured using environment variables as defined in the official - /// OpenTelemetry documentation. See our operator manual for further details. - #[arg(long = "enable-otel", value_name = "BOOL", default_value_t = false, env = ENV_ENABLE_OTEL)] - open_telemetry: bool, - - /// Explorer URL. - #[arg(long = "explorer-url", value_name = "URL", env = ENV_EXPLORER_URL)] - explorer_url: Option, - - /// The maximum number of requests to process in each batch. Each batch is processed in a - /// single transaction. - #[arg(long = "batch-size", value_name = "USIZE", default_value = "32", env = ENV_BATCH_SIZE)] - batch_size: usize, - }, -} - -/// Configuration for the faucet client. -#[derive(Parser, Debug, Clone)] -pub struct ClientConfig { - /// Path to the `SQLite` store. - #[arg(long = "store", value_name = "FILE", default_value = "faucet_client_store.sqlite3", env = ENV_STORE)] - store_path: PathBuf, - - /// Timeout for attempting to connect to the node. - #[arg(long = "timeout", value_name = "DURATION", default_value = "5s", env = ENV_TIMEOUT, value_parser = humantime::parse_duration)] - timeout: Duration, - - /// Network configuration to use. Options are `devnet`, `testnet`, `localhost` or a custom - /// network. It is used to display the correct bech32 addresses in the UI. - #[arg(long = "network", value_name = "NETWORK", default_value = "localhost", env = ENV_NETWORK)] - network: FaucetNetwork, - - /// Endpoint of the remote transaction prover in the format `://[:]`. - #[arg(long = "remote-tx-prover-url", value_name = "URL", env = ENV_REMOTE_TX_PROVER_URL)] - remote_tx_prover_url: Option, - - /// Node RPC gRPC endpoint in the format `http://[:]`. If not set, the url is derived - /// from the specified network. - #[arg(long = "node-url", value_name = "URL", env = ENV_NODE_URL)] - node_url: Option, -} - -impl Command { - fn open_telemetry(&self) -> OpenTelemetry { - if match *self { - Command::Start { open_telemetry, .. } => open_telemetry, - _ => false, - } { - OpenTelemetry::Enabled - } else { - OpenTelemetry::Disabled - } - } -} - -// MAIN -// ================================================================================================= - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); - - // Configure tracing with optional OpenTelemetry exporting support. - let _otel_guard = logging::setup_tracing(cli.command.open_telemetry()) - .context("failed to initialize logging")?; - - Box::pin(run_faucet_command(cli)).await -} - -#[allow(clippy::too_many_lines)] -async fn run_faucet_command(cli: Cli) -> anyhow::Result<()> { - // Note: open-telemetry is handled in main. - match cli.command { - Command::Init { - config: - ClientConfig { - node_url, - timeout, - remote_tx_prover_url, - network, - store_path, - }, - token_symbol, - decimals, - max_supply, - import_account_path, - deploy, - } => { - let (account, secret) = if let Some(account_path) = import_account_path { - // Import existing faucet account - let account_data = AccountFile::read(account_path) - .context("failed to read account data from file")?; - let secret = account_data - .auth_secret_keys - .first() - .context("auth secret key is required")? - .clone(); - (account_data.account, secret) - } else { - println!("Generating new faucet account. This may take a few seconds..."); - let token_symbol = - token_symbol.expect("token_symbol should be present when not importing"); - let decimals = decimals.expect("decimals should be present when not importing"); - let max_supply = - max_supply.expect("max_supply should be present when not importing"); - create_faucet_account(token_symbol.as_str(), max_supply, decimals)? - }; - let node_endpoint = parse_node_endpoint(node_url, &network)?; - let faucet_config = FaucetConfig { - store_path, - node_endpoint, - network_id: network.to_network_id()?, - timeout, - remote_tx_prover_url, - }; - Box::pin(Faucet::init(&faucet_config, account, &secret, deploy)) - .await - .context("failed to initialize faucet")?; - - println!("Faucet account successfully initialized"); - }, - - Command::CreateApiKey => { - let mut rng = ChaCha20Rng::from_seed(rand::random()); - let key = ApiKey::generate(&mut rng).encode(); - println!("{key}"); - }, - - Command::Start { - config: - ClientConfig { - node_url, - timeout, - remote_tx_prover_url, - network, - store_path, - }, - api_bind_url, - api_public_url, - frontend_url, - max_claimable_amount, - pow_secret, - pow_challenge_lifetime, - pow_cleanup_interval, - pow_growth_rate, - pow_baseline, - base_amount, - api_keys, - open_telemetry: _, - explorer_url, - batch_size, - } => { - let node_endpoint = parse_node_endpoint(node_url, &network)?; - let config = FaucetConfig { - store_path: store_path.clone(), - node_endpoint: node_endpoint.clone(), - network_id: network.to_network_id()?, - timeout, - remote_tx_prover_url, - }; - let faucet = Faucet::load(&config).await.context("failed to load faucet")?; - - let store = - Arc::new(SqliteStore::new(store_path).await.context("failed to create store")?); - - // Maximum of 1000 requests in-queue at once. Overflow is rejected for faster feedback. - let (tx_mint_requests, rx_mint_requests) = mpsc::channel(REQUESTS_QUEUE_SIZE); - - let api_keys = api_keys - .iter() - .map(|k| ApiKey::decode(k)) - .collect::, _>>() - .context("failed to decode API keys")?; - let max_claimable_amount = AssetAmount::new(max_claimable_amount)?; - let rate_limiter_config = PoWRateLimiterConfig { - challenge_lifetime: pow_challenge_lifetime, - cleanup_interval: pow_cleanup_interval, - growth_rate: pow_growth_rate, - baseline: pow_baseline, - }; - let faucet_account = faucet.faucet_account().await?; - let faucet_component = BasicFungibleFaucet::try_from(&faucet_account)?; - let max_supply = AssetAmount::new(faucet_component.max_supply().as_int())?; - let decimals = faucet_component.decimals(); - - let metadata = Metadata { - id: faucet.faucet_id(), - issuance: faucet.issuance(), - max_supply, - decimals, - explorer_url, - base_amount, - }; - - // We keep a channel sender open in the main thread to avoid the faucet closing before - // servers can propagate any errors. - let tx_mint_requests_clone = tx_mint_requests.clone(); - let api_server = ApiServer::new( - metadata, - max_claimable_amount, - tx_mint_requests_clone, - pow_secret.as_str(), - rate_limiter_config, - &api_keys, - store, - ); - - // Use select to concurrently: - // - Run and wait for the faucet (on current thread) - // - Run and wait for API server (in a spawned task) - // - Run and wait for frontend server (in a spawned task, only if set) - let faucet_future = faucet.run(rx_mint_requests, batch_size); - - let mut tasks = JoinSet::new(); - let mut tasks_ids = HashMap::new(); - - let api_id = tasks.spawn(api_server.serve(api_bind_url.clone())).id(); - tasks_ids.insert(api_id, "api"); - - if let Some(frontend_url) = frontend_url { - let frontend_id = tasks - .spawn(serve_frontend( - frontend_url, - api_public_url.unwrap_or(api_bind_url), - node_endpoint.to_string(), - )) - .id(); - tasks_ids.insert(frontend_id, "frontend"); - } - - tokio::select! { - serve_result = tasks.join_next_with_id() => { - let (id, err) = match serve_result.unwrap() { - Ok((id, Ok(_))) => (id, Err(anyhow::anyhow!("completed unexpectedly"))), - Ok((id, Err(err))) => (id, Err(err)), - Err(join_err) => (join_err.id(), Err(join_err).context("failed to join task")), - }; - let component = tasks_ids.get(&id).unwrap_or(&"unknown"); - err.context(format!("{component} server failed")) - }, - faucet_result = faucet_future => { - // Faucet completed, return its result - faucet_result.context("faucet failed") - }, - }?; - }, - } - - Ok(()) -} - -// UTILITIES -// ================================================================================================= - -/// Parses the node endpoint from the cli arguments. If an explicit url is provided, it is used. -/// Otherwise, it is derived from the specified network. -fn parse_node_endpoint(node_url: Option, network: &FaucetNetwork) -> anyhow::Result { - let url = if let Some(node_url) = node_url { - node_url.to_string() - } else { - network - .to_rpc_endpoint() - .context("no node url provided for the custom network")? - }; - - Endpoint::try_from(url.as_str()) - .map_err(anyhow::Error::msg) - .with_context(|| format!("failed to parse node url: {url}")) -} - -/// Creates a new faucet account from the given parameters. -fn create_faucet_account( - token_symbol: &str, - max_supply: u64, - decimals: u8, -) -> anyhow::Result<(Account, AuthSecretKey)> { - let mut rng = ChaCha20Rng::from_seed(rand::random()); - let secret = { - let auth_seed: [u64; 4] = rng.random(); - let rng_seed = Word::from(auth_seed.map(Felt::new)); - SecretKey::with_rng(&mut RpoRandomCoin::new(rng_seed)) - }; - - let symbol = TokenSymbol::try_from(token_symbol).context("failed to parse token symbol")?; - let max_supply = Felt::try_from(max_supply) - .map_err(anyhow::Error::msg) - .context("max supply value is greater than or equal to the field modulus")?; - let auth_component = AuthRpoFalcon512::new(secret.public_key().to_commitment().into()); - - let account = AccountBuilder::new(rng.random()) - .account_type(AccountType::FungibleFaucet) - .storage_mode(AccountStorageMode::Public) - .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?) - .with_auth_component(auth_component) - .build() - .context("failed to create basic fungible faucet account")?; - - Ok((account, AuthSecretKey::RpoFalcon512(secret))) -} - -// INTEGRATION TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use std::env::temp_dir; - use std::process::Stdio; - use std::str::FromStr; - use std::time::{Duration, Instant}; - - use clap::Parser; - use fantoccini::ClientBuilder; - use miden_client::account::{AccountFile, AccountId, Address, NetworkId}; - use serde_json::{Map, json}; - use tokio::io::AsyncBufReadExt; - use tokio::net::TcpListener; - use tokio::time::sleep; - use url::Url; - use uuid::Uuid; - - use crate::network::FaucetNetwork; - use crate::testing::stub_rpc_api::serve_stub; - use crate::{Cli, ClientConfig, create_faucet_account, run_faucet_command}; - - // CLI TESTS - // --------------------------------------------------------------------------------------------- - - #[tokio::test] - async fn init_with_new_token() { - let stub_node_url = run_stub_node().await; - let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); - let result = Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet-client", - "init", - "--token-symbol", - "TEST", - "--decimals", - "6", - "--max-supply", - "100000000000000000", - "--node-url", - stub_node_url.to_string().as_str(), - "--store", - store_path.to_str().unwrap(), - ]))) - .await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn init_importing_account_file() { - let stub_node_url = run_stub_node().await; - let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); - let account_path = temp_dir().join("test_account.mac"); - let (account, secret) = create_faucet_account("TEST", 100_000_000, 3).unwrap(); - let account_data = AccountFile::new(account, vec![secret]); - account_data.write(&account_path).unwrap(); - - let result = Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet-client", - "init", - "--import", - account_path.to_str().unwrap(), - "--node-url", - stub_node_url.to_string().as_str(), - "--store", - store_path.to_str().unwrap(), - ]))) - .await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn init_with_deploy() { - let stub_node_url = run_stub_node().await; - let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); - let result = Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet-client", - "init", - "--token-symbol", - "TEST", - "--decimals", - "6", - "--max-supply", - "100000000000000000", - "--node-url", - stub_node_url.to_string().as_str(), - "--store", - store_path.to_str().unwrap(), - "--deploy", - ]))) - .await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn serve_fails_without_init() { - let stub_node_url = run_stub_node().await; - let store_path = temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())); - - let result = Box::pin(run_faucet_command(Cli::parse_from([ - "miden-faucet-client", - "start", - "--api-bind-url", - "http://0.0.0.0:8000", - "--frontend-url", - "http://0.0.0.0:8081", - "--node-url", - stub_node_url.to_string().as_str(), - "--store", - store_path.to_str().unwrap(), - ]))) - .await; - assert!(result.is_err()); - } - - // INTEGRATION TEST - // --------------------------------------------------------------------------------------------- - - /// 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, mints tokens, and checks that all the - /// requests returned status 200. - #[tokio::test] - async fn frontend_mint_tokens() { - let stub_node_url = run_stub_node().await; - let website_url = run_faucet_server(stub_node_url).await; - let client = start_fantoccini_client().await; - - // Open the website - client.goto(website_url.as_str()).await.unwrap(); - - let title = client.title().await.unwrap(); - assert_eq!(title, "Miden Faucet"); - - let network_id = NetworkId::Testnet; - let account_id = AccountId::try_from(0).unwrap(); - let address = Address::new(account_id); - let address_bech32 = address.encode(network_id); - - // Fill in the account address - client - .find(fantoccini::Locator::Css("#recipient-address")) - .await - .unwrap() - .send_keys(&address_bech32) - .await - .unwrap(); - - // Select the first asset amount option - client - .find(fantoccini::Locator::Css("#token-amount")) - .await - .unwrap() - .click() - .await - .unwrap(); - client - .find(fantoccini::Locator::Css("#token-amount option")) - .await - .unwrap() - .click() - .await - .unwrap(); - - // Click the public note button - client - .find(fantoccini::Locator::Css("#send-public-button")) - .await - .unwrap() - .click() - .await - .unwrap(); - - // 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(); - - // Verify all requests are successful - assert!(failed_requests.as_array().unwrap().is_empty()); - - client.close().await.unwrap(); - } - - // TESTING HELPERS - // --------------------------------------------------------------------------------------------- - - pub async fn run_stub_node() -> Url { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let listener_addr = listener.local_addr().unwrap(); - let stub_node_url = Url::from_str(&format!("http://{listener_addr}")).unwrap(); - tokio::spawn({ - let stub_node_url = stub_node_url.clone(); - async move { serve_stub(&stub_node_url).await.unwrap() } - }); - stub_node_url - } - - async fn run_faucet_server(stub_node_url: Url) -> String { - let config = ClientConfig { - node_url: Some(stub_node_url.clone()), - timeout: Duration::from_millis(5000), - network: FaucetNetwork::Localhost, - store_path: temp_dir().join(format!("{}.sqlite3", Uuid::new_v4())), - remote_tx_prover_url: None, - }; - - Box::pin(run_faucet_command(Cli { - command: crate::Command::Init { - config: config.clone(), - token_symbol: Some("TEST".to_owned()), - decimals: Some(6), - max_supply: Some(1_000_000_000_000), - import_account_path: None, - deploy: false, - }, - })) - .await - .expect("failed to create faucet account"); - - let api_url = "http://localhost:8000"; - let frontend_url = "http://localhost:8080"; - - // Use std::thread to launch faucet - avoids Send requirements - std::thread::spawn(move || { - // Create a new runtime for this thread - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Failed to build runtime"); - - // Run the faucet on this thread's runtime - rt.block_on(async { - Box::pin(run_faucet_command(Cli { - command: crate::Command::Start { - config, - api_bind_url: Url::try_from(api_url).unwrap(), - api_public_url: None, - frontend_url: Some(Url::parse(frontend_url).unwrap()), - max_claimable_amount: 1_000_000_000, - api_keys: vec![], - pow_secret: "test".to_string(), - pow_challenge_lifetime: Duration::from_secs(30), - pow_cleanup_interval: Duration::from_secs(1), - pow_growth_rate: 1.0, - pow_baseline: 12, - base_amount: 100_000, - open_telemetry: false, - explorer_url: None, - batch_size: 8, - }, - })) - .await - .expect("failed to start faucet"); - }); - }); - - // Wait for faucet to be up - let api_url = Url::parse(api_url).unwrap(); - let addrs = api_url.socket_addrs(|| None).unwrap(); - let start = Instant::now(); - let timeout = Duration::from_secs(10); - loop { - match tokio::net::TcpStream::connect(&addrs[..]).await { - Ok(_) => break, - Err(_) if start.elapsed() < timeout => { - sleep(Duration::from_millis(200)).await; - }, - Err(e) => panic!("faucet never became reachable: {e}"), - } - } - - frontend_url.to_string() - } - - async fn start_fantoccini_client() -> fantoccini::Client { - // Start chromedriver. This requires having chromedriver and chrome installed - let chromedriver_port = "57708"; - let mut chromedriver = tokio::process::Command::new("chromedriver") - .arg(format!("--port={chromedriver_port}")) - .stdout(Stdio::piped()) - .kill_on_drop(true) - .spawn() - .expect("failed to start chromedriver"); - let stdout = chromedriver.stdout.take().unwrap(); - tokio::spawn( - async move { chromedriver.wait().await.expect("chromedriver process failed") }, - ); - // Wait for chromedriver to be running - let mut reader = tokio::io::BufReader::new(stdout).lines(); - while let Some(line) = reader.next_line().await.unwrap() { - if line.contains("ChromeDriver was started successfully") { - break; - } - } - - // Start fantoccini 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") - } -} diff --git a/docs/src/getting-started/cli.md b/docs/src/getting-started/cli.md index 61b3e4ec..9fe2b80b 100644 --- a/docs/src/getting-started/cli.md +++ b/docs/src/getting-started/cli.md @@ -23,7 +23,7 @@ The Miden Faucet can be configured using: ### Basic Configuration ```bash -miden-faucet-client init \ +miden-faucet-operator init \ --token-symbol \ --decimals \ --max-supply \ @@ -32,8 +32,8 @@ miden-faucet-client init \ ``` ```bash -miden-faucet-client start \ - --api-bind-url \ +miden-faucet-operator start \ + --api-url \ --frontend-url \ --node-url \ --network @@ -64,7 +64,7 @@ miden-faucet-client start \ | Option | Description | Default | Required | |--------|-------------|---------|----------| -| `--api-bind-url` | URL to serve the faucet API | - | Yes | +| `--api-url` | URL to serve the faucet API | - | Yes | | `--api-public-url` | Public URL to access the faucet API. If not set, the bind url will be used. | - | No | | `--frontend-url` | URL to serve the Frontend API | - | No | | `--node-url` | Miden node RPC endpoint. If not set, it will be derived from the network | - | No | @@ -177,7 +177,7 @@ export MIDEN_FAUCET_API_KEYS=key1,key2,key3 ### Generate API Keys ```bash -miden-faucet-client create-api-key +miden-faucet-operator create-api-key ``` This generates an API key that can be used for authentication. It is printed to stdout. @@ -210,24 +210,24 @@ Enable OpenTelemetry for production monitoring: ## Configuration Example ```bash -miden-faucet-client init \ +miden-faucet-operator init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --node-url http://localhost:57291 -miden-faucet-client start \ +miden-faucet-operator start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --node-url http://localhost:57291 \ --network localhost ``` -For detailed options, run `miden-faucet-client [COMMAND] --help`. The legacy alias `miden-faucet` is still available for backwards compatibility. +For detailed options, run `miden-faucet-operator [COMMAND] --help`. The legacy alias `miden-faucet` is still available for backwards compatibility. -To request tokens from a remote faucet, use the separate operator CLI: +To request tokens from a remote faucet, use the client mint CLI: ```bash -miden-faucet-operator mint --url --account --amount +miden-faucet-client mint --url --account --amount ``` To see available options: diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index e6f1f502..c5cb767a 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -66,7 +66,7 @@ cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-fauce cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --rev cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --rev -> The legacy `miden-faucet` binary name is still available as an alias for backwards compatibility. +> Use `miden-faucet-operator` to initialize/start the faucet service, and `miden-faucet-client` to mint from a running faucet. The legacy `miden-faucet` binary name is still available as an alias for the operator. ``` More information on the various `cargo install` options can be found diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index 2b92c911..21b2e630 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -12,7 +12,7 @@ Get the Miden Faucet running in minutes. First, we need to initialize the faucet with a new account that will hold the tokens to be distributed. This command generates a new account with the specified token configuration and saves the account data to a local SQLite store. The account is not yet deployed to the network - that will happen when the faucet is running and the first transaction is sent to the node. ```bash -miden-faucet-client init \ +miden-faucet-operator init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ @@ -24,7 +24,7 @@ miden-faucet-client init \ Next, start the faucet by specifying the addresses where the API and the frontend will be served, the address of the Miden node, and the network configuration. The API server will handle incoming token requests and manage the minting process. ```bash -miden-faucet-client start \ +miden-faucet-operator start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --node-url https://rpc.testnet.miden.io \ @@ -36,12 +36,12 @@ miden-faucet-client start \ Once the faucet is running, you can request test tokens through either the web interface, the operator CLI, or the REST API. -### Via Operator CLI +### Via Client CLI Use the dedicated mint binary: ```bash -miden-faucet-operator mint \ +miden-faucet-client mint \ --url http://localhost:8000 \ --account \ --amount 1000 @@ -71,12 +71,12 @@ You can also programmatically interact with the REST API to mint tokens. Check o If you have a Miden Node running locally, you can run the faucet against that node. ```bash -miden-faucet-client init \ +miden-faucet-operator init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 -miden-faucet-client start \ +miden-faucet-operator start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 ``` @@ -86,13 +86,13 @@ miden-faucet-client start \ Connect to the node deployed in Miden Devnet. ```bash -miden-faucet-client init \ +miden-faucet-operator init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --network devnet -miden-faucet-client start \ +miden-faucet-operator start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --network devnet @@ -103,13 +103,13 @@ miden-faucet-client start \ Connect to the node deployed in Miden Testnet. ```bash -miden-faucet-client init \ +miden-faucet-operator init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --network testnet -miden-faucet-client start \ +miden-faucet-operator start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --explorer-url https://testnet.midenscan.com \ @@ -125,5 +125,3 @@ miden-faucet start \ --api-bind-url http://localhost:8000 \ --network testnet ``` - -If you need to mint from a remote faucet instance, use `miden-faucet-operator mint ...`. The legacy `miden-faucet` alias still works for the client commands. diff --git a/packaging/faucet/miden-faucet.service b/packaging/faucet/miden-faucet.service index 8ebef46c..260302d9 100644 --- a/packaging/faucet/miden-faucet.service +++ b/packaging/faucet/miden-faucet.service @@ -7,9 +7,9 @@ WantedBy=multi-user.target [Service] Type=exec -Environment="OTEL_SERVICE_NAME=miden-faucet-client" +Environment="OTEL_SERVICE_NAME=miden-faucet-operator" EnvironmentFile=/lib/systemd/system/miden-faucet.env -ExecStart=/usr/bin/miden-faucet-client start +ExecStart=/usr/bin/miden-faucet-operator start WorkingDirectory=/opt/miden-faucet User=miden-faucet RestartSec=5 From 5324f7c0eb803fac7825fa739324c19df076f21b Mon Sep 17 00:00:00 2001 From: keinberger Date: Mon, 15 Dec 2025 13:29:43 +0200 Subject: [PATCH 07/23] chore(CHANGELOG): amend statement fix(Makefile): install faucet instructions target correct binary docs(README): added explainer for binary split chore: removed redundant readme key in bin/faucet-client/Cargo.toml chore: amend description satement in bin/faucet-operator/Cargo.toml --- CHANGELOG.md | 2 +- Makefile | 2 +- README.md | 10 ++++++++-- bin/faucet-client/Cargo.toml | 1 - bin/faucet-operator/Cargo.toml | 4 ++-- packaging/faucet/miden-faucet.service | 16 ---------------- 6 files changed, 12 insertions(+), 23 deletions(-) delete mode 100644 packaging/faucet/miden-faucet.service diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d917df5..b794d850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.12.5 (TBD) -- Renamed the faucet CLI to `miden-faucet-client` and kept a `miden-faucet` alias for compatibility; added a new `miden-faucet-operator` binary with the `mint` command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). +- Renamed the faucet CLI to `miden-faucet-operator`, added a new `miden-faucet-client` binary with the `mint` command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). ## 0.12.4 (2025-12-04) diff --git a/Makefile b/Makefile index 11f60061..c24855a7 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,7 @@ check: ## Check all targets and features for errors without code generation .PHONY: install-faucet install-faucet: ## Installs faucet - ${BUILD_PROTO} cargo install --path bin/faucet --locked + ${BUILD_PROTO} cargo install --path bin/faucet-client --locked ${BUILD_PROTO} cargo install --path bin/faucet-operator --locked .PHONY: check-tools diff --git a/README.md b/README.md index 0da6b751..1c8d33dd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,11 @@ For comprehensive guides, API reference, and examples, see the [Miden Faucet Doc ## Running the faucet -1. Install the faucet binaries: +The faucet comes with two CLI tools: +- **miden-faucet-operator**: Runs the faucet, used for initializing and starting the faucet. +- **miden-faucet-client**: Used for interacting with a live faucet, i.e. for requesting tokens from a running faucet. + +1. Install both faucet binaries: ```bash make install-faucet ``` @@ -36,7 +40,9 @@ miden-faucet-operator start \ --network testnet ``` -Requesting tokens from a faucet endpoint is handled by the separate `miden-faucet-client` binary: +## Requesting tokens from a live faucet + +You can use the `miden-faucet-client` binary to request tokens from any running faucet instance, whether it's your local faucet or the remote testnet faucet: ```bash miden-faucet-client mint --url --account --amount ``` diff --git a/bin/faucet-client/Cargo.toml b/bin/faucet-client/Cargo.toml index de137887..3ee0aa33 100644 --- a/bin/faucet-client/Cargo.toml +++ b/bin/faucet-client/Cargo.toml @@ -5,7 +5,6 @@ edition.workspace = true homepage.workspace = true license.workspace = true name = "miden-faucet-client" -readme = "README.md" repository.workspace = true rust-version.workspace = true version.workspace = true diff --git a/bin/faucet-operator/Cargo.toml b/bin/faucet-operator/Cargo.toml index 94951a7f..5174e829 100644 --- a/bin/faucet-operator/Cargo.toml +++ b/bin/faucet-operator/Cargo.toml @@ -1,9 +1,9 @@ [package] authors.workspace = true -description = "Token faucet operator (init/start) for Miden faucet" +description = "Token faucet operator for Miden faucet" edition.workspace = true homepage.workspace = true -keywords = ["faucet", "miden", "node", "operator"] +keywords = ["faucet", "miden", "operator"] license.workspace = true name = "miden-faucet-operator" readme = "README.md" diff --git a/packaging/faucet/miden-faucet.service b/packaging/faucet/miden-faucet.service deleted file mode 100644 index 260302d9..00000000 --- a/packaging/faucet/miden-faucet.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Miden faucet -Wants=network-online.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=exec -Environment="OTEL_SERVICE_NAME=miden-faucet-operator" -EnvironmentFile=/lib/systemd/system/miden-faucet.env -ExecStart=/usr/bin/miden-faucet-operator start -WorkingDirectory=/opt/miden-faucet -User=miden-faucet -RestartSec=5 -Restart=always From 8b0a2c4710545efbe4ec7f0b4361d03da2bba142 Mon Sep 17 00:00:00 2001 From: keinberger Date: Mon, 15 Dec 2025 13:29:55 +0200 Subject: [PATCH 08/23] chore: rename miden-faucet.service file --- packaging/faucet/miden-faucet-operator.service | 16 ++++++++++++++++ packaging/faucet/postinst | 16 ++++++++-------- packaging/faucet/postrm | 8 ++++---- 3 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 packaging/faucet/miden-faucet-operator.service diff --git a/packaging/faucet/miden-faucet-operator.service b/packaging/faucet/miden-faucet-operator.service new file mode 100644 index 00000000..8e845075 --- /dev/null +++ b/packaging/faucet/miden-faucet-operator.service @@ -0,0 +1,16 @@ +[Unit] +Description=Miden faucet +Wants=network-online.target + +[Install] +WantedBy=multi-user.target + +[Service] +Type=exec +Environment="OTEL_SERVICE_NAME=miden-faucet-operator" +EnvironmentFile=/lib/systemd/system/miden-faucet-operator.env +ExecStart=/usr/bin/miden-faucet-operator start +WorkingDirectory=/opt/miden-faucet-operator +User=miden-faucet-operator +RestartSec=5 +Restart=always diff --git a/packaging/faucet/postinst b/packaging/faucet/postinst index c7c4b8b8..a88a0864 100644 --- a/packaging/faucet/postinst +++ b/packaging/faucet/postinst @@ -6,21 +6,21 @@ sudo adduser --disabled-password --disabled-login --shell /usr/sbin/nologin --quiet --system --no-create-home --home /nonexistent miden-faucet # Working folder. -if [ -d "/opt/miden-faucet" ] +if [ -d "/opt/miden-faucet-operator" ] then - echo "Directory /opt/miden-faucet exists." + echo "Directory /opt/miden-faucet-operator exists." else - mkdir -p /opt/miden-faucet + mkdir -p /opt/miden-faucet-operator fi -sudo chown -R miden-faucet /opt/miden-faucet +sudo chown -R miden-faucet-operator /opt/miden-faucet-operator # Configuration folder -if [ -d "/etc/opt/miden-faucet" ] +if [ -d "/etc/opt/miden-faucet-operator" ] then - echo "Directory /etc/opt/miden-faucet exists." + echo "Directory /etc/opt/miden-faucet-operator exists." else - mkdir -p /etc/opt/miden-faucet + mkdir -p /etc/opt/miden-faucet-operator fi -sudo chown -R miden-faucet /etc/opt/miden-faucet +sudo chown -R miden-faucet-operator /etc/opt/miden-faucet-operator sudo systemctl daemon-reload diff --git a/packaging/faucet/postrm b/packaging/faucet/postrm index 646b5144..c110d1b5 100644 --- a/packaging/faucet/postrm +++ b/packaging/faucet/postrm @@ -1,9 +1,9 @@ #!/bin/bash # ############### -# Remove miden-faucet installs +# Remove miden-faucet-operator installs ############## -sudo rm -rf /lib/systemd/system/miden-faucet.service -sudo rm -rf /etc/opt/miden-faucet -sudo deluser miden-faucet +sudo rm -rf /lib/systemd/system/miden-faucet-operator.service +sudo rm -rf /etc/opt/miden-faucet-operator +sudo deluser miden-faucet-operator sudo systemctl daemon-reload From 735310e646f4df0ce4426aa9f8c7add496bd181e Mon Sep 17 00:00:00 2001 From: keinberger Date: Mon, 15 Dec 2025 13:31:27 +0200 Subject: [PATCH 09/23] fix(docs): correct naming for miden-faucet binaries --- docs/src/getting-started/cli.md | 11 +++++++++-- docs/src/getting-started/installation.md | 10 +++++----- docs/src/getting-started/quick-start.md | 7 ++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/src/getting-started/cli.md b/docs/src/getting-started/cli.md index 9fe2b80b..10ac6c1e 100644 --- a/docs/src/getting-started/cli.md +++ b/docs/src/getting-started/cli.md @@ -2,6 +2,11 @@ This guide shows the available commands and their configuration options to run with the Miden Faucet CLI. +The faucet comes with two CLI tools: + +- **miden-faucet-operator**: Runs the faucet, used for initializing and starting the faucet. +- **miden-faucet-client**: Used for interacting with a live faucet, i.e. for requesting tokens from a running faucet. + ## Available Commands | Command | Description | @@ -225,12 +230,14 @@ miden-faucet-operator start \ For detailed options, run `miden-faucet-operator [COMMAND] --help`. The legacy alias `miden-faucet` is still available for backwards compatibility. -To request tokens from a remote faucet, use the client mint CLI: +## Requesting tokens from a live faucet + +You can use the `miden-faucet-client` binary to request tokens from any running faucet instance, whether it's your local faucet or the remote testnet faucet: ```bash miden-faucet-client mint --url --account --amount ``` To see available options: ```bash -miden-faucet-operator mint --help +miden-faucet-client mint --help ``` diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index c5cb767a..a5691752 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -38,15 +38,15 @@ sudo apt install llvm clang bindgen pkg-config libssl-dev libsqlite3-dev Install the latest faucet binary: ```sh -cargo install miden-faucet-client --locked cargo install miden-faucet-operator --locked +cargo install miden-faucet-client --locked ``` This will install the latest official version of the faucet. You can install a specific version `x.y.z` using ```sh -cargo install miden-faucet-client --locked --version x.y.z cargo install miden-faucet-operator --locked --version x.y.z +cargo install miden-faucet-client --locked --version x.y.z ``` You can also use `cargo` to compile the node from the source code if for some reason you need a specific git revision. @@ -55,16 +55,16 @@ this for advanced use only. The incantation is a little different as you'll be t ```sh # Install from a specific branch -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --branch cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --branch +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --branch # Install a specific tag -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --tag cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --tag +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --tag # Install a specific git revision -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --rev cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --rev +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --rev > Use `miden-faucet-operator` to initialize/start the faucet service, and `miden-faucet-client` to mint from a running faucet. The legacy `miden-faucet` binary name is still available as an alias for the operator. ``` diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index 21b2e630..17fbe913 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -34,11 +34,11 @@ miden-faucet-operator start \ ## Step 3: Request Test Tokens -Once the faucet is running, you can request test tokens through either the web interface, the operator CLI, or the REST API. +Once the faucet is running, you can request test tokens through either the web interface, the client CLI, or the REST API. ### Via Client CLI -Use the dedicated mint binary: +Use the dedicated mint command: ```bash miden-faucet-client mint \ @@ -61,6 +61,7 @@ Open `http://localhost:8080` in your browser to access the web interface for gen ### Via API You can also programmatically interact with the REST API to mint tokens. Check out the complete working examples below. Make sure the faucet REST API is running at `http://localhost:8000` before using them. + - [Rust](../examples/rust/request_tokens.rs) - [TypeScript](../examples/typescript/request_tokens.ts) @@ -114,7 +115,7 @@ miden-faucet-operator start \ --api-bind-url http://localhost:8000 \ --explorer-url https://testnet.midenscan.com \ --network testnet -``` +``` ### Faucet API Only (No Frontend) From 9d81d0df1c0e51d16a4ed36b5197b276fb9dd7f3 Mon Sep 17 00:00:00 2001 From: keinberger Date: Tue, 16 Dec 2025 09:23:58 +0200 Subject: [PATCH 10/23] docs: use correct api bind flag docs: Clarify mint command technical accuracy --- docs/src/getting-started/cli.md | 6 ++++-- docs/src/getting-started/quick-start.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/getting-started/cli.md b/docs/src/getting-started/cli.md index 10ac6c1e..aa0d8a2a 100644 --- a/docs/src/getting-started/cli.md +++ b/docs/src/getting-started/cli.md @@ -38,7 +38,7 @@ miden-faucet-operator init \ ```bash miden-faucet-operator start \ - --api-url \ + --api-bind-url \ --frontend-url \ --node-url \ --network @@ -69,7 +69,7 @@ miden-faucet-operator start \ | Option | Description | Default | Required | |--------|-------------|---------|----------| -| `--api-url` | URL to serve the faucet API | - | Yes | +| `--api-bind-url` | URL to serve the faucet API | - | Yes | | `--api-public-url` | Public URL to access the faucet API. If not set, the bind url will be used. | - | No | | `--frontend-url` | URL to serve the Frontend API | - | No | | `--node-url` | Miden node RPC endpoint. If not set, it will be derived from the network | - | No | @@ -237,6 +237,8 @@ You can use the `miden-faucet-client` binary to request tokens from any running miden-faucet-client mint --url --account --amount ``` +Although the command is named `mint`, in technical terms it makes a request to the faucet to request a public P2ID note. + To see available options: ```bash miden-faucet-client mint --help diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index 17fbe913..965be7bf 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -47,7 +47,7 @@ miden-faucet-client mint \ --amount 1000 ``` -This solves the PoW challenge and mints a public P2ID note. (The legacy `miden-faucet mint` subcommand is removed.) +Although the command is named `mint`, in technical terms it makes a request to the faucet, solves the PoW challenge and creates a public P2ID note. ### Via Web Interface (if frontend is enabled) From c04b641dd0664384ce94ddb26ddb500294bafee1 Mon Sep 17 00:00:00 2001 From: keinberger Date: Tue, 16 Dec 2025 09:27:31 +0200 Subject: [PATCH 11/23] feat: implement common response types across operator and client binaries feat: implement backwards compatibility for legacy "miden-faucet" CLI command --- Cargo.lock | 2 + bin/faucet-client/Cargo.toml | 11 ++--- bin/faucet-client/src/mint.rs | 51 +++++++++-------------- bin/faucet-client/tests/mint.rs | 17 ++++---- bin/faucet-operator/Cargo.toml | 9 ++++ bin/faucet-operator/src/api/get_pow.rs | 10 +---- bin/faucet-operator/src/api/get_tokens.rs | 15 ++++--- crates/faucet/Cargo.toml | 1 + crates/faucet/src/requests.rs | 16 +++++++ 9 files changed, 72 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e8cb0ee..2004567f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2163,6 +2163,7 @@ dependencies = [ "clap", "hex", "miden-client", + "miden-faucet-lib", "rand", "reqwest", "serde", @@ -2181,6 +2182,7 @@ dependencies = [ "miden-client", "miden-client-sqlite-store", "rand", + "serde", "thiserror 2.0.17", "tokio", "tracing", diff --git a/bin/faucet-client/Cargo.toml b/bin/faucet-client/Cargo.toml index 3ee0aa33..4af3b443 100644 --- a/bin/faucet-client/Cargo.toml +++ b/bin/faucet-client/Cargo.toml @@ -13,11 +13,12 @@ version.workspace = true workspace = true [dependencies] -anyhow = { workspace = true } -clap = { features = ["derive", "env", "string"], version = "4.5" } -hex = { version = "0.4" } -miden-client = { workspace = true } -rand = { features = ["thread_rng"], workspace = true } +anyhow = { workspace = true } +clap = { features = ["derive", "env", "string"], version = "4.5" } +hex = { version = "0.4" } +miden-client = { workspace = true } +miden-faucet-lib = { workspace = true } +rand = { features = ["thread_rng"], workspace = true } reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } serde = { workspace = true } serde_json = { workspace = true } diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index 2e38ade3..8be8af7c 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -1,4 +1,4 @@ -//! CLI command to mint tokens from a remote faucet by solving its `PoW` challenge. +//! CLI command to request a public P2ID note from a remote faucet by solving its `PoW` challenge. use std::time::Duration; @@ -6,9 +6,11 @@ use clap::Parser; use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; use miden_client::note::NoteId; +use miden_client::transaction::TransactionId; +use miden_client::Word; +use miden_faucet_lib::requests::{GetPowResponse, GetTokensResponse, MintResponse}; use rand::Rng; use reqwest::{Client as HttpClient, Url}; -use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tokio::task; @@ -16,7 +18,7 @@ use tokio::task; // ================================================================================================= const DEFAULT_FAUCET_URL: &str = "https://faucet-api.testnet.miden.io"; -const DEFAULT_TIMEOUT_MS: u64 = 30_000; +const REQUEST_TIMEOUT_MS: u64 = 30_000; // CLI // ================================================================================================= @@ -51,7 +53,7 @@ impl MintCmd { let account_id = parse_account_id(&self.account)?; let faucet_client = - FaucetHttpClient::new(&self.api_url, DEFAULT_TIMEOUT_MS, self.api_key.clone())?; + FaucetHttpClient::new(&self.api_url, REQUEST_TIMEOUT_MS, self.api_key.clone())?; println!( "Requesting PoW challenge for account {} from faucet at {}...", @@ -65,12 +67,12 @@ impl MintCmd { let nonce = solve_challenge(&challenge, target).await?; println!("Submitting mint request for a public P2ID note..."); - let minted_note = faucet_client + let mint_response = faucet_client .request_tokens(&challenge, nonce, &account_id, self.quantity) .await?; - println!("Mint request accepted. Transaction: {}", minted_note.tx_id); - println!("Public P2ID note commitment: {}", minted_note.note_id.to_hex()); + println!("Mint request accepted. Transaction: {}", mint_response.tx_id.to_hex()); + println!("Public P2ID note commitment: {}", mint_response.note_id.to_hex()); Ok(()) } @@ -136,7 +138,7 @@ impl FaucetHttpClient { let body = response.text().await.map_err(|err| MintClientError::ResponseBody("pow", err))?; - let parsed = serde_json::from_str::(&body) + let parsed = serde_json::from_str::(&body) .map_err(|err| MintClientError::ParseResponse("PoW", err, body.clone()))?; Ok((parsed.challenge, parsed.target)) @@ -149,7 +151,7 @@ impl FaucetHttpClient { nonce: u64, account_id: &AccountId, amount: u64, - ) -> Result { + ) -> Result { let url = self .base_url .join("get_tokens") @@ -192,34 +194,19 @@ impl FaucetHttpClient { MintClientError::InvalidNoteId(parsed.note_id.clone(), err.to_string()) })?; - Ok(MintNote { note_id, tx_id: parsed.tx_id }) + let tx_id = Word::try_from(parsed.tx_id.as_str()) + .map(TransactionId::from) + .map_err(|err| { + MintClientError::InvalidTransactionId(parsed.tx_id.clone(), err.to_string()) + })?; + + Ok(MintResponse { note_id, tx_id }) } } // RESPONSES // ================================================================================================= -/// Response from the `/pow` endpoint. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct PowResponse { - pub challenge: String, - pub target: u64, -} - -/// Response from the `/get_tokens` endpoint. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct GetTokensResponse { - pub note_id: String, - pub tx_id: String, -} - -/// Represents a minted note with its ID and transaction ID. -#[derive(Debug, Clone)] -struct MintNote { - note_id: NoteId, - tx_id: String, -} - // ERRORS // ================================================================================================= @@ -250,6 +237,8 @@ pub enum MintClientError { PowTask(String), #[error("invalid note id `{0}`: {1}")] InvalidNoteId(String, String), + #[error("invalid transaction id `{0}`: {1}")] + InvalidTransactionId(String, String), } // HELPERS diff --git a/bin/faucet-client/tests/mint.rs b/bin/faucet-client/tests/mint.rs index 09b1de92..4752ca24 100644 --- a/bin/faucet-client/tests/mint.rs +++ b/bin/faucet-client/tests/mint.rs @@ -6,7 +6,8 @@ use axum::{Json, Router}; use clap::Parser; use miden_client::account::AccountId; use miden_client::note::NoteId; -use miden_faucet_client::mint::{GetTokensResponse, MintCmd, PowResponse}; +use miden_faucet_client::mint::MintCmd; +use miden_faucet_lib::requests::{GetPowResponse, GetTokensResponse}; use serde::Deserialize; use tokio::net::TcpListener; use tokio::sync::Mutex; @@ -22,7 +23,7 @@ struct RecordedRequest { #[derive(Clone)] struct AppState { - pow_response: PowResponse, + pow_response: GetPowResponse, note_id_hex: String, tx_id: String, recorded: Arc>, @@ -51,16 +52,16 @@ async fn mint_command_requests_public_note() { let account_hex = "0xca8203e8e58cf72049b061afca78ce"; let account_id = AccountId::from_hex(account_hex).unwrap(); let expected_amount = 123_000; - let pow_response = PowResponse { - challenge: "00".repeat(32), - target: u64::MAX, - }; + let pow_response = + GetPowResponse { challenge: "00".repeat(32), target: u64::MAX, timestamp: 0 }; let note_id_hex = format!("0x{}", "00".repeat(32)); let _note_id = NoteId::try_from_hex(¬e_id_hex).expect("hex string should produce a note id"); + // TransactionId requires a valid 32-byte Word (64 hex chars) + let tx_id_hex = format!("0x{}", "ab".repeat(32)); let app_state = AppState { pow_response, note_id_hex, - tx_id: "0xdeadbeef".to_string(), + tx_id: tx_id_hex, recorded: Arc::new(Mutex::new(RecordedRequest::default())), }; @@ -99,7 +100,7 @@ async fn mint_command_requests_public_note() { async fn pow_handler( State(state): State, Query(params): Query, -) -> Json { +) -> Json { { let mut recorded = state.recorded.lock().await; recorded.account_id = Some(params.account_id); diff --git a/bin/faucet-operator/Cargo.toml b/bin/faucet-operator/Cargo.toml index 5174e829..92b4654b 100644 --- a/bin/faucet-operator/Cargo.toml +++ b/bin/faucet-operator/Cargo.toml @@ -11,6 +11,15 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +# Define both binary names pointing to the same source for backwards compatibility +[[bin]] +name = "miden-faucet-operator" +path = "src/main.rs" + +[[bin]] +name = "miden-faucet" +path = "src/main.rs" + [lints] workspace = true diff --git a/bin/faucet-operator/src/api/get_pow.rs b/bin/faucet-operator/src/api/get_pow.rs index 73cd3cb1..5a27852c 100644 --- a/bin/faucet-operator/src/api/get_pow.rs +++ b/bin/faucet-operator/src/api/get_pow.rs @@ -5,12 +5,13 @@ use http::StatusCode; use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; use miden_client::utils::ToHex; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use tracing::{info_span, instrument}; use crate::COMPONENT; use crate::api::{AccountError, ApiServer}; use crate::api_key::ApiKey; +use miden_faucet_lib::requests::GetPowResponse; // ENDPOINT // ================================================================================================ @@ -48,13 +49,6 @@ pub async fn get_pow( })) } -#[derive(Serialize, Debug)] -pub struct GetPowResponse { - challenge: String, - target: u64, - timestamp: u64, -} - // REQUEST VALIDATION // ================================================================================================ diff --git a/bin/faucet-operator/src/api/get_tokens.rs b/bin/faucet-operator/src/api/get_tokens.rs index 9e6fb71b..edde4f58 100644 --- a/bin/faucet-operator/src/api/get_tokens.rs +++ b/bin/faucet-operator/src/api/get_tokens.rs @@ -4,10 +4,15 @@ use axum::http::{HeaderMap, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Response}; use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; -use miden_faucet_lib::requests::{MintError, MintRequest, MintRequestSender}; +use miden_faucet_lib::requests::{ + GetTokensResponse, + MintError, + MintRequest, + MintRequestSender, +}; use miden_faucet_lib::types::{AssetAmount, AssetAmountError, NoteType}; use miden_pow_rate_limiter::ChallengeError; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use tokio::sync::mpsc::error::TrySendError; use tokio::sync::oneshot; use tracing::{Instrument, info_span, instrument}; @@ -60,12 +65,6 @@ pub async fn get_tokens( })) } -#[derive(Serialize, Debug)] -pub struct GetTokensResponse { - tx_id: String, - note_id: String, -} - // STATE // ================================================================================================ diff --git a/crates/faucet/Cargo.toml b/crates/faucet/Cargo.toml index 34da090a..77ae9299 100644 --- a/crates/faucet/Cargo.toml +++ b/crates/faucet/Cargo.toml @@ -22,6 +22,7 @@ miden-client-sqlite-store = { workspace = true } # External dependencies. anyhow = { workspace = true } rand = { features = ["thread_rng"], workspace = true } +serde = { workspace = true } thiserror = { workspace = true } tokio = { features = ["fs"], workspace = true } tracing = { workspace = true } diff --git a/crates/faucet/src/requests.rs b/crates/faucet/src/requests.rs index 4a7a9ada..bb9d3b44 100644 --- a/crates/faucet/src/requests.rs +++ b/crates/faucet/src/requests.rs @@ -1,6 +1,7 @@ use miden_client::account::AccountId; use miden_client::note::NoteId; use miden_client::transaction::TransactionId; +use serde::{Deserialize, Serialize}; use tokio::sync::{mpsc, oneshot}; use crate::types::{AssetAmount, NoteType}; @@ -8,6 +9,21 @@ use crate::types::{AssetAmount, NoteType}; pub type MintResponseSender = oneshot::Sender>; pub type MintRequestSender = mpsc::Sender<(MintRequest, MintResponseSender)>; +/// Response from the `/pow` endpoint. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct GetPowResponse { + pub challenge: String, + pub target: u64, + pub timestamp: u64, +} + +/// Response from the `/get_tokens` endpoint. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct GetTokensResponse { + pub tx_id: String, + pub note_id: String, +} + /// A request for minting to the Faucet. pub struct MintRequest { /// Destination account. From 799ce95a1f4b87471252ba43b8aaccf4689d7361 Mon Sep 17 00:00:00 2001 From: keinberger Date: Tue, 16 Dec 2025 13:20:28 +0200 Subject: [PATCH 12/23] docs: add README for miden-faucet-client binary --- bin/faucet-client/Cargo.toml | 1 + bin/faucet-client/README.md | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 bin/faucet-client/README.md diff --git a/bin/faucet-client/Cargo.toml b/bin/faucet-client/Cargo.toml index 4af3b443..9c3c3b8b 100644 --- a/bin/faucet-client/Cargo.toml +++ b/bin/faucet-client/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true homepage.workspace = true license.workspace = true name = "miden-faucet-client" +readme = "README.md" repository.workspace = true rust-version.workspace = true version.workspace = true diff --git a/bin/faucet-client/README.md b/bin/faucet-client/README.md new file mode 100644 index 00000000..15b82e42 --- /dev/null +++ b/bin/faucet-client/README.md @@ -0,0 +1,13 @@ +# miden-faucet-client + +Command-line tool for interacting with a live Miden faucet. + +## Commands + +### `mint` + +Requests tokens from a faucet by solving its PoW challenge and receiving a public P2ID note. + +```bash +miden-faucet-client mint --url --account --quantity +``` From 6213ac87faf11ba260f303a3e1bcadd2d98dea9b Mon Sep 17 00:00:00 2001 From: keinberger Date: Tue, 16 Dec 2025 14:21:00 +0200 Subject: [PATCH 13/23] chore: linting --- bin/faucet-client/Cargo.toml | 14 +++++++------- bin/faucet-client/src/mint.rs | 7 +++---- bin/faucet-client/tests/mint.rs | 7 +++++-- bin/faucet-operator/src/api/get_pow.rs | 2 +- bin/faucet-operator/src/api/get_tokens.rs | 7 +------ 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/bin/faucet-client/Cargo.toml b/bin/faucet-client/Cargo.toml index 9c3c3b8b..61215963 100644 --- a/bin/faucet-client/Cargo.toml +++ b/bin/faucet-client/Cargo.toml @@ -20,13 +20,13 @@ hex = { version = "0.4" } miden-client = { workspace = true } miden-faucet-lib = { workspace = true } rand = { features = ["thread_rng"], workspace = true } -reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } -serde = { workspace = true } -serde_json = { workspace = true } -sha2 = { workspace = true } -thiserror = { workspace = true } -tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true } -url = { workspace = true } +reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } +tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true } +url = { workspace = true } [dev-dependencies] axum = { features = ["tokio"], version = "0.8" } diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index 8be8af7c..95978a2a 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -3,11 +3,11 @@ use std::time::Duration; use clap::Parser; +use miden_client::Word; use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; use miden_client::note::NoteId; use miden_client::transaction::TransactionId; -use miden_client::Word; use miden_faucet_lib::requests::{GetPowResponse, GetTokensResponse, MintResponse}; use rand::Rng; use reqwest::{Client as HttpClient, Url}; @@ -194,9 +194,8 @@ impl FaucetHttpClient { MintClientError::InvalidNoteId(parsed.note_id.clone(), err.to_string()) })?; - let tx_id = Word::try_from(parsed.tx_id.as_str()) - .map(TransactionId::from) - .map_err(|err| { + let tx_id = + Word::try_from(parsed.tx_id.as_str()).map(TransactionId::from).map_err(|err| { MintClientError::InvalidTransactionId(parsed.tx_id.clone(), err.to_string()) })?; diff --git a/bin/faucet-client/tests/mint.rs b/bin/faucet-client/tests/mint.rs index 4752ca24..670d5bbc 100644 --- a/bin/faucet-client/tests/mint.rs +++ b/bin/faucet-client/tests/mint.rs @@ -52,8 +52,11 @@ async fn mint_command_requests_public_note() { let account_hex = "0xca8203e8e58cf72049b061afca78ce"; let account_id = AccountId::from_hex(account_hex).unwrap(); let expected_amount = 123_000; - let pow_response = - GetPowResponse { challenge: "00".repeat(32), target: u64::MAX, timestamp: 0 }; + let pow_response = GetPowResponse { + challenge: "00".repeat(32), + target: u64::MAX, + timestamp: 0, + }; let note_id_hex = format!("0x{}", "00".repeat(32)); let _note_id = NoteId::try_from_hex(¬e_id_hex).expect("hex string should produce a note id"); // TransactionId requires a valid 32-byte Word (64 hex chars) diff --git a/bin/faucet-operator/src/api/get_pow.rs b/bin/faucet-operator/src/api/get_pow.rs index 5a27852c..822996c4 100644 --- a/bin/faucet-operator/src/api/get_pow.rs +++ b/bin/faucet-operator/src/api/get_pow.rs @@ -5,13 +5,13 @@ use http::StatusCode; use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; use miden_client::utils::ToHex; +use miden_faucet_lib::requests::GetPowResponse; use serde::Deserialize; use tracing::{info_span, instrument}; use crate::COMPONENT; use crate::api::{AccountError, ApiServer}; use crate::api_key::ApiKey; -use miden_faucet_lib::requests::GetPowResponse; // ENDPOINT // ================================================================================================ diff --git a/bin/faucet-operator/src/api/get_tokens.rs b/bin/faucet-operator/src/api/get_tokens.rs index edde4f58..c63a6d87 100644 --- a/bin/faucet-operator/src/api/get_tokens.rs +++ b/bin/faucet-operator/src/api/get_tokens.rs @@ -4,12 +4,7 @@ use axum::http::{HeaderMap, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Response}; use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; -use miden_faucet_lib::requests::{ - GetTokensResponse, - MintError, - MintRequest, - MintRequestSender, -}; +use miden_faucet_lib::requests::{GetTokensResponse, MintError, MintRequest, MintRequestSender}; use miden_faucet_lib::types::{AssetAmount, AssetAmountError, NoteType}; use miden_pow_rate_limiter::ChallengeError; use serde::Deserialize; From f5029ac10e23e23badb871bc8bc2441c70065bb4 Mon Sep 17 00:00:00 2001 From: keinberger Date: Tue, 16 Dec 2025 16:07:14 +0200 Subject: [PATCH 14/23] fix: prevent tests from being run on miden-faucet binary alias --- bin/faucet-operator/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/faucet-operator/Cargo.toml b/bin/faucet-operator/Cargo.toml index 92b4654b..843271aa 100644 --- a/bin/faucet-operator/Cargo.toml +++ b/bin/faucet-operator/Cargo.toml @@ -19,6 +19,10 @@ path = "src/main.rs" [[bin]] name = "miden-faucet" path = "src/main.rs" +# Avoid running tests twice for the same binary target; tests are exercised via `miden-faucet-operator`. +bench = false +doctest = false +test = false [lints] workspace = true From 94e7328b7b4bdbd30fd8688b2c23472eed76d304 Mon Sep 17 00:00:00 2001 From: keinberger Date: Wed, 17 Dec 2025 14:05:10 +0200 Subject: [PATCH 15/23] chore: export raw request parameters from miden-faucet-operator --- bin/faucet-client/src/mint.rs | 51 +++++--- bin/faucet-client/tests/mint.rs | 66 ++++------ bin/faucet-operator/src/api/get_pow.rs | 61 ++++----- bin/faucet-operator/src/api/get_tokens.rs | 150 ++++++++++------------ crates/faucet/src/requests.rs | 31 +++++ 5 files changed, 184 insertions(+), 175 deletions(-) diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index 95978a2a..acb46a5a 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -8,7 +8,13 @@ use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; use miden_client::note::NoteId; use miden_client::transaction::TransactionId; -use miden_faucet_lib::requests::{GetPowResponse, GetTokensResponse, MintResponse}; +use miden_faucet_lib::requests::{ + GetPowResponse, + GetTokensQueryParams, + GetTokensResponse, + MintResponse, + PowQueryParams, +}; use rand::Rng; use reqwest::{Client as HttpClient, Url}; use sha2::{Digest, Sha256}; @@ -118,16 +124,19 @@ impl FaucetHttpClient { .join("pow") .map_err(|err| MintClientError::InvalidUrl(self.base_url.to_string(), err))?; - let mut request = self + let params = PowQueryParams { + account_id: account_id.to_hex(), + amount, + api_key: self.api_key.clone(), + }; + + let response = self .http_client .get(pow_url) - .query(&[("account_id", account_id.to_hex()), ("amount", amount.to_string())]); - - if let Some(key) = &self.api_key { - request = request.query(&[("api_key", key)]); - } - - let response = request.send().await.map_err(|err| MintClientError::Request("PoW", err))?; + .query(¶ms) + .send() + .await + .map_err(|err| MintClientError::Request("PoW", err))?; if !response.status().is_success() { let status = response.status(); @@ -157,19 +166,19 @@ impl FaucetHttpClient { .join("get_tokens") .map_err(|err| MintClientError::InvalidUrl(self.base_url.to_string(), err))?; - let mut request = self.http_client.get(url).query(&[ - ("account_id", account_id.to_hex()), - ("asset_amount", amount.to_string()), - ("is_private_note", false.to_string()), - ("challenge", challenge.to_owned()), - ("nonce", nonce.to_string()), - ]); - - if let Some(key) = &self.api_key { - request = request.query(&[("api_key", key)]); - } + let params = GetTokensQueryParams { + account_id: account_id.to_hex(), + asset_amount: amount, + is_private_note: false, + challenge: challenge.to_owned(), + nonce, + api_key: self.api_key.clone(), + }; - let response = request + let response = self + .http_client + .get(url) + .query(¶ms) .send() .await .map_err(|err| MintClientError::Request("get_tokens", err))?; diff --git a/bin/faucet-client/tests/mint.rs b/bin/faucet-client/tests/mint.rs index 670d5bbc..030b2153 100644 --- a/bin/faucet-client/tests/mint.rs +++ b/bin/faucet-client/tests/mint.rs @@ -7,18 +7,19 @@ use clap::Parser; use miden_client::account::AccountId; use miden_client::note::NoteId; use miden_faucet_client::mint::MintCmd; -use miden_faucet_lib::requests::{GetPowResponse, GetTokensResponse}; -use serde::Deserialize; +use miden_faucet_lib::requests::{ + GetPowResponse, + GetTokensQueryParams, + GetTokensResponse, + PowQueryParams, +}; use tokio::net::TcpListener; use tokio::sync::Mutex; #[derive(Clone, Default)] struct RecordedRequest { - account_id: Option, - amount: Option, - is_private_note: Option, - api_key: Option, - challenge: Option, + pow_params: Option, + tokens_params: Option, } #[derive(Clone)] @@ -29,24 +30,6 @@ struct AppState { recorded: Arc>, } -#[derive(Deserialize)] -struct PowQuery { - amount: u64, - account_id: String, - api_key: Option, -} - -#[derive(Deserialize)] -struct TokensQuery { - account_id: String, - is_private_note: String, - asset_amount: u64, - challenge: String, - #[allow(dead_code)] - nonce: u64, - api_key: Option, -} - #[tokio::test] async fn mint_command_requests_public_note() { let account_hex = "0xca8203e8e58cf72049b061afca78ce"; @@ -93,37 +76,40 @@ async fn mint_command_requests_public_note() { cli.execute().await.unwrap(); let recorded = app_state.recorded.lock().await.clone(); - assert_eq!(recorded.account_id, Some(account_id.to_hex())); - assert_eq!(recorded.amount, Some(expected_amount)); - assert_eq!(recorded.is_private_note.as_deref(), Some("false")); - assert_eq!(recorded.api_key.as_deref(), Some("test-key")); - assert_eq!(recorded.challenge, Some("00".repeat(32))); + + // Verify PoW request params + let pow_params = recorded.pow_params.expect("pow_params should be recorded"); + assert_eq!(pow_params.account_id, account_id.to_hex()); + assert_eq!(pow_params.amount, expected_amount); + assert_eq!(pow_params.api_key.as_deref(), Some("test-key")); + + // Verify get_tokens request params + let tokens_params = recorded.tokens_params.expect("tokens_params should be recorded"); + assert_eq!(tokens_params.account_id, account_id.to_hex()); + assert_eq!(tokens_params.asset_amount, expected_amount); + assert!(!tokens_params.is_private_note); + assert_eq!(tokens_params.api_key.as_deref(), Some("test-key")); + assert_eq!(tokens_params.challenge, "00".repeat(32)); } async fn pow_handler( State(state): State, - Query(params): Query, + Query(params): Query, ) -> Json { { let mut recorded = state.recorded.lock().await; - recorded.account_id = Some(params.account_id); - recorded.amount = Some(params.amount); - recorded.api_key = params.api_key; + recorded.pow_params = Some(params); } Json(state.pow_response.clone()) } async fn tokens_handler( State(state): State, - Query(params): Query, + Query(params): Query, ) -> Json { { let mut recorded = state.recorded.lock().await; - recorded.account_id = Some(params.account_id.clone()); - recorded.amount = Some(params.asset_amount); - recorded.is_private_note = Some(params.is_private_note.clone()); - recorded.api_key.clone_from(¶ms.api_key); - recorded.challenge = Some(params.challenge); + recorded.tokens_params = Some(params); } Json(GetTokensResponse { note_id: state.note_id_hex.clone(), diff --git a/bin/faucet-operator/src/api/get_pow.rs b/bin/faucet-operator/src/api/get_pow.rs index 822996c4..cf555b32 100644 --- a/bin/faucet-operator/src/api/get_pow.rs +++ b/bin/faucet-operator/src/api/get_pow.rs @@ -5,8 +5,7 @@ use http::StatusCode; use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; use miden_client::utils::ToHex; -use miden_faucet_lib::requests::GetPowResponse; -use serde::Deserialize; +use miden_faucet_lib::requests::{GetPowResponse, PowQueryParams}; use tracing::{info_span, instrument}; use crate::COMPONENT; @@ -22,9 +21,9 @@ use crate::api_key::ApiKey; )] pub async fn get_pow( State(server): State, - Query(params): Query, + Query(params): Query, ) -> Result, PowRequestError> { - let request = params.validate()?; + let request = validate_pow_params(params)?; let account_id_bytes: [u8; AccountId::SERIALIZED_SIZE] = request.account_id.into(); let mut requestor = [0u8; 32]; requestor[..AccountId::SERIALIZED_SIZE].copy_from_slice(&account_id_bytes); @@ -59,38 +58,32 @@ pub struct PowRequest { pub api_key: ApiKey, } -/// Used to receive the initial `get_pow` request from the user. -#[derive(Deserialize)] -pub struct RawPowRequest { - amount: u64, - account_id: String, - api_key: Option, -} - -impl RawPowRequest { - pub fn validate(self) -> Result { - let account_id = if self.account_id.starts_with("0x") { - AccountId::from_hex(&self.account_id).map_err(AccountError::ParseId) - } else { - Address::decode(&self.account_id).map_err(AccountError::ParseAddress).and_then( - |(_, address)| match address.id() { - AddressId::AccountId(account_id) => Ok(account_id), - _ => Err(AccountError::AddressNotIdBased), - }, - ) - } - .map_err(PowRequestError::AccountError)?; +fn validate_pow_params(params: PowQueryParams) -> Result { + let account_id = if params.account_id.starts_with("0x") { + AccountId::from_hex(¶ms.account_id).map_err(AccountError::ParseId) + } else { + Address::decode(¶ms.account_id) + .map_err(AccountError::ParseAddress) + .and_then(|(_, address)| match address.id() { + AddressId::AccountId(account_id) => Ok(account_id), + _ => Err(AccountError::AddressNotIdBased), + }) + } + .map_err(PowRequestError::AccountError)?; - let api_key = self - .api_key - .as_deref() - .map(ApiKey::decode) - .transpose() - .map_err(|_| PowRequestError::InvalidApiKey(self.api_key.unwrap_or_default()))? - .unwrap_or_default(); + let api_key = params + .api_key + .as_deref() + .map(ApiKey::decode) + .transpose() + .map_err(|_| PowRequestError::InvalidApiKey(params.api_key.unwrap_or_default()))? + .unwrap_or_default(); - Ok(PowRequest { amount: self.amount, account_id, api_key }) - } + Ok(PowRequest { + amount: params.amount, + account_id, + api_key, + }) } #[derive(Debug, thiserror::Error)] diff --git a/bin/faucet-operator/src/api/get_tokens.rs b/bin/faucet-operator/src/api/get_tokens.rs index c63a6d87..4009dbeb 100644 --- a/bin/faucet-operator/src/api/get_tokens.rs +++ b/bin/faucet-operator/src/api/get_tokens.rs @@ -4,10 +4,15 @@ use axum::http::{HeaderMap, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Response}; use miden_client::account::{AccountId, Address}; use miden_client::address::AddressId; -use miden_faucet_lib::requests::{GetTokensResponse, MintError, MintRequest, MintRequestSender}; +use miden_faucet_lib::requests::{ + GetTokensQueryParams, + GetTokensResponse, + MintError, + MintRequest, + MintRequestSender, +}; use miden_faucet_lib::types::{AssetAmount, AssetAmountError, NoteType}; use miden_pow_rate_limiter::ChallengeError; -use serde::Deserialize; use tokio::sync::mpsc::error::TrySendError; use tokio::sync::oneshot; use tracing::{Instrument, info_span, instrument}; @@ -29,11 +34,12 @@ use crate::api_key::ApiKey; )] pub async fn get_tokens( State(server): State, - Query(request): Query, + Query(request): Query, ) -> Result, GetTokenError> { let (mint_response_sender, mint_response_receiver) = oneshot::channel(); - let validated_request = request.validate(&server).map_err(GetTokenError::InvalidRequest)?; + let validated_request = + validate_get_tokens_params(&request, &server).map_err(GetTokenError::InvalidRequest)?; let enqueue_span = info_span!(target: COMPONENT, "server.get_tokens.enqueue"); { @@ -78,19 +84,6 @@ impl GetTokensState { // REQUEST VALIDATION // ================================================================================================ -/// Used to receive the initial request from the user. -/// -/// Further parsing is done to get the expected [`MintRequest`] expected by the faucet client. -#[derive(Deserialize)] -pub struct RawMintRequest { - pub account_id: String, - pub is_private_note: bool, - pub asset_amount: u64, - pub challenge: Option, - pub nonce: Option, - pub api_key: Option, -} - #[derive(Debug, thiserror::Error)] pub enum MintRequestError { #[error(transparent)] @@ -103,8 +96,6 @@ pub enum MintRequestError { PowError(#[from] ChallengeError), #[error("API key {0} is invalid")] InvalidApiKey(String), - #[error("PoW parameters are missing")] - MissingPowParameters, } #[derive(Debug, thiserror::Error)] @@ -183,69 +174,68 @@ impl IntoResponse for GetTokenError { } } -impl RawMintRequest { - /// Further validates a raw request, turning it into a valid [`MintRequest`] which can be - /// submitted to the faucet client. - /// - /// # Errors - /// - /// Returns an error if: - /// - the account ID is not a valid hex string - /// - the asset amount is not one of the provided options - /// - the API key is invalid - /// - the challenge is missing or invalid - /// - the nonce is missing or doesn't solve the challenge - /// - the challenge timestamp is expired - /// - the challenge has already been used - fn validate(self, server: &ApiServer) -> Result { - let note_type = if self.is_private_note { - NoteType::Private - } else { - NoteType::Public - }; - - let account_id = if self.account_id.starts_with("0x") { - AccountId::from_hex(&self.account_id).map_err(AccountError::ParseId) - } else { - Address::decode(&self.account_id).map_err(AccountError::ParseAddress).and_then( - |(_, address)| match address.id() { - AddressId::AccountId(account_id) => Ok(account_id), - _ => Err(AccountError::AddressNotIdBased), - }, - ) - } - .map_err(MintRequestError::AccountError)?; - - let asset_amount = - AssetAmount::new(self.asset_amount).map_err(MintRequestError::InvalidAssetAmount)?; - if asset_amount > server.mint_state.max_claimable_amount { - return Err(MintRequestError::AssetAmountTooBig( - asset_amount, - server.mint_state.max_claimable_amount, - )); - } +/// Further validates a raw request, turning it into a valid [`MintRequest`] which can be +/// submitted to the faucet client. +/// +/// # Errors +/// +/// Returns an error if: +/// - the account ID is not a valid hex string +/// - the asset amount is not one of the provided options +/// - the API key is invalid +/// - the challenge is invalid +/// - the nonce doesn't solve the challenge +/// - the challenge timestamp is expired +/// - the challenge has already been used +fn validate_get_tokens_params( + params: &GetTokensQueryParams, + server: &ApiServer, +) -> Result { + let note_type = if params.is_private_note { + NoteType::Private + } else { + NoteType::Public + }; + + let account_id = if params.account_id.starts_with("0x") { + AccountId::from_hex(¶ms.account_id).map_err(AccountError::ParseId) + } else { + Address::decode(¶ms.account_id) + .map_err(AccountError::ParseAddress) + .and_then(|(_, address)| match address.id() { + AddressId::AccountId(account_id) => Ok(account_id), + _ => Err(AccountError::AddressNotIdBased), + }) + } + .map_err(MintRequestError::AccountError)?; + + let asset_amount = + AssetAmount::new(params.asset_amount).map_err(MintRequestError::InvalidAssetAmount)?; + if asset_amount > server.mint_state.max_claimable_amount { + return Err(MintRequestError::AssetAmountTooBig( + asset_amount, + server.mint_state.max_claimable_amount, + )); + } - // Check the API key, if provided - let api_key = self.api_key.as_deref().map(ApiKey::decode).transpose()?; - if let Some(api_key) = &api_key - && !server.api_keys.contains(api_key) - { - return Err(MintRequestError::InvalidApiKey(api_key.encode())); - } + // Check the API key, if provided + let api_key = params.api_key.as_deref().map(ApiKey::decode).transpose()?; + if let Some(api_key) = &api_key + && !server.api_keys.contains(api_key) + { + return Err(MintRequestError::InvalidApiKey(api_key.encode())); + } - // Validate Challenge and nonce - let challenge_str = self.challenge.ok_or(MintRequestError::MissingPowParameters)?; - let nonce = self.nonce.ok_or(MintRequestError::MissingPowParameters)?; - let request_complexity = server.compute_request_complexity(asset_amount.base_units()); + // Validate Challenge and nonce + let request_complexity = server.compute_request_complexity(asset_amount.base_units()); - server.submit_challenge( - &challenge_str, - nonce, - account_id, - api_key.unwrap_or_default(), - request_complexity, - )?; + server.submit_challenge( + ¶ms.challenge, + params.nonce, + account_id, + api_key.unwrap_or_default(), + request_complexity, + )?; - Ok(MintRequest { account_id, note_type, asset_amount }) - } + Ok(MintRequest { account_id, note_type, asset_amount }) } diff --git a/crates/faucet/src/requests.rs b/crates/faucet/src/requests.rs index bb9d3b44..7ec0b59f 100644 --- a/crates/faucet/src/requests.rs +++ b/crates/faucet/src/requests.rs @@ -9,6 +9,37 @@ use crate::types::{AssetAmount, NoteType}; pub type MintResponseSender = oneshot::Sender>; pub type MintRequestSender = mpsc::Sender<(MintRequest, MintResponseSender)>; +// QUERY PARAMETERS +// ================================================================================================ + +/// Query parameters for the `/pow` endpoint. +/// +/// Used by both the client (to serialize) and the server (to deserialize). +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PowQueryParams { + pub account_id: String, + pub amount: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub api_key: Option, +} + +/// Query parameters for the `/get_tokens` endpoint. +/// +/// Used by both the client (to serialize) and the server (to deserialize). +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct GetTokensQueryParams { + pub account_id: String, + pub asset_amount: u64, + pub is_private_note: bool, + pub challenge: String, + pub nonce: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub api_key: Option, +} + +// RESPONSES +// ================================================================================================ + /// Response from the `/pow` endpoint. #[derive(Debug, Deserialize, Serialize, Clone)] pub struct GetPowResponse { From 4a8c51c4dea97262e27bc36784163316378fa75a Mon Sep 17 00:00:00 2001 From: keinberger Date: Wed, 17 Dec 2025 14:23:03 +0200 Subject: [PATCH 16/23] chore: implement native miden_pow_rate_limiter package in faucet-client --- Cargo.lock | 2 +- bin/faucet-client/Cargo.toml | 25 ++++++++++++------------- bin/faucet-client/src/mint.rs | 33 ++++++++++++++------------------- bin/faucet-client/tests/mint.rs | 20 ++++++++++++++++++-- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2004567f..2dc34d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2164,11 +2164,11 @@ dependencies = [ "hex", "miden-client", "miden-faucet-lib", + "miden-pow-rate-limiter", "rand", "reqwest", "serde", "serde_json", - "sha2", "thiserror 2.0.17", "tokio", "url", diff --git a/bin/faucet-client/Cargo.toml b/bin/faucet-client/Cargo.toml index 61215963..d66b90fb 100644 --- a/bin/faucet-client/Cargo.toml +++ b/bin/faucet-client/Cargo.toml @@ -14,19 +14,18 @@ version.workspace = true workspace = true [dependencies] -anyhow = { workspace = true } -clap = { features = ["derive", "env", "string"], version = "4.5" } -hex = { version = "0.4" } -miden-client = { workspace = true } -miden-faucet-lib = { workspace = true } -rand = { features = ["thread_rng"], workspace = true } -reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } -serde = { workspace = true } -serde_json = { workspace = true } -sha2 = { workspace = true } -thiserror = { workspace = true } -tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true } -url = { workspace = true } +anyhow = { workspace = true } +clap = { features = ["derive", "env", "string"], version = "4.5" } +hex = { version = "0.4" } +miden-client = { workspace = true } +miden-faucet-lib = { workspace = true } +miden-pow-rate-limiter = { workspace = true } +rand = { features = ["thread_rng"], workspace = true } +reqwest = { default-features = false, features = ["json", "rustls-tls"], version = "0.12" } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { features = ["macros", "net", "rt-multi-thread", "sync", "time"], workspace = true } +url = { workspace = true } [dev-dependencies] axum = { features = ["tokio"], version = "0.8" } diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index acb46a5a..387c003a 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -15,9 +15,9 @@ use miden_faucet_lib::requests::{ MintResponse, PowQueryParams, }; +use miden_pow_rate_limiter::{Challenge, ChallengeError}; use rand::Rng; use reqwest::{Client as HttpClient, Url}; -use sha2::{Digest, Sha256}; use tokio::task; // CONSTANTS @@ -239,8 +239,10 @@ pub enum MintClientError { ResponseBody(&'static str, #[source] reqwest::Error), #[error("faucet returned a PoW target of 0")] ZeroTarget, - #[error("invalid challenge bytes returned by faucet: {0}")] - InvalidChallenge(#[source] hex::FromHexError), + #[error("invalid challenge hex returned by faucet: {0}")] + InvalidChallengeHex(#[source] hex::FromHexError), + #[error("invalid challenge returned by faucet: {0}")] + InvalidChallenge(#[source] ChallengeError), #[error("PoW solving task failed: {0}")] PowTask(String), #[error("invalid note id `{0}`: {1}")] @@ -272,35 +274,28 @@ fn parse_account_id(input: &str) -> Result { /// Solves the `PoW` challenge and returns the nonce that satisfies the target. /// -/// The faucet expects the first 8 bytes of the SHA-256 digest (big endian) to be lower than -/// the target. -/// -/// Heavy work runs on a blocking thread so we don't stall the async runtime +/// Heavy work runs on a blocking thread so we don't stall the async runtime. async fn solve_challenge(challenge_hex: &str, target: u64) -> Result { if target == 0 { return Err(MintClientError::ZeroTarget); } - let challenge_bytes = hex::decode(challenge_hex).map_err(MintClientError::InvalidChallenge)?; + let challenge_bytes = + hex::decode(challenge_hex).map_err(MintClientError::InvalidChallengeHex)?; + let challenge = Challenge::try_from(challenge_bytes.as_slice()) + .map_err(MintClientError::InvalidChallenge)?; - task::spawn_blocking(move || -> Result { + task::spawn_blocking(move || { let mut rng = rand::rng(); loop { let nonce: u64 = rng.random(); - let mut hasher = Sha256::new(); - hasher.update(&challenge_bytes); - hasher.update(nonce.to_be_bytes()); - let hash = hasher.finalize(); - let digest = - u64::from_be_bytes(hash[..8].try_into().expect("hash should be 32 bytes long")); - - if digest < target { - return Ok(nonce); + if challenge.validate_pow(nonce) { + return nonce; } } }) .await - .map_err(|err| MintClientError::PowTask(err.to_string()))? + .map_err(|err| MintClientError::PowTask(err.to_string())) } diff --git a/bin/faucet-client/tests/mint.rs b/bin/faucet-client/tests/mint.rs index 030b2153..4645b188 100644 --- a/bin/faucet-client/tests/mint.rs +++ b/bin/faucet-client/tests/mint.rs @@ -6,6 +6,7 @@ use axum::{Json, Router}; use clap::Parser; use miden_client::account::AccountId; use miden_client::note::NoteId; +use miden_client::utils::ToHex; use miden_faucet_client::mint::MintCmd; use miden_faucet_lib::requests::{ GetPowResponse, @@ -13,6 +14,7 @@ use miden_faucet_lib::requests::{ GetTokensResponse, PowQueryParams, }; +use miden_pow_rate_limiter::Challenge; use tokio::net::TcpListener; use tokio::sync::Mutex; @@ -28,6 +30,7 @@ struct AppState { note_id_hex: String, tx_id: String, recorded: Arc>, + challenge_hex: String, } #[tokio::test] @@ -35,8 +38,20 @@ async fn mint_command_requests_public_note() { let account_hex = "0xca8203e8e58cf72049b061afca78ce"; let account_id = AccountId::from_hex(account_hex).unwrap(); let expected_amount = 123_000; + + // Create a valid Challenge with target = u64::MAX so any nonce will solve it + let challenge = Challenge::from_parts( + u64::MAX, // target - any nonce will pass + 0, // timestamp + 1, // request_complexity + [0u8; 32], // requestor + [0u8; 32], // domain + [0u8; 32], // signature (doesn't matter for client-side validation) + ); + let challenge_hex = challenge.to_bytes().to_hex(); + let pow_response = GetPowResponse { - challenge: "00".repeat(32), + challenge: challenge_hex.clone(), target: u64::MAX, timestamp: 0, }; @@ -49,6 +64,7 @@ async fn mint_command_requests_public_note() { note_id_hex, tx_id: tx_id_hex, recorded: Arc::new(Mutex::new(RecordedRequest::default())), + challenge_hex, }; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -89,7 +105,7 @@ async fn mint_command_requests_public_note() { assert_eq!(tokens_params.asset_amount, expected_amount); assert!(!tokens_params.is_private_note); assert_eq!(tokens_params.api_key.as_deref(), Some("test-key")); - assert_eq!(tokens_params.challenge, "00".repeat(32)); + assert_eq!(tokens_params.challenge, app_state.challenge_hex); } async fn pow_handler( From c7352b94a4395f186dbe32f2529e8d73f18cf52b Mon Sep 17 00:00:00 2001 From: keinberger Date: Thu, 18 Dec 2025 12:34:40 +0200 Subject: [PATCH 17/23] chore(faucet-client): rename account flag to target, quantity to amount chore: amend character size of separators docs(client/README): amend mint command explanation to be more technically accurate chore(CHANGELOG): mark binary separation as breaking --- CHANGELOG.md | 2 +- README.md | 2 +- bin/faucet-client/README.md | 4 ++-- bin/faucet-client/src/mint.rs | 31 +++++++++++-------------- bin/faucet-client/tests/mint.rs | 4 ++-- docs/src/getting-started/cli.md | 2 +- docs/src/getting-started/quick-start.md | 2 +- 7 files changed, 22 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b794d850..4c218b05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.12.5 (TBD) -- Renamed the faucet CLI to `miden-faucet-operator`, added a new `miden-faucet-client` binary with the `mint` command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). +- [BREAKING] Renamed the faucet CLI to `miden-faucet-operator`, added a new `miden-faucet-client` binary with the `mint` command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). ## 0.12.4 (2025-12-04) diff --git a/README.md b/README.md index 1c8d33dd..84a3994e 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ miden-faucet-operator start \ You can use the `miden-faucet-client` binary to request tokens from any running faucet instance, whether it's your local faucet or the remote testnet faucet: ```bash -miden-faucet-client mint --url --account --amount +miden-faucet-client mint --url --target-account --amount ``` After a few seconds you may go to `http://localhost:8080` and see the faucet UI. diff --git a/bin/faucet-client/README.md b/bin/faucet-client/README.md index 15b82e42..d13d1d43 100644 --- a/bin/faucet-client/README.md +++ b/bin/faucet-client/README.md @@ -6,8 +6,8 @@ Command-line tool for interacting with a live Miden faucet. ### `mint` -Requests tokens from a faucet by solving its PoW challenge and receiving a public P2ID note. +Requests tokens from a faucet by solving its PoW challenge and minting a public P2ID note. ```bash -miden-faucet-client mint --url --account --quantity +miden-faucet-client mint --url --target-account --amount ``` diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index 387c003a..3df0868b 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -21,13 +21,13 @@ use reqwest::{Client as HttpClient, Url}; use tokio::task; // CONSTANTS -// ================================================================================================= +// ================================================================================================ const DEFAULT_FAUCET_URL: &str = "https://faucet-api.testnet.miden.io"; const REQUEST_TIMEOUT_MS: u64 = 30_000; // CLI -// ================================================================================================= +// ================================================================================================ /// Mint tokens from a remote faucet by solving its `PoW` challenge and requesting a **public** /// P2ID note. @@ -38,12 +38,12 @@ pub struct MintCmd { api_url: String, /// Account ID or address to receive the minted tokens. - #[arg(short = 'a', long = "account", value_name = "ACCOUNT")] - account: String, + #[arg(short = 't', long = "target-account", value_name = "ACCOUNT")] + target_account: String, - /// Quantity to mint (in base units). - #[arg(short = 'q', long = "quantity", value_name = "U64", alias = "amount")] - quantity: u64, + /// Amount to mint (in base units). + #[arg(short = 'a', long = "amount", value_name = "U64")] + amount: u64, /// Optional faucet API key. #[arg(long = "api-key", value_name = "STRING")] @@ -53,11 +53,11 @@ pub struct MintCmd { impl MintCmd { /// Executes the mint command. pub async fn execute(&self) -> Result<(), MintClientError> { - if self.quantity == 0 { + if self.amount == 0 { return Err(MintClientError::AmountZero); } - let account_id = parse_account_id(&self.account)?; + let account_id = parse_account_id(&self.target_account)?; let faucet_client = FaucetHttpClient::new(&self.api_url, REQUEST_TIMEOUT_MS, self.api_key.clone())?; @@ -67,14 +67,14 @@ impl MintCmd { faucet_client.base_url ); - let (challenge, target) = faucet_client.request_pow(&account_id, self.quantity).await?; + let (challenge, target) = faucet_client.request_pow(&account_id, self.amount).await?; println!("Solving faucet PoW challenge, this can take some time..."); let nonce = solve_challenge(&challenge, target).await?; println!("Submitting mint request for a public P2ID note..."); let mint_response = faucet_client - .request_tokens(&challenge, nonce, &account_id, self.quantity) + .request_tokens(&challenge, nonce, &account_id, self.amount) .await?; println!("Mint request accepted. Transaction: {}", mint_response.tx_id.to_hex()); @@ -85,7 +85,7 @@ impl MintCmd { } // HTTP CLIENT -// ================================================================================================= +// ================================================================================================ /// HTTP client for interacting with the faucet API. #[derive(Clone)] @@ -212,11 +212,8 @@ impl FaucetHttpClient { } } -// RESPONSES -// ================================================================================================= - // ERRORS -// ================================================================================================= +// ================================================================================================ /// Errors that can occur while interacting with the faucet API. #[derive(Debug, thiserror::Error)] @@ -252,7 +249,7 @@ pub enum MintClientError { } // HELPERS -// ================================================================================================= +// ================================================================================================ /// Parses a user provided account ID string and returns the corresponding `AccountId` fn parse_account_id(input: &str) -> Result { diff --git a/bin/faucet-client/tests/mint.rs b/bin/faucet-client/tests/mint.rs index 4645b188..e34cce14 100644 --- a/bin/faucet-client/tests/mint.rs +++ b/bin/faucet-client/tests/mint.rs @@ -81,9 +81,9 @@ async fn mint_command_requests_public_note() { "mint", "--url", format!("http://{addr}").as_str(), - "--account", + "--target-account", account_id.to_hex().as_str(), - "--quantity", + "--amount", &expected_amount.to_string(), "--api-key", "test-key", diff --git a/docs/src/getting-started/cli.md b/docs/src/getting-started/cli.md index aa0d8a2a..14c33ad0 100644 --- a/docs/src/getting-started/cli.md +++ b/docs/src/getting-started/cli.md @@ -234,7 +234,7 @@ For detailed options, run `miden-faucet-operator [COMMAND] --help`. The legacy a You can use the `miden-faucet-client` binary to request tokens from any running faucet instance, whether it's your local faucet or the remote testnet faucet: ```bash -miden-faucet-client mint --url --account --amount +miden-faucet-client mint --url --target-account --amount ``` Although the command is named `mint`, in technical terms it makes a request to the faucet to request a public P2ID note. diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index 965be7bf..a1576db3 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -43,7 +43,7 @@ Use the dedicated mint command: ```bash miden-faucet-client mint \ --url http://localhost:8000 \ - --account \ + --target-account \ --amount 1000 ``` From 49e14b300e1a8f463d813fec46bbe7dee965ea15 Mon Sep 17 00:00:00 2001 From: keinberger Date: Thu, 18 Dec 2025 12:35:09 +0200 Subject: [PATCH 18/23] chore(action.yml): amend action workflow according to binary separation --- .github/actions/debian/action.yml | 6 +++--- packaging/faucet/postinst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/debian/action.yml b/.github/actions/debian/action.yml index 1c2271ff..4b0df516 100644 --- a/.github/actions/debian/action.yml +++ b/.github/actions/debian/action.yml @@ -19,19 +19,19 @@ inputs: description: Name of binary crate being packaged. type: choice options: - - miden-faucet + - miden-faucet-operator crate_dir: required: true description: Name of crate being packaged. type: choice options: - - miden-faucet + - faucet-operator service: required: true description: The service to build the packages for. type: choice options: - - miden-faucet + - miden-faucet-operator package: required: true description: Name of packaging directory. diff --git a/packaging/faucet/postinst b/packaging/faucet/postinst index a88a0864..d321ee85 100644 --- a/packaging/faucet/postinst +++ b/packaging/faucet/postinst @@ -3,7 +3,7 @@ # This is a postinstallation script so the service can be configured and started when requested. # user is expected by the systemd service file and `/opt/` is its working directory, -sudo adduser --disabled-password --disabled-login --shell /usr/sbin/nologin --quiet --system --no-create-home --home /nonexistent miden-faucet +sudo adduser --disabled-password --disabled-login --shell /usr/sbin/nologin --quiet --system --no-create-home --home /nonexistent miden-faucet-operator # Working folder. if [ -d "/opt/miden-faucet-operator" ] From 04487c5a100e7982b28820e89086960a87c915df Mon Sep 17 00:00:00 2001 From: keinberger Date: Mon, 22 Dec 2025 14:29:08 +0100 Subject: [PATCH 19/23] chore(publish-debian.yml): amend workflow for binary separation --- .github/workflows/publish-debian.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-debian.yml b/.github/workflows/publish-debian.yml index d78bf945..ee62a809 100644 --- a/.github/workflows/publish-debian.yml +++ b/.github/workflows/publish-debian.yml @@ -19,8 +19,8 @@ permissions: contents: write jobs: - publish-faucet: - name: Publish Faucet ${{ matrix.arch }} Debian + publish-faucet-operator: + name: Publish Faucet Operator ${{ matrix.arch }} Debian strategy: matrix: arch: [amd64, arm64] @@ -31,13 +31,13 @@ jobs: uses: actions/checkout@main with: fetch-depth: 0 - - name: Build and Publish Faucet + - name: Build and Publish Faucet Operator uses: ./.github/actions/debian with: github_token: ${{ secrets.GITHUB_TOKEN }} gitref: ${{ env.version }} - crate_dir: faucet - service: miden-faucet + crate_dir: faucet-operator + service: miden-faucet-operator package: faucet - crate: miden-faucet + crate: miden-faucet-operator arch: ${{ matrix.arch }} From 4fee9530e9ebcff90885067fde253c48869a34cc Mon Sep 17 00:00:00 2001 From: keinberger Date: Fri, 16 Jan 2026 13:39:13 +0200 Subject: [PATCH 20/23] chore(faucet): revert binary rename from miden-faucet-operator to miden-faucet --- .github/actions/debian/action.yml | 6 +- .github/workflows/publish-debian.yml | 12 ++-- Cargo.lock | 64 +++++++++--------- Cargo.toml | 2 +- Makefile | 2 +- README.md | 8 +-- bin/{faucet-operator => faucet}/.env | 0 bin/{faucet-operator => faucet}/Cargo.toml | 19 +----- bin/{faucet-operator => faucet}/README.md | 0 bin/{faucet-operator => faucet}/build.rs | 0 .../frontend/api.js | 0 .../frontend/app.js | 0 .../frontend/background.png | Bin .../frontend/favicon.ico | Bin .../frontend/index.css | 0 .../frontend/index.html | 0 .../frontend/index.js | 0 .../frontend/not_found.html | 0 .../frontend/package.json | 0 .../frontend/ui.js | 0 .../frontend/utils.js | 0 .../frontend/wallet-icon.png | Bin .../src/api/get_metadata.rs | 0 .../src/api/get_note.rs | 0 .../src/api/get_pow.rs | 0 .../src/api/get_tokens.rs | 0 .../src/api/mod.rs | 0 .../src/api_key.rs | 0 .../src/frontend.rs | 0 .../src/logging.rs | 0 bin/{faucet-operator => faucet}/src/main.rs | 0 .../src/network.rs | 0 .../src/testing/mod.rs | 0 .../src/testing/stub_rpc_api.rs | 0 docs/src/getting-started/cli.md | 14 ++-- docs/src/getting-started/installation.md | 12 ++-- docs/src/getting-started/quick-start.md | 16 ++--- .../faucet/miden-faucet-operator.service | 16 ----- packaging/faucet/miden-faucet.service | 16 +++++ packaging/faucet/postinst | 18 ++--- packaging/faucet/postrm | 8 +-- 41 files changed, 100 insertions(+), 113 deletions(-) rename bin/{faucet-operator => faucet}/.env (100%) rename bin/{faucet-operator => faucet}/Cargo.toml (82%) rename bin/{faucet-operator => faucet}/README.md (100%) rename bin/{faucet-operator => faucet}/build.rs (100%) rename bin/{faucet-operator => faucet}/frontend/api.js (100%) rename bin/{faucet-operator => faucet}/frontend/app.js (100%) rename bin/{faucet-operator => faucet}/frontend/background.png (100%) rename bin/{faucet-operator => faucet}/frontend/favicon.ico (100%) rename bin/{faucet-operator => faucet}/frontend/index.css (100%) rename bin/{faucet-operator => faucet}/frontend/index.html (100%) rename bin/{faucet-operator => faucet}/frontend/index.js (100%) rename bin/{faucet-operator => faucet}/frontend/not_found.html (100%) rename bin/{faucet-operator => faucet}/frontend/package.json (100%) rename bin/{faucet-operator => faucet}/frontend/ui.js (100%) rename bin/{faucet-operator => faucet}/frontend/utils.js (100%) rename bin/{faucet-operator => faucet}/frontend/wallet-icon.png (100%) rename bin/{faucet-operator => faucet}/src/api/get_metadata.rs (100%) rename bin/{faucet-operator => faucet}/src/api/get_note.rs (100%) rename bin/{faucet-operator => faucet}/src/api/get_pow.rs (100%) rename bin/{faucet-operator => faucet}/src/api/get_tokens.rs (100%) rename bin/{faucet-operator => faucet}/src/api/mod.rs (100%) rename bin/{faucet-operator => faucet}/src/api_key.rs (100%) rename bin/{faucet-operator => faucet}/src/frontend.rs (100%) rename bin/{faucet-operator => faucet}/src/logging.rs (100%) rename bin/{faucet-operator => faucet}/src/main.rs (100%) rename bin/{faucet-operator => faucet}/src/network.rs (100%) rename bin/{faucet-operator => faucet}/src/testing/mod.rs (100%) rename bin/{faucet-operator => faucet}/src/testing/stub_rpc_api.rs (100%) delete mode 100644 packaging/faucet/miden-faucet-operator.service create mode 100644 packaging/faucet/miden-faucet.service diff --git a/.github/actions/debian/action.yml b/.github/actions/debian/action.yml index 4b0df516..1c2271ff 100644 --- a/.github/actions/debian/action.yml +++ b/.github/actions/debian/action.yml @@ -19,19 +19,19 @@ inputs: description: Name of binary crate being packaged. type: choice options: - - miden-faucet-operator + - miden-faucet crate_dir: required: true description: Name of crate being packaged. type: choice options: - - faucet-operator + - miden-faucet service: required: true description: The service to build the packages for. type: choice options: - - miden-faucet-operator + - miden-faucet package: required: true description: Name of packaging directory. diff --git a/.github/workflows/publish-debian.yml b/.github/workflows/publish-debian.yml index ee62a809..d78bf945 100644 --- a/.github/workflows/publish-debian.yml +++ b/.github/workflows/publish-debian.yml @@ -19,8 +19,8 @@ permissions: contents: write jobs: - publish-faucet-operator: - name: Publish Faucet Operator ${{ matrix.arch }} Debian + publish-faucet: + name: Publish Faucet ${{ matrix.arch }} Debian strategy: matrix: arch: [amd64, arm64] @@ -31,13 +31,13 @@ jobs: uses: actions/checkout@main with: fetch-depth: 0 - - name: Build and Publish Faucet Operator + - name: Build and Publish Faucet uses: ./.github/actions/debian with: github_token: ${{ secrets.GITHUB_TOKEN }} gitref: ${{ env.version }} - crate_dir: faucet-operator - service: miden-faucet-operator + crate_dir: faucet + service: miden-faucet package: faucet - crate: miden-faucet-operator + crate: miden-faucet arch: ${{ matrix.arch }} diff --git a/Cargo.lock b/Cargo.lock index 2dc34d21..66e84e1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2155,78 +2155,78 @@ dependencies = [ ] [[package]] -name = "miden-faucet-client" +name = "miden-faucet" version = "0.12.4" dependencies = [ "anyhow", + "async-trait", "axum", + "axum-extra", + "base64", "clap", - "hex", + "fantoccini", + "http 1.4.0", + "humantime", "miden-client", + "miden-client-sqlite-store", "miden-faucet-lib", + "miden-node-proto", "miden-pow-rate-limiter", + "miden-testing", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "rand", + "rand_chacha", "reqwest", "serde", "serde_json", + "sha2", "thiserror 2.0.17", "tokio", + "tokio-stream", + "tonic", + "tonic-web", + "tower", + "tower-http", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", "url", ] [[package]] -name = "miden-faucet-lib" +name = "miden-faucet-client" version = "0.12.4" dependencies = [ "anyhow", + "axum", + "clap", + "hex", "miden-client", - "miden-client-sqlite-store", + "miden-faucet-lib", + "miden-pow-rate-limiter", "rand", + "reqwest", "serde", + "serde_json", "thiserror 2.0.17", "tokio", - "tracing", "url", ] [[package]] -name = "miden-faucet-operator" +name = "miden-faucet-lib" version = "0.12.4" dependencies = [ "anyhow", - "async-trait", - "axum", - "axum-extra", - "base64", - "clap", - "fantoccini", - "http 1.4.0", - "humantime", "miden-client", "miden-client-sqlite-store", - "miden-faucet-lib", - "miden-node-proto", - "miden-pow-rate-limiter", - "miden-testing", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", "rand", - "rand_chacha", - "reqwest", "serde", - "serde_json", - "sha2", "thiserror 2.0.17", "tokio", - "tokio-stream", - "tonic", - "tonic-web", - "tower", - "tower-http", "tracing", - "tracing-opentelemetry", - "tracing-subscriber", "url", ] diff --git a/Cargo.toml b/Cargo.toml index b2f87a62..3a0a1787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bin/faucet-client", "bin/faucet-operator", "crates/faucet"] +members = ["bin/faucet", "bin/faucet-client", "crates/faucet"] resolver = "2" diff --git a/Makefile b/Makefile index c24855a7..555351e6 100644 --- a/Makefile +++ b/Makefile @@ -80,8 +80,8 @@ check: ## Check all targets and features for errors without code generation .PHONY: install-faucet install-faucet: ## Installs faucet + ${BUILD_PROTO} cargo install --path bin/faucet --locked ${BUILD_PROTO} cargo install --path bin/faucet-client --locked - ${BUILD_PROTO} cargo install --path bin/faucet-operator --locked .PHONY: check-tools check-tools: ## Checks if development tools are installed diff --git a/README.md b/README.md index 84a3994e..8ed2097e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ For comprehensive guides, API reference, and examples, see the [Miden Faucet Doc ## Running the faucet The faucet comes with two CLI tools: -- **miden-faucet-operator**: Runs the faucet, used for initializing and starting the faucet. +- **miden-faucet**: Runs the faucet, used for initializing and starting the faucet. - **miden-faucet-client**: Used for interacting with a live faucet, i.e. for requesting tokens from a running faucet. 1. Install both faucet binaries: @@ -20,7 +20,7 @@ make install-faucet 2. Initialize the faucet server. This will generate a new account with the specified token configuration and save the account data to a local SQLite store: ```bash -miden-faucet-operator init \ +miden-faucet init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ @@ -29,11 +29,11 @@ miden-faucet-operator init \ > [!TIP] > This account will not be created on chain yet, creation on chain will happen on the first minting transaction. -> You can also run the legacy alias `miden-faucet` for backwards compatibility; it runs the same `miden-faucet-operator` binary. +> You can also run the legacy alias `miden-faucet` for backwards compatibility; it runs the same `miden-faucet` binary. 3. Start the faucet: ```bash -miden-faucet-operator start \ +miden-faucet start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --explorer-url https://testnet.midenscan.com \ diff --git a/bin/faucet-operator/.env b/bin/faucet/.env similarity index 100% rename from bin/faucet-operator/.env rename to bin/faucet/.env diff --git a/bin/faucet-operator/Cargo.toml b/bin/faucet/Cargo.toml similarity index 82% rename from bin/faucet-operator/Cargo.toml rename to bin/faucet/Cargo.toml index 843271aa..ecfd1e28 100644 --- a/bin/faucet-operator/Cargo.toml +++ b/bin/faucet/Cargo.toml @@ -1,29 +1,16 @@ [package] authors.workspace = true -description = "Token faucet operator for Miden faucet" +description = "Token faucet application for Miden testnet" edition.workspace = true homepage.workspace = true -keywords = ["faucet", "miden", "operator"] +keywords = ["faucet", "miden", "node"] license.workspace = true -name = "miden-faucet-operator" +name = "miden-faucet" readme = "README.md" repository.workspace = true rust-version.workspace = true version.workspace = true -# Define both binary names pointing to the same source for backwards compatibility -[[bin]] -name = "miden-faucet-operator" -path = "src/main.rs" - -[[bin]] -name = "miden-faucet" -path = "src/main.rs" -# Avoid running tests twice for the same binary target; tests are exercised via `miden-faucet-operator`. -bench = false -doctest = false -test = false - [lints] workspace = true diff --git a/bin/faucet-operator/README.md b/bin/faucet/README.md similarity index 100% rename from bin/faucet-operator/README.md rename to bin/faucet/README.md diff --git a/bin/faucet-operator/build.rs b/bin/faucet/build.rs similarity index 100% rename from bin/faucet-operator/build.rs rename to bin/faucet/build.rs diff --git a/bin/faucet-operator/frontend/api.js b/bin/faucet/frontend/api.js similarity index 100% rename from bin/faucet-operator/frontend/api.js rename to bin/faucet/frontend/api.js diff --git a/bin/faucet-operator/frontend/app.js b/bin/faucet/frontend/app.js similarity index 100% rename from bin/faucet-operator/frontend/app.js rename to bin/faucet/frontend/app.js diff --git a/bin/faucet-operator/frontend/background.png b/bin/faucet/frontend/background.png similarity index 100% rename from bin/faucet-operator/frontend/background.png rename to bin/faucet/frontend/background.png diff --git a/bin/faucet-operator/frontend/favicon.ico b/bin/faucet/frontend/favicon.ico similarity index 100% rename from bin/faucet-operator/frontend/favicon.ico rename to bin/faucet/frontend/favicon.ico diff --git a/bin/faucet-operator/frontend/index.css b/bin/faucet/frontend/index.css similarity index 100% rename from bin/faucet-operator/frontend/index.css rename to bin/faucet/frontend/index.css diff --git a/bin/faucet-operator/frontend/index.html b/bin/faucet/frontend/index.html similarity index 100% rename from bin/faucet-operator/frontend/index.html rename to bin/faucet/frontend/index.html diff --git a/bin/faucet-operator/frontend/index.js b/bin/faucet/frontend/index.js similarity index 100% rename from bin/faucet-operator/frontend/index.js rename to bin/faucet/frontend/index.js diff --git a/bin/faucet-operator/frontend/not_found.html b/bin/faucet/frontend/not_found.html similarity index 100% rename from bin/faucet-operator/frontend/not_found.html rename to bin/faucet/frontend/not_found.html diff --git a/bin/faucet-operator/frontend/package.json b/bin/faucet/frontend/package.json similarity index 100% rename from bin/faucet-operator/frontend/package.json rename to bin/faucet/frontend/package.json diff --git a/bin/faucet-operator/frontend/ui.js b/bin/faucet/frontend/ui.js similarity index 100% rename from bin/faucet-operator/frontend/ui.js rename to bin/faucet/frontend/ui.js diff --git a/bin/faucet-operator/frontend/utils.js b/bin/faucet/frontend/utils.js similarity index 100% rename from bin/faucet-operator/frontend/utils.js rename to bin/faucet/frontend/utils.js diff --git a/bin/faucet-operator/frontend/wallet-icon.png b/bin/faucet/frontend/wallet-icon.png similarity index 100% rename from bin/faucet-operator/frontend/wallet-icon.png rename to bin/faucet/frontend/wallet-icon.png diff --git a/bin/faucet-operator/src/api/get_metadata.rs b/bin/faucet/src/api/get_metadata.rs similarity index 100% rename from bin/faucet-operator/src/api/get_metadata.rs rename to bin/faucet/src/api/get_metadata.rs diff --git a/bin/faucet-operator/src/api/get_note.rs b/bin/faucet/src/api/get_note.rs similarity index 100% rename from bin/faucet-operator/src/api/get_note.rs rename to bin/faucet/src/api/get_note.rs diff --git a/bin/faucet-operator/src/api/get_pow.rs b/bin/faucet/src/api/get_pow.rs similarity index 100% rename from bin/faucet-operator/src/api/get_pow.rs rename to bin/faucet/src/api/get_pow.rs diff --git a/bin/faucet-operator/src/api/get_tokens.rs b/bin/faucet/src/api/get_tokens.rs similarity index 100% rename from bin/faucet-operator/src/api/get_tokens.rs rename to bin/faucet/src/api/get_tokens.rs diff --git a/bin/faucet-operator/src/api/mod.rs b/bin/faucet/src/api/mod.rs similarity index 100% rename from bin/faucet-operator/src/api/mod.rs rename to bin/faucet/src/api/mod.rs diff --git a/bin/faucet-operator/src/api_key.rs b/bin/faucet/src/api_key.rs similarity index 100% rename from bin/faucet-operator/src/api_key.rs rename to bin/faucet/src/api_key.rs diff --git a/bin/faucet-operator/src/frontend.rs b/bin/faucet/src/frontend.rs similarity index 100% rename from bin/faucet-operator/src/frontend.rs rename to bin/faucet/src/frontend.rs diff --git a/bin/faucet-operator/src/logging.rs b/bin/faucet/src/logging.rs similarity index 100% rename from bin/faucet-operator/src/logging.rs rename to bin/faucet/src/logging.rs diff --git a/bin/faucet-operator/src/main.rs b/bin/faucet/src/main.rs similarity index 100% rename from bin/faucet-operator/src/main.rs rename to bin/faucet/src/main.rs diff --git a/bin/faucet-operator/src/network.rs b/bin/faucet/src/network.rs similarity index 100% rename from bin/faucet-operator/src/network.rs rename to bin/faucet/src/network.rs diff --git a/bin/faucet-operator/src/testing/mod.rs b/bin/faucet/src/testing/mod.rs similarity index 100% rename from bin/faucet-operator/src/testing/mod.rs rename to bin/faucet/src/testing/mod.rs diff --git a/bin/faucet-operator/src/testing/stub_rpc_api.rs b/bin/faucet/src/testing/stub_rpc_api.rs similarity index 100% rename from bin/faucet-operator/src/testing/stub_rpc_api.rs rename to bin/faucet/src/testing/stub_rpc_api.rs diff --git a/docs/src/getting-started/cli.md b/docs/src/getting-started/cli.md index 14c33ad0..a431d7f9 100644 --- a/docs/src/getting-started/cli.md +++ b/docs/src/getting-started/cli.md @@ -4,7 +4,7 @@ This guide shows the available commands and their configuration options to run w The faucet comes with two CLI tools: -- **miden-faucet-operator**: Runs the faucet, used for initializing and starting the faucet. +- **miden-faucet**: Runs the faucet, used for initializing and starting the faucet. - **miden-faucet-client**: Used for interacting with a live faucet, i.e. for requesting tokens from a running faucet. ## Available Commands @@ -28,7 +28,7 @@ The Miden Faucet can be configured using: ### Basic Configuration ```bash -miden-faucet-operator init \ +miden-faucet init \ --token-symbol \ --decimals \ --max-supply \ @@ -37,7 +37,7 @@ miden-faucet-operator init \ ``` ```bash -miden-faucet-operator start \ +miden-faucet start \ --api-bind-url \ --frontend-url \ --node-url \ @@ -182,7 +182,7 @@ export MIDEN_FAUCET_API_KEYS=key1,key2,key3 ### Generate API Keys ```bash -miden-faucet-operator create-api-key +miden-faucet create-api-key ``` This generates an API key that can be used for authentication. It is printed to stdout. @@ -215,20 +215,20 @@ Enable OpenTelemetry for production monitoring: ## Configuration Example ```bash -miden-faucet-operator init \ +miden-faucet init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --node-url http://localhost:57291 -miden-faucet-operator start \ +miden-faucet start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --node-url http://localhost:57291 \ --network localhost ``` -For detailed options, run `miden-faucet-operator [COMMAND] --help`. The legacy alias `miden-faucet` is still available for backwards compatibility. +For detailed options, run `miden-faucet [COMMAND] --help`. The legacy alias `miden-faucet` is still available for backwards compatibility. ## Requesting tokens from a live faucet diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index a5691752..afe62bc5 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -38,14 +38,14 @@ sudo apt install llvm clang bindgen pkg-config libssl-dev libsqlite3-dev Install the latest faucet binary: ```sh -cargo install miden-faucet-operator --locked +cargo install miden-faucet --locked cargo install miden-faucet-client --locked ``` This will install the latest official version of the faucet. You can install a specific version `x.y.z` using ```sh -cargo install miden-faucet-operator --locked --version x.y.z +cargo install miden-faucet --locked --version x.y.z cargo install miden-faucet-client --locked --version x.y.z ``` @@ -55,18 +55,18 @@ this for advanced use only. The incantation is a little different as you'll be t ```sh # Install from a specific branch -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --branch +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet --branch cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --branch # Install a specific tag -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --tag +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet --tag cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --tag # Install a specific git revision -cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-operator --rev +cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet --rev cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --rev -> Use `miden-faucet-operator` to initialize/start the faucet service, and `miden-faucet-client` to mint from a running faucet. The legacy `miden-faucet` binary name is still available as an alias for the operator. +> Use `miden-faucet` to initialize/start the faucet service, and `miden-faucet-client` to mint from a running faucet. The legacy `miden-faucet` binary name is still available as an alias for the operator. ``` More information on the various `cargo install` options can be found diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index a1576db3..9c9d4804 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -12,7 +12,7 @@ Get the Miden Faucet running in minutes. First, we need to initialize the faucet with a new account that will hold the tokens to be distributed. This command generates a new account with the specified token configuration and saves the account data to a local SQLite store. The account is not yet deployed to the network - that will happen when the faucet is running and the first transaction is sent to the node. ```bash -miden-faucet-operator init \ +miden-faucet init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ @@ -24,7 +24,7 @@ miden-faucet-operator init \ Next, start the faucet by specifying the addresses where the API and the frontend will be served, the address of the Miden node, and the network configuration. The API server will handle incoming token requests and manage the minting process. ```bash -miden-faucet-operator start \ +miden-faucet start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --node-url https://rpc.testnet.miden.io \ @@ -72,12 +72,12 @@ You can also programmatically interact with the REST API to mint tokens. Check o If you have a Miden Node running locally, you can run the faucet against that node. ```bash -miden-faucet-operator init \ +miden-faucet init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 -miden-faucet-operator start \ +miden-faucet start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 ``` @@ -87,13 +87,13 @@ miden-faucet-operator start \ Connect to the node deployed in Miden Devnet. ```bash -miden-faucet-operator init \ +miden-faucet init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --network devnet -miden-faucet-operator start \ +miden-faucet start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --network devnet @@ -104,13 +104,13 @@ miden-faucet-operator start \ Connect to the node deployed in Miden Testnet. ```bash -miden-faucet-operator init \ +miden-faucet init \ --token-symbol MIDEN \ --decimals 6 \ --max-supply 100000000000000000 \ --network testnet -miden-faucet-operator start \ +miden-faucet start \ --frontend-url http://localhost:8080 \ --api-bind-url http://localhost:8000 \ --explorer-url https://testnet.midenscan.com \ diff --git a/packaging/faucet/miden-faucet-operator.service b/packaging/faucet/miden-faucet-operator.service deleted file mode 100644 index 8e845075..00000000 --- a/packaging/faucet/miden-faucet-operator.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Miden faucet -Wants=network-online.target - -[Install] -WantedBy=multi-user.target - -[Service] -Type=exec -Environment="OTEL_SERVICE_NAME=miden-faucet-operator" -EnvironmentFile=/lib/systemd/system/miden-faucet-operator.env -ExecStart=/usr/bin/miden-faucet-operator start -WorkingDirectory=/opt/miden-faucet-operator -User=miden-faucet-operator -RestartSec=5 -Restart=always diff --git a/packaging/faucet/miden-faucet.service b/packaging/faucet/miden-faucet.service new file mode 100644 index 00000000..babbda70 --- /dev/null +++ b/packaging/faucet/miden-faucet.service @@ -0,0 +1,16 @@ +[Unit] +Description=Miden faucet +Wants=network-online.target + +[Install] +WantedBy=multi-user.target + +[Service] +Type=exec +Environment="OTEL_SERVICE_NAME=miden-faucet" +EnvironmentFile=/lib/systemd/system/miden-faucet.env +ExecStart=/usr/bin/miden-faucet start +WorkingDirectory=/opt/miden-faucet +User=miden-faucet +RestartSec=5 +Restart=always diff --git a/packaging/faucet/postinst b/packaging/faucet/postinst index d321ee85..c7c4b8b8 100644 --- a/packaging/faucet/postinst +++ b/packaging/faucet/postinst @@ -3,24 +3,24 @@ # This is a postinstallation script so the service can be configured and started when requested. # user is expected by the systemd service file and `/opt/` is its working directory, -sudo adduser --disabled-password --disabled-login --shell /usr/sbin/nologin --quiet --system --no-create-home --home /nonexistent miden-faucet-operator +sudo adduser --disabled-password --disabled-login --shell /usr/sbin/nologin --quiet --system --no-create-home --home /nonexistent miden-faucet # Working folder. -if [ -d "/opt/miden-faucet-operator" ] +if [ -d "/opt/miden-faucet" ] then - echo "Directory /opt/miden-faucet-operator exists." + echo "Directory /opt/miden-faucet exists." else - mkdir -p /opt/miden-faucet-operator + mkdir -p /opt/miden-faucet fi -sudo chown -R miden-faucet-operator /opt/miden-faucet-operator +sudo chown -R miden-faucet /opt/miden-faucet # Configuration folder -if [ -d "/etc/opt/miden-faucet-operator" ] +if [ -d "/etc/opt/miden-faucet" ] then - echo "Directory /etc/opt/miden-faucet-operator exists." + echo "Directory /etc/opt/miden-faucet exists." else - mkdir -p /etc/opt/miden-faucet-operator + mkdir -p /etc/opt/miden-faucet fi -sudo chown -R miden-faucet-operator /etc/opt/miden-faucet-operator +sudo chown -R miden-faucet /etc/opt/miden-faucet sudo systemctl daemon-reload diff --git a/packaging/faucet/postrm b/packaging/faucet/postrm index c110d1b5..646b5144 100644 --- a/packaging/faucet/postrm +++ b/packaging/faucet/postrm @@ -1,9 +1,9 @@ #!/bin/bash # ############### -# Remove miden-faucet-operator installs +# Remove miden-faucet installs ############## -sudo rm -rf /lib/systemd/system/miden-faucet-operator.service -sudo rm -rf /etc/opt/miden-faucet-operator -sudo deluser miden-faucet-operator +sudo rm -rf /lib/systemd/system/miden-faucet.service +sudo rm -rf /etc/opt/miden-faucet +sudo deluser miden-faucet sudo systemctl daemon-reload From 3874b6eccf49f2201b74ea17f80ba6c8b9c23199 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 16 Jan 2026 13:57:06 +0200 Subject: [PATCH 21/23] Update CHANGELOG.md Co-authored-by: Marti --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c218b05..ca837905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.12.5 (TBD) -- [BREAKING] Renamed the faucet CLI to `miden-faucet-operator`, added a new `miden-faucet-client` binary with the `mint` command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). +- [BREAKING] Added a new `miden-faucet-client` binary with the `mint` command ([#195](https://github.com/0xMiden/miden-faucet/pull/195)). ## 0.12.4 (2025-12-04) From b5b483c70ca49cb610f53be2f6f8fbca39eaaec1 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 16 Jan 2026 13:57:23 +0200 Subject: [PATCH 22/23] Update bin/faucet-client/src/main.rs Co-authored-by: Marti --- bin/faucet-client/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/faucet-client/src/main.rs b/bin/faucet-client/src/main.rs index a0569f16..adf44cce 100644 --- a/bin/faucet-client/src/main.rs +++ b/bin/faucet-client/src/main.rs @@ -1,7 +1,7 @@ use clap::{Parser, Subcommand}; use miden_faucet_client::mint; -/// Operator CLI for interacting with a live faucet. +/// Client CLI for interacting with a live faucet. #[derive(Parser)] #[command(version, about, long_about = None)] struct Cli { From 2c1c2bdc3658efa41b81321cd7cf6896ce214933 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 16 Jan 2026 13:57:43 +0200 Subject: [PATCH 23/23] Update docs/src/getting-started/installation.md Co-authored-by: Marti --- docs/src/getting-started/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index afe62bc5..a4fc802b 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -66,7 +66,7 @@ cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-fauce cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet --rev cargo install --locked --git https://github.com/0xMiden/miden-faucet miden-faucet-client --rev -> Use `miden-faucet` to initialize/start the faucet service, and `miden-faucet-client` to mint from a running faucet. The legacy `miden-faucet` binary name is still available as an alias for the operator. +> Use `miden-faucet` to initialize/start the faucet service, and `miden-faucet-client` to mint from a running faucet. ``` More information on the various `cargo install` options can be found