Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
### Breaking Changes

* [BREAKING][type][rust] Added the `TransactionRequestError::SwapNoteWithZeroAsset` variant, so exhaustive matches on `TransactionRequestError` must handle it ([#2459](https://github.com/0xMiden/rust-sdk/pull/2459)).
* [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][rust] Added validation of cached transaction encryption keys during deserialization. Unsupported encryption schemes and empty or oversized key IDs are rejected before reading the key ID bytes ([#2411](https://github.com/0xMiden/rust-sdk/pull/2411)).
* [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][rust] `TransactionRequestBuilder::build_swap` and `build_pswap_create` now reject a zero-amount asset on either side of the exchange. A zero requested asset produced a payback P2ID note carrying nothing, and a zero offered asset produced a note whose consumer pays and receives nothing ([#2459](https://github.com/0xMiden/rust-sdk/pull/2459)).
* [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
29 changes: 14 additions & 15 deletions bin/integration-tests/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,21 +118,19 @@ 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))
}

/// Creates a `TestClient` builder and keystore.
/// Creates a `TestClient` builder.
///
/// The store is a `SQLite` database at a temporary location, and the keystore a temporary
/// directory, both created here rather than held on the config, so every client this is called
/// on gets its own.
pub fn into_client_builder(
self,
) -> Result<(ClientBuilder<FilesystemKeyStore>, FilesystemKeyStore)> {
pub fn into_client_builder(self) -> Result<ClientBuilder<FilesystemKeyStore>> {
let store_config = create_test_store_path();
let auth_path = create_test_auth_path();

Expand All @@ -154,7 +152,7 @@ impl ClientConfig {
.rpc(rpc_client)
.rng(Box::new(rng))
.sqlite_store(store_config)
.authenticator(Arc::new(keystore.clone()))
.authenticator(Arc::new(keystore))
.tx_discard_delta(None);

if let Some(prover_url) = &self.prover_endpoint {
Expand All @@ -172,31 +170,32 @@ impl ClientConfig {
builder = builder.note_transport(nt_client);
}

Ok((builder, keystore))
Ok(builder)
}

/// 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 = self.into_client_builder()?;

let client = builder.build().await.with_context(|| "failed to build test client")?;
Comment on lines 183 to 184

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is the only instance of into_client_builder() that I could find. Should we remove it? It doesn't do anything interesting with it


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