From 4043ea6f1666e3183c52b1da5c90ff4cb6a40e7f Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 14:20:26 -0300 Subject: [PATCH 01/26] chore: list make targets whose name contains a digit --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 25aeda96..9e580d18 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ .PHONY: help help: - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' # -- variables ------------------------------------------------------------------------------------ From 566223a8007b54f2b3c6263861a976b39c10502d Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 14:20:37 -0300 Subject: [PATCH 02/26] test: add an end-to-end test against a real node and funding service --- Makefile | 34 ++++++++ bin/faucet/tests/e2e.rs | 177 ++++++++++++++++++++++++++++++++++++++++ docker-compose.e2e.yml | 14 ++++ 3 files changed, 225 insertions(+) create mode 100644 bin/faucet/tests/e2e.rs create mode 100644 docker-compose.e2e.yml diff --git a/Makefile b/Makefile index 9e580d18..f1cc8e21 100644 --- a/Makefile +++ b/Makefile @@ -78,6 +78,40 @@ book: ## Builds the book & serves documentation site test: ## Runs all tests cargo nextest run --release --all-features --workspace +# The end-to-end test runs the faucet against a real node and funding service, taken from the node +# repo's compose stack. The version follows the `miden-node-proto-build` pin, so bumping that +# dependency moves the whole network with it and nothing here needs editing. +NODE_VERSION = $(shell sed -n 's/^miden-node-proto-build *= *{ *version *= *"=\{0,1\}\([^"]*\)".*/\1/p' bin/faucet/Cargo.toml) +NODE_CHECKOUT = target/e2e/node-$(NODE_VERSION) +NODE_REGISTRY = ghcr.io/0xmiden +E2E_IMAGES = MIDEN_NODE_IMAGE=$(NODE_REGISTRY)/miden-node:v$(NODE_VERSION) \ + MIDEN_VALIDATOR_IMAGE=$(NODE_REGISTRY)/miden-validator:v$(NODE_VERSION) \ + MIDEN_NTX_BUILDER_IMAGE=$(NODE_REGISTRY)/miden-ntx-builder:v$(NODE_VERSION) \ + MIDEN_REMOTE_PROVER_IMAGE=$(NODE_REGISTRY)/miden-remote-prover:v$(NODE_VERSION) \ + MIDEN_FUNDING_SERVICE_IMAGE=$(NODE_REGISTRY)/miden-funding-service:v$(NODE_VERSION) \ + MIDEN_USDCX_GENESIS_IMAGE=$(NODE_REGISTRY)/miden-usdcx-genesis:v$(NODE_VERSION) +E2E_COMPOSE = $(E2E_IMAGES) docker compose --project-name miden-faucet-e2e -f $(NODE_CHECKOUT)/docker-compose.yml -f docker-compose.e2e.yml + +$(NODE_CHECKOUT): + git clone --depth 1 --branch v$(NODE_VERSION) https://github.com/0xMiden/node $(NODE_CHECKOUT) + +.PHONY: e2e-network-up +e2e-network-up: $(NODE_CHECKOUT) ## Starts the node and funding service the end-to-end test needs + $(E2E_COMPOSE) up --detach funding-service + +.PHONY: e2e-network-down +e2e-network-down: ## Stops the end-to-end network and deletes its data + $(E2E_COMPOSE) down --volumes --remove-orphans + +.PHONY: e2e-network-logs +e2e-network-logs: ## Prints the logs of the end-to-end network + $(E2E_COMPOSE) logs --no-color + +.PHONY: test-e2e +test-e2e: e2e-network-up ## Runs the end-to-end test against a real node and funding service + cargo test --locked -p miden-faucet --test e2e -- --ignored --nocapture; \ + status=$$?; $(MAKE) e2e-network-down; exit $$status + # --- checking ------------------------------------------------------------------------------------ .PHONY: check diff --git a/bin/faucet/tests/e2e.rs b/bin/faucet/tests/e2e.rs new file mode 100644 index 00000000..4a587cbd --- /dev/null +++ b/bin/faucet/tests/e2e.rs @@ -0,0 +1,177 @@ +//! End-to-end test against a real node and a real funding service. +//! +//! The network is not started here. `make test-e2e` brings up the node's compose stack, waits for +//! it, runs this test and tears it down again. The test is ignored by default so that `make test` +//! keeps working without Docker. +//! +//! Point it at an existing network with `MIDEN_FAUCET_E2E_NODE_URL` and +//! `MIDEN_FAUCET_E2E_FUNDING_SERVICE_URL`. + +use std::process::{Child, Command, Stdio}; +use std::time::{Duration, Instant}; + +use miden_faucet_lib::requests::{GetPowResponse, GetTokensResponse}; +use miden_pow_rate_limiter::Challenge; +use miden_protocol::account::AccountId; +use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; + +/// Where `make test-e2e` publishes the network. These are not the node's usual ports: the stack is +/// moved aside so it does not clash with a network the developer is already running. +const DEFAULT_NODE_URL: &str = "http://127.0.0.1:57391"; +const DEFAULT_FUNDING_SERVICE_URL: &str = "http://127.0.0.1:50501"; + +/// A port the faucet is unlikely to share with a faucet the developer is already running. +const FAUCET_API_PORT: u16 = 18000; + +/// How long the network gets to answer before the test gives up. The stack builds a genesis block +/// and starts six processes, so the first request can take a while on a cold runner. +const NETWORK_TIMEOUT: Duration = Duration::from_secs(180); + +/// How long the faucet gets to bind its API once the network is up. +const FAUCET_TIMEOUT: Duration = Duration::from_secs(30); + +/// Requesting the whole allowance would empty the funding account for later runs. +const REQUESTED_AMOUNT: u64 = 100; + +/// Requests tokens the way the frontend does, and checks that the funding service answers with a +/// note for the recipient. +#[tokio::test] +#[ignore = "needs a node and a funding service, run it with `make test-e2e`"] +async fn a_token_request_reaches_the_funding_service() { + let node_url = url_from_env("MIDEN_FAUCET_E2E_NODE_URL", DEFAULT_NODE_URL); + let funding_service_url = + url_from_env("MIDEN_FAUCET_E2E_FUNDING_SERVICE_URL", DEFAULT_FUNDING_SERVICE_URL); + let client = reqwest::Client::new(); + + wait_for(&client, &format!("{funding_service_url}/status"), NETWORK_TIMEOUT) + .await + .expect("the funding service should be reachable, is the network up?"); + + let faucet_url = format!("http://127.0.0.1:{FAUCET_API_PORT}"); + let _faucet = Faucet::start(&node_url, &funding_service_url); + wait_for(&client, &format!("{faucet_url}/get_metadata"), FAUCET_TIMEOUT) + .await + .expect("the faucet should serve its metadata"); + + let recipient = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE) + .expect("the test account id is valid") + .to_hex(); + + // The faucet hands out a challenge per request and rejects a request without a solution to it. + let challenge: GetPowResponse = client + .get(format!("{faucet_url}/pow")) + .query(&[("account_id", recipient.as_str()), ("amount", &REQUESTED_AMOUNT.to_string())]) + .send() + .await + .expect("the challenge request should reach the faucet") + .json() + .await + .expect("the faucet should answer with a challenge"); + + let nonce = solve(&challenge.challenge); + + let response = client + .get(format!("{faucet_url}/get_tokens")) + .query(&[ + ("account_id", recipient.as_str()), + ("asset_amount", &REQUESTED_AMOUNT.to_string()), + ("challenge", &challenge.challenge), + ("nonce", &nonce.to_string()), + ]) + .send() + .await + .expect("the token request should reach the faucet"); + + let status = response.status(); + let body = response.text().await.expect("the faucet should answer with a body"); + assert!(status.is_success(), "the faucet answered {status}: {body}"); + + let tokens: GetTokensResponse = + serde_json::from_str(&body).unwrap_or_else(|_| panic!("unexpected response body: {body}")); + assert_eq!( + tokens.note_id.len(), + 66, + "the note id should be a 32-byte hex string: {tokens:?}" + ); +} + +/// The faucet under test, stopped when it goes out of scope. +struct Faucet(Child); + +impl Faucet { + fn start(node_url: &str, funding_service_url: &str) -> Self { + // The faucet reads its API keys on startup, so the file has to exist even with no keys in + // it. Writing one keeps the test independent of the directory it runs from. + let api_keys_path = std::env::temp_dir().join("miden-faucet-e2e.keys"); + std::fs::write(&api_keys_path, "").expect("the API keys file should be written"); + + let faucet = Command::new(env!("CARGO_BIN_EXE_miden-faucet")) + .args([ + "start", + "--funding-service-url", + funding_service_url, + "--node-url", + node_url, + "--network", + "localhost", + "--decimals", + "6", + "--api-bind-port", + &FAUCET_API_PORT.to_string(), + "--api-keys-file", + &api_keys_path.to_string_lossy(), + "--no-frontend", + ]) + // Its logs go to stdout and would drown the test's output, but a startup failure is + // reported on stderr and is the first thing to look at when the test cannot reach it. + .stdout(Stdio::null()) + .stderr(Stdio::inherit()) + .spawn() + .expect("the faucet binary should start"); + + Self(faucet) + } +} + +impl Drop for Faucet { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} + +/// Reads a URL from the environment, falling back to where the compose stack publishes it. +fn url_from_env(variable: &str, default: &str) -> String { + std::env::var(variable) + .unwrap_or_else(|_| default.to_owned()) + .trim_end_matches('/') + .to_owned() +} + +/// Polls `url` until it answers, or gives up after `timeout`. +async fn wait_for(client: &reqwest::Client, url: &str, timeout: Duration) -> Result<(), String> { + let deadline = Instant::now() + timeout; + let mut last_error = String::new(); + + while Instant::now() < deadline { + match client.get(url).send().await { + Ok(response) if response.status().is_success() => return Ok(()), + Ok(response) => last_error = format!("{url} answered {}", response.status()), + Err(error) => last_error = format!("{url}: {error}"), + } + + tokio::time::sleep(Duration::from_millis(500)).await; + } + + Err(format!("gave up after {}s, last error: {last_error}", timeout.as_secs())) +} + +/// Finds a nonce which solves the faucet's challenge. +fn solve(challenge_hex: &str) -> u64 { + let bytes = hex::decode(challenge_hex).expect("the challenge should be hex"); + let challenge = Challenge::try_from(bytes.as_slice()).expect("the challenge should decode"); + + (0..u64::MAX) + .find(|nonce| challenge.validate_pow(*nonce)) + .expect("a nonce should solve the challenge") +} diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 00000000..008b73e8 --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,14 @@ +# Port overrides for the end-to-end test, merged on top of the node repo's compose stack by +# `make test-e2e`. +# +# The stack publishes the RPC on 57291 and the funding service on 50401, which are the ports a +# developer's own local network already uses. Moving them lets the test run without stopping it. + +services: + sequencer: + ports: !override + - "127.0.0.1:57391:57291" + + funding-service: + ports: !override + - "127.0.0.1:50501:50401" From 70e9bbff0620fbde9319afe35a78b02b8011704e Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 14:20:37 -0300 Subject: [PATCH 03/26] ci: run the end-to-end test --- .github/workflows/test-e2e.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/test-e2e.yml diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml new file mode 100644 index 00000000..30f293e2 --- /dev/null +++ b/.github/workflows/test-e2e.yml @@ -0,0 +1,34 @@ +# Runs the faucet against a real node and funding service. + +name: test-e2e + +on: + push: + branches: [main, next] + pull_request: + types: [opened, reopened, synchronize] + +# Limits workflow concurrency to only the latest commit in the PR. +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + test-e2e: + name: test-e2e + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@main + - name: Rustup + run: rustup update --no-self-update + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} + # Brings up the node's compose stack from published images, runs the test against it, and + # tears it down again. + - name: Run the end-to-end test + run: make test-e2e + - name: Network logs + if: failure() + run: make e2e-network-logs From 38bf444555a9abdca755c781b5b4968f8be52613 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 14:56:57 -0300 Subject: [PATCH 04/26] test: run the end-to-end test through the client binary --- .github/workflows/test-e2e.yml | 28 +++++- Makefile | 21 +++- bin/faucet/tests/e2e.rs | 177 --------------------------------- scripts/e2e-faucet.sh | 75 ++++++++++++++ scripts/e2e-request-tokens.sh | 34 +++++++ 5 files changed, 151 insertions(+), 184 deletions(-) delete mode 100644 bin/faucet/tests/e2e.rs create mode 100755 scripts/e2e-faucet.sh create mode 100755 scripts/e2e-request-tokens.sh diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 30f293e2..542e2258 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,4 +1,7 @@ # Runs the faucet against a real node and funding service. +# +# The setup is split across steps so a failure points at the part that broke: pulling the images, +# starting the network, starting the faucet, or the request itself. name: test-e2e @@ -25,10 +28,27 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - # Brings up the node's compose stack from published images, runs the test against it, and - # tears it down again. - - name: Run the end-to-end test - run: make test-e2e + + - name: Build the faucet and its client + run: cargo build --locked -p miden-faucet -p miden-faucet-client + + - name: Start the node and the funding service + run: make e2e-network-up + + - name: Start the faucet + run: make e2e-faucet-up + + - name: Request tokens + run: make e2e-request-tokens + + - name: Faucet logs + if: failure() + run: cat target/e2e/faucet.log + - name: Network logs if: failure() run: make e2e-network-logs + + - name: Stop everything + if: always() + run: make e2e-faucet-down e2e-network-down diff --git a/Makefile b/Makefile index f1cc8e21..dfb9b605 100644 --- a/Makefile +++ b/Makefile @@ -82,6 +82,7 @@ test: ## Runs all tests # repo's compose stack. The version follows the `miden-node-proto-build` pin, so bumping that # dependency moves the whole network with it and nothing here needs editing. NODE_VERSION = $(shell sed -n 's/^miden-node-proto-build *= *{ *version *= *"=\{0,1\}\([^"]*\)".*/\1/p' bin/faucet/Cargo.toml) + NODE_CHECKOUT = target/e2e/node-$(NODE_VERSION) NODE_REGISTRY = ghcr.io/0xmiden E2E_IMAGES = MIDEN_NODE_IMAGE=$(NODE_REGISTRY)/miden-node:v$(NODE_VERSION) \ @@ -107,10 +108,24 @@ e2e-network-down: ## Stops the end-to-end network and deletes its data e2e-network-logs: ## Prints the logs of the end-to-end network $(E2E_COMPOSE) logs --no-color +.PHONY: e2e-faucet-up +e2e-faucet-up: ## Starts a faucet against the end-to-end network + cargo build --locked -p miden-faucet + scripts/e2e-faucet.sh up + +.PHONY: e2e-faucet-down +e2e-faucet-down: ## Stops the faucet started for the end-to-end test + scripts/e2e-faucet.sh down + +.PHONY: e2e-request-tokens +e2e-request-tokens: ## Requests tokens from the end-to-end faucet with the client binary + cargo build --locked -p miden-faucet-client + scripts/e2e-request-tokens.sh + .PHONY: test-e2e -test-e2e: e2e-network-up ## Runs the end-to-end test against a real node and funding service - cargo test --locked -p miden-faucet --test e2e -- --ignored --nocapture; \ - status=$$?; $(MAKE) e2e-network-down; exit $$status +test-e2e: e2e-network-up e2e-faucet-up ## Runs the end-to-end test against a real node and funding service + $(MAKE) e2e-request-tokens; \ + status=$$?; $(MAKE) e2e-faucet-down e2e-network-down; exit $$status # --- checking ------------------------------------------------------------------------------------ diff --git a/bin/faucet/tests/e2e.rs b/bin/faucet/tests/e2e.rs deleted file mode 100644 index 4a587cbd..00000000 --- a/bin/faucet/tests/e2e.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! End-to-end test against a real node and a real funding service. -//! -//! The network is not started here. `make test-e2e` brings up the node's compose stack, waits for -//! it, runs this test and tears it down again. The test is ignored by default so that `make test` -//! keeps working without Docker. -//! -//! Point it at an existing network with `MIDEN_FAUCET_E2E_NODE_URL` and -//! `MIDEN_FAUCET_E2E_FUNDING_SERVICE_URL`. - -use std::process::{Child, Command, Stdio}; -use std::time::{Duration, Instant}; - -use miden_faucet_lib::requests::{GetPowResponse, GetTokensResponse}; -use miden_pow_rate_limiter::Challenge; -use miden_protocol::account::AccountId; -use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; - -/// Where `make test-e2e` publishes the network. These are not the node's usual ports: the stack is -/// moved aside so it does not clash with a network the developer is already running. -const DEFAULT_NODE_URL: &str = "http://127.0.0.1:57391"; -const DEFAULT_FUNDING_SERVICE_URL: &str = "http://127.0.0.1:50501"; - -/// A port the faucet is unlikely to share with a faucet the developer is already running. -const FAUCET_API_PORT: u16 = 18000; - -/// How long the network gets to answer before the test gives up. The stack builds a genesis block -/// and starts six processes, so the first request can take a while on a cold runner. -const NETWORK_TIMEOUT: Duration = Duration::from_secs(180); - -/// How long the faucet gets to bind its API once the network is up. -const FAUCET_TIMEOUT: Duration = Duration::from_secs(30); - -/// Requesting the whole allowance would empty the funding account for later runs. -const REQUESTED_AMOUNT: u64 = 100; - -/// Requests tokens the way the frontend does, and checks that the funding service answers with a -/// note for the recipient. -#[tokio::test] -#[ignore = "needs a node and a funding service, run it with `make test-e2e`"] -async fn a_token_request_reaches_the_funding_service() { - let node_url = url_from_env("MIDEN_FAUCET_E2E_NODE_URL", DEFAULT_NODE_URL); - let funding_service_url = - url_from_env("MIDEN_FAUCET_E2E_FUNDING_SERVICE_URL", DEFAULT_FUNDING_SERVICE_URL); - let client = reqwest::Client::new(); - - wait_for(&client, &format!("{funding_service_url}/status"), NETWORK_TIMEOUT) - .await - .expect("the funding service should be reachable, is the network up?"); - - let faucet_url = format!("http://127.0.0.1:{FAUCET_API_PORT}"); - let _faucet = Faucet::start(&node_url, &funding_service_url); - wait_for(&client, &format!("{faucet_url}/get_metadata"), FAUCET_TIMEOUT) - .await - .expect("the faucet should serve its metadata"); - - let recipient = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE) - .expect("the test account id is valid") - .to_hex(); - - // The faucet hands out a challenge per request and rejects a request without a solution to it. - let challenge: GetPowResponse = client - .get(format!("{faucet_url}/pow")) - .query(&[("account_id", recipient.as_str()), ("amount", &REQUESTED_AMOUNT.to_string())]) - .send() - .await - .expect("the challenge request should reach the faucet") - .json() - .await - .expect("the faucet should answer with a challenge"); - - let nonce = solve(&challenge.challenge); - - let response = client - .get(format!("{faucet_url}/get_tokens")) - .query(&[ - ("account_id", recipient.as_str()), - ("asset_amount", &REQUESTED_AMOUNT.to_string()), - ("challenge", &challenge.challenge), - ("nonce", &nonce.to_string()), - ]) - .send() - .await - .expect("the token request should reach the faucet"); - - let status = response.status(); - let body = response.text().await.expect("the faucet should answer with a body"); - assert!(status.is_success(), "the faucet answered {status}: {body}"); - - let tokens: GetTokensResponse = - serde_json::from_str(&body).unwrap_or_else(|_| panic!("unexpected response body: {body}")); - assert_eq!( - tokens.note_id.len(), - 66, - "the note id should be a 32-byte hex string: {tokens:?}" - ); -} - -/// The faucet under test, stopped when it goes out of scope. -struct Faucet(Child); - -impl Faucet { - fn start(node_url: &str, funding_service_url: &str) -> Self { - // The faucet reads its API keys on startup, so the file has to exist even with no keys in - // it. Writing one keeps the test independent of the directory it runs from. - let api_keys_path = std::env::temp_dir().join("miden-faucet-e2e.keys"); - std::fs::write(&api_keys_path, "").expect("the API keys file should be written"); - - let faucet = Command::new(env!("CARGO_BIN_EXE_miden-faucet")) - .args([ - "start", - "--funding-service-url", - funding_service_url, - "--node-url", - node_url, - "--network", - "localhost", - "--decimals", - "6", - "--api-bind-port", - &FAUCET_API_PORT.to_string(), - "--api-keys-file", - &api_keys_path.to_string_lossy(), - "--no-frontend", - ]) - // Its logs go to stdout and would drown the test's output, but a startup failure is - // reported on stderr and is the first thing to look at when the test cannot reach it. - .stdout(Stdio::null()) - .stderr(Stdio::inherit()) - .spawn() - .expect("the faucet binary should start"); - - Self(faucet) - } -} - -impl Drop for Faucet { - fn drop(&mut self) { - let _ = self.0.kill(); - let _ = self.0.wait(); - } -} - -/// Reads a URL from the environment, falling back to where the compose stack publishes it. -fn url_from_env(variable: &str, default: &str) -> String { - std::env::var(variable) - .unwrap_or_else(|_| default.to_owned()) - .trim_end_matches('/') - .to_owned() -} - -/// Polls `url` until it answers, or gives up after `timeout`. -async fn wait_for(client: &reqwest::Client, url: &str, timeout: Duration) -> Result<(), String> { - let deadline = Instant::now() + timeout; - let mut last_error = String::new(); - - while Instant::now() < deadline { - match client.get(url).send().await { - Ok(response) if response.status().is_success() => return Ok(()), - Ok(response) => last_error = format!("{url} answered {}", response.status()), - Err(error) => last_error = format!("{url}: {error}"), - } - - tokio::time::sleep(Duration::from_millis(500)).await; - } - - Err(format!("gave up after {}s, last error: {last_error}", timeout.as_secs())) -} - -/// Finds a nonce which solves the faucet's challenge. -fn solve(challenge_hex: &str) -> u64 { - let bytes = hex::decode(challenge_hex).expect("the challenge should be hex"); - let challenge = Challenge::try_from(bytes.as_slice()).expect("the challenge should decode"); - - (0..u64::MAX) - .find(|nonce| challenge.validate_pow(*nonce)) - .expect("a nonce should solve the challenge") -} diff --git a/scripts/e2e-faucet.sh b/scripts/e2e-faucet.sh new file mode 100755 index 00000000..fefddd2e --- /dev/null +++ b/scripts/e2e-faucet.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Starts and stops the faucet the end-to-end test runs against. +# +# The faucet runs in the background so the test is a step of its own, which keeps a failure easy to +# read: the setup either worked or it did not, and the logs of each part stay separate. + +set -euo pipefail + +FAUCET_BIN="${FAUCET_BIN:-./target/debug/miden-faucet}" +FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" +NODE_URL="${NODE_URL:-http://127.0.0.1:57391}" +FUNDING_SERVICE_URL="${FUNDING_SERVICE_URL:-http://127.0.0.1:50501}" +DECIMALS="${DECIMALS:-6}" + +WORK_DIR="${WORK_DIR:-target/e2e}" +PID_FILE="${WORK_DIR}/faucet.pid" +LOG_FILE="${WORK_DIR}/faucet.log" +API_KEYS_FILE="${WORK_DIR}/api_keys.txt" + +# How long the faucet gets to reach the funding service and bind its API. +START_TIMEOUT_SECONDS="${START_TIMEOUT_SECONDS:-60}" + +start() { + stop + + mkdir -p "${WORK_DIR}" + # The faucet reads its API keys on startup, so the file has to exist even with no keys in it. + : >"${API_KEYS_FILE}" + : >"${LOG_FILE}" + + echo "Starting the faucet on ${FAUCET_URL}, logging to ${LOG_FILE}" + "${FAUCET_BIN}" start \ + --funding-service-url "${FUNDING_SERVICE_URL}" \ + --node-url "${NODE_URL}" \ + --network localhost \ + --decimals "${DECIMALS}" \ + --api-bind-port "${FAUCET_URL##*:}" \ + --api-keys-file "${API_KEYS_FILE}" \ + --no-frontend \ + >>"${LOG_FILE}" 2>&1 & + echo $! >"${PID_FILE}" + + for _ in $(seq "${START_TIMEOUT_SECONDS}"); do + if curl --silent --fail --output /dev/null "${FAUCET_URL}/get_metadata"; then + echo "The faucet is serving its metadata" + return 0 + fi + sleep 1 + done + + echo "error: the faucet did not answer within ${START_TIMEOUT_SECONDS}s" >&2 + cat "${LOG_FILE}" >&2 + return 1 +} + +stop() { + [[ -f "${PID_FILE}" ]] || return 0 + + local pid + pid="$(cat "${PID_FILE}")" + rm -f "${PID_FILE}" + + kill "${pid}" 2>/dev/null || return 0 + echo "Stopped the faucet (pid ${pid})" +} + +case "${1:-}" in + up) start ;; + down) stop ;; + *) + echo "usage: $0 up|down" >&2 + exit 2 + ;; +esac diff --git a/scripts/e2e-request-tokens.sh b/scripts/e2e-request-tokens.sh new file mode 100755 index 00000000..16e93435 --- /dev/null +++ b/scripts/e2e-request-tokens.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# The end-to-end test itself: requests tokens from a running faucet with the client binary, and +# checks that the faucet answers with a note. +# +# The network, the funding service and the faucet have to be running already; `make test-e2e` +# starts them. + +set -euo pipefail + +CLIENT_BIN="${CLIENT_BIN:-./target/debug/miden-faucet-client}" +FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" +# A well-formed account id. The funding service creates the note for it whether or not the account +# exists on chain, and nothing here consumes the note, so it needs no keys. +TARGET_ACCOUNT="${TARGET_ACCOUNT:-0x52488050da9336511810b97a086a5e}" +# Small enough to leave the funding account with something for the next run. +AMOUNT="${AMOUNT:-100}" + +echo "Requesting ${AMOUNT} base units for ${TARGET_ACCOUNT} from ${FAUCET_URL}" +output="$( + "${CLIENT_BIN}" mint \ + --url "${FAUCET_URL}" \ + --target-account "${TARGET_ACCOUNT}" \ + --amount "${AMOUNT}" \ + --no-consume +)" +echo "${output}" + +if ! grep -q 'note commitment: 0x' <<<"${output}"; then + echo "error: the faucet did not report a note" >&2 + exit 1 +fi + +echo "The faucet returned a note" From d7684b3467dd5a15dc35bf7f12418e03ccb4c2b0 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 15:37:22 -0300 Subject: [PATCH 05/26] test: consume the note in the end-to-end test --- .github/workflows/test-e2e.yml | 5 ++- Makefile | 12 ++++--- scripts/e2e-faucet.sh | 2 +- scripts/e2e-miden-client.sh | 42 ++++++++++++++++++++++ scripts/e2e-request-tokens.sh | 66 ++++++++++++++++++++++------------ 5 files changed, 99 insertions(+), 28 deletions(-) create mode 100755 scripts/e2e-miden-client.sh diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 542e2258..ddb313a3 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -30,7 +30,10 @@ jobs: save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - name: Build the faucet and its client - run: cargo build --locked -p miden-faucet -p miden-faucet-client + run: cargo build --release --locked -p miden-faucet -p miden-faucet-client + + - name: Download the Miden client CLI + run: make e2e-miden-client - name: Start the node and the funding service run: make e2e-network-up diff --git a/Makefile b/Makefile index dfb9b605..dbcdf374 100644 --- a/Makefile +++ b/Makefile @@ -110,20 +110,24 @@ e2e-network-logs: ## Prints the logs of the end-to-end network .PHONY: e2e-faucet-up e2e-faucet-up: ## Starts a faucet against the end-to-end network - cargo build --locked -p miden-faucet + cargo build --release --locked -p miden-faucet scripts/e2e-faucet.sh up .PHONY: e2e-faucet-down e2e-faucet-down: ## Stops the faucet started for the end-to-end test scripts/e2e-faucet.sh down +.PHONY: e2e-miden-client +e2e-miden-client: ## Downloads the Miden client CLI the end-to-end test uses + scripts/e2e-miden-client.sh + .PHONY: e2e-request-tokens -e2e-request-tokens: ## Requests tokens from the end-to-end faucet with the client binary - cargo build --locked -p miden-faucet-client +e2e-request-tokens: ## Requests tokens from the end-to-end faucet and consumes the note + cargo build --release --locked -p miden-faucet-client scripts/e2e-request-tokens.sh .PHONY: test-e2e -test-e2e: e2e-network-up e2e-faucet-up ## Runs the end-to-end test against a real node and funding service +test-e2e: e2e-network-up e2e-miden-client e2e-faucet-up ## Runs the end-to-end test against a real node and funding service $(MAKE) e2e-request-tokens; \ status=$$?; $(MAKE) e2e-faucet-down e2e-network-down; exit $$status diff --git a/scripts/e2e-faucet.sh b/scripts/e2e-faucet.sh index fefddd2e..571d8593 100755 --- a/scripts/e2e-faucet.sh +++ b/scripts/e2e-faucet.sh @@ -7,7 +7,7 @@ set -euo pipefail -FAUCET_BIN="${FAUCET_BIN:-./target/debug/miden-faucet}" +FAUCET_BIN="${FAUCET_BIN:-./target/release/miden-faucet}" FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" NODE_URL="${NODE_URL:-http://127.0.0.1:57391}" FUNDING_SERVICE_URL="${FUNDING_SERVICE_URL:-http://127.0.0.1:50501}" diff --git a/scripts/e2e-miden-client.sh b/scripts/e2e-miden-client.sh new file mode 100755 index 00000000..82f1b058 --- /dev/null +++ b/scripts/e2e-miden-client.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# Downloads the `miden-client` CLI the end-to-end test uses to create the recipient account and to +# read its balance. +# +# The release assets are prebuilt, so this takes a couple of seconds instead of the several minutes +# `cargo install miden-client-cli` needs. The version comes from the `miden-client` pin in the +# workspace `Cargo.toml`, so bumping that dependency moves this too. + +set -euo pipefail + +WORK_DIR="${WORK_DIR:-target/e2e}" +MIDEN_CLIENT_BIN="${MIDEN_CLIENT_BIN:-${WORK_DIR}/bin/miden-client}" +VERSION="${MIDEN_CLIENT_VERSION:-$(sed -n 's/^miden-client *= *{ *version *= *"=\{0,1\}\([^"]*\)".*/\1/p' Cargo.toml)}" + +if [[ -z "${VERSION}" ]]; then + echo "error: could not read the miden-client version from Cargo.toml" >&2 + exit 1 +fi + +if [[ -x "${MIDEN_CLIENT_BIN}" ]] && "${MIDEN_CLIENT_BIN}" --version 2>/dev/null | grep -q "${VERSION}"; then + echo "miden-client ${VERSION} is already at ${MIDEN_CLIENT_BIN}" + exit 0 +fi + +case "$(uname -s)-$(uname -m)" in + Darwin-arm64) asset="miden-client-aarch64-apple-darwin" ;; + Linux-x86_64) asset="miden-client-x86_64-unknown-linux-gnu" ;; + *) + echo "error: no published miden-client for $(uname -s)-$(uname -m)" >&2 + exit 1 + ;; +esac + +url="https://github.com/0xMiden/rust-sdk/releases/download/v${VERSION}/${asset}" +echo "Downloading ${url}" + +mkdir -p "$(dirname "${MIDEN_CLIENT_BIN}")" +curl --location --fail --silent --show-error --output "${MIDEN_CLIENT_BIN}" "${url}" +chmod +x "${MIDEN_CLIENT_BIN}" + +"${MIDEN_CLIENT_BIN}" --version diff --git a/scripts/e2e-request-tokens.sh b/scripts/e2e-request-tokens.sh index 16e93435..dbf3f854 100755 --- a/scripts/e2e-request-tokens.sh +++ b/scripts/e2e-request-tokens.sh @@ -1,34 +1,56 @@ #!/usr/bin/env bash # -# The end-to-end test itself: requests tokens from a running faucet with the client binary, and -# checks that the faucet answers with a note. +# The end-to-end test itself. +# +# Creates a recipient account with the `miden-client` CLI, requests tokens for it from the faucet, +# lets the faucet client consume the resulting note, and checks that the account ends up holding +# the asset. # # The network, the funding service and the faucet have to be running already; `make test-e2e` # starts them. set -euo pipefail -CLIENT_BIN="${CLIENT_BIN:-./target/debug/miden-faucet-client}" +CLIENT_BIN="${CLIENT_BIN:-./target/release/miden-faucet-client}" +WORK_DIR="${WORK_DIR:-target/e2e}" +MIDEN_CLIENT_BIN="${MIDEN_CLIENT_BIN:-${WORK_DIR}/bin/miden-client}" FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" -# A well-formed account id. The funding service creates the note for it whether or not the account -# exists on chain, and nothing here consumes the note, so it needs no keys. -TARGET_ACCOUNT="${TARGET_ACCOUNT:-0x52488050da9336511810b97a086a5e}" -# Small enough to leave the funding account with something for the next run. -AMOUNT="${AMOUNT:-100}" - -echo "Requesting ${AMOUNT} base units for ${TARGET_ACCOUNT} from ${FAUCET_URL}" -output="$( - "${CLIENT_BIN}" mint \ - --url "${FAUCET_URL}" \ - --target-account "${TARGET_ACCOUNT}" \ - --amount "${AMOUNT}" \ - --no-consume -)" -echo "${output}" - -if ! grep -q 'note commitment: 0x' <<<"${output}"; then - echo "error: the faucet did not report a note" >&2 +NODE_URL="${NODE_URL:-http://127.0.0.1:57391}" +# Bech32 prefix the faucet uses for a local network, so the client prints matching addresses. +NETWORK_ID="${NETWORK_ID:-mlcl}" +# Consuming the note pays a fee out of the note itself, so a request too small to cover the fee +# fails inside the transaction kernel. +AMOUNT="${AMOUNT:-1000000}" + +# Keep the client's config, store and keys out of the developer's own ~/.miden. +MIDEN_CLIENT_HOME="${PWD}/${WORK_DIR}/miden-client" +export MIDEN_CLIENT_HOME +rm -rf "${MIDEN_CLIENT_HOME}" +mkdir -p "${MIDEN_CLIENT_HOME}" + +echo "Pointing the client at ${NODE_URL}" +"${MIDEN_CLIENT_BIN}" init --network "${NODE_URL}" --network-id "${NETWORK_ID}" + +echo "Creating the recipient account" +account="$("${MIDEN_CLIENT_BIN}" new-wallet --account-type public | grep -o '0x[0-9a-f]\{2,\}' | head -1)" +if [[ -z "${account}" ]]; then + echo "error: could not read the new account's id" >&2 + exit 1 +fi +echo "Recipient: ${account}" + +echo "Requesting ${AMOUNT} base units from ${FAUCET_URL}" +"${CLIENT_BIN}" mint --url "${FAUCET_URL}" --target-account "${account}" --amount "${AMOUNT}" + +# The consume transaction pays the fee out of the note, so the balance is the requested amount less +# that fee. What matters here is that the account ends up holding the asset at all. +echo "Reading the recipient's balance" +balance="$("${MIDEN_CLIENT_BIN}" account --show "${account}")" +echo "${balance}" + +if ! grep -q "Fungible Asset" <<<"${balance}"; then + echo "error: the recipient holds no asset after consuming the note" >&2 exit 1 fi -echo "The faucet returned a note" +echo "The recipient received and consumed the note" From e59fc9afa87cdf3f96b15d9272be08eaea508d56 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 16:09:21 -0300 Subject: [PATCH 06/26] fix: wait longer for the note before consuming it --- bin/faucet-client/src/mint.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index e1ec5889..d22596e3 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -26,7 +26,13 @@ use tokio::task; const DEFAULT_FAUCET_URL: &str = "https://faucet-api.testnet.miden.io"; const REQUEST_TIMEOUT_MS: u64 = 30_000; -const MAX_SYNC_RETRIES: u32 = 3; +/// How long to keep looking for the note before giving up. +/// +/// The funding service answers as soon as it has queued the note, so the note reaches the chain a +/// few blocks later and the client has to wait for it. Three attempts covered the old behaviour, +/// where the service only answered once the note was committed, but not this one: a loaded network +/// takes longer than fifteen seconds to put the note in a block. +const MAX_SYNC_RETRIES: u32 = 10; const SYNC_RETRY_DELAY_SECS: u64 = 5; // CLI @@ -136,8 +142,10 @@ impl MintCmd { let note_record = note_record.ok_or_else(|| { MintClientError::NoteNotFound(format!( - "Note {note_id_hex} not found after {MAX_SYNC_RETRIES} sync attempts. \ - You can manually consume it later using: miden-client consume-notes {note_id_hex}" + "Note {note_id_hex} not found after {MAX_SYNC_RETRIES} sync attempts over \ + {}s. You can manually consume it later using: \ + miden-client consume-notes {note_id_hex}", + u64::from(MAX_SYNC_RETRIES) * SYNC_RETRY_DELAY_SECS )) })?; From 26cfb3d32b07128a44c96b8f55da7daff45e7ad4 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 16:09:54 -0300 Subject: [PATCH 07/26] ci: share the release cargo cache between the test jobs --- .github/workflows/test-e2e.yml | 7 ++++++- .github/workflows/test.yml | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index ddb313a3..a76d4dc5 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -27,7 +27,12 @@ jobs: run: rustup update --no-self-update - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} + # `make test` builds the whole workspace in release, which covers everything this job + # compiles, so both share one cache instead of each filling its own. + shared-key: release + # Saving on a pull request too means a second push to the same branch does not recompile. + # GitHub scopes a branch's cache to that branch, so this does not disturb other runs. + save-if: true - name: Build the faucet and its client run: cargo build --release --locked -p miden-faucet -p miden-faucet-client diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4624fd0..0c9c5410 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,8 @@ jobs: run: rustup update --no-self-update - uses: Swatinem/rust-cache@v2 with: + # Shared with the end-to-end job, which builds the same release artifacts. + shared-key: release save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - uses: taiki-e/install-action@nextest - name: Run tests From 5871820aeaaee64c366ea32386ebcf337dd75265 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 16:09:54 -0300 Subject: [PATCH 08/26] test: build the end-to-end binaries in a single invocation --- .github/workflows/test-e2e.yml | 2 +- Makefile | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index a76d4dc5..1e18d79a 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -35,7 +35,7 @@ jobs: save-if: true - name: Build the faucet and its client - run: cargo build --release --locked -p miden-faucet -p miden-faucet-client + run: make e2e-build - name: Download the Miden client CLI run: make e2e-miden-client diff --git a/Makefile b/Makefile index dbcdf374..334be231 100644 --- a/Makefile +++ b/Makefile @@ -108,9 +108,15 @@ e2e-network-down: ## Stops the end-to-end network and deletes its data e2e-network-logs: ## Prints the logs of the end-to-end network $(E2E_COMPOSE) logs --no-color +# One invocation for both binaries, so nothing is compiled twice under two feature resolutions. +# The other targets assume the binaries are there, which keeps each of them to the work its name +# describes. +.PHONY: e2e-build +e2e-build: ## Builds the binaries the end-to-end test runs + cargo build --release --locked -p miden-faucet -p miden-faucet-client + .PHONY: e2e-faucet-up e2e-faucet-up: ## Starts a faucet against the end-to-end network - cargo build --release --locked -p miden-faucet scripts/e2e-faucet.sh up .PHONY: e2e-faucet-down @@ -123,11 +129,10 @@ e2e-miden-client: ## Downloads the Miden client CLI the end-to-end test uses .PHONY: e2e-request-tokens e2e-request-tokens: ## Requests tokens from the end-to-end faucet and consumes the note - cargo build --release --locked -p miden-faucet-client scripts/e2e-request-tokens.sh .PHONY: test-e2e -test-e2e: e2e-network-up e2e-miden-client e2e-faucet-up ## Runs the end-to-end test against a real node and funding service +test-e2e: e2e-build e2e-network-up e2e-miden-client e2e-faucet-up ## Runs the end-to-end test against a real node and funding service $(MAKE) e2e-request-tokens; \ status=$$?; $(MAKE) e2e-faucet-down e2e-network-down; exit $$status From 6bc3b6de47806b2460df3f723adffe21a80328e9 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 16:33:44 -0300 Subject: [PATCH 09/26] test: give the end-to-end setup one make target and its own scripts directory --- .github/workflows/test-e2e.yml | 21 +++--- Makefile | 69 ++++--------------- docker-compose.e2e.yml | 14 ---- scripts/{e2e-faucet.sh => e2e/faucet.sh} | 4 +- .../miden-client.sh} | 0 scripts/e2e/network.sh | 58 ++++++++++++++++ .../request-tokens.sh} | 2 +- 7 files changed, 86 insertions(+), 82 deletions(-) delete mode 100644 docker-compose.e2e.yml rename scripts/{e2e-faucet.sh => e2e/faucet.sh} (94%) rename scripts/{e2e-miden-client.sh => e2e/miden-client.sh} (100%) create mode 100755 scripts/e2e/network.sh rename scripts/{e2e-request-tokens.sh => e2e/request-tokens.sh} (97%) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 1e18d79a..26d797fd 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,7 +1,8 @@ # Runs the faucet against a real node and funding service. # -# The setup is split across steps so a failure points at the part that broke: pulling the images, -# starting the network, starting the faucet, or the request itself. +# Each part of the setup is a step of its own so a failure points at what broke: the build, the +# images, the network, the faucet, or the request itself. `make test-e2e` chains the same scripts +# for running the whole thing locally. name: test-e2e @@ -35,19 +36,19 @@ jobs: save-if: true - name: Build the faucet and its client - run: make e2e-build + run: cargo build --release --locked -p miden-faucet -p miden-faucet-client - name: Download the Miden client CLI - run: make e2e-miden-client + run: scripts/e2e/miden-client.sh - name: Start the node and the funding service - run: make e2e-network-up + run: scripts/e2e/network.sh up - name: Start the faucet - run: make e2e-faucet-up + run: scripts/e2e/faucet.sh up - name: Request tokens - run: make e2e-request-tokens + run: scripts/e2e/request-tokens.sh - name: Faucet logs if: failure() @@ -55,8 +56,10 @@ jobs: - name: Network logs if: failure() - run: make e2e-network-logs + run: scripts/e2e/network.sh logs - name: Stop everything if: always() - run: make e2e-faucet-down e2e-network-down + run: | + scripts/e2e/faucet.sh down + scripts/e2e/network.sh down diff --git a/Makefile b/Makefile index 334be231..5681b7e6 100644 --- a/Makefile +++ b/Makefile @@ -78,63 +78,20 @@ book: ## Builds the book & serves documentation site test: ## Runs all tests cargo nextest run --release --all-features --workspace -# The end-to-end test runs the faucet against a real node and funding service, taken from the node -# repo's compose stack. The version follows the `miden-node-proto-build` pin, so bumping that -# dependency moves the whole network with it and nothing here needs editing. -NODE_VERSION = $(shell sed -n 's/^miden-node-proto-build *= *{ *version *= *"=\{0,1\}\([^"]*\)".*/\1/p' bin/faucet/Cargo.toml) - -NODE_CHECKOUT = target/e2e/node-$(NODE_VERSION) -NODE_REGISTRY = ghcr.io/0xmiden -E2E_IMAGES = MIDEN_NODE_IMAGE=$(NODE_REGISTRY)/miden-node:v$(NODE_VERSION) \ - MIDEN_VALIDATOR_IMAGE=$(NODE_REGISTRY)/miden-validator:v$(NODE_VERSION) \ - MIDEN_NTX_BUILDER_IMAGE=$(NODE_REGISTRY)/miden-ntx-builder:v$(NODE_VERSION) \ - MIDEN_REMOTE_PROVER_IMAGE=$(NODE_REGISTRY)/miden-remote-prover:v$(NODE_VERSION) \ - MIDEN_FUNDING_SERVICE_IMAGE=$(NODE_REGISTRY)/miden-funding-service:v$(NODE_VERSION) \ - MIDEN_USDCX_GENESIS_IMAGE=$(NODE_REGISTRY)/miden-usdcx-genesis:v$(NODE_VERSION) -E2E_COMPOSE = $(E2E_IMAGES) docker compose --project-name miden-faucet-e2e -f $(NODE_CHECKOUT)/docker-compose.yml -f docker-compose.e2e.yml - -$(NODE_CHECKOUT): - git clone --depth 1 --branch v$(NODE_VERSION) https://github.com/0xMiden/node $(NODE_CHECKOUT) - -.PHONY: e2e-network-up -e2e-network-up: $(NODE_CHECKOUT) ## Starts the node and funding service the end-to-end test needs - $(E2E_COMPOSE) up --detach funding-service - -.PHONY: e2e-network-down -e2e-network-down: ## Stops the end-to-end network and deletes its data - $(E2E_COMPOSE) down --volumes --remove-orphans - -.PHONY: e2e-network-logs -e2e-network-logs: ## Prints the logs of the end-to-end network - $(E2E_COMPOSE) logs --no-color - -# One invocation for both binaries, so nothing is compiled twice under two feature resolutions. -# The other targets assume the binaries are there, which keeps each of them to the work its name -# describes. -.PHONY: e2e-build -e2e-build: ## Builds the binaries the end-to-end test runs - cargo build --release --locked -p miden-faucet -p miden-faucet-client - -.PHONY: e2e-faucet-up -e2e-faucet-up: ## Starts a faucet against the end-to-end network - scripts/e2e-faucet.sh up - -.PHONY: e2e-faucet-down -e2e-faucet-down: ## Stops the faucet started for the end-to-end test - scripts/e2e-faucet.sh down - -.PHONY: e2e-miden-client -e2e-miden-client: ## Downloads the Miden client CLI the end-to-end test uses - scripts/e2e-miden-client.sh - -.PHONY: e2e-request-tokens -e2e-request-tokens: ## Requests tokens from the end-to-end faucet and consumes the note - scripts/e2e-request-tokens.sh - +# The end-to-end test starts a real node, funding service and faucet, requests tokens and consumes +# the resulting note. Each part is a script of its own so CI can run them as separate steps and see +# which one failed; this target chains them for a developer who just wants to run the whole thing. .PHONY: test-e2e -test-e2e: e2e-build e2e-network-up e2e-miden-client e2e-faucet-up ## Runs the end-to-end test against a real node and funding service - $(MAKE) e2e-request-tokens; \ - status=$$?; $(MAKE) e2e-faucet-down e2e-network-down; exit $$status +test-e2e: ## Runs the end-to-end test against a real node and funding service + cargo build --release --locked -p miden-faucet -p miden-faucet-client + scripts/e2e/miden-client.sh + scripts/e2e/network.sh up + scripts/e2e/faucet.sh up + scripts/e2e/request-tokens.sh; \ + status=$$?; \ + scripts/e2e/faucet.sh down; \ + scripts/e2e/network.sh down; \ + exit $$status # --- checking ------------------------------------------------------------------------------------ diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml deleted file mode 100644 index 008b73e8..00000000 --- a/docker-compose.e2e.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Port overrides for the end-to-end test, merged on top of the node repo's compose stack by -# `make test-e2e`. -# -# The stack publishes the RPC on 57291 and the funding service on 50401, which are the ports a -# developer's own local network already uses. Moving them lets the test run without stopping it. - -services: - sequencer: - ports: !override - - "127.0.0.1:57391:57291" - - funding-service: - ports: !override - - "127.0.0.1:50501:50401" diff --git a/scripts/e2e-faucet.sh b/scripts/e2e/faucet.sh similarity index 94% rename from scripts/e2e-faucet.sh rename to scripts/e2e/faucet.sh index 571d8593..5468787f 100755 --- a/scripts/e2e-faucet.sh +++ b/scripts/e2e/faucet.sh @@ -9,8 +9,8 @@ set -euo pipefail FAUCET_BIN="${FAUCET_BIN:-./target/release/miden-faucet}" FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" -NODE_URL="${NODE_URL:-http://127.0.0.1:57391}" -FUNDING_SERVICE_URL="${FUNDING_SERVICE_URL:-http://127.0.0.1:50501}" +NODE_URL="${NODE_URL:-http://127.0.0.1:57291}" +FUNDING_SERVICE_URL="${FUNDING_SERVICE_URL:-http://127.0.0.1:50401}" DECIMALS="${DECIMALS:-6}" WORK_DIR="${WORK_DIR:-target/e2e}" diff --git a/scripts/e2e-miden-client.sh b/scripts/e2e/miden-client.sh similarity index 100% rename from scripts/e2e-miden-client.sh rename to scripts/e2e/miden-client.sh diff --git a/scripts/e2e/network.sh b/scripts/e2e/network.sh new file mode 100755 index 00000000..30902ace --- /dev/null +++ b/scripts/e2e/network.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# +# Starts and stops the node and funding service the end-to-end test runs against. +# +# The node repo already has a compose stack for a complete local network, so it is cloned and used +# as is. The images are the published ones, so nothing is built. +# +# The stack publishes the node's RPC on 57291 and the funding service on 50401, the same ports a +# local network uses, so stop yours before running this. +# +# The version comes from the `miden-node-proto-build` pin in `bin/faucet/Cargo.toml`: bumping that +# dependency moves the whole network with it and nothing here needs editing. + +set -euo pipefail + +WORK_DIR="${WORK_DIR:-target/e2e}" +REGISTRY="${REGISTRY:-ghcr.io/0xmiden}" +PROJECT="${PROJECT:-miden-faucet-e2e}" +VERSION="${NODE_VERSION:-$(sed -n 's/^miden-node-proto-build *= *{ *version *= *"=\{0,1\}\([^"]*\)".*/\1/p' bin/faucet/Cargo.toml)}" + +if [[ -z "${VERSION}" ]]; then + echo "error: could not read the node version from bin/faucet/Cargo.toml" >&2 + exit 1 +fi + +CHECKOUT="${WORK_DIR}/node-${VERSION}" + +export MIDEN_NODE_IMAGE="${REGISTRY}/miden-node:v${VERSION}" +export MIDEN_VALIDATOR_IMAGE="${REGISTRY}/miden-validator:v${VERSION}" +export MIDEN_NTX_BUILDER_IMAGE="${REGISTRY}/miden-ntx-builder:v${VERSION}" +export MIDEN_REMOTE_PROVER_IMAGE="${REGISTRY}/miden-remote-prover:v${VERSION}" +export MIDEN_FUNDING_SERVICE_IMAGE="${REGISTRY}/miden-funding-service:v${VERSION}" +export MIDEN_USDCX_GENESIS_IMAGE="${REGISTRY}/miden-usdcx-genesis:v${VERSION}" + +compose() { + docker compose \ + --project-name "${PROJECT}" \ + --file "${CHECKOUT}/docker-compose.yml" \ + "$@" +} + +case "${1:-}" in + up) + if [[ ! -d "${CHECKOUT}" ]]; then + echo "Cloning the node repo at v${VERSION}" + git clone --depth 1 --branch "v${VERSION}" https://github.com/0xMiden/node "${CHECKOUT}" + fi + # Starting the funding service pulls in everything it depends on: the genesis bootstrap, + # the validators, the sequencer and the prover. + compose up --detach funding-service + ;; + down) compose down --volumes --remove-orphans ;; + logs) compose logs --no-color ;; + *) + echo "usage: $0 up|down|logs" >&2 + exit 2 + ;; +esac diff --git a/scripts/e2e-request-tokens.sh b/scripts/e2e/request-tokens.sh similarity index 97% rename from scripts/e2e-request-tokens.sh rename to scripts/e2e/request-tokens.sh index dbf3f854..c88a0c76 100755 --- a/scripts/e2e-request-tokens.sh +++ b/scripts/e2e/request-tokens.sh @@ -15,7 +15,7 @@ CLIENT_BIN="${CLIENT_BIN:-./target/release/miden-faucet-client}" WORK_DIR="${WORK_DIR:-target/e2e}" MIDEN_CLIENT_BIN="${MIDEN_CLIENT_BIN:-${WORK_DIR}/bin/miden-client}" FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" -NODE_URL="${NODE_URL:-http://127.0.0.1:57391}" +NODE_URL="${NODE_URL:-http://127.0.0.1:57291}" # Bech32 prefix the faucet uses for a local network, so the client prints matching addresses. NETWORK_ID="${NETWORK_ID:-mlcl}" # Consuming the note pays a fee out of the note itself, so a request too small to cover the fee From 6a3a561bafa1bad7b3fdc979d31c18a0132d7050 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 16:40:29 -0300 Subject: [PATCH 10/26] chore: cleanup docs --- .github/workflows/test-e2e.yml | 4 ---- Makefile | 3 +-- bin/faucet-client/src/mint.rs | 6 ------ scripts/e2e/faucet.sh | 3 --- scripts/e2e/network.sh | 6 ------ 5 files changed, 1 insertion(+), 21 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 26d797fd..564c9bb6 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -1,8 +1,4 @@ # Runs the faucet against a real node and funding service. -# -# Each part of the setup is a step of its own so a failure points at what broke: the build, the -# images, the network, the faucet, or the request itself. `make test-e2e` chains the same scripts -# for running the whole thing locally. name: test-e2e diff --git a/Makefile b/Makefile index 5681b7e6..69249113 100644 --- a/Makefile +++ b/Makefile @@ -79,8 +79,7 @@ test: ## Runs all tests cargo nextest run --release --all-features --workspace # The end-to-end test starts a real node, funding service and faucet, requests tokens and consumes -# the resulting note. Each part is a script of its own so CI can run them as separate steps and see -# which one failed; this target chains them for a developer who just wants to run the whole thing. +# the resulting note. .PHONY: test-e2e test-e2e: ## Runs the end-to-end test against a real node and funding service cargo build --release --locked -p miden-faucet -p miden-faucet-client diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index d22596e3..ee4b001c 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -26,12 +26,6 @@ use tokio::task; const DEFAULT_FAUCET_URL: &str = "https://faucet-api.testnet.miden.io"; const REQUEST_TIMEOUT_MS: u64 = 30_000; -/// How long to keep looking for the note before giving up. -/// -/// The funding service answers as soon as it has queued the note, so the note reaches the chain a -/// few blocks later and the client has to wait for it. Three attempts covered the old behaviour, -/// where the service only answered once the note was committed, but not this one: a loaded network -/// takes longer than fifteen seconds to put the note in a block. const MAX_SYNC_RETRIES: u32 = 10; const SYNC_RETRY_DELAY_SECS: u64 = 5; diff --git a/scripts/e2e/faucet.sh b/scripts/e2e/faucet.sh index 5468787f..39c53172 100755 --- a/scripts/e2e/faucet.sh +++ b/scripts/e2e/faucet.sh @@ -1,9 +1,6 @@ #!/usr/bin/env bash # # Starts and stops the faucet the end-to-end test runs against. -# -# The faucet runs in the background so the test is a step of its own, which keeps a failure easy to -# read: the setup either worked or it did not, and the logs of each part stay separate. set -euo pipefail diff --git a/scripts/e2e/network.sh b/scripts/e2e/network.sh index 30902ace..59a8e025 100755 --- a/scripts/e2e/network.sh +++ b/scripts/e2e/network.sh @@ -4,12 +4,6 @@ # # The node repo already has a compose stack for a complete local network, so it is cloned and used # as is. The images are the published ones, so nothing is built. -# -# The stack publishes the node's RPC on 57291 and the funding service on 50401, the same ports a -# local network uses, so stop yours before running this. -# -# The version comes from the `miden-node-proto-build` pin in `bin/faucet/Cargo.toml`: bumping that -# dependency moves the whole network with it and nothing here needs editing. set -euo pipefail From b895caf0ed639469b6156cf2a2d032b289040775 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 16:44:27 -0300 Subject: [PATCH 11/26] fix: re-clone the node repo when the cargo cache empties it --- .github/workflows/test-e2e.yml | 2 +- scripts/e2e/network.sh | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 564c9bb6..8ee80f83 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -48,7 +48,7 @@ jobs: - name: Faucet logs if: failure() - run: cat target/e2e/faucet.log + run: cat target/e2e/faucet.log || true - name: Network logs if: failure() diff --git a/scripts/e2e/network.sh b/scripts/e2e/network.sh index 59a8e025..0c0ed5fe 100755 --- a/scripts/e2e/network.sh +++ b/scripts/e2e/network.sh @@ -35,16 +35,29 @@ compose() { case "${1:-}" in up) - if [[ ! -d "${CHECKOUT}" ]]; then + if [[ ! -f "${CHECKOUT}/docker-compose.yml" ]]; then echo "Cloning the node repo at v${VERSION}" + rm -rf "${CHECKOUT}" git clone --depth 1 --branch "v${VERSION}" https://github.com/0xMiden/node "${CHECKOUT}" fi # Starting the funding service pulls in everything it depends on: the genesis bootstrap, # the validators, the sequencer and the prover. compose up --detach funding-service ;; - down) compose down --volumes --remove-orphans ;; - logs) compose logs --no-color ;; + down) + if [[ ! -f "${CHECKOUT}/docker-compose.yml" ]]; then + echo "Nothing to stop: ${CHECKOUT} does not exist" + exit 0 + fi + compose down --volumes --remove-orphans + ;; + logs) + if [[ ! -f "${CHECKOUT}/docker-compose.yml" ]]; then + echo "No logs: ${CHECKOUT} does not exist" + exit 0 + fi + compose logs --no-color + ;; *) echo "usage: $0 up|down|logs" >&2 exit 2 From 0b2612dbb8dcdfe6e8d2a249d768c5b020d00375 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 17:13:45 -0300 Subject: [PATCH 12/26] chore: restore error message --- bin/faucet-client/src/mint.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index ee4b001c..d2c32f8e 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -136,10 +136,8 @@ impl MintCmd { let note_record = note_record.ok_or_else(|| { MintClientError::NoteNotFound(format!( - "Note {note_id_hex} not found after {MAX_SYNC_RETRIES} sync attempts over \ - {}s. You can manually consume it later using: \ - miden-client consume-notes {note_id_hex}", - u64::from(MAX_SYNC_RETRIES) * SYNC_RETRY_DELAY_SECS + "Note {note_id_hex} not found after {MAX_SYNC_RETRIES} sync attempts. \ + You can manually consume it later using: miden-client consume-notes {note_id_hex}" )) })?; From 97eb0a77159b0b707551de9b83d9490e55eb3e8d Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 17:24:23 -0300 Subject: [PATCH 13/26] chore: rename miden-client.sh to install-miden-client.sh --- .github/workflows/test-e2e.yml | 2 +- Makefile | 2 +- scripts/e2e/install-miden-client.sh | 36 +++++++++++++++++++++++++ scripts/e2e/miden-client.sh | 42 ----------------------------- 4 files changed, 38 insertions(+), 44 deletions(-) create mode 100755 scripts/e2e/install-miden-client.sh delete mode 100755 scripts/e2e/miden-client.sh diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 8ee80f83..7824ba82 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -35,7 +35,7 @@ jobs: run: cargo build --release --locked -p miden-faucet -p miden-faucet-client - name: Download the Miden client CLI - run: scripts/e2e/miden-client.sh + run: scripts/e2e/install-miden-client.sh - name: Start the node and the funding service run: scripts/e2e/network.sh up diff --git a/Makefile b/Makefile index 69249113..8cb3f4e4 100644 --- a/Makefile +++ b/Makefile @@ -83,7 +83,7 @@ test: ## Runs all tests .PHONY: test-e2e test-e2e: ## Runs the end-to-end test against a real node and funding service cargo build --release --locked -p miden-faucet -p miden-faucet-client - scripts/e2e/miden-client.sh + scripts/e2e/install-miden-client.sh scripts/e2e/network.sh up scripts/e2e/faucet.sh up scripts/e2e/request-tokens.sh; \ diff --git a/scripts/e2e/install-miden-client.sh b/scripts/e2e/install-miden-client.sh new file mode 100755 index 00000000..ced09c1a --- /dev/null +++ b/scripts/e2e/install-miden-client.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Downloads the `miden-client` CLI the end-to-end test uses to create the recipient account and to +# read its balance. +# +# The release assets are prebuilt, so this takes a couple of seconds instead of the several minutes +# `cargo install miden-client-cli` needs. The version comes from the `miden-client` pin in the +# workspace `Cargo.toml`, so bumping that dependency moves this too. + +set -euo pipefail + +BIN=target/e2e/bin/miden-client +# Reads the version out of the line `miden-client = { version = "..." }`. +VERSION=$(grep '^miden-client ' Cargo.toml | cut -d '"' -f 2 | tr -d '=') + +if [[ -x "$BIN" ]] && "$BIN" --version | grep -q "$VERSION"; then + echo "miden-client $VERSION is already at $BIN" + exit 0 +fi + +platform="$(uname -s)-$(uname -m)" +case "$platform" in + Darwin-arm64) asset=miden-client-aarch64-apple-darwin ;; + Linux-x86_64) asset=miden-client-x86_64-unknown-linux-gnu ;; + *) + echo "error: no published miden-client for $platform" >&2 + exit 1 + ;; +esac + +url="https://github.com/0xMiden/rust-sdk/releases/download/v$VERSION/$asset" +echo "Downloading $url" +mkdir -p "$(dirname "$BIN")" +curl --location --fail --silent --show-error --output "$BIN" "$url" +chmod +x "$BIN" +"$BIN" --version diff --git a/scripts/e2e/miden-client.sh b/scripts/e2e/miden-client.sh deleted file mode 100755 index 82f1b058..00000000 --- a/scripts/e2e/miden-client.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -# -# Downloads the `miden-client` CLI the end-to-end test uses to create the recipient account and to -# read its balance. -# -# The release assets are prebuilt, so this takes a couple of seconds instead of the several minutes -# `cargo install miden-client-cli` needs. The version comes from the `miden-client` pin in the -# workspace `Cargo.toml`, so bumping that dependency moves this too. - -set -euo pipefail - -WORK_DIR="${WORK_DIR:-target/e2e}" -MIDEN_CLIENT_BIN="${MIDEN_CLIENT_BIN:-${WORK_DIR}/bin/miden-client}" -VERSION="${MIDEN_CLIENT_VERSION:-$(sed -n 's/^miden-client *= *{ *version *= *"=\{0,1\}\([^"]*\)".*/\1/p' Cargo.toml)}" - -if [[ -z "${VERSION}" ]]; then - echo "error: could not read the miden-client version from Cargo.toml" >&2 - exit 1 -fi - -if [[ -x "${MIDEN_CLIENT_BIN}" ]] && "${MIDEN_CLIENT_BIN}" --version 2>/dev/null | grep -q "${VERSION}"; then - echo "miden-client ${VERSION} is already at ${MIDEN_CLIENT_BIN}" - exit 0 -fi - -case "$(uname -s)-$(uname -m)" in - Darwin-arm64) asset="miden-client-aarch64-apple-darwin" ;; - Linux-x86_64) asset="miden-client-x86_64-unknown-linux-gnu" ;; - *) - echo "error: no published miden-client for $(uname -s)-$(uname -m)" >&2 - exit 1 - ;; -esac - -url="https://github.com/0xMiden/rust-sdk/releases/download/v${VERSION}/${asset}" -echo "Downloading ${url}" - -mkdir -p "$(dirname "${MIDEN_CLIENT_BIN}")" -curl --location --fail --silent --show-error --output "${MIDEN_CLIENT_BIN}" "${url}" -chmod +x "${MIDEN_CLIENT_BIN}" - -"${MIDEN_CLIENT_BIN}" --version From 79c47703e6e3fe3a18d28f80f0803e6ef4f47e51 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 24 Sep 2026 17:40:01 -0300 Subject: [PATCH 14/26] chore: cleanup --- scripts/e2e/faucet.sh | 9 ++++----- scripts/e2e/install-miden-client.sh | 4 ---- scripts/e2e/request-tokens.sh | 10 ++++------ 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/scripts/e2e/faucet.sh b/scripts/e2e/faucet.sh index 39c53172..723a18aa 100755 --- a/scripts/e2e/faucet.sh +++ b/scripts/e2e/faucet.sh @@ -23,8 +23,7 @@ start() { mkdir -p "${WORK_DIR}" # The faucet reads its API keys on startup, so the file has to exist even with no keys in it. - : >"${API_KEYS_FILE}" - : >"${LOG_FILE}" + touch "${API_KEYS_FILE}" echo "Starting the faucet on ${FAUCET_URL}, logging to ${LOG_FILE}" "${FAUCET_BIN}" start \ @@ -35,8 +34,8 @@ start() { --api-bind-port "${FAUCET_URL##*:}" \ --api-keys-file "${API_KEYS_FILE}" \ --no-frontend \ - >>"${LOG_FILE}" 2>&1 & - echo $! >"${PID_FILE}" + > "${LOG_FILE}" 2>&1 & + echo $! > "${PID_FILE}" for _ in $(seq "${START_TIMEOUT_SECONDS}"); do if curl --silent --fail --output /dev/null "${FAUCET_URL}/get_metadata"; then @@ -58,7 +57,7 @@ stop() { pid="$(cat "${PID_FILE}")" rm -f "${PID_FILE}" - kill "${pid}" 2>/dev/null || return 0 + kill "${pid}" 2> /dev/null || return 0 echo "Stopped the faucet (pid ${pid})" } diff --git a/scripts/e2e/install-miden-client.sh b/scripts/e2e/install-miden-client.sh index ced09c1a..361b022d 100755 --- a/scripts/e2e/install-miden-client.sh +++ b/scripts/e2e/install-miden-client.sh @@ -2,10 +2,6 @@ # # Downloads the `miden-client` CLI the end-to-end test uses to create the recipient account and to # read its balance. -# -# The release assets are prebuilt, so this takes a couple of seconds instead of the several minutes -# `cargo install miden-client-cli` needs. The version comes from the `miden-client` pin in the -# workspace `Cargo.toml`, so bumping that dependency moves this too. set -euo pipefail diff --git a/scripts/e2e/request-tokens.sh b/scripts/e2e/request-tokens.sh index c88a0c76..5e56c5da 100755 --- a/scripts/e2e/request-tokens.sh +++ b/scripts/e2e/request-tokens.sh @@ -11,13 +11,11 @@ set -euo pipefail -CLIENT_BIN="${CLIENT_BIN:-./target/release/miden-faucet-client}" +FAUCET_CLIENT_BIN="${FAUCET_CLIENT_BIN:-./target/release/miden-faucet-client}" WORK_DIR="${WORK_DIR:-target/e2e}" MIDEN_CLIENT_BIN="${MIDEN_CLIENT_BIN:-${WORK_DIR}/bin/miden-client}" FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" NODE_URL="${NODE_URL:-http://127.0.0.1:57291}" -# Bech32 prefix the faucet uses for a local network, so the client prints matching addresses. -NETWORK_ID="${NETWORK_ID:-mlcl}" # Consuming the note pays a fee out of the note itself, so a request too small to cover the fee # fails inside the transaction kernel. AMOUNT="${AMOUNT:-1000000}" @@ -29,7 +27,7 @@ rm -rf "${MIDEN_CLIENT_HOME}" mkdir -p "${MIDEN_CLIENT_HOME}" echo "Pointing the client at ${NODE_URL}" -"${MIDEN_CLIENT_BIN}" init --network "${NODE_URL}" --network-id "${NETWORK_ID}" +"${MIDEN_CLIENT_BIN}" init --network "${NODE_URL}" echo "Creating the recipient account" account="$("${MIDEN_CLIENT_BIN}" new-wallet --account-type public | grep -o '0x[0-9a-f]\{2,\}' | head -1)" @@ -40,7 +38,7 @@ fi echo "Recipient: ${account}" echo "Requesting ${AMOUNT} base units from ${FAUCET_URL}" -"${CLIENT_BIN}" mint --url "${FAUCET_URL}" --target-account "${account}" --amount "${AMOUNT}" +"${FAUCET_CLIENT_BIN}" mint --url "${FAUCET_URL}" --target-account "${account}" --amount "${AMOUNT}" # The consume transaction pays the fee out of the note, so the balance is the requested amount less # that fee. What matters here is that the account ends up holding the asset at all. @@ -48,7 +46,7 @@ echo "Reading the recipient's balance" balance="$("${MIDEN_CLIENT_BIN}" account --show "${account}")" echo "${balance}" -if ! grep -q "Fungible Asset" <<<"${balance}"; then +if ! grep -q "Fungible Asset" <<< "${balance}"; then echo "error: the recipient holds no asset after consuming the note" >&2 exit 1 fi From 8c6c8b1ddf4201e945eadca6ca0a79cbe062bd87 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Fri, 25 Sep 2026 10:37:39 -0300 Subject: [PATCH 15/26] test: replace the end-to-end shell test with a rust one --- .github/workflows/test-e2e.yml | 13 +- Cargo.lock | 1 + Makefile | 5 +- bin/faucet-client/Cargo.toml | 9 +- bin/faucet-client/src/mint.rs | 10 +- bin/faucet-client/tests/e2e.rs | 200 ++++++++++++++++++++++++++++ scripts/e2e/install-miden-client.sh | 32 ----- scripts/e2e/request-tokens.sh | 54 -------- 8 files changed, 219 insertions(+), 105 deletions(-) create mode 100644 bin/faucet-client/tests/e2e.rs delete mode 100755 scripts/e2e/install-miden-client.sh delete mode 100755 scripts/e2e/request-tokens.sh diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 7824ba82..a36b537a 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -31,11 +31,10 @@ jobs: # GitHub scopes a branch's cache to that branch, so this does not disturb other runs. save-if: true - - name: Build the faucet and its client - run: cargo build --release --locked -p miden-faucet -p miden-faucet-client - - - name: Download the Miden client CLI - run: scripts/e2e/install-miden-client.sh + - name: Build the faucet and the test + run: | + cargo build --release --locked -p miden-faucet + cargo test --release --locked -p miden-faucet-client --test e2e --no-run - name: Start the node and the funding service run: scripts/e2e/network.sh up @@ -43,8 +42,8 @@ jobs: - name: Start the faucet run: scripts/e2e/faucet.sh up - - name: Request tokens - run: scripts/e2e/request-tokens.sh + - name: Run the test + run: cargo test --release --locked -p miden-faucet-client --test e2e -- --ignored --nocapture - name: Faucet logs if: failure() diff --git a/Cargo.lock b/Cargo.lock index 2eb8908e..158732c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3422,6 +3422,7 @@ dependencies = [ "hex", "miden-client", "miden-client-cli", + "miden-client-sqlite-store", "miden-faucet-lib", "miden-pow-rate-limiter", "rand 0.10.3", diff --git a/Makefile b/Makefile index 8cb3f4e4..dcbf2782 100644 --- a/Makefile +++ b/Makefile @@ -82,11 +82,10 @@ test: ## Runs all tests # the resulting note. .PHONY: test-e2e test-e2e: ## Runs the end-to-end test against a real node and funding service - cargo build --release --locked -p miden-faucet -p miden-faucet-client - scripts/e2e/install-miden-client.sh + cargo build --release --locked -p miden-faucet scripts/e2e/network.sh up scripts/e2e/faucet.sh up - scripts/e2e/request-tokens.sh; \ + cargo test --release --locked -p miden-faucet-client --test e2e -- --ignored --nocapture; \ status=$$?; \ scripts/e2e/faucet.sh down; \ scripts/e2e/network.sh down; \ diff --git a/bin/faucet-client/Cargo.toml b/bin/faucet-client/Cargo.toml index 2f4cd26d..d584c799 100644 --- a/bin/faucet-client/Cargo.toml +++ b/bin/faucet-client/Cargo.toml @@ -29,7 +29,8 @@ tokio = { features = ["macros", "net", "rt-multi-thread", "sync url = { workspace = true } [dev-dependencies] -axum = { features = ["tokio"], version = "0.8" } -miden-client = { features = ["testing"], workspace = true } -serde = { workspace = true } -tokio = { features = ["macros", "net", "rt-multi-thread", "time"], workspace = true } +axum = { features = ["tokio"], version = "0.8" } +miden-client = { features = ["testing"], workspace = true } +miden-client-sqlite-store = { workspace = true } +serde = { workspace = true } +tokio = { features = ["macros", "net", "rt-multi-thread", "time"], workspace = true } diff --git a/bin/faucet-client/src/mint.rs b/bin/faucet-client/src/mint.rs index d2c32f8e..e259c2eb 100644 --- a/bin/faucet-client/src/mint.rs +++ b/bin/faucet-client/src/mint.rs @@ -166,7 +166,7 @@ impl MintCmd { /// HTTP client for interacting with the faucet API. #[derive(Clone)] -struct FaucetHttpClient { +pub struct FaucetHttpClient { http_client: HttpClient, base_url: Url, api_key: Option, @@ -174,7 +174,7 @@ struct FaucetHttpClient { impl FaucetHttpClient { /// Creates a new `FaucetHttpClient` instance. - fn new( + pub fn new( endpoint: &str, timeout_ms: u64, api_key: Option, @@ -191,7 +191,7 @@ impl FaucetHttpClient { } /// Requests a `PoW` challenge from the faucet API. - async fn request_pow( + pub async fn request_pow( &self, account_id: &AccountId, amount: u64, @@ -231,7 +231,7 @@ impl FaucetHttpClient { } /// Requests tokens from the faucet API. - async fn request_tokens( + pub async fn request_tokens( &self, challenge: &str, nonce: u64, @@ -349,7 +349,7 @@ fn parse_account_id(input: &str) -> Result { /// Solves the `PoW` challenge and returns the nonce that satisfies 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 { +pub async fn solve_challenge(challenge_hex: &str, target: u64) -> Result { if target == 0 { return Err(MintClientError::ZeroTarget); } diff --git a/bin/faucet-client/tests/e2e.rs b/bin/faucet-client/tests/e2e.rs new file mode 100644 index 00000000..703f21c5 --- /dev/null +++ b/bin/faucet-client/tests/e2e.rs @@ -0,0 +1,200 @@ +//! End-to-end test against a real node, funding service and faucet. +//! +//! Creates an account, requests tokens for it, waits for the note to be committed, consumes it and +//! checks the balance. Ignored by default because it needs the network: `make test-e2e` starts +//! everything and runs it. + +use std::sync::Arc; +use std::time::Duration; + +use miden_client::Client; +use miden_client::account::component::BasicWallet; +use miden_client::account::{ + Account, AccountBuilder, AccountBuilderSchemaCommitmentExt, AccountId, AccountType, +}; +use miden_client::auth::{Approver, AuthSchemeId, AuthSecretKey, AuthSingleSig}; +use miden_client::builder::ClientBuilder; +use miden_client::keystore::{FilesystemKeyStore, Keystore}; +use miden_client::note::{Note, NoteId}; +use miden_client::rpc::{Endpoint, GrpcClient, VerifyingRpcClient}; +use miden_client::transaction::TransactionRequestBuilder; +use miden_client_sqlite_store::SqliteStore; +use miden_faucet_client::mint::{FaucetHttpClient, solve_challenge}; + +const DEFAULT_FAUCET_URL: &str = "http://127.0.0.1:18000"; +const DEFAULT_NODE_URL: &str = "http://127.0.0.1:57291"; + +/// The store and keys of this test, kept out of the developer's own `~/.miden`. +const WORK_DIR: &str = "target/e2e/client"; + +const REQUEST_TIMEOUT_MS: u64 = 30_000; + +/// Consuming the note pays a fee out of the note itself, so a request too small to cover the fee +/// fails inside the transaction kernel. +const AMOUNT: u64 = 1_000_000; + +/// The funding service answers before the note is committed, so the note takes a few blocks to +/// arrive and a loaded network takes longer than a couple of them. +const NOTE_ATTEMPTS: u32 = 30; +const NOTE_DELAY: Duration = Duration::from_secs(2); + +#[tokio::test] +#[ignore = "needs a node, funding service and faucet, run it with `make test-e2e`"] +async fn request_tokens_and_consume_the_note() { + let faucet_url = env_or("FAUCET_URL", DEFAULT_FAUCET_URL); + let node_url = env_or("NODE_URL", DEFAULT_NODE_URL); + + let mut client = build_client(&node_url).await; + let account_id = create_account(&mut client).await; + println!("Recipient: {}", account_id.to_hex()); + + let note_id = request_tokens(&faucet_url, account_id).await; + println!("Note: {}", note_id.to_hex()); + + let note = wait_for_note(&mut client, account_id, note_id).await; + consume_note(&mut client, account_id, note).await; + + let account = client + .get_account(account_id) + .await + .expect("the account should be readable") + .expect("the account should be tracked"); + let balance = fungible_balance(&account); + + // The consume transaction pays its fee out of the note, so the balance is the requested amount + // less that fee. + assert!(balance > 0, "the recipient holds no asset after consuming the note"); + assert!( + balance <= AMOUNT, + "the recipient holds {balance}, more than the {AMOUNT} requested" + ); + println!("Balance: {balance} of the {AMOUNT} requested, the rest paid the fee"); +} + +/// Builds a client with its own store and keystore, pointed at the test network. +async fn build_client(node_url: &str) -> Client { + let work_dir = std::path::Path::new(WORK_DIR); + // A fresh store every run, so a note or account from an earlier run cannot satisfy the test. + let _ = std::fs::remove_dir_all(work_dir); + std::fs::create_dir_all(work_dir.join("keys")).expect("the work directory should be created"); + + let store = SqliteStore::new(work_dir.join("store.sqlite3")) + .await + .expect("the store should be created"); + let keystore = + FilesystemKeyStore::new(work_dir.join("keys")).expect("the keystore should be created"); + let endpoint = Endpoint::try_from(node_url).expect("the node url should be an endpoint"); + + ClientBuilder::new() + .rpc(Arc::new(VerifyingRpcClient::new(GrpcClient::new( + &endpoint, + REQUEST_TIMEOUT_MS, + )))) + .store(Arc::new(store)) + .authenticator(Arc::new(keystore)) + .build() + .await + .expect("the client should connect to the node") +} + +/// Creates a public wallet the faucet can send a note to, with its key in the client's keystore. +async fn create_account(client: &mut Client) -> AccountId { + let key = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); + let auth = AuthSingleSig::new(Approver::new( + key.public_key().to_commitment(), + AuthSchemeId::Falcon512Poseidon2, + )); + + let account = AccountBuilder::new(rand::random()) + .account_type(AccountType::Public) + .with_component(auth) + .with_component(BasicWallet) + .build_with_schema_commitment() + .expect("the account should build"); + + client + .authenticator() + .expect("the client has a keystore") + .add_key(&key, account.id()) + .await + .expect("the key should be stored"); + client + .add_account(&account, false) + .await + .expect("the account should be tracked"); + + account.id() +} + +/// Requests tokens the way the faucet client does: a challenge, a nonce that solves it, and the +/// request itself. +async fn request_tokens(faucet_url: &str, account_id: AccountId) -> NoteId { + let faucet = FaucetHttpClient::new(faucet_url, REQUEST_TIMEOUT_MS, None) + .expect("the faucet url should be valid"); + + let (challenge, target) = faucet + .request_pow(&account_id, AMOUNT) + .await + .expect("the faucet should answer with a challenge"); + let nonce = solve_challenge(&challenge, target) + .await + .expect("the challenge should be solvable"); + + faucet + .request_tokens(&challenge, nonce, &account_id, AMOUNT) + .await + .expect("the faucet should answer with a note") + .note_id +} + +/// Syncs until the note is committed and consumable by the account. +async fn wait_for_note( + client: &mut Client, + account_id: AccountId, + note_id: NoteId, +) -> Note { + for attempt in 1..=NOTE_ATTEMPTS { + client.sync_state().await.expect("the client should sync"); + + let consumable = client + .get_consumable_notes(Some(account_id)) + .await + .expect("the consumable notes should be readable"); + + let found = consumable.into_iter().find(|(record, _)| record.id() == Some(note_id)); + if let Some((record, _)) = found { + return record.try_into().expect("a committed note record holds its note"); + } + + println!("Note not committed yet, attempt {attempt}/{NOTE_ATTEMPTS}"); + tokio::time::sleep(NOTE_DELAY).await; + } + + panic!("note {} was not committed in time", note_id.to_hex()); +} + +/// Consumes the note, which also creates the account on chain. +async fn consume_note(client: &mut Client, account_id: AccountId, note: Note) { + let request = TransactionRequestBuilder::new() + .input_notes(vec![(note, None)]) + .build() + .expect("the consume request should build"); + + client + .submit_new_transaction(account_id, request) + .await + .expect("the node should accept the consume transaction"); +} + +/// Sums the fungible assets the account holds. +fn fungible_balance(account: &Account) -> u64 { + account + .vault() + .assets() + .filter_map(|asset| asset.as_fungible().map(|asset| asset.amount().as_u64())) + .sum() +} + +fn env_or(variable: &str, default: &str) -> String { + std::env::var(variable).unwrap_or_else(|_| default.to_owned()) +} diff --git a/scripts/e2e/install-miden-client.sh b/scripts/e2e/install-miden-client.sh deleted file mode 100755 index 361b022d..00000000 --- a/scripts/e2e/install-miden-client.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# -# Downloads the `miden-client` CLI the end-to-end test uses to create the recipient account and to -# read its balance. - -set -euo pipefail - -BIN=target/e2e/bin/miden-client -# Reads the version out of the line `miden-client = { version = "..." }`. -VERSION=$(grep '^miden-client ' Cargo.toml | cut -d '"' -f 2 | tr -d '=') - -if [[ -x "$BIN" ]] && "$BIN" --version | grep -q "$VERSION"; then - echo "miden-client $VERSION is already at $BIN" - exit 0 -fi - -platform="$(uname -s)-$(uname -m)" -case "$platform" in - Darwin-arm64) asset=miden-client-aarch64-apple-darwin ;; - Linux-x86_64) asset=miden-client-x86_64-unknown-linux-gnu ;; - *) - echo "error: no published miden-client for $platform" >&2 - exit 1 - ;; -esac - -url="https://github.com/0xMiden/rust-sdk/releases/download/v$VERSION/$asset" -echo "Downloading $url" -mkdir -p "$(dirname "$BIN")" -curl --location --fail --silent --show-error --output "$BIN" "$url" -chmod +x "$BIN" -"$BIN" --version diff --git a/scripts/e2e/request-tokens.sh b/scripts/e2e/request-tokens.sh deleted file mode 100755 index 5e56c5da..00000000 --- a/scripts/e2e/request-tokens.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# -# The end-to-end test itself. -# -# Creates a recipient account with the `miden-client` CLI, requests tokens for it from the faucet, -# lets the faucet client consume the resulting note, and checks that the account ends up holding -# the asset. -# -# The network, the funding service and the faucet have to be running already; `make test-e2e` -# starts them. - -set -euo pipefail - -FAUCET_CLIENT_BIN="${FAUCET_CLIENT_BIN:-./target/release/miden-faucet-client}" -WORK_DIR="${WORK_DIR:-target/e2e}" -MIDEN_CLIENT_BIN="${MIDEN_CLIENT_BIN:-${WORK_DIR}/bin/miden-client}" -FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" -NODE_URL="${NODE_URL:-http://127.0.0.1:57291}" -# Consuming the note pays a fee out of the note itself, so a request too small to cover the fee -# fails inside the transaction kernel. -AMOUNT="${AMOUNT:-1000000}" - -# Keep the client's config, store and keys out of the developer's own ~/.miden. -MIDEN_CLIENT_HOME="${PWD}/${WORK_DIR}/miden-client" -export MIDEN_CLIENT_HOME -rm -rf "${MIDEN_CLIENT_HOME}" -mkdir -p "${MIDEN_CLIENT_HOME}" - -echo "Pointing the client at ${NODE_URL}" -"${MIDEN_CLIENT_BIN}" init --network "${NODE_URL}" - -echo "Creating the recipient account" -account="$("${MIDEN_CLIENT_BIN}" new-wallet --account-type public | grep -o '0x[0-9a-f]\{2,\}' | head -1)" -if [[ -z "${account}" ]]; then - echo "error: could not read the new account's id" >&2 - exit 1 -fi -echo "Recipient: ${account}" - -echo "Requesting ${AMOUNT} base units from ${FAUCET_URL}" -"${FAUCET_CLIENT_BIN}" mint --url "${FAUCET_URL}" --target-account "${account}" --amount "${AMOUNT}" - -# The consume transaction pays the fee out of the note, so the balance is the requested amount less -# that fee. What matters here is that the account ends up holding the asset at all. -echo "Reading the recipient's balance" -balance="$("${MIDEN_CLIENT_BIN}" account --show "${account}")" -echo "${balance}" - -if ! grep -q "Fungible Asset" <<< "${balance}"; then - echo "error: the recipient holds no asset after consuming the note" >&2 - exit 1 -fi - -echo "The recipient received and consumed the note" From c1840928bdb60ecee568490d8d7210641ce9dd20 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Fri, 25 Sep 2026 11:39:24 -0300 Subject: [PATCH 16/26] chore: lint --- bin/faucet-client/tests/e2e.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/faucet-client/tests/e2e.rs b/bin/faucet-client/tests/e2e.rs index 703f21c5..44e92906 100644 --- a/bin/faucet-client/tests/e2e.rs +++ b/bin/faucet-client/tests/e2e.rs @@ -10,7 +10,11 @@ use std::time::Duration; use miden_client::Client; use miden_client::account::component::BasicWallet; use miden_client::account::{ - Account, AccountBuilder, AccountBuilderSchemaCommitmentExt, AccountId, AccountType, + Account, + AccountBuilder, + AccountBuilderSchemaCommitmentExt, + AccountId, + AccountType, }; use miden_client::auth::{Approver, AuthSchemeId, AuthSecretKey, AuthSingleSig}; use miden_client::builder::ClientBuilder; From 94121fab635afe51394b8e848d5aba0190bc751f Mon Sep 17 00:00:00 2001 From: ricomateo Date: Fri, 25 Sep 2026 12:13:30 -0300 Subject: [PATCH 17/26] fix: pin NODE_VERSION to the latest available version with published docker images --- .github/workflows/test-e2e.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index a36b537a..d183d2bc 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -18,6 +18,12 @@ jobs: name: test-e2e runs-on: ubuntu-latest timeout-minutes: 30 + env: + # `scripts/e2e/network.sh` takes the node version from the `miden-node-proto-build` pin, which + # is 0.17.0-rc.3. The release run for that tag failed before it built anything, so no images + # were published for it and the newest ones are 0.17.0-rc.2. Remove this once the images are + # published. + NODE_VERSION: 0.17.0-rc.2 steps: - uses: actions/checkout@main - name: Rustup From 70c7f6a2c1871b10cab76d48f3779f96c47db072 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Fri, 2 Oct 2026 17:26:33 -0300 Subject: [PATCH 18/26] chore: unpin node version in ci e2e test --- .github/workflows/test-e2e.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index d183d2bc..a36b537a 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -18,12 +18,6 @@ jobs: name: test-e2e runs-on: ubuntu-latest timeout-minutes: 30 - env: - # `scripts/e2e/network.sh` takes the node version from the `miden-node-proto-build` pin, which - # is 0.17.0-rc.3. The release run for that tag failed before it built anything, so no images - # were published for it and the newest ones are 0.17.0-rc.2. Remove this once the images are - # published. - NODE_VERSION: 0.17.0-rc.2 steps: - uses: actions/checkout@main - name: Rustup From f3f9b328cfcd1751d05d24546327d1a529aa398a Mon Sep 17 00:00:00 2001 From: ricomateo Date: Fri, 2 Oct 2026 18:15:42 -0300 Subject: [PATCH 19/26] chore: address review comments --- bin/faucet-client/tests/e2e.rs | 178 ++++++++++++--------------------- 1 file changed, 64 insertions(+), 114 deletions(-) diff --git a/bin/faucet-client/tests/e2e.rs b/bin/faucet-client/tests/e2e.rs index 44e92906..075fdd03 100644 --- a/bin/faucet-client/tests/e2e.rs +++ b/bin/faucet-client/tests/e2e.rs @@ -4,33 +4,20 @@ //! checks the balance. Ignored by default because it needs the network: `make test-e2e` starts //! everything and runs it. -use std::sync::Arc; -use std::time::Duration; - -use miden_client::Client; -use miden_client::account::component::BasicWallet; -use miden_client::account::{ - Account, - AccountBuilder, - AccountBuilderSchemaCommitmentExt, - AccountId, - AccountType, -}; -use miden_client::auth::{Approver, AuthSchemeId, AuthSecretKey, AuthSingleSig}; +use std::env::temp_dir; + +use miden_client::account::{AccountId, AccountType}; use miden_client::builder::ClientBuilder; -use miden_client::keystore::{FilesystemKeyStore, Keystore}; +use miden_client::keystore::FilesystemKeyStore; use miden_client::note::{Note, NoteId}; -use miden_client::rpc::{Endpoint, GrpcClient, VerifyingRpcClient}; -use miden_client::transaction::TransactionRequestBuilder; -use miden_client_sqlite_store::SqliteStore; +use miden_client::rpc::Endpoint; +use miden_client::testing::common::{AccountSetup, TestClient}; +use miden_client_sqlite_store::ClientBuilderSqliteExt; use miden_faucet_client::mint::{FaucetHttpClient, solve_challenge}; const DEFAULT_FAUCET_URL: &str = "http://127.0.0.1:18000"; const DEFAULT_NODE_URL: &str = "http://127.0.0.1:57291"; -/// The store and keys of this test, kept out of the developer's own `~/.miden`. -const WORK_DIR: &str = "target/e2e/client"; - const REQUEST_TIMEOUT_MS: u64 = 30_000; /// Consuming the note pays a fee out of the note itself, so a request too small to cover the fee @@ -39,8 +26,7 @@ const AMOUNT: u64 = 1_000_000; /// The funding service answers before the note is committed, so the note takes a few blocks to /// arrive and a loaded network takes longer than a couple of them. -const NOTE_ATTEMPTS: u32 = 30; -const NOTE_DELAY: Duration = Duration::from_secs(2); +const NOTE_MAX_BLOCKS: u32 = 60; #[tokio::test] #[ignore = "needs a node, funding service and faucet, run it with `make test-e2e`"] @@ -49,21 +35,37 @@ async fn request_tokens_and_consume_the_note() { let node_url = env_or("NODE_URL", DEFAULT_NODE_URL); let mut client = build_client(&node_url).await; - let account_id = create_account(&mut client).await; + + // Unfunded: the note the faucet sends is what pays for the transaction that consumes it, and + // funding would need a fee funder this client does not have. + let (account, _) = client + .insert_account(AccountSetup::wallet(AccountType::Public).unfunded()) + .await + .expect("the account should be created"); + let account_id = account.id(); println!("Recipient: {}", account_id.to_hex()); let note_id = request_tokens(&faucet_url, account_id).await; println!("Note: {}", note_id.to_hex()); let note = wait_for_note(&mut client, account_id, note_id).await; + // The note carries the asset of the faucet the funding service pays from, which is the balance + // to check once the note is consumed. + let faucet_id = note + .assets() + .iter_fungible() + .next() + .expect("the note should carry a fungible asset") + .faucet_id(); + consume_note(&mut client, account_id, note).await; - let account = client - .get_account(account_id) + let balance = client + .account_reader(account_id) + .get_balance(faucet_id) .await - .expect("the account should be readable") - .expect("the account should be tracked"); - let balance = fungible_balance(&account); + .expect("the balance should be readable") + .as_u64(); // The consume transaction pays its fee out of the note, so the balance is the requested amount // less that fee. @@ -76,58 +78,24 @@ async fn request_tokens_and_consume_the_note() { } /// Builds a client with its own store and keystore, pointed at the test network. -async fn build_client(node_url: &str) -> Client { - let work_dir = std::path::Path::new(WORK_DIR); - // A fresh store every run, so a note or account from an earlier run cannot satisfy the test. - let _ = std::fs::remove_dir_all(work_dir); - std::fs::create_dir_all(work_dir.join("keys")).expect("the work directory should be created"); +async fn build_client(node_url: &str) -> TestClient { + // Emptied first, so no account or note from an earlier run can take part in the test. + let work_dir = temp_dir().join("miden-faucet-e2e"); + let _ = std::fs::remove_dir_all(&work_dir); + std::fs::create_dir_all(&work_dir).expect("the work directory should be created"); - let store = SqliteStore::new(work_dir.join("store.sqlite3")) - .await - .expect("the store should be created"); - let keystore = - FilesystemKeyStore::new(work_dir.join("keys")).expect("the keystore should be created"); let endpoint = Endpoint::try_from(node_url).expect("the node url should be an endpoint"); - ClientBuilder::new() - .rpc(Arc::new(VerifyingRpcClient::new(GrpcClient::new( - &endpoint, - REQUEST_TIMEOUT_MS, - )))) - .store(Arc::new(store)) - .authenticator(Arc::new(keystore)) + let client = ClientBuilder::::new() + .grpc_client(&endpoint, Some(REQUEST_TIMEOUT_MS)) + .filesystem_keystore(work_dir.join("keys")) + .expect("the keystore should be created") + .sqlite_store(work_dir.join("store.sqlite3")) .build() .await - .expect("the client should connect to the node") -} - -/// Creates a public wallet the faucet can send a note to, with its key in the client's keystore. -async fn create_account(client: &mut Client) -> AccountId { - let key = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng()); - let auth = AuthSingleSig::new(Approver::new( - key.public_key().to_commitment(), - AuthSchemeId::Falcon512Poseidon2, - )); - - let account = AccountBuilder::new(rand::random()) - .account_type(AccountType::Public) - .with_component(auth) - .with_component(BasicWallet) - .build_with_schema_commitment() - .expect("the account should build"); - - client - .authenticator() - .expect("the client has a keystore") - .add_key(&key, account.id()) - .await - .expect("the key should be stored"); - client - .add_account(&account, false) - .await - .expect("the account should be tracked"); + .expect("the client should connect to the node"); - account.id() + TestClient::new(client) } /// Requests tokens the way the faucet client does: a challenge, a nonce that solves it, and the @@ -152,51 +120,33 @@ async fn request_tokens(faucet_url: &str, account_id: AccountId) -> NoteId { } /// Syncs until the note is committed and consumable by the account. -async fn wait_for_note( - client: &mut Client, - account_id: AccountId, - note_id: NoteId, -) -> Note { - for attempt in 1..=NOTE_ATTEMPTS { - client.sync_state().await.expect("the client should sync"); - - let consumable = client - .get_consumable_notes(Some(account_id)) - .await - .expect("the consumable notes should be readable"); - - let found = consumable.into_iter().find(|(record, _)| record.id() == Some(note_id)); - if let Some((record, _)) = found { - return record.try_into().expect("a committed note record holds its note"); - } - - println!("Note not committed yet, attempt {attempt}/{NOTE_ATTEMPTS}"); - tokio::time::sleep(NOTE_DELAY).await; - } - - panic!("note {} was not committed in time", note_id.to_hex()); -} +async fn wait_for_note(client: &mut TestClient, account_id: AccountId, note_id: NoteId) -> Note { + let consumable = client + .wait_for_consumable_notes(account_id, NOTE_MAX_BLOCKS) + .await + .expect("the note should be committed"); -/// Consumes the note, which also creates the account on chain. -async fn consume_note(client: &mut Client, account_id: AccountId, note: Note) { - let request = TransactionRequestBuilder::new() - .input_notes(vec![(note, None)]) - .build() - .expect("the consume request should build"); + let (record, _) = consumable + .into_iter() + .find(|(record, _)| record.id() == Some(note_id)) + .unwrap_or_else(|| { + panic!("the faucet's note {} is not the one that arrived", note_id.to_hex()) + }); - client - .submit_new_transaction(account_id, request) + record.try_into().expect("a committed note record holds its note") +} + +/// Consumes the note, which also creates the account on chain, and waits for the block holding it. +async fn consume_note(client: &mut TestClient, account_id: AccountId, note: Note) { + let transaction_id = client + .consume_notes(account_id, &[note]) .await .expect("the node should accept the consume transaction"); -} -/// Sums the fungible assets the account holds. -fn fungible_balance(account: &Account) -> u64 { - account - .vault() - .assets() - .filter_map(|asset| asset.as_fungible().map(|asset| asset.amount().as_u64())) - .sum() + client + .wait_for_tx(transaction_id) + .await + .expect("the consume transaction should be committed"); } fn env_or(variable: &str, default: &str) -> String { From d44a6e400992c53c3e21371b67ea9b5a2a76352c Mon Sep 17 00:00:00 2001 From: ricomateo Date: Wed, 7 Oct 2026 18:01:11 -0300 Subject: [PATCH 20/26] chore: reuse Endpoint::localhost() instead of redefining it --- bin/faucet-client/tests/e2e.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/faucet-client/tests/e2e.rs b/bin/faucet-client/tests/e2e.rs index 075fdd03..8ba09e7a 100644 --- a/bin/faucet-client/tests/e2e.rs +++ b/bin/faucet-client/tests/e2e.rs @@ -16,7 +16,6 @@ use miden_client_sqlite_store::ClientBuilderSqliteExt; use miden_faucet_client::mint::{FaucetHttpClient, solve_challenge}; const DEFAULT_FAUCET_URL: &str = "http://127.0.0.1:18000"; -const DEFAULT_NODE_URL: &str = "http://127.0.0.1:57291"; const REQUEST_TIMEOUT_MS: u64 = 30_000; @@ -32,7 +31,7 @@ const NOTE_MAX_BLOCKS: u32 = 60; #[ignore = "needs a node, funding service and faucet, run it with `make test-e2e`"] async fn request_tokens_and_consume_the_note() { let faucet_url = env_or("FAUCET_URL", DEFAULT_FAUCET_URL); - let node_url = env_or("NODE_URL", DEFAULT_NODE_URL); + let node_url = env_or("NODE_URL", &Endpoint::localhost().to_string()); let mut client = build_client(&node_url).await; From 8f4a49282c432022096771fafbd497a2a874762a Mon Sep 17 00:00:00 2001 From: ricomateo Date: Wed, 7 Oct 2026 18:17:17 -0300 Subject: [PATCH 21/26] test: remove faucet.sh script and start the faucet in the rust test instead --- .github/workflows/test-e2e.yml | 16 +----- Cargo.lock | 1 + Cargo.toml | 1 + Makefile | 3 -- bin/faucet-client/Cargo.toml | 1 + bin/faucet-client/tests/e2e.rs | 80 ++++++++++++++++++++++++++++-- bin/faucet/src/{main.rs => lib.rs} | 29 ++++------- scripts/e2e/faucet.sh | 71 -------------------------- 8 files changed, 90 insertions(+), 112 deletions(-) rename bin/faucet/src/{main.rs => lib.rs} (98%) delete mode 100755 scripts/e2e/faucet.sh diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index a36b537a..d8484f22 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -31,30 +31,16 @@ jobs: # GitHub scopes a branch's cache to that branch, so this does not disturb other runs. save-if: true - - name: Build the faucet and the test - run: | - cargo build --release --locked -p miden-faucet - cargo test --release --locked -p miden-faucet-client --test e2e --no-run - - name: Start the node and the funding service run: scripts/e2e/network.sh up - - name: Start the faucet - run: scripts/e2e/faucet.sh up - - name: Run the test run: cargo test --release --locked -p miden-faucet-client --test e2e -- --ignored --nocapture - - name: Faucet logs - if: failure() - run: cat target/e2e/faucet.log || true - - name: Network logs if: failure() run: scripts/e2e/network.sh logs - name: Stop everything if: always() - run: | - scripts/e2e/faucet.sh down - scripts/e2e/network.sh down + run: scripts/e2e/network.sh down diff --git a/Cargo.lock b/Cargo.lock index 74f6fba7..77f28965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3429,6 +3429,7 @@ dependencies = [ "miden-client", "miden-client-cli", "miden-client-sqlite-store", + "miden-faucet", "miden-faucet-lib", "miden-pow-rate-limiter", "rand 0.10.3", diff --git a/Cargo.toml b/Cargo.toml index 94a54936..2415ff93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ version = "0.17.0-rc.3" opt-level = 2 [workspace.dependencies] +miden-faucet = { path = "bin/faucet", version = "0.17.0-rc.3" } miden-faucet-lib = { path = "crates/faucet", version = "0.17.0-rc.3" } miden-pow-rate-limiter = { path = "crates/pow", version = "0.17.0-rc.3" } diff --git a/Makefile b/Makefile index dcbf2782..5bc3b465 100644 --- a/Makefile +++ b/Makefile @@ -82,12 +82,9 @@ test: ## Runs all tests # the resulting note. .PHONY: test-e2e test-e2e: ## Runs the end-to-end test against a real node and funding service - cargo build --release --locked -p miden-faucet scripts/e2e/network.sh up - scripts/e2e/faucet.sh up cargo test --release --locked -p miden-faucet-client --test e2e -- --ignored --nocapture; \ status=$$?; \ - scripts/e2e/faucet.sh down; \ scripts/e2e/network.sh down; \ exit $$status diff --git a/bin/faucet-client/Cargo.toml b/bin/faucet-client/Cargo.toml index d584c799..e5c5f937 100644 --- a/bin/faucet-client/Cargo.toml +++ b/bin/faucet-client/Cargo.toml @@ -32,5 +32,6 @@ url = { workspace = true } axum = { features = ["tokio"], version = "0.8" } miden-client = { features = ["testing"], workspace = true } miden-client-sqlite-store = { workspace = true } +miden-faucet = { workspace = true } serde = { workspace = true } tokio = { features = ["macros", "net", "rt-multi-thread", "time"], workspace = true } diff --git a/bin/faucet-client/tests/e2e.rs b/bin/faucet-client/tests/e2e.rs index 8ba09e7a..62fa1487 100644 --- a/bin/faucet-client/tests/e2e.rs +++ b/bin/faucet-client/tests/e2e.rs @@ -1,10 +1,11 @@ //! End-to-end test against a real node, funding service and faucet. //! -//! Creates an account, requests tokens for it, waits for the note to be committed, consumes it and -//! checks the balance. Ignored by default because it needs the network: `make test-e2e` starts -//! everything and runs it. +//! Starts a faucet, creates an account, requests tokens for it, waits for the note to be committed, +//! consumes it and checks the balance. Ignored by default because it needs a node and a funding +//! service: `make test-e2e` starts them and runs it. use std::env::temp_dir; +use std::time::Duration; use miden_client::account::{AccountId, AccountType}; use miden_client::builder::ClientBuilder; @@ -13,9 +14,16 @@ use miden_client::note::{Note, NoteId}; use miden_client::rpc::Endpoint; use miden_client::testing::common::{AccountSetup, TestClient}; use miden_client_sqlite_store::ClientBuilderSqliteExt; +use miden_faucet::network::FaucetNetwork; +use miden_faucet::{Cli, Command, FaucetConfig, run_faucet_command}; use miden_faucet_client::mint::{FaucetHttpClient, solve_challenge}; const DEFAULT_FAUCET_URL: &str = "http://127.0.0.1:18000"; +const DEFAULT_FUNDING_SERVICE_URL: &str = "http://127.0.0.1:50401"; + +/// How long the faucet gets to reach the funding service and bind its API. +const FAUCET_ATTEMPTS: u32 = 60; +const FAUCET_DELAY: Duration = Duration::from_secs(1); const REQUEST_TIMEOUT_MS: u64 = 30_000; @@ -32,6 +40,9 @@ const NOTE_MAX_BLOCKS: u32 = 60; async fn request_tokens_and_consume_the_note() { let faucet_url = env_or("FAUCET_URL", DEFAULT_FAUCET_URL); let node_url = env_or("NODE_URL", &Endpoint::localhost().to_string()); + let funding_service_url = env_or("FUNDING_SERVICE_URL", DEFAULT_FUNDING_SERVICE_URL); + + start_faucet(&faucet_url, &node_url, &funding_service_url).await; let mut client = build_client(&node_url).await; @@ -76,6 +87,69 @@ async fn request_tokens_and_consume_the_note() { println!("Balance: {balance} of the {AMOUNT} requested, the rest paid the fee"); } +/// Starts a faucet against the node and the funding service, and waits for it to serve. +async fn start_faucet(faucet_url: &str, node_url: &str, funding_service_url: &str) { + let port = faucet_url + .rsplit(':') + .next() + .and_then(|port| port.parse().ok()) + .expect("the faucet url should carry a port"); + + let command = Command::Start { + config: FaucetConfig { + node_url: Some(node_url.parse().expect("the node url should be a url")), + timeout: Duration::from_secs(5), + network: FaucetNetwork::Localhost, + api_keys_path: None, + }, + funding_service_url: funding_service_url + .parse() + .expect("the funding service url should be a url"), + decimals: 6, + api_bind_port: port, + api_public_url: faucet_url.parse().expect("the faucet url should be a url"), + frontend_bind_port: 0, + no_frontend: true, + max_claimable_amount: 1_000_000_000, + pow_secret: Some("e2e".to_owned()), + 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, + }; + + std::thread::spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("the faucet runtime should build"); + + runtime.block_on(async { + Box::pin(run_faucet_command(Cli { command })) + .await + .expect("the faucet should serve"); + }); + }); + + let metadata_url = format!("{faucet_url}/get_metadata"); + for attempt in 1..=FAUCET_ATTEMPTS { + if reqwest::get(&metadata_url) + .await + .is_ok_and(|response| response.status().is_success()) + { + return; + } + + println!("Faucet not serving yet, attempt {attempt}/{FAUCET_ATTEMPTS}"); + tokio::time::sleep(FAUCET_DELAY).await; + } + + panic!("the faucet did not answer on {faucet_url}"); +} + /// Builds a client with its own store and keystore, pointed at the test network. async fn build_client(node_url: &str) -> TestClient { // Emptied first, so no account or note from an earlier run can take part in the test. diff --git a/bin/faucet/src/main.rs b/bin/faucet/src/lib.rs similarity index 98% rename from bin/faucet/src/main.rs rename to bin/faucet/src/lib.rs index d303fbff..7a9e5e6b 100644 --- a/bin/faucet/src/main.rs +++ b/bin/faucet/src/lib.rs @@ -2,8 +2,8 @@ mod api; mod api_key; mod frontend; mod funding_service_client; -mod logging; -mod network; +pub mod logging; +pub mod network; #[cfg(test)] mod testing; @@ -199,25 +199,25 @@ pub enum ApiKeyCommand { pub struct FaucetConfig { /// Path to the file holding the API keys, one key per line. #[arg(long = "api-keys-file", value_name = "FILE", env = ENV_API_KEYS)] - api_keys_path: Option, + pub api_keys_path: Option, /// 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, + pub 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, + pub network: FaucetNetwork, /// 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, + pub node_url: Option, } impl Command { - fn open_telemetry(&self) -> OpenTelemetry { + pub fn open_telemetry(&self) -> OpenTelemetry { if matches!(*self, Command::Start { open_telemetry: true, .. }) { OpenTelemetry::Enabled } else { @@ -226,22 +226,11 @@ impl Command { } } -// MAIN +// COMMANDS // ================================================================================================= -#[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<()> { +pub async fn run_faucet_command(cli: Cli) -> anyhow::Result<()> { // Note: open-telemetry is handled in main. match cli.command { Command::ApiKey { command } => match command { diff --git a/scripts/e2e/faucet.sh b/scripts/e2e/faucet.sh deleted file mode 100755 index 723a18aa..00000000 --- a/scripts/e2e/faucet.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -# -# Starts and stops the faucet the end-to-end test runs against. - -set -euo pipefail - -FAUCET_BIN="${FAUCET_BIN:-./target/release/miden-faucet}" -FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:18000}" -NODE_URL="${NODE_URL:-http://127.0.0.1:57291}" -FUNDING_SERVICE_URL="${FUNDING_SERVICE_URL:-http://127.0.0.1:50401}" -DECIMALS="${DECIMALS:-6}" - -WORK_DIR="${WORK_DIR:-target/e2e}" -PID_FILE="${WORK_DIR}/faucet.pid" -LOG_FILE="${WORK_DIR}/faucet.log" -API_KEYS_FILE="${WORK_DIR}/api_keys.txt" - -# How long the faucet gets to reach the funding service and bind its API. -START_TIMEOUT_SECONDS="${START_TIMEOUT_SECONDS:-60}" - -start() { - stop - - mkdir -p "${WORK_DIR}" - # The faucet reads its API keys on startup, so the file has to exist even with no keys in it. - touch "${API_KEYS_FILE}" - - echo "Starting the faucet on ${FAUCET_URL}, logging to ${LOG_FILE}" - "${FAUCET_BIN}" start \ - --funding-service-url "${FUNDING_SERVICE_URL}" \ - --node-url "${NODE_URL}" \ - --network localhost \ - --decimals "${DECIMALS}" \ - --api-bind-port "${FAUCET_URL##*:}" \ - --api-keys-file "${API_KEYS_FILE}" \ - --no-frontend \ - > "${LOG_FILE}" 2>&1 & - echo $! > "${PID_FILE}" - - for _ in $(seq "${START_TIMEOUT_SECONDS}"); do - if curl --silent --fail --output /dev/null "${FAUCET_URL}/get_metadata"; then - echo "The faucet is serving its metadata" - return 0 - fi - sleep 1 - done - - echo "error: the faucet did not answer within ${START_TIMEOUT_SECONDS}s" >&2 - cat "${LOG_FILE}" >&2 - return 1 -} - -stop() { - [[ -f "${PID_FILE}" ]] || return 0 - - local pid - pid="$(cat "${PID_FILE}")" - rm -f "${PID_FILE}" - - kill "${pid}" 2> /dev/null || return 0 - echo "Stopped the faucet (pid ${pid})" -} - -case "${1:-}" in - up) start ;; - down) stop ;; - *) - echo "usage: $0 up|down" >&2 - exit 2 - ;; -esac From 9a58f29b44affcb1ba894da078599cb9d8829bb5 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Wed, 7 Oct 2026 18:27:18 -0300 Subject: [PATCH 22/26] fix: change network.sh to read node version from Cargo.lock instead of Cargo.toml --- scripts/e2e/network.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/e2e/network.sh b/scripts/e2e/network.sh index 0c0ed5fe..e41b9418 100755 --- a/scripts/e2e/network.sh +++ b/scripts/e2e/network.sh @@ -10,10 +10,10 @@ set -euo pipefail WORK_DIR="${WORK_DIR:-target/e2e}" REGISTRY="${REGISTRY:-ghcr.io/0xmiden}" PROJECT="${PROJECT:-miden-faucet-e2e}" -VERSION="${NODE_VERSION:-$(sed -n 's/^miden-node-proto-build *= *{ *version *= *"=\{0,1\}\([^"]*\)".*/\1/p' bin/faucet/Cargo.toml)}" +VERSION="${NODE_VERSION:-$(sed -n '/^name = "miden-node-proto-build"$/{n;s/^version = "\(.*\)"$/\1/p;}' Cargo.lock)}" if [[ -z "${VERSION}" ]]; then - echo "error: could not read the node version from bin/faucet/Cargo.toml" >&2 + echo "error: could not read the miden-node-proto-build version from Cargo.lock" >&2 exit 1 fi From 82e4a64c06378cc2c66af5cfc641ef4bf96edbf2 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Wed, 7 Oct 2026 18:32:58 -0300 Subject: [PATCH 23/26] fix: add missing field when creating FaucetConfig --- bin/faucet-client/tests/e2e.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/faucet-client/tests/e2e.rs b/bin/faucet-client/tests/e2e.rs index 62fa1487..92c77d4f 100644 --- a/bin/faucet-client/tests/e2e.rs +++ b/bin/faucet-client/tests/e2e.rs @@ -119,6 +119,7 @@ async fn start_faucet(faucet_url: &str, node_url: &str, funding_service_url: &st base_amount: 100_000, open_telemetry: false, explorer_url: None, + token_amounts: vec![1, 10, 100], }; std::thread::spawn(move || { From f4c580da3a3b4b5e14797fb167af5b6664a7ee5e Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 8 Oct 2026 10:00:38 -0300 Subject: [PATCH 24/26] fix(test): sync the client before waiting for the note to be committed --- bin/faucet-client/tests/e2e.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/faucet-client/tests/e2e.rs b/bin/faucet-client/tests/e2e.rs index 92c77d4f..2ce06b1b 100644 --- a/bin/faucet-client/tests/e2e.rs +++ b/bin/faucet-client/tests/e2e.rs @@ -195,6 +195,8 @@ async fn request_tokens(faucet_url: &str, account_id: AccountId) -> NoteId { /// Syncs until the note is committed and consumable by the account. async fn wait_for_note(client: &mut TestClient, account_id: AccountId, note_id: NoteId) -> Note { + client.sync_state().await.expect("the client should sync"); + let consumable = client .wait_for_consumable_notes(account_id, NOTE_MAX_BLOCKS) .await From 3dfc5ce2da2e918de00113731646b752945048e1 Mon Sep 17 00:00:00 2001 From: ricomateo Date: Thu, 8 Oct 2026 10:04:00 -0300 Subject: [PATCH 25/26] chore: restore main.rs --- bin/faucet/src/main.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 bin/faucet/src/main.rs diff --git a/bin/faucet/src/main.rs b/bin/faucet/src/main.rs new file mode 100644 index 00000000..f220dc9c --- /dev/null +++ b/bin/faucet/src/main.rs @@ -0,0 +1,14 @@ +use anyhow::Context; +use clap::Parser; +use miden_faucet::{Cli, logging, run_faucet_command}; + +#[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 +} From 17ffd52d354394459bdc9fb7d06b08a82a41453e Mon Sep 17 00:00:00 2001 From: tomasarrachea Date: Tue, 20 Oct 2026 15:07:52 -0300 Subject: [PATCH 26/26] fix: github security bot comment --- .github/workflows/test-e2e.yml | 3 +++ .github/workflows/test.yml | 2 +- bin/faucet-client/tests/e2e.rs | 15 +++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index d8484f22..175b7a8d 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -13,6 +13,9 @@ concurrency: group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" cancel-in-progress: true +permissions: + contents: read + jobs: test-e2e: name: test-e2e diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c9c5410..49619042 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,6 @@ jobs: # Shared with the end-to-end job, which builds the same release artifacts. shared-key: release save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/next' }} - - uses: taiki-e/install-action@nextest + - uses: taiki-e/install-action@nextest - name: Run tests run: make test diff --git a/bin/faucet-client/tests/e2e.rs b/bin/faucet-client/tests/e2e.rs index 2ce06b1b..c0b8fedf 100644 --- a/bin/faucet-client/tests/e2e.rs +++ b/bin/faucet-client/tests/e2e.rs @@ -122,21 +122,24 @@ async fn start_faucet(faucet_url: &str, node_url: &str, funding_service_url: &st token_amounts: vec![1, 10, 100], }; - std::thread::spawn(move || { + let faucet = std::thread::spawn(move || { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("the faucet runtime should build"); - runtime.block_on(async { - Box::pin(run_faucet_command(Cli { command })) - .await - .expect("the faucet should serve"); - }); + runtime.block_on(Box::pin(run_faucet_command(Cli { command }))) }); let metadata_url = format!("{faucet_url}/get_metadata"); for attempt in 1..=FAUCET_ATTEMPTS { + // A faucet that fails to start, e.g. because the funding service is unreachable, stops + // right away, so there is no point in waiting for it to answer. + if faucet.is_finished() { + let result = faucet.join().expect("the faucet thread should not panic"); + panic!("the faucet stopped before serving: {result:?}"); + } + if reqwest::get(&metadata_url) .await .is_ok_and(|response| response.status().is_success())