Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

## Unreleased

### Breaking Changes

* [BREAKING][removal][test] Loose helper functions in `miden_client::testing::common` are now methods on `TestClient`. `TestClient::keystore()` exposes the client's keystore, so `ClientConfig::into_client` and `into_unsynced_client` return just the `TestClient` instead of a client/keystore pair ([#2481](https://github.com/0xMiden/rust-sdk/pull/2481)).

### Fixes

* [FIX][cli] `miden-client import` now rejects invocations without a file path instead of silently succeeding ([#2450](https://github.com/0xMiden/rust-sdk/pull/2450)).
* [FIX][test] The integration tests run again on a chain that charges no fee. A `--funders` path (`MIDEN_FUNDER_ACCOUNTS_DIR`) that is unset, empty, missing, or holds no `.mac` file now leaves the run without funders instead of failing, which is all a fee-free genesis needs, since it declares no wallets for the path to hold. A `.mac` file that is present but unusable stays a hard error ([#2481](https://github.com/0xMiden/rust-sdk/pull/2481)).

## 0.16.0 (2026-09-07)

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ TEST_MIDEN_NOTE_TRANSPORT_URL?=http://127.0.0.1:57292

# Pre-funded wallets the integration tests draw transaction fees from, either one `.mac` file or a
# directory of them, written here by `start-test-node.sh`. Against a deployed network, point this at
# wallets funded out of band. Unused on a fee-free chain, where no account needs funding.
# wallets funded out of band. A path naming no `.mac` file, means no funders.
MIDEN_FUNDER_ACCOUNTS_DIR?=$(CURDIR)/data/funders

# Pre-deployed agglayer accounts the agglayer tests transact with, written here by
Expand Down Expand Up @@ -160,7 +160,7 @@ integration-test-dev: ## Run integration tests with debug assertions enabled via

.PHONY: integration-test-binary
integration-test-binary: ## Run the integration tests using the standalone binary (requires note transport service)
TEST_MIDEN_NOTE_TRANSPORT_URL=$(TEST_MIDEN_NOTE_TRANSPORT_URL) AGGLAYER_ACCOUNTS_DIR=$(AGGLAYER_ACCOUNTS_DIR) cargo run --package miden-client-integration-tests --release --locked -- --funders $(MIDEN_FUNDER_ACCOUNTS_DIR)
TEST_MIDEN_NOTE_TRANSPORT_URL=$(TEST_MIDEN_NOTE_TRANSPORT_URL) MIDEN_FUNDER_ACCOUNTS_DIR=$(MIDEN_FUNDER_ACCOUNTS_DIR) AGGLAYER_ACCOUNTS_DIR=$(AGGLAYER_ACCOUNTS_DIR) cargo run --package miden-client-integration-tests --release --locked

# --- Installing ----------------------------------------------------------------------------------

Expand Down
7 changes: 5 additions & 2 deletions bin/integration-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ MIDEN_VERIFICATION_BASE_FEE=0 make start-node-background
```

The suite never mints. It draws the native asset from pre-funded basic wallets named by `--funders`
(or `MIDEN_FUNDER_ACCOUNTS_DIR`), either one `.mac` file or a directory of them.
(or `MIDEN_FUNDER_ACCOUNTS_DIR`), either one `.mac` file or a directory of them. A path naming no
`.mac` file means no funders (useful for fee-free chains) so the Make targets can pass the
same path either way.

```bash
# Local node: its genesis pre-funds the wallets and start-test-node.sh writes them here.
Expand Down Expand Up @@ -242,7 +244,8 @@ chain before every payment, which is what makes sharing one between test process

### Environment variables

- `MIDEN_FUNDER_ACCOUNTS_DIR` - funder `.mac` file or directory, same as `--funders`
- `MIDEN_FUNDER_ACCOUNTS_DIR` - funder `.mac` file or directory, same as `--funders`. Unset, empty
or naming no `.mac` file leaves the run without funders
- `MIDEN_VERIFICATION_BASE_FEE` - genesis `verification_base_fee` for the testing node (default
`500`; `0` runs the node fee-free and declares no funder wallets)
- `MIDEN_NUM_FUNDER_WALLETS` - number of wallets a fee-charging genesis pre-funds (default `16`)
Expand Down
19 changes: 10 additions & 9 deletions bin/integration-tests/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ impl ClientConfig {
}

/// Loads the pre-funded wallets at `funders`, one `.mac` account file or a directory of them,
/// as the fee funder. `None` leaves the config without one, which is all a fee-free chain
/// needs.
/// as the fee funder. A path naming no funder file leaves the config without one, which is all
/// a fee-free chain needs.
pub fn with_funders(self, funders: Option<&Path>) -> Result<Self> {
let fee_funder = fee_funding::load(&self, funders)?;
Ok(self.with_fee_funder(fee_funder))
Expand Down Expand Up @@ -177,26 +177,27 @@ impl ClientConfig {

/// Creates a `TestClient` without syncing it, for tests that have to wait for the node first.
///
/// The client gets its own store and keystore.
pub async fn into_unsynced_client(self) -> Result<(TestClient, FilesystemKeyStore)> {
/// The client gets its own store and keystore, the latter reachable through
/// `TestClient::keystore`.
pub async fn into_unsynced_client(self) -> Result<TestClient> {
let fee_funder = self.fee_funder.clone();
let (builder, keystore) = self.into_client_builder()?;
let (builder, _keystore) = self.into_client_builder()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we could make into_client_builder() drop the keystore return entirely? It seems it is no longer being used anywhere since now TestClient::keystore() handles it. Same with create_test_client_builder()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, removed the keystore from the return values in ad8c0e8


let client = builder.build().await.with_context(|| "failed to build test client")?;

Ok((TestClient::from(client).with_fee_funder(fee_funder), keystore))
Ok(TestClient::from(client).with_fee_funder(fee_funder))
}

/// Creates a `TestClient`.
///
/// The client gets its own store and keystore, and is synced to the current state before being
/// returned.
pub async fn into_client(self) -> Result<(TestClient, FilesystemKeyStore)> {
let (mut client, keystore) = self.into_unsynced_client().await?;
pub async fn into_client(self) -> Result<TestClient> {
let mut client = self.into_unsynced_client().await?;

client.sync_state().await.with_context(|| "failed to sync client state")?;

Ok((client, keystore))
Ok(client)
}
}

Expand Down
49 changes: 27 additions & 22 deletions bin/integration-tests/src/fee_funding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use miden_client::asset::FungibleAsset;
use miden_client::block::BlockNumber;
use miden_client::keystore::Keystore;
use miden_client::note::{Note, NoteType, P2idNote};
use miden_client::testing::common::{TestClient, wait_for_node, wait_for_tx};
use miden_client::testing::common::TestClient;
use miden_client::testing::fee::FeeFunder;
use miden_client::transaction::TransactionRequestBuilder;
use rand::RngExt;
Expand All @@ -40,30 +40,36 @@ const FUNDING_AMOUNT: u64 = 10_000_000;
// LOADING
// ================================================================================================

/// Returns the funder path named by [`FUNDER_ACCOUNTS_ENV`], for runners that take no arguments.
/// Returns the funder path named by [`FUNDER_ACCOUNTS_ENV`], for runners that read it themselves
/// rather than taking it as an argument. [`load`] decides what a path holding no funder means.
pub fn funders_path_from_env() -> Option<PathBuf> {
std::env::var_os(FUNDER_ACCOUNTS_ENV)
.map(PathBuf::from)
.filter(|p| !p.as_os_str().is_empty())
std::env::var_os(FUNDER_ACCOUNTS_ENV).map(PathBuf::from)
}

/// Loads the wallets at `funders` as a [`FeeFunder`] paying out of whichever one is free. `None`
/// yields no funder. The funder client is built from `client_config`'s endpoints.
/// Loads the wallets at `funders` as a [`FeeFunder`] paying out of whichever one is free. The
/// funder client is built from `client_config`'s endpoints.
///
/// Yields no funder when `funders` names no funder file. A file that is there but cannot be used as
/// a funder is still an error.
pub fn load(
client_config: &ClientConfig,
funders: Option<&Path>,
) -> Result<Option<Arc<dyn FeeFunder>>> {
let Some(path) = funders else {
let wallets = load_funders(funders)?;
if wallets.is_empty() {
return Ok(None);
};

let wallets = load_funders(path)?;
}

Ok(Some(Arc::new(Funder::new(client_config, wallets))))
}

/// Loads the funder wallets at `path`, which is either one `.mac` file or a directory of them.
fn load_funders(path: &Path) -> Result<Vec<AccountFile>> {
/// Loads the funder wallets at `path`, which is either one `.mac` file or a directory of them, and
/// none at all when `path` names no such file.
fn load_funders(path: Option<&Path>) -> Result<Vec<AccountFile>> {
let Some(path) = path.filter(|path| !path.as_os_str().is_empty()) else {
return Ok(Vec::new());
};

let paths = if path.is_dir() {
let mut mac_files: Vec<PathBuf> = std::fs::read_dir(path)
.with_context(|| format!("failed to read funder directory {}", path.display()))?
Expand All @@ -77,14 +83,12 @@ fn load_funders(path: &Path) -> Result<Vec<AccountFile>> {
// tests over distinct wallets.
mac_files.sort();
mac_files
} else {
} else if path.is_file() {
vec![path.to_path_buf()]
} else {
Vec::new()
};

if paths.is_empty() {
bail!("no `.mac` funder account files in {}", path.display());
}

// A private funder's state lives only in the file, so sharing one across processes would build
// every transaction from the same stale snapshot. A public one is re-read from the chain.
paths
Expand Down Expand Up @@ -152,21 +156,21 @@ impl Funder {
}

async fn build_client(&self) -> Result<TestClient> {
let (mut client, keystore) = self
let mut client = self
.client_config
.clone()
.into_unsynced_client()
.await
.context("failed to build the funder client")?;

// Some tests create their accounts before waiting for the node, so the wait happens here.
wait_for_node(&mut client).await;
client.wait_for_node().await;
client.sync_state().await.context("failed to sync the funder client")?;

for wallet in &self.wallets {
let id = wallet.account.id();
for key in &wallet.auth_secret_keys {
keystore.add_key(key, id).await.context("failed to add a funder key")?;
client.keystore().add_key(key, id).await.context("failed to add a funder key")?;
}
}

Expand Down Expand Up @@ -234,7 +238,8 @@ impl Funder {

// Waited on before the wallet is released: another process claiming it reads its state from
// the chain, which does not carry this payment until it commits.
wait_for_tx(client, tx_id)
client
.wait_for_tx(tx_id)
.await
.with_context(|| format!("the payment from funder {wallet_id} never committed"))?;

Expand Down
12 changes: 9 additions & 3 deletions bin/integration-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@ struct Args {
note_transport_url: Option<String>,

/// Path to the pre-funded basic wallets the tests draw transaction fees from: either one `.mac`
/// account file or a directory of them.
#[arg(long, env = fee_funding::FUNDER_ACCOUNTS_ENV)]
/// account file or a directory of them. Defaults to `MIDEN_FUNDER_ACCOUNTS_DIR`. A path naming
/// no such file leaves the run without funders.
#[arg(long)]
funders: Option<PathBuf>,

/// Enable verbose tracing output (info-level logs from tests and client).
Expand Down Expand Up @@ -235,12 +236,17 @@ impl TryFrom<Args> for BaseConfig {
}
};

let funders = args
.funders
.or_else(fee_funding::funders_path_from_env)
.filter(|path| !path.as_os_str().is_empty());

Ok(BaseConfig {
rpc_endpoint: endpoint,
timeout: timeout_ms,
prover_endpoint,
note_transport_endpoint,
funders: args.funders,
funders,
verbose: args.verbose,
})
}
Expand Down
Loading
Loading