Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding client builder for simple initialization of the Rust client #741

Merged
merged 2 commits into from
Feb 25, 2025

Conversation

partylikeits1983
Copy link

@partylikeits1983 partylikeits1983 commented Feb 18, 2025

Adding client builder for simple initialization of the Rust client

Overview

This PR introduces a new client builder to the Rust client, streamlining the initialization process and reduces the amount of boilerplate code when getting started with a Miden dApp. With the new client builder, setting up a Miden dApp in Rust becomes significantly easier. Personally, this was one of the features I really wanted to have as I was building the tutorial code examples.

By integrating this feature, we lower the barrier to entry and reduce the lines of code needed to deploy and interact with smart contracts and notes. For context, our Miden dApp tutorials currently showcase the older, more verbose approach where the initialize_client() function is used in each code example.

This improvement was discussed internally in this issue.

What This Solves

  • Reduced Boilerplate: Eliminates the need for the repetitive initialize_client() function in the tutorial docs.
  • Simpler Onboarding: New users can now instantiate the client with minimal configuration.
  • Enhanced Flexibility: The builder allows for easy configuration of RPC endpoints, timeouts, store paths, and debug modes.
  • Reduces Lines of Code: The builder reduces the lines of code it takes to build a simple dApp on Miden using the rust client.

Usage

Initializing the client is now as simple as:

let mut client = Client::initialize()
    .with_rpc("https://rpc.testnet.miden.io:443")
    .in_debug_mode(true)
    .build()
    .await?;

Tests

I added a simple test of instantiating the client using the new client builder. I also tested the new client builder in a separate repository.

Future Work

There may be further improvements in the instantiation process. However, the primary goal here is to reduce the number of lines and the complexity required to get started with building on Miden.


Please let me know if I can make any improvements.

@bobbinth bobbinth requested a review from igamigo February 18, 2025 19:47
Copy link
Author

Choose a reason for hiding this comment

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

Is it ok to use #[tokio::test] here? And is it ok that this test requires an internet connection to ping the testnet?

Copy link
Collaborator

Choose a reason for hiding this comment

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

An integration test is probably better suited for tests that require syncing against the node. Integration tests are executed in the CI against a locally running node running the next branch. The builder itself may also probably be successfully tested with unit tests as well.

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.

Thanks for the PR! I think a builder could be useful as the client's instantiation API becomes more complex. I had made some comments a while ago about potentially introducing one, but it's probably a very stale comment.

In general we might not be able to provide "good" defaults (for the context of the tutorials, at least), because we need to be generic enough that the Client can be compiled for no_std environments, so not entirely sure if this would provide a ton of value for your use-case, but I still think it probably makes for a good addition.

use miden_objects::crypto::rand::RpoRandomCoin;
use rand::Rng;

pub struct ClientBuilder {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should add doc comments to the struct and the fields.

Copy link
Collaborator

Choose a reason for hiding this comment

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

With this, we might have to update library.md to reflect these changes as well.

Copy link
Author

Choose a reason for hiding this comment

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

Added doc comments. Let me know if this is the correct format.

}

/// Sets the RPC endpoint via a URL.
pub fn with_rpc(mut self, url: &str) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this not provide a generic dyn NodeRpcClient + Send? Because otheriwse if we hardcode it to a TonicRpcClient, then the std feature becomes a requirement and this means we cannot compile the client for wasm32

Copy link
Author

Choose a reason for hiding this comment

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

What about putting it behind a tonic feature flag? This would mean we could still compile for wasm32.

#[cfg(feature = "tonic")]
pub fn with_rpc(mut self, url: &str) -> Self {
    ...
}

Copy link
Collaborator

@igamigo igamigo Feb 20, 2025

Choose a reason for hiding this comment

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

I think we could add a ClientBuilder block within the TonicRpcClient that contains this method and we could avoid having the #[cfg(feature="std")] guard for this mod.
Could look something like this:

// (on rpc/tonic_client/mod.rs):
impl ClientBuilder {
    #[must_use]
    pub fn with_tonic_rpc(mut self, url:&Endpoint, timeout: u64) -> Self {
        let tonic_client = TonicRpcClient::new(..,);
        self.with_rpc_client(Box::new(tonic_client))
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe the same could be done for SQLite store. I think this might be a bit of an antipattern in Rust but it should work well here for making the builder available but also making it more ergonomic if certain features are turned on.

}

/// Optionally set a custom store path.
pub fn with_store_path(mut self, path: &str) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar to the previous comment, we should provide the store generically instead of always building a SqliteStore

Copy link
Collaborator

Choose a reason for hiding this comment

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

We probably want to do the same for all components that were not defined here as well (like the RNG component)

Copy link
Contributor

@bobbinth bobbinth Feb 21, 2025

Choose a reason for hiding this comment

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

I think the more generic method is with_store() right? I think one of the intentions of this method was it to make it configuring sqlite store as easy as possible. So, maybe the way to go there is to rename this into something like with_sqlite_store() so that it is clear that this method is intended to work specifically with sqlite store.

Copy link
Author

Choose a reason for hiding this comment

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

That makes sense. Will modify.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah yes, true. Agree with the rename. I'd also move it to the SQLite mod but maybe we could refactor it separately

Copy link
Collaborator

Choose a reason for hiding this comment

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

An integration test is probably better suited for tests that require syncing against the node. Integration tests are executed in the CI against a locally running node running the next branch. The builder itself may also probably be successfully tested with unit tests as well.

@partylikeits1983
Copy link
Author

partylikeits1983 commented Feb 19, 2025

  1. Refactored the src/builder.rs to not use std.
  2. Added the builder test to the integration test directory.
  3. Fixed small nits regarding comments.

After adding tonic and Sqlite feature flags to certain parts of the code, the builder.rs can be compiled for wasm32

Compiled successfully when I ran this:

cargo build --manifest-path=crates/rust-client/Cargo.toml --target wasm32-unknown-unknown --no-default-features

@partylikeits1983
Copy link
Author

Testing again locally with my miden dApp testing repo, this PR makes client instantiation much cleaner!

Now it is possible to do this:

let mut client = Client::initialize()
    .with_rpc("https://rpc.testnet.miden.io:443")
    .in_debug_mode(true)
    .build()
    .await?;

Instead of this: https://github.com/partylikeits1983/miden-tx/blob/ad0e378687ce950078b0bccd62ca030ac0ff09fb/src/bin/ephemeral_notes.rs#L26-L53

Comment on lines 71 to 94
// Determine the scheme and strip it from the URL.
let (scheme, rest) = if let Some(stripped) = url.strip_prefix("https://") {
("https", stripped)
} else if let Some(stripped) = url.strip_prefix("http://") {
("http", stripped)
} else {
("https", url)
};

// Attempt to find a colon indicating a port.
let (host, port) = if let Some(colon_index) = rest.find(':') {
// Split the host and port.
let host = &rest[..colon_index];
let port_str = &rest[colon_index + 1..];
// Try parsing the port. If it fails, use None.
let port = port_str.parse::<u16>().ok();
(host.to_string(), port)
} else {
// No colon found, so use the entire string as the host and no port.
(rest.to_string(), None)
};

// Create the endpoint using the parsed scheme, host, and port.
let endpoint = Endpoint::new(scheme.to_string(), host, port);
Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be possible to replace this with Endpoint::try_from(&str).

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 so too, this function could even receive a built Endpoint object. This way the user can use the known endpoints (like Endpoint::testnet()).

Copy link
Author

Choose a reason for hiding this comment

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

Yes it does! Will fix.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed!

Comment on lines 71 to 94
// Determine the scheme and strip it from the URL.
let (scheme, rest) = if let Some(stripped) = url.strip_prefix("https://") {
("https", stripped)
} else if let Some(stripped) = url.strip_prefix("http://") {
("http", stripped)
} else {
("https", url)
};

// Attempt to find a colon indicating a port.
let (host, port) = if let Some(colon_index) = rest.find(':') {
// Split the host and port.
let host = &rest[..colon_index];
let port_str = &rest[colon_index + 1..];
// Try parsing the port. If it fails, use None.
let port = port_str.parse::<u16>().ok();
(host.to_string(), port)
} else {
// No colon found, so use the entire string as the host and no port.
(rest.to_string(), None)
};

// Create the endpoint using the parsed scheme, host, and port.
let endpoint = Endpoint::new(scheme.to_string(), host, port);
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 so too, this function could even receive a built Endpoint object. This way the user can use the known endpoints (like Endpoint::testnet()).

Comment on lines 95 to 100
self.rpc_api = Some(Box::new(TonicRpcClient::new(endpoint, self.timeout_ms)));
self
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit

Suggested change
self.rpc_api = Some(Box::new(TonicRpcClient::new(endpoint, self.timeout_ms)));
self
self.with_rpc_api(Box::new(TonicRpcClient::new(endpoint, self.timeout_ms))));

Copy link
Author

Choose a reason for hiding this comment

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

I think so too, this function could even receive a built Endpoint object. This way the user can use the known endpoints (like Endpoint::testnet()).

The only thing about doing this is that it might make initializing the client take one extra line of code in a Miden app.

Would it mean doing this?

let endpoint = Endpoint::testnet();
let mut client = Client::initialize()
    .with_rpc(endpoint)
    .in_debug_mode(true)
    .build()
    .await?;

Copy link
Collaborator

@tomyrd tomyrd Feb 19, 2025

Choose a reason for hiding this comment

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

You could create the endpoint inside the function call. Like this:

let mut client = Client::initialize()
    .with_rpc(Endpoint::testnet())
    .in_debug_mode(true)
    .build()
    .await?;

And if the user wants a custom endpoint they can build it from a string like the old interface:

let mut client = Client::initialize()
    .with_rpc(Endpoint::try_from("https://test:123")?)
    .in_debug_mode(true)
    .build()
    .await?;

Copy link
Author

Choose a reason for hiding this comment

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

Ok this makes sense. Will change.

Comment on lines 99 to 110
/// Optionally set a custom timeout (in ms).
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.timeout_ms = timeout_ms;
self
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We would need to specify that this function should be called before with_rpc (if not, the parameter would be ignored). I think it would be even better if we move the timeout_ms parameter to the with_rpc function. The parameter could be optional if we want to allow for a default timeout.

Comment on lines 146 to 190
// Set up the store.
// If the user provided a store, use it.
// Otherwise, if the `sqlite` feature is enabled, build one from the store_path.
// If not, return an error.
let arc_store: Arc<dyn Store> = {
#[cfg(feature = "sqlite")]
{
if let Some(store) = self.store {
store
} else {
let store = SqliteStore::new(self.store_path.into())
.await
.map_err(ClientError::StoreError)?;
Arc::new(store)
}
}
#[cfg(not(feature = "sqlite"))]
{
self.store.ok_or_else(|| {
ClientError::ClientInitializationError(
"No store provided and the sqlite feature is disabled.".into(),
)
})?
}
};
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 the store could be instantiated in with_store_path. Similar to what we do with the rpc.

};

// Create the authenticator.
let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());
Copy link
Collaborator

Choose a reason for hiding this comment

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

After merging #718, the authenticator now needs a KeyStore to be built, this keystore would need to be created externally and not by the builder (because the user of the client needs the keystore to add new keys).

We can probably add a with_keystore function to the builder and use the provided keystore here. It should fail if no keystore is defined as there's no clear default implementation (one is std and the other no-std and they don't share parameters).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Nevermind, this PR is going to main so those changes aren't relevant here. We will need to merge these changes to next when this gets merged so we can add this.

Copy link
Author

Choose a reason for hiding this comment

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

@tomyrd I can rebase this to the next branch if it makes things easier. Would be useful to have this feature on 0.7, but if it's too complicated can rebase to next

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks! But I also think it's best for 0.7, it won't be difficult to bring these changes to next once they get merged. I was just mentioning it so we don't forget.

Copy link
Author

Choose a reason for hiding this comment

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

Just saw this. @igamigo told me to rebase to next. Not an urgent change or anything. Rebasing to next means the PR can be ironed out a bit more.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perfect! Let me know if you have questions about the new authenticator

Comment on lines 276 to 318

// BUILDER ENTRY POINT (OPTIONAL HELPER)
// --------------------------------------------------------------------------------------------

impl Client<RpoRandomCoin> {
pub fn initialize() -> ClientBuilder {
ClientBuilder::new()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want this helper? I think it may hide the fact that we are using an underlying ClientBuilder and not a Client to initialize. We may want them to be separate (like the TransactionRequest and TransactionRequestBuilder).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree, is there any benefit to this helper?

Copy link
Author

Choose a reason for hiding this comment

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

I see now, definitely makes sense to remove it.

@partylikeits1983
Copy link
Author

partylikeits1983 commented Feb 19, 2025

Rebased this PR from main to next.

Also I made the entire client builder abstraction to only compile when std is available.

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks great! Thank you for doing this. I left one small comment above.

Also, what needs to be done to make the CI green?

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.

Thanks! Leaving some more comments for now. As @bobbinth mentioned, the CI checks should be reviewed. Upon a quick look, errors seem quite simple. You can check them locally using the make targets (make clippy, make clippy-wasm, make fix for applying superficial fixes, etc.). Feel free to let me know if you have any questions about any of this

/// An optional store provided by the user.
store: Option<Arc<dyn Store>>,
/// An optional RNG provided by the user.
rng: Option<RpoRandomCoin>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this be generic as well? There might not be a lot of value in forcing an RpoRandomCoin, but I could be wrong.

Copy link
Author

@partylikeits1983 partylikeits1983 Feb 21, 2025

Choose a reason for hiding this comment

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

Would this mean that when initializing the client the user will have to specify type annotations for the client builder?

I implemented a basic version making RpoRandomCoin generic, but it requires that I specify type annotations when initializing the client.

This is how it looks:

let mut client = ClientBuilder::<RpoRandomCoin>::new()
    .with_rpc(Endpoint::try_from("https://rpc.testnet.miden.io:443").unwrap())
    .with_timeout(10_000)
    .with_sqlite_store("store.sqlite3")
    .in_debug_mode(true)
    .build()
    .await?;

Maybe there is a way to make RpoRandomCoin generic and not have to specify the type annotation?

@@ -0,0 +1,155 @@
#![cfg(feature = "std")]
Copy link
Collaborator

@igamigo igamigo Feb 20, 2025

Choose a reason for hiding this comment

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

Usually this would be in the mod declaration (ie, in lib.rs).

EDIT: Just saw you added this there, so this can be removed.

}

/// Sets the RPC client directly (for custom NodeRpcClient implementations).
pub fn with_rpc_client(mut self, client: Box<dyn NodeRpcClient + Send>) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: We could add #[must_use] to these functions.

Comment on lines 276 to 318

// BUILDER ENTRY POINT (OPTIONAL HELPER)
// --------------------------------------------------------------------------------------------

impl Client<RpoRandomCoin> {
pub fn initialize() -> ClientBuilder {
ClientBuilder::new()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree, is there any benefit to this helper?

}

/// Sets the RPC endpoint via a URL.
pub fn with_rpc(mut self, url: &str) -> Self {
Copy link
Collaborator

@igamigo igamigo Feb 20, 2025

Choose a reason for hiding this comment

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

I think we could add a ClientBuilder block within the TonicRpcClient that contains this method and we could avoid having the #[cfg(feature="std")] guard for this mod.
Could look something like this:

// (on rpc/tonic_client/mod.rs):
impl ClientBuilder {
    #[must_use]
    pub fn with_tonic_rpc(mut self, url:&Endpoint, timeout: u64) -> Self {
        let tonic_client = TonicRpcClient::new(..,);
        self.with_rpc_client(Box::new(tonic_client))
    }
}

}

/// Sets the RPC endpoint via a URL.
pub fn with_rpc(mut self, url: &str) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe the same could be done for SQLite store. I think this might be a bit of an antipattern in Rust but it should work well here for making the builder available but also making it more ergonomic if certain features are turned on.

@partylikeits1983
Copy link
Author

partylikeits1983 commented Feb 20, 2025

Addressed several comments from above.

  1. Remove the helper from lib.rs
  2. Added method to set the keystore
  3. Addressed clippy & CI warnings
  4. .with_rpc() now takes an Endpoint type
  5. .with_timeout() can be called in any order before .build()

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! It seems like the integration tests are not passing - but maybe that's something can be fixed rebasing from next?

@igamigo
Copy link
Collaborator

igamigo commented Feb 20, 2025

Looks good! Thank you! It seems like the integration tests are not passing - but maybe that's something can be fixed rebasing from next?

Yes, I think this may be the case.

I'd also see if this could be addressed before merging.

Copy link
Collaborator

@tomyrd tomyrd left a comment

Choose a reason for hiding this comment

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

LGTM! Left a small comment to be addressed before merging.

Comment on lines 173 to 205
None => FilesystemKeyStore::new(temp_dir())
.map_err(|err| ClientError::ClientInitializationError(err.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.

Should we fail if the keystore is not set? If not, I wouldn't use the temp_dir to store the keys, it might get cleared up.

Copy link
Author

Choose a reason for hiding this comment

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

Ah yes that is true. What would be a good reasonable default? I think I was following the cli tests as a guide when writing this, that's why I use 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.

In the CLI we create a "keystore" dir on the current directory and use that.

But in this case, I feel like we should have it so that the user always provides the keystore themselves. My reasoning is that if we create the keystore in the builder, then the user won't have a reference to it. This means that when adding new accounts, the user won't be able to add the authentication keys to the keystore.

Copy link
Author

Choose a reason for hiding this comment

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

Would this make sense as a standard use example? This creates a /keystore directory.

let keystore = FilesystemKeyStore::new("keystore".into()).unwrap();
let mut client = ClientBuilder::new()
    .with_rpc(Endpoint::try_from("https://rpc.testnet.miden.io:443").unwrap())
    .with_timeout(10_000)
    .with_keystore(keystore)
    .with_sqlite_store("store.sqlite3")
    .in_debug_mode(true)
    .build()
    .await?;

Copy link
Author

Choose a reason for hiding this comment

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

This way the user has reference to the keystore.

Copy link
Author

@partylikeits1983 partylikeits1983 Feb 21, 2025

Choose a reason for hiding this comment

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

@tomyrd @igamigo Or could the .with_keystore() method take a string that is the name of the directory? Just trying to reduce the # lines it takes and make it as visually simple as possible to set up the client. What do y'all think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we make it so that the with_keystore() method takes a string, then we would have to change it so that the builder also returns the keystore it created internally, something like:

let (mut client, mut keystore) = ClientBuilder::new()
    .with_rpc(Endpoint::try_from("https://rpc.testnet.miden.io:443").unwrap())
    .with_timeout(10_000)
    .with_keystore("keystore")
    .with_sqlite_store("store.sqlite3")
    .in_debug_mode(true)
    .build()
    .await?;

I'm open to either one but I think I prefer the first one.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think one of the goals here is to make assumptions about a reasonable set of defaults while also keeping some flexibility. So, I'd probably go with more specialized methods that make ergonomics a bit better. For example:

let mut client = ClientBuilder::new()
    .with_tonic_rpc("https://rpc.testnet.miden.io:443")
    .with_timeout(10_000)
    .with_filesystem_keystore("keystore")
    .with_sqlite_store("store.sqlite3")
    .in_debug_mode(true)
    .build()
    .await?;

Ff the user wanted to customize things beyond these defaults, they could also do something like:

let mut client = ClientBuilder::new()
    .with_rpc(my_rpc)
    .with_keystore(my_keystore)
    .with_store(my_store)
    .in_debug_mode(true)
    .build()
    .await?;

The only thing which I think doesn't fit this pattern would be the random coin. But maybe we keep it as is for now and make it more flexible in another PR.

Copy link
Collaborator

@SantiagoPittella SantiagoPittella left a comment

Choose a reason for hiding this comment

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

@partylikeits1983
Copy link
Author

partylikeits1983 commented Feb 21, 2025

Addressed several comments from above.

  1. keystore must be specified during build phase.
  2. ClientBuilder now has 2 methods for setting the keystore: .with_keystore() and .with_filesystem_keystore()

The following is possible:

let mut client = ClientBuilder::new()
    .with_tonic_rpc("https://rpc.testnet.miden.io:443")
    .with_timeout(10_000)
    .with_filesystem_keystore("keystore")
    .with_sqlite_store("store.sqlite3")
    .in_debug_mode(true)
    .build()
    .await?;

The .with_keystore() method allows the user to directly set the keystore.

@partylikeits1983 partylikeits1983 force-pushed the main branch 3 times, most recently from 5eb3859 to f686789 Compare February 21, 2025 20:44
@partylikeits1983
Copy link
Author

Force pushed to keep everything in a single commit & to fix CI/CD issues.

@partylikeits1983
Copy link
Author

@igamigo Regarding this: https://github.com/0xPolygonMiden/miden-client/pull/741/files#r1962738199

Would it be possible to address this in a separate PR? I tried implementing it but I believe it could get a bit complicated. Also not 100% sure how to make RpoRandomCoin generic without having to specify the type in the ClientBuilder, which means my initial implementation made things look like this:

let mut client = ClientBuilder::<RpoRandomCoin>::new();

Copy link
Contributor

@bobbinth bobbinth left a comment

Choose a reason for hiding this comment

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

Looks good! Thank you! I left a coupe of optional comments inline.

Beyond these, I think we can make a couple of improvements in the future:

  • Make handling of KeyStore more generic (so that we could use non-FilesystemKeyStores as well).
  • Make handling of Rng more generic (so that we could use non-RpoRandomCoin as well).

But I would do it in a separate PR - so, let's create an issue for this.

Comment on lines 89 to 98
pub fn with_rpc(mut self, endpoint: Endpoint) -> Self {
self.rpc_endpoint = Some(endpoint);
self
}

/// Sets a custom RPC client directly.
///
/// This method overrides any previously set RPC endpoint.
#[must_use]
pub fn with_rpc_client(mut self, client: Box<dyn NodeRpcClient + Send>) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I would maybe rename the current with_rpc_client() into just with_rpc(), and the current with_rpc() into with_tonic_rpc().

Also, with_tonic_rpc() could probably take a string rather than Endpoint as an argument - or is there a reason not to do this?

Copy link
Author

@partylikeits1983 partylikeits1983 Feb 24, 2025

Choose a reason for hiding this comment

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

Also, with_tonic_rpc() could probably take a string rather than Endpoint as an argument - or is there a reason not to do this?

The reason why the with_rpc() function takes an Endpoint input was because I was following @tomyrd suggestion here:

I think so too, this function could even receive a built Endpoint object. This way the user can use the known endpoints (like Endpoint::testnet())
#741 (comment)

I do like the idea of just passing in the RPC url string as it is cleaner, but maybe it is useful to be able to initialize the endpoing like: Endpoint::testnet()

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, makes sense. I'm fine with either approach.

Copy link
Contributor

Choose a reason for hiding this comment

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

btw, another option is to use something like AsRef<str> or maybe Into<String> - e.g.:

pub fn with_tonic_rpc(mut self, url: impl Into<String>) -> Self {
    ...
}

Then, if we have From<Endpoint> for String implemented, we'd be able to pass it into the method as well. We would need to change the internal field type of rpc_endpoint to String, but I don't think this should be a problem.

@igamigo
Copy link
Collaborator

igamigo commented Feb 24, 2025

@igamigo Regarding this: https://github.com/0xPolygonMiden/miden-client/pull/741/files#r1962738199

Would it be possible to address this in a separate PR? I tried implementing it but I believe it could get a bit complicated. Also not 100% sure how to make RpoRandomCoin generic without having to specify the type in the ClientBuilder, which means my initial implementation made things look like this:

let mut client = ClientBuilder::<RpoRandomCoin>::new();

Sure! I think we could make the RNG a trait object. In any case, because the RNG component is necessary, it could be an argument of the new() function. It could look something like ClientBuilder::new(rng: R) and then the type can be inferred and you wouldn't need to specify the type within brackets.

BTW the std feature can be enabled without (for example) the tonic feature. In this case the build will probably fail because the tonic client will not be present but the builder will. We should tacke it as explained here. Let's add this to the issue alongside @bobbinth 's points.

EDIT: Also, seems like you should rebase from the latest next

@partylikeits1983
Copy link
Author

partylikeits1983 commented Feb 24, 2025

  1. Changed the name of the function with_rpc_client() to with_tonic_client() as suggested by @bobbinth
  2. Added a test for using the ClientBuilder with with_tonic_client()
  3. Rebased to latest next branch updates.

There are a few outstanding issues that I can address in a separate PR:

  1. Modifying rpc/tonic_client/mod.rs to avoid having the #[cfg(feature="std")] guard for the ClientBuilder mod
  2. Make handling of KeyStore more generic (so that we could use non-FilesystemKeyStores as well).
  3. Make handling of Rng more generic (so that we could use non-RpoRandomCoin as well).

@partylikeits1983
Copy link
Author

Opened an issue for a separate PR to improve the ClientBuilder implementation. #759

Comment on lines 89 to 98
pub fn with_rpc(mut self, endpoint: Endpoint) -> Self {
self.rpc_endpoint = Some(endpoint);
self
}

/// Sets a custom RPC client directly.
///
/// This method overrides any previously set RPC endpoint.
#[must_use]
pub fn with_tonic_rpc(mut self, client: Box<dyn NodeRpcClient + Send>) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the naming here is reversed. It should be:

pub fn with_rpc(mut self, client: Box<dyn NodeRpcClient + Send>) -> Self {
    ...
}

pub fn with_tonic_rpc(mut self, endpoint: Endpoint) -> Self {
    ...
}

The idea is that with_tonic_rpc() is more specialized - i.e., we know that we'll be using the TonicRpcClient.

Copy link
Author

Choose a reason for hiding this comment

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

That makes sense. I changed the naming of the functions.

@igamigo
Copy link
Collaborator

igamigo commented Feb 25, 2025

Unused dependencies check was fixed on #761 so we can merge this safely

@igamigo igamigo merged commit d957e87 into 0xPolygonMiden:next Feb 25, 2025
12 of 13 checks passed
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.

5 participants