Skip to content

Conversation

@Keinberger
Copy link
Collaborator

@Keinberger Keinberger commented Jan 7, 2026

Updates

There has been the following update since the beginning of this PR: #1642 (comment)


Summary

Closes #1639

This PR implements a from_system_user_config() method for CliClient that creates a Client instance using the same configuration as the CLI tool (reading from .miden/miden-client.toml or local config). This enables anyone to programmatically obtain a properly configured CLI client.

This change is required for implementing the mint command https://github.com/0xMiden/midenup/issues/128, in particular with the new implementation strategy described in 0xMiden/midenup#130 (comment). The https://github.com/0xMiden/miden-faucet repository needs to create a CLI-configured client instance to consume notes and sync state after making the mint request when using the miden-faucet-client, see this PR description for more details: 0xMiden/miden-faucet#196.

Details

New CliClient wrapper type

A newtype wrapper CliClient(Client<CliKeyStore>) has been added to miden-client-cli with Deref and DerefMut implementations. This allows external projects to:

  1. Initialize a client with CLI configuration via CliClient::from_system_user_config()
  2. Use all Client methods transparently through deref coercion
  3. Access the inner Client<CliKeyStore> when needed via into_inner(), inner(), or inner_mut()

Configuration loading behavior

The from_system_user_config() method searches for configuration files in order:

  1. Local .miden/miden-client.toml in the current working directory
  2. Global .miden/miden-client.toml in the home directory

The client is initialized with the same settings as the CLI:

  • SQLite store from configured path
  • gRPC client connection to configured RPC endpoint
  • Filesystem-based keystore authenticator
  • Optional note transport client (if configured)
  • Transaction graceful blocks delta
  • Optional max block number delta
  • Debug mode (accepts DebugMode enum, matching ClientBuilder::in_debug_mode() API)

The new CliClient has been integrated into the existing CLI logic to ensure that the CliClient is the single source of configuration for the CLI client instance.

Usage example

use miden_client::DebugMode;
use miden_client_cli::CliClient;

// Create a CLI-configured client
let client = CliClient::from_system_user_config(DebugMode::Disabled).await?;

// Use it like a regular Client
client.sync_state().await?;

// Build and submit transactions
let req = TransactionRequest::builder()
    .consume_notes(recently_minted_note)
    .build()?;

client.submit_new_transaction(req, target_account_id)?;

This pattern was originally described by @igamigo in the following comment: 0xMiden/midenup#130 (comment)

@Keinberger Keinberger requested a review from igamigo January 7, 2026 13:57
@Keinberger Keinberger linked an issue Jan 7, 2026 that may be closed by this pull request
@Keinberger Keinberger self-assigned this Jan 7, 2026
@Keinberger Keinberger marked this pull request as draft January 7, 2026 14:02
@Keinberger Keinberger marked this pull request as ready for review January 7, 2026 16:13
@Keinberger Keinberger requested a review from mmagician January 7, 2026 16:13
@Keinberger Keinberger changed the title feat: implement and export CliClient for default Client initializatio… feat: implement CliClient Jan 7, 2026
Comment on lines +1405 to +1406
// Clean up any existing global config
cleanup_global_config();
Copy link
Collaborator

Choose a reason for hiding this comment

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

These home dirs are process-global environments, right? So depending on how nextest runs tests there could be cases where this messes up with some other tests, is that correct? If so, I wonder if we should run these serially.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! You're absolutely right, these tests manipulate process-global resources (~/.miden home directory and env::set_current_dir()). I've added #[serial_test::serial(global_config)] to all four tests to ensure they run serially and avoid race conditions with nextest or parallel test execution.

Comment on lines 1312 to 1314
// Verify the client is functional by syncing
let sync_result = client.sync_state().await;
assert!(sync_result.is_ok(), "Client sync failed: {:?}", sync_result.err());
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this doesn't really assert that the test ends up using the local config (although this might not be simple anyway)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same applies for the global one

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I think unfortuantely there is no good way to test this. The assertions didn't actually verify which config was used.

I've fixed this by looking at the actual store file path that was created.

Comment on lines 1333 to 1335
// Attempt to create a client (should fail)
let client = miden_client_cli::CliClient::from_system_user_config(DebugMode::Disabled).await;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this not silently initialize the client? Or is that not ideal?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Absolutely correct, I missed that one. I implemented that now!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated the implementation to call InitCmd::default().execute() when no config is found, and adjusted the test to verify silent initialization works correctly.

/// # Ok(())
/// # }
/// ```
pub async fn from_system_user_config(debug_mode: DebugMode) -> Result<Self, CliError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

One thing that would be nice is passing a CliConfig to a client constructor, and then have different constructors/loaders for CliConfig (for example, one for local config, one for global, one that respects the priority); it can also save the TOML automatically on the correct directories. We can still keep this from_system_user_config() as well.
I think this also makes tests better in the sense that you don't have to change the working dir.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I completely agree with your suggestion! However, are you fine if we move this to a separate issue to speed up this PR and the mint command (mint has taken quite long already)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nevermind, I addressed this too and refactored the CLI config

Comment on lines 1293 to 1295
// Change to the temp directory to ensure the local config is picked up
let original_dir = env::current_dir().unwrap();
env::set_current_dir(&temp_dir)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is also process-wide which could be problematic

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed according with #1642 (comment)

feat(cli): add silent initialization for CliClient

test(cli): refactor tests not to use global resources

test(cli): modify from_system_user_config CliClient tests  to run serially

test(cli): modify from_system_user_config tests to effectively assert global and local config prioritization

test(cli): add silent initialization tests for from_system_user_config
@Keinberger
Copy link
Collaborator Author

Update

Hey everyone, I made a commit with several changes to address the code feedback from @igamigo .

Here's what changed:

  • Config separation architecture: Split configuration loading from client construction. Added CliConfig::from_system(), from_dir(), from_local_dir(), and from_global_dir() methods. Added CliClient::from_config() to build client from explicit config.

  • Test changes: Removed all env::set_current_dir() calls from tests (process-wide side effect). Tests now use explicit paths via CliConfig::from_dir().

  • Safety warnings: Added prominent "⚠️ WARNING: Advanced Use Only" documentation to from_dir(), from_local_dir(), from_global_dir(), and from_config(). These methods bypass CLI's standard config discovery logic (local → global priority). I think this is important: without warnings, external developers might use these testing/advanced methods in production, causing their apps to behave differently than the CLI tool (e.g., ignoring local configs). Marked from_system() and from_system_user_config() as "✅ Recommended" for standard use. The emoji usage serves as eye-catchers to catch awareness when reading the docs.

  • Silent initialization: from_system_user_config() now auto-initializes config if none exists, matching the standard CLI behavior.

Copy link
Collaborator

@igamigo igamigo left a comment

Choose a reason for hiding this comment

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

LGTM. I left some comments, most of which are minor. If you prefer to tackle them separately, let's open an issue for them before merging this.

Comment on lines 87 to 91
///
/// # Deprecated
/// This function is deprecated in favor of `CliConfig::from_system()` which provides
/// the same functionality with a cleaner API.
pub(super) fn load_config_file() -> Result<(CliConfig, PathBuf), CliError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can just delete this and use the new functions instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done! Deleted load_config_file() from utils.rs and replaced all usages with CliConfig::from_system() directly

///
/// # Returns
///
/// A configured `CliClient` instance.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: These can be doc-linked if you use [] instead:

Suggested change
/// A configured `CliClient` instance.
/// A configured [`CliClient`] instance.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This happens throughout the file. It's not critical though so if you prefer the current state that's fine too

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! Updated all documentation to use proper doc-links like [CliClient], [CliConfig],[CliError],[DebugMode::Enabled], etc. Also added cross-references between related methods.

Comment on lines 292 to 304
// Try local first
match Self::from_local_dir() {
Ok(config) => Ok(config),
Err(_) => {
// Fall back to global
Self::from_global_dir().map_err(|_| {
CliError::Config(
"No configuration file found".to_string().into(),
"Neither local nor global config file exists. Run 'miden-client init' to create one.".to_string()
)
})
},
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is fine, but it will also silently eat any errors that are not "file not found" too. For example, if I have a local config but for some reason it fails to parse correctly, it will go through the Err(_) branch and load the global config and this could be confusing. I believe we should check that the file has not been found instead of matching for any error, what do you think?
Maybe it could be addressed on a different issue unless you prefer doing it here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Great point! Added a new CliError::ConfigNotFound variant specifically for "file not found" errors. Now from_system() only falls back to global config when encountering ConfigNotFound, parse errors and other issues are propagated immediately


// FROM_SYSTEM_USER_CONFIG TESTS
// ================================================================================================
/// Tests that `CliClient::from_system_user_config()` successfully creates a client with the same
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see a call to CliClient::from_system_user_config in this test

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed! The test now properly changes the working directory to the temp directory and calls CliClient::from_system_user_config() instead of from_config(). Also added cleanup_global_config() for proper isolation.

@igamigo
Copy link
Collaborator

igamigo commented Jan 13, 2026

One more small thing: For users to make use of client structs and code, I think we'll want to re-export everything from miden-client through the CLI library. This way, they won't have to also manually import miden-client as well as the CLI library.

@Keinberger
Copy link
Collaborator Author

One more small thing: For users to make use of client structs and code, I think we'll want to re-export everything from miden-client through the CLI library. This way, they won't have to also manually import miden-client as well as the CLI library.

Added pub use miden_client as client. All miden-client types can now be accessed through miden_client_cli::client::*

feat(cli): add ConfigNotFound error variant for better error handling

feat(cli): re-export miden_client as client module

docs(cli): use doc-links for type references in documentation

fix(cli): update test to call from_system_user_config
@igamigo
Copy link
Collaborator

igamigo commented Jan 15, 2026

One more small thing: For users to make use of client structs and code, I think we'll want to re-export everything from miden-client through the CLI library. This way, they won't have to also manually import miden-client as well as the CLI library.

Added pub use miden_client as client. All miden-client types can now be accessed through miden_client_cli::client::*

I think it might be better to do pub use miden_client::* unless there is a good reason not to (sorry I didn't specify more in my previous comment). The alternative would be to do a curated set of re-exports at the top-level and the rest under a namespace.
Otherwise, if you wanted to refer to the Client type you'd have to do miden_client_cli::client::Client which is a bit redundant and doesn't add much in terms of code organization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Client::from_system_user_config() helper function

4 participants